1
- //! The machinery used to alter and mutate commits in various ways.
1
+ //! The machinery used to alter and mutate commits in various ways whilst adjusting descendant commits within a [reference frame](ReferenceFrame) .
2
2
3
3
use anyhow:: { bail, Context } ;
4
4
use bstr:: { BString , ByteSlice } ;
5
5
use but_core:: unified_diff:: DiffHunk ;
6
6
use but_core:: { RepositoryExt , UnifiedDiff } ;
7
+ use gitbutler_stack:: VirtualBranchesState ;
7
8
use gix:: filter:: plumbing:: driver:: apply:: { Delay , MaybeDelayed } ;
8
9
use gix:: filter:: plumbing:: pipeline:: convert:: { ToGitOutcome , ToWorktreeOutcome } ;
9
10
use gix:: merge:: tree:: TreatAsUnresolved ;
10
11
use gix:: objs:: tree:: EntryKind ;
11
12
use gix:: prelude:: ObjectIdExt ;
13
+ use gix:: refs:: transaction:: PreviousValue ;
12
14
use serde:: { Deserialize , Serialize } ;
13
15
use std:: io:: Read ;
14
16
@@ -35,6 +37,35 @@ pub enum Destination {
35
37
AmendCommit ( gix:: ObjectId ) ,
36
38
}
37
39
40
+ /// Quite literally a set of commits that surround the commit to be altered or created to help finding descendant
41
+ /// commits that should be rewritten to integrate the new commit.
42
+ ///
43
+ /// Or explained differently, a set of commits that helps to navigate the commit graph to steer the necessary
44
+ /// [rewrites](but_rebase::RebaseBuilder::rebase()), keeping them linear.
45
+ #[ derive( Debug , Clone ) ]
46
+ pub struct ReferenceFrame {
47
+ /// The commit at the top of the branch which has the parent of the new commit or the amended commit
48
+ /// in its ancestry.
49
+ pub branch_tip : gix:: ObjectId ,
50
+ /// If present, the merge commit that integrates all branches into one workspace.
51
+ /// Must have the [`branch_tip`](Self::branch_tip) as direct ancestor/parent.
52
+ pub workspace_tip : Option < gix:: ObjectId > ,
53
+ /// A set of literal references along with the commit they point to, ideally (but not necessarily) filtered to only
54
+ /// the commits we would have to rebase.
55
+ /// If a reference is unborn, it can't be listed here and needs special treatment.
56
+ pub references : Vec < ( but_core:: Reference , gix:: ObjectId ) > ,
57
+ }
58
+
59
+ /// Identify the commit that contains the patches to be moved, along with the branch that should be rewritten.
60
+ #[ derive( Debug , Clone , Copy ) ]
61
+ pub struct MoveSourceCommit {
62
+ /// The commit that acts as the source of all changes. Note that these changes will be *removed* from the
63
+ /// commit, which gets rewritten in the process.
64
+ pub commit_id : gix:: ObjectId ,
65
+ /// The commit at the top of the branch which has the commit that acts as source of changes in its ancestry.
66
+ pub branch_tip : gix:: ObjectId ,
67
+ }
68
+
38
69
/// A change that should be used to create a new commit or alter an existing one, along with enough information to know where to find it.
39
70
#[ derive( Debug , Default , Clone , PartialEq ) ]
40
71
pub struct DiffSpec {
@@ -93,6 +124,10 @@ pub struct CreateCommitOutcome {
93
124
pub rejected_specs : Vec < DiffSpec > ,
94
125
/// The newly created commit, or `None` if no commit could be created as all changes-requests were rejected.
95
126
pub new_commit : Option < gix:: ObjectId > ,
127
+ /// The rewritten references `(old, new, reference)`, along with their `old` and `new` commit location, along
128
+ /// with the reference itself.
129
+ /// If `new_commit` is `None`, this array will be an empty.
130
+ pub references : Vec < ( gix:: ObjectId , gix:: ObjectId , but_core:: Reference ) > ,
96
131
}
97
132
98
133
/// Additional information about the outcome of a [`create_tree()`] call.
@@ -102,15 +137,16 @@ pub struct CreateTreeOutcome {
102
137
/// when merging the workspace commit, or because the specified hunks didn't match exactly due to changes
103
138
/// that happened in the meantime, or if a file without a change was specified.
104
139
pub rejected_specs : Vec < DiffSpec > ,
105
- /// The newly created tree, or `None` if no commit could be created as all changes-requests were rejected.
106
- pub new_tree : Option < gix:: ObjectId > ,
140
+ /// The newly created tree that acts as the destination of the changes, or `None` if no commit could be
141
+ /// created as all changes-requests were rejected.
142
+ pub destination_tree : Option < gix:: ObjectId > ,
107
143
}
108
144
109
145
/// Like [`create_commit()`], but lower-level and only returns a new tree, without finally associating it with a commit.
110
146
pub fn create_tree (
111
147
repo : & gix:: Repository ,
112
148
destination : & Destination ,
113
- origin_commit : Option < gix :: ObjectId > ,
149
+ move_source : Option < MoveSourceCommit > ,
114
150
changes : Vec < DiffSpec > ,
115
151
context_lines : u32 ,
116
152
) -> anyhow:: Result < CreateTreeOutcome > {
@@ -136,7 +172,7 @@ pub fn create_tree(
136
172
137
173
let mut changes: Vec < _ > = changes. into_iter ( ) . map ( Ok ) . collect ( ) ;
138
174
let new_tree = ' retry: loop {
139
- let ( maybe_new_tree, actual_base_tree) = if let Some ( _origin_commit ) = origin_commit {
175
+ let ( maybe_new_tree, actual_base_tree) = if let Some ( _source ) = move_source {
140
176
todo ! ( "get base tree and apply changes by cherry-picking, probably can all be done by one function, but optimizations are different" )
141
177
} else {
142
178
let changes_base_tree = repo. head ( ) ?. id ( ) . and_then ( |id| {
@@ -196,25 +232,25 @@ pub fn create_tree(
196
232
} ;
197
233
Ok ( CreateTreeOutcome {
198
234
rejected_specs : changes. into_iter ( ) . filter_map ( Result :: err) . collect ( ) ,
199
- new_tree,
235
+ destination_tree : new_tree,
200
236
} )
201
237
}
202
238
203
239
/// Alter the single `destination` in a given `frame` with as many `changes` as possible and write new objects into `repo`,
204
240
/// but only if the commit succeeds.
205
- /// If `origin_commit` is `Some(commit)`, all changes are considered to originate from the given commit, otherwise they originate from the worktree.
241
+ ///
242
+ /// If `move_source` is `Some(source)`, all changes are considered to originate from the given commit to move out of, otherwise they originate from the worktree.
206
243
/// `context_lines` is the amount of lines of context included in each [`HunkHeader`], and the value that will be used to recover the existing hunks,
207
244
/// so that the hunks can be matched.
208
245
///
209
246
/// Return additional information that helps to understand to what extent the commit was created, as the commit might not contain all the [`DiffSpecs`](DiffSpec)
210
247
/// that were requested if they failed to apply.
211
248
///
212
- /// Note that the ref pointed to by `HEAD` will be updated with the new commit if the new commits parent was pointed to by `HEAD` before. Detached heads will cause failure.
213
- /// If `allow_ref_change` is false, `HEAD` will never be adjusted to prevent additional side-effects.
249
+ /// No reference is touched in the process.
214
250
pub fn create_commit (
215
251
repo : & gix:: Repository ,
216
252
destination : Destination ,
217
- origin_commit : Option < gix :: ObjectId > ,
253
+ move_source : Option < MoveSourceCommit > ,
218
254
changes : Vec < DiffSpec > ,
219
255
context_lines : u32 ,
220
256
) -> anyhow:: Result < CreateCommitOutcome > {
@@ -242,9 +278,9 @@ pub fn create_commit(
242
278
243
279
let CreateTreeOutcome {
244
280
rejected_specs,
245
- new_tree ,
246
- } = create_tree ( repo, & destination, origin_commit , changes, context_lines) ?;
247
- let new_commit = if let Some ( new_tree) = new_tree {
281
+ destination_tree ,
282
+ } = create_tree ( repo, & destination, move_source , changes, context_lines) ?;
283
+ let new_commit = if let Some ( new_tree) = destination_tree {
248
284
match destination {
249
285
Destination :: NewCommit {
250
286
message,
@@ -273,9 +309,74 @@ pub fn create_commit(
273
309
Ok ( CreateCommitOutcome {
274
310
rejected_specs,
275
311
new_commit,
312
+ references : Vec :: new ( ) ,
276
313
} )
277
314
}
278
315
316
+ /// Like [`create_commit()`], but allows to also update virtual branches and git references pointing to commits
317
+ ///
318
+ /// `frame` is used to rebase all descendants to rewrite the relevant history to include the new or amended commit.
319
+ pub fn create_commit_and_update_refs (
320
+ repo : & gix:: Repository ,
321
+ branches : & mut VirtualBranchesState ,
322
+ destination : Destination ,
323
+ move_source : Option < MoveSourceCommit > ,
324
+ changes : Vec < DiffSpec > ,
325
+ context_lines : u32 ,
326
+ ) -> anyhow:: Result < CreateCommitOutcome > {
327
+ let mut out = create_commit (
328
+ repo,
329
+ destination. clone ( ) ,
330
+ move_source,
331
+ changes,
332
+ context_lines,
333
+ ) ?;
334
+
335
+ // TODO: if move_source is used, we will have another field here.
336
+ let CreateCommitOutcome {
337
+ rejected_specs,
338
+ new_commit,
339
+ references,
340
+ } = & mut out;
341
+
342
+ let Some ( new_commit) = new_commit else {
343
+ return Ok ( out) ;
344
+ } ;
345
+
346
+ let commit_to_find = match destination {
347
+ Destination :: NewCommit {
348
+ parent_commit_id, ..
349
+ } => parent_commit_id,
350
+ Destination :: AmendCommit ( commit) => Some ( commit) ,
351
+ } ;
352
+
353
+ if let Some ( destination_commit) = commit_to_find {
354
+ // Traverse all stacks in the workspace and find the one that contains the destination commit
355
+ // branches.list_stacks_in_workspace()
356
+ // but_rebase::RebaseBuilder::new()?.step
357
+ todo ! ( "rewrite existing commits" )
358
+ } else {
359
+ // unborn branch special case.
360
+ repo. reference (
361
+ repo. head_name ( ) ?
362
+ . context ( "unborn HEAD must contain a ref-name" ) ?,
363
+ * new_commit,
364
+ PreviousValue :: Any ,
365
+ format ! (
366
+ "commit (initial): {title}" ,
367
+ title = new_commit
368
+ . attach( repo)
369
+ . object( ) ?
370
+ . into_commit( )
371
+ . message( ) ?
372
+ . title
373
+ ) ,
374
+ ) ?;
375
+ }
376
+
377
+ Ok ( out)
378
+ }
379
+
279
380
fn into_err_spec ( input : & mut PossibleChange ) {
280
381
* input = match std:: mem:: replace ( input, Ok ( Default :: default ( ) ) ) {
281
382
// What we thought was a good change turned out to be a no-op, rejected.
0 commit comments