]>
Commit | Line | Data |
---|---|---|
0a29b90c FG |
1 | use std::ffi::CString; |
2 | use std::{marker, mem, ptr, str}; | |
3 | ||
4 | use crate::build::CheckoutBuilder; | |
5 | use crate::util::Binding; | |
6 | use crate::{raw, Error, Index, MergeOptions, Oid, Signature}; | |
7 | ||
8 | /// Rebase options | |
9 | /// | |
10 | /// Use to tell the rebase machinery how to operate. | |
11 | pub struct RebaseOptions<'cb> { | |
12 | raw: raw::git_rebase_options, | |
13 | rewrite_notes_ref: Option<CString>, | |
14 | merge_options: Option<MergeOptions>, | |
15 | checkout_options: Option<CheckoutBuilder<'cb>>, | |
16 | } | |
17 | ||
18 | impl<'cb> Default for RebaseOptions<'cb> { | |
19 | fn default() -> Self { | |
20 | Self::new() | |
21 | } | |
22 | } | |
23 | ||
24 | impl<'cb> RebaseOptions<'cb> { | |
25 | /// Creates a new default set of rebase options. | |
26 | pub fn new() -> RebaseOptions<'cb> { | |
27 | let mut opts = RebaseOptions { | |
28 | raw: unsafe { mem::zeroed() }, | |
29 | rewrite_notes_ref: None, | |
30 | merge_options: None, | |
31 | checkout_options: None, | |
32 | }; | |
33 | assert_eq!(unsafe { raw::git_rebase_init_options(&mut opts.raw, 1) }, 0); | |
34 | opts | |
35 | } | |
36 | ||
37 | /// Used by `Repository::rebase`, this will instruct other clients working on this | |
38 | /// rebase that you want a quiet rebase experience, which they may choose to | |
39 | /// provide in an application-specific manner. This has no effect upon | |
40 | /// libgit2 directly, but is provided for interoperability between Git | |
41 | /// tools. | |
42 | pub fn quiet(&mut self, quiet: bool) -> &mut RebaseOptions<'cb> { | |
43 | self.raw.quiet = quiet as i32; | |
44 | self | |
45 | } | |
46 | ||
47 | /// Used by `Repository::rebase`, this will begin an in-memory rebase, | |
48 | /// which will allow callers to step through the rebase operations and | |
49 | /// commit the rebased changes, but will not rewind HEAD or update the | |
50 | /// repository to be in a rebasing state. This will not interfere with | |
51 | /// the working directory (if there is one). | |
52 | pub fn inmemory(&mut self, inmemory: bool) -> &mut RebaseOptions<'cb> { | |
53 | self.raw.inmemory = inmemory as i32; | |
54 | self | |
55 | } | |
56 | ||
57 | /// Used by `finish()`, this is the name of the notes reference | |
58 | /// used to rewrite notes for rebased commits when finishing the rebase; | |
59 | /// if NULL, the contents of the configuration option `notes.rewriteRef` | |
60 | /// is examined, unless the configuration option `notes.rewrite.rebase` | |
61 | /// is set to false. If `notes.rewriteRef` is also NULL, notes will | |
62 | /// not be rewritten. | |
63 | pub fn rewrite_notes_ref(&mut self, rewrite_notes_ref: &str) -> &mut RebaseOptions<'cb> { | |
64 | self.rewrite_notes_ref = Some(CString::new(rewrite_notes_ref).unwrap()); | |
65 | self | |
66 | } | |
67 | ||
68 | /// Options to control how trees are merged during `next()`. | |
69 | pub fn merge_options(&mut self, opts: MergeOptions) -> &mut RebaseOptions<'cb> { | |
70 | self.merge_options = Some(opts); | |
71 | self | |
72 | } | |
73 | ||
74 | /// Options to control how files are written during `Repository::rebase`, | |
75 | /// `next()` and `abort()`. Note that a minimum strategy of | |
76 | /// `GIT_CHECKOUT_SAFE` is defaulted in `init` and `next`, and a minimum | |
77 | /// strategy of `GIT_CHECKOUT_FORCE` is defaulted in `abort` to match git | |
78 | /// semantics. | |
79 | pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut RebaseOptions<'cb> { | |
80 | self.checkout_options = Some(opts); | |
81 | self | |
82 | } | |
83 | ||
84 | /// Acquire a pointer to the underlying raw options. | |
85 | pub fn raw(&mut self) -> *const raw::git_rebase_options { | |
86 | unsafe { | |
87 | if let Some(opts) = self.merge_options.as_mut().take() { | |
88 | ptr::copy_nonoverlapping(opts.raw(), &mut self.raw.merge_options, 1); | |
89 | } | |
90 | if let Some(opts) = self.checkout_options.as_mut() { | |
91 | opts.configure(&mut self.raw.checkout_options); | |
92 | } | |
93 | self.raw.rewrite_notes_ref = self | |
94 | .rewrite_notes_ref | |
95 | .as_ref() | |
96 | .map(|s| s.as_ptr()) | |
97 | .unwrap_or(ptr::null()); | |
98 | } | |
99 | &self.raw | |
100 | } | |
101 | } | |
102 | ||
103 | /// Representation of a rebase | |
104 | pub struct Rebase<'repo> { | |
105 | raw: *mut raw::git_rebase, | |
106 | _marker: marker::PhantomData<&'repo raw::git_rebase>, | |
107 | } | |
108 | ||
109 | impl<'repo> Rebase<'repo> { | |
110 | /// Gets the count of rebase operations that are to be applied. | |
111 | pub fn len(&self) -> usize { | |
112 | unsafe { raw::git_rebase_operation_entrycount(self.raw) } | |
113 | } | |
114 | ||
115 | /// Gets the original `HEAD` ref name for merge rebases. | |
116 | pub fn orig_head_name(&self) -> Option<&str> { | |
117 | let name_bytes = | |
118 | unsafe { crate::opt_bytes(self, raw::git_rebase_orig_head_name(self.raw)) }; | |
119 | name_bytes.and_then(|s| str::from_utf8(s).ok()) | |
120 | } | |
121 | ||
122 | /// Gets the original HEAD id for merge rebases. | |
123 | pub fn orig_head_id(&self) -> Option<Oid> { | |
124 | unsafe { Oid::from_raw_opt(raw::git_rebase_orig_head_id(self.raw)) } | |
125 | } | |
126 | ||
127 | /// Gets the rebase operation specified by the given index. | |
128 | pub fn nth(&mut self, n: usize) -> Option<RebaseOperation<'_>> { | |
129 | unsafe { | |
130 | let op = raw::git_rebase_operation_byindex(self.raw, n); | |
131 | if op.is_null() { | |
132 | None | |
133 | } else { | |
134 | Some(RebaseOperation::from_raw(op)) | |
135 | } | |
136 | } | |
137 | } | |
138 | ||
139 | /// Gets the index of the rebase operation that is currently being applied. | |
140 | /// If the first operation has not yet been applied (because you have called | |
141 | /// `init` but not yet `next`) then this returns None. | |
142 | pub fn operation_current(&mut self) -> Option<usize> { | |
143 | let cur = unsafe { raw::git_rebase_operation_current(self.raw) }; | |
144 | if cur == raw::GIT_REBASE_NO_OPERATION { | |
145 | None | |
146 | } else { | |
147 | Some(cur) | |
148 | } | |
149 | } | |
150 | ||
151 | /// Gets the index produced by the last operation, which is the result of | |
152 | /// `next()` and which will be committed by the next invocation of | |
153 | /// `commit()`. This is useful for resolving conflicts in an in-memory | |
154 | /// rebase before committing them. | |
155 | /// | |
156 | /// This is only applicable for in-memory rebases; for rebases within a | |
157 | /// working directory, the changes were applied to the repository's index. | |
158 | pub fn inmemory_index(&mut self) -> Result<Index, Error> { | |
159 | let mut idx = ptr::null_mut(); | |
160 | unsafe { | |
161 | try_call!(raw::git_rebase_inmemory_index(&mut idx, self.raw)); | |
162 | Ok(Binding::from_raw(idx)) | |
163 | } | |
164 | } | |
165 | ||
166 | /// Commits the current patch. You must have resolved any conflicts that | |
167 | /// were introduced during the patch application from the `git_rebase_next` | |
168 | /// invocation. To keep the author and message from the original commit leave | |
169 | /// them as None | |
170 | pub fn commit( | |
171 | &mut self, | |
172 | author: Option<&Signature<'_>>, | |
173 | committer: &Signature<'_>, | |
174 | message: Option<&str>, | |
175 | ) -> Result<Oid, Error> { | |
176 | let mut id: raw::git_oid = unsafe { mem::zeroed() }; | |
177 | let message = crate::opt_cstr(message)?; | |
178 | unsafe { | |
179 | try_call!(raw::git_rebase_commit( | |
180 | &mut id, | |
181 | self.raw, | |
182 | author.map(|a| a.raw()), | |
183 | committer.raw(), | |
184 | ptr::null(), | |
185 | message | |
186 | )); | |
187 | Ok(Binding::from_raw(&id as *const _)) | |
188 | } | |
189 | } | |
190 | ||
191 | /// Aborts a rebase that is currently in progress, resetting the repository | |
192 | /// and working directory to their state before rebase began. | |
193 | pub fn abort(&mut self) -> Result<(), Error> { | |
194 | unsafe { | |
195 | try_call!(raw::git_rebase_abort(self.raw)); | |
196 | } | |
197 | ||
198 | Ok(()) | |
199 | } | |
200 | ||
201 | /// Finishes a rebase that is currently in progress once all patches have | |
202 | /// been applied. | |
203 | pub fn finish(&mut self, signature: Option<&Signature<'_>>) -> Result<(), Error> { | |
204 | unsafe { | |
205 | try_call!(raw::git_rebase_finish(self.raw, signature.map(|s| s.raw()))); | |
206 | } | |
207 | ||
208 | Ok(()) | |
209 | } | |
210 | } | |
211 | ||
212 | impl<'rebase> Iterator for Rebase<'rebase> { | |
213 | type Item = Result<RebaseOperation<'rebase>, Error>; | |
214 | ||
215 | /// Performs the next rebase operation and returns the information about it. | |
216 | /// If the operation is one that applies a patch (which is any operation except | |
217 | /// GitRebaseOperation::Exec) then the patch will be applied and the index and | |
218 | /// working directory will be updated with the changes. If there are conflicts, | |
219 | /// you will need to address those before committing the changes. | |
220 | fn next(&mut self) -> Option<Result<RebaseOperation<'rebase>, Error>> { | |
221 | let mut out = ptr::null_mut(); | |
222 | unsafe { | |
223 | try_call_iter!(raw::git_rebase_next(&mut out, self.raw)); | |
224 | Some(Ok(RebaseOperation::from_raw(out))) | |
225 | } | |
226 | } | |
227 | } | |
228 | ||
229 | impl<'repo> Binding for Rebase<'repo> { | |
230 | type Raw = *mut raw::git_rebase; | |
231 | unsafe fn from_raw(raw: *mut raw::git_rebase) -> Rebase<'repo> { | |
232 | Rebase { | |
233 | raw, | |
234 | _marker: marker::PhantomData, | |
235 | } | |
236 | } | |
237 | fn raw(&self) -> *mut raw::git_rebase { | |
238 | self.raw | |
239 | } | |
240 | } | |
241 | ||
242 | impl<'repo> Drop for Rebase<'repo> { | |
243 | fn drop(&mut self) { | |
244 | unsafe { raw::git_rebase_free(self.raw) } | |
245 | } | |
246 | } | |
247 | ||
248 | /// A rebase operation | |
249 | /// | |
250 | /// Describes a single instruction/operation to be performed during the | |
251 | /// rebase. | |
252 | #[derive(Debug, PartialEq)] | |
253 | pub enum RebaseOperationType { | |
254 | /// The given commit is to be cherry-picked. The client should commit the | |
255 | /// changes and continue if there are no conflicts. | |
256 | Pick, | |
257 | ||
258 | /// The given commit is to be cherry-picked, but the client should prompt | |
259 | /// the user to provide an updated commit message. | |
260 | Reword, | |
261 | ||
262 | /// The given commit is to be cherry-picked, but the client should stop to | |
263 | /// allow the user to edit the changes before committing them. | |
264 | Edit, | |
265 | ||
266 | /// The given commit is to be squashed into the previous commit. The commit | |
267 | /// message will be merged with the previous message. | |
268 | Squash, | |
269 | ||
270 | /// The given commit is to be squashed into the previous commit. The commit | |
271 | /// message from this commit will be discarded. | |
272 | Fixup, | |
273 | ||
274 | /// No commit will be cherry-picked. The client should run the given command | |
275 | /// and (if successful) continue. | |
276 | Exec, | |
277 | } | |
278 | ||
279 | impl RebaseOperationType { | |
280 | /// Convert from the int into an enum. Returns None if invalid. | |
281 | pub fn from_raw(raw: raw::git_rebase_operation_t) -> Option<RebaseOperationType> { | |
282 | match raw { | |
283 | raw::GIT_REBASE_OPERATION_PICK => Some(RebaseOperationType::Pick), | |
284 | raw::GIT_REBASE_OPERATION_REWORD => Some(RebaseOperationType::Reword), | |
285 | raw::GIT_REBASE_OPERATION_EDIT => Some(RebaseOperationType::Edit), | |
286 | raw::GIT_REBASE_OPERATION_SQUASH => Some(RebaseOperationType::Squash), | |
287 | raw::GIT_REBASE_OPERATION_FIXUP => Some(RebaseOperationType::Fixup), | |
288 | raw::GIT_REBASE_OPERATION_EXEC => Some(RebaseOperationType::Exec), | |
289 | _ => None, | |
290 | } | |
291 | } | |
292 | } | |
293 | ||
294 | /// A rebase operation | |
295 | /// | |
296 | /// Describes a single instruction/operation to be performed during the | |
297 | /// rebase. | |
298 | #[derive(Debug)] | |
299 | pub struct RebaseOperation<'rebase> { | |
300 | raw: *const raw::git_rebase_operation, | |
301 | _marker: marker::PhantomData<Rebase<'rebase>>, | |
302 | } | |
303 | ||
304 | impl<'rebase> RebaseOperation<'rebase> { | |
305 | /// The type of rebase operation | |
306 | pub fn kind(&self) -> Option<RebaseOperationType> { | |
307 | unsafe { RebaseOperationType::from_raw((*self.raw).kind) } | |
308 | } | |
309 | ||
310 | /// The commit ID being cherry-picked. This will be populated for all | |
311 | /// operations except those of type `GIT_REBASE_OPERATION_EXEC`. | |
312 | pub fn id(&self) -> Oid { | |
313 | unsafe { Binding::from_raw(&(*self.raw).id as *const _) } | |
314 | } | |
315 | ||
316 | ///The executable the user has requested be run. This will only | |
317 | /// be populated for operations of type RebaseOperationType::Exec | |
318 | pub fn exec(&self) -> Option<&str> { | |
319 | unsafe { str::from_utf8(crate::opt_bytes(self, (*self.raw).exec).unwrap()).ok() } | |
320 | } | |
321 | } | |
322 | ||
323 | impl<'rebase> Binding for RebaseOperation<'rebase> { | |
324 | type Raw = *const raw::git_rebase_operation; | |
325 | unsafe fn from_raw(raw: *const raw::git_rebase_operation) -> RebaseOperation<'rebase> { | |
326 | RebaseOperation { | |
327 | raw, | |
328 | _marker: marker::PhantomData, | |
329 | } | |
330 | } | |
331 | fn raw(&self) -> *const raw::git_rebase_operation { | |
332 | self.raw | |
333 | } | |
334 | } | |
335 | ||
336 | #[cfg(test)] | |
337 | mod tests { | |
338 | use crate::{RebaseOperationType, RebaseOptions, Signature}; | |
339 | use std::{fs, path}; | |
340 | ||
341 | #[test] | |
342 | fn smoke() { | |
343 | let (_td, repo) = crate::test::repo_init(); | |
344 | let head_target = repo.head().unwrap().target().unwrap(); | |
345 | let tip = repo.find_commit(head_target).unwrap(); | |
346 | let sig = tip.author(); | |
347 | let tree = tip.tree().unwrap(); | |
348 | ||
349 | // We just want to see the iteration work so we can create commits with | |
350 | // no changes | |
351 | let c1 = repo | |
352 | .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&tip]) | |
353 | .unwrap(); | |
354 | let c1 = repo.find_commit(c1).unwrap(); | |
355 | let c2 = repo | |
356 | .commit(Some("refs/heads/main"), &sig, &sig, "foo", &tree, &[&c1]) | |
357 | .unwrap(); | |
358 | ||
359 | let head = repo.find_reference("refs/heads/main").unwrap(); | |
360 | let branch = repo.reference_to_annotated_commit(&head).unwrap(); | |
361 | let upstream = repo.find_annotated_commit(tip.id()).unwrap(); | |
362 | let mut rebase = repo | |
363 | .rebase(Some(&branch), Some(&upstream), None, None) | |
364 | .unwrap(); | |
365 | ||
366 | assert_eq!(Some("refs/heads/main"), rebase.orig_head_name()); | |
367 | assert_eq!(Some(c2), rebase.orig_head_id()); | |
368 | ||
369 | assert_eq!(rebase.len(), 2); | |
370 | { | |
371 | let op = rebase.next().unwrap().unwrap(); | |
372 | assert_eq!(op.kind(), Some(RebaseOperationType::Pick)); | |
373 | assert_eq!(op.id(), c1.id()); | |
374 | } | |
375 | { | |
376 | let op = rebase.next().unwrap().unwrap(); | |
377 | assert_eq!(op.kind(), Some(RebaseOperationType::Pick)); | |
378 | assert_eq!(op.id(), c2); | |
379 | } | |
380 | { | |
381 | let op = rebase.next(); | |
382 | assert!(op.is_none()); | |
383 | } | |
384 | } | |
385 | ||
386 | #[test] | |
387 | fn keeping_original_author_msg() { | |
388 | let (td, repo) = crate::test::repo_init(); | |
389 | let head_target = repo.head().unwrap().target().unwrap(); | |
390 | let tip = repo.find_commit(head_target).unwrap(); | |
391 | let sig = Signature::now("testname", "testemail").unwrap(); | |
392 | let mut index = repo.index().unwrap(); | |
393 | ||
394 | fs::File::create(td.path().join("file_a")).unwrap(); | |
395 | index.add_path(path::Path::new("file_a")).unwrap(); | |
396 | index.write().unwrap(); | |
397 | let tree_id_a = index.write_tree().unwrap(); | |
398 | let tree_a = repo.find_tree(tree_id_a).unwrap(); | |
399 | let c1 = repo | |
400 | .commit(Some("refs/heads/main"), &sig, &sig, "A", &tree_a, &[&tip]) | |
401 | .unwrap(); | |
402 | let c1 = repo.find_commit(c1).unwrap(); | |
403 | ||
404 | fs::File::create(td.path().join("file_b")).unwrap(); | |
405 | index.add_path(path::Path::new("file_b")).unwrap(); | |
406 | index.write().unwrap(); | |
407 | let tree_id_b = index.write_tree().unwrap(); | |
408 | let tree_b = repo.find_tree(tree_id_b).unwrap(); | |
409 | let c2 = repo | |
410 | .commit(Some("refs/heads/main"), &sig, &sig, "B", &tree_b, &[&c1]) | |
411 | .unwrap(); | |
412 | ||
413 | let branch = repo.find_annotated_commit(c2).unwrap(); | |
414 | let upstream = repo.find_annotated_commit(tip.id()).unwrap(); | |
415 | let mut opts: RebaseOptions<'_> = Default::default(); | |
416 | let mut rebase = repo | |
417 | .rebase(Some(&branch), Some(&upstream), None, Some(&mut opts)) | |
418 | .unwrap(); | |
419 | ||
420 | assert_eq!(rebase.len(), 2); | |
421 | ||
422 | { | |
423 | rebase.next().unwrap().unwrap(); | |
424 | let id = rebase.commit(None, &sig, None).unwrap(); | |
425 | let commit = repo.find_commit(id).unwrap(); | |
426 | assert_eq!(commit.message(), Some("A")); | |
427 | assert_eq!(commit.author().name(), Some("testname")); | |
428 | assert_eq!(commit.author().email(), Some("testemail")); | |
429 | } | |
430 | ||
431 | { | |
432 | rebase.next().unwrap().unwrap(); | |
433 | let id = rebase.commit(None, &sig, None).unwrap(); | |
434 | let commit = repo.find_commit(id).unwrap(); | |
435 | assert_eq!(commit.message(), Some("B")); | |
436 | assert_eq!(commit.author().name(), Some("testname")); | |
437 | assert_eq!(commit.author().email(), Some("testemail")); | |
438 | } | |
439 | rebase.finish(None).unwrap(); | |
440 | } | |
441 | } |