@@ -6,7 +6,7 @@ use std::sync::Arc;
66
77use chashmap:: CHashMap ;
88use eyre:: Context ;
9- use itertools:: Itertools ;
9+ use itertools:: { intersperse , Itertools } ;
1010use rayon:: { prelude:: * , ThreadPool } ;
1111use tracing:: { instrument, warn} ;
1212
@@ -148,7 +148,65 @@ impl ToString for RebaseCommand {
148148 } => match commits_to_apply_oids. as_slice ( ) {
149149 [ ] => String :: new ( ) ,
150150 [ commit_oid] => format ! ( "pick {commit_oid}" ) ,
151- [ ..] => unimplemented ! ( "On-disk fixups" ) ,
151+ [ pick_oid, fixup_oids @ ..] => {
152+ let mut picks = vec ! [ format!( "pick {pick_oid}" ) ] ;
153+ let mut fixups = fixup_oids
154+ . iter ( )
155+ . map ( |oid| format ! ( "fixup {oid}" ) )
156+ . collect :: < Vec < String > > ( ) ;
157+ let mut cleanups = vec ! [ ] ;
158+
159+ // Since 0ca8681, the intermediate commits created as each
160+ // fixup is applied are left behind in the smartlog. This
161+ // forcibly removes all but the last of them. I don't
162+ // understand why this happens during `git branchless`
163+ // initiated rebases, but not during "normal" fixup rebases,
164+ // but this makes these artifacts go away.
165+ if fixups. len ( ) > 1 {
166+ fixups = intersperse (
167+ fixups,
168+ // FIXME Is $(...) portable?
169+ // I had assumed not, but the tests seem to pass on all platforms, so maybe this is OK?
170+ "exec git branchless hook-skip-upstream-applied-commit $(git rev-parse HEAD)" . to_string ( )
171+ ) . collect ( )
172+ }
173+
174+ // If the destination commit (ie `original_commit_oid`) does
175+ // not come first topologically among the commits being
176+ // rebased, then the final squashed commit will end up with
177+ // the wrong metadata. (It will end up with the metadata
178+ // from the commit that *does* come first, ie `pick_oid`.)
179+ // We have to add some additional steps to make sure the
180+ // smartlog and commit metadata are left as the user
181+ // expects.
182+ if pick_oid != original_commit_oid {
183+ // See above comment related to 0ca8681
184+ picks. insert (
185+ 1 ,
186+ "exec git branchless hook-skip-upstream-applied-commit $(git rev-parse HEAD)" . to_string ( )
187+ ) ;
188+
189+ cleanups = vec ! [
190+ // Hide the final squashed commit
191+ "exec git branchless hook-skip-upstream-applied-commit $(git rev-parse HEAD)" . to_string( ) ,
192+
193+ // Create a new final commit by applying the
194+ // metadata from the destination commit to the just
195+ // now hidden commit.
196+ format!( "exec git commit --amend --no-edit --reuse-message {original_commit_oid}" ) ,
197+
198+ // Finally, register the new final commit as the
199+ // rewritten version of original_commit_oid
200+ format!( "exec git branchless hook-skip-upstream-applied-commit {original_commit_oid} $(git rev-parse HEAD)" )
201+ ] ;
202+ }
203+
204+ picks
205+ . iter ( )
206+ . chain ( fixups. iter ( ) )
207+ . chain ( cleanups. iter ( ) )
208+ . join ( "\n " )
209+ }
152210 } ,
153211 RebaseCommand :: Merge {
154212 commit_oid,
@@ -1172,9 +1230,16 @@ impl<'a> RebasePlanBuilder<'a> {
11721230
11731231 let first_parent_oid = * parent_oids. first ( ) . unwrap ( ) ;
11741232 first_dest_oid. get_or_insert ( first_parent_oid) ;
1175- acc. push ( RebaseCommand :: Reset {
1176- target : OidOrLabel :: Oid ( first_parent_oid) ,
1177- } ) ;
1233+ // FIXME This feels heavy handed, but seems to be necessary for some fixup cases.
1234+ if !state
1235+ . constraints
1236+ . commits_to_move ( )
1237+ . contains ( & first_parent_oid)
1238+ {
1239+ acc. push ( RebaseCommand :: Reset {
1240+ target : OidOrLabel :: Oid ( first_parent_oid) ,
1241+ } ) ;
1242+ }
11781243
11791244 let upstream_patch_ids = if * detect_duplicate_commits_via_patch_id {
11801245 let ( effects, _progress) =
0 commit comments