2 * "git replay" builtin command
5 #include "git-compat-util.h"
8 #include "environment.h"
11 #include "merge-ort.h"
12 #include "object-name.h"
13 #include "parse-options.h"
20 static const char *short_commit_name(struct commit
*commit
)
22 return repo_find_unique_abbrev(the_repository
, &commit
->object
.oid
,
26 static struct commit
*peel_committish(const char *name
)
31 if (repo_get_oid(the_repository
, name
, &oid
))
33 obj
= parse_object(the_repository
, &oid
);
34 return (struct commit
*)repo_peel_to_type(the_repository
, name
, 0, obj
,
38 static char *get_author(const char *message
)
43 a
= find_commit_header(message
, "author", &len
);
45 return xmemdupz(a
, len
);
50 static struct commit
*create_commit(struct tree
*tree
,
51 struct commit
*based_on
,
52 struct commit
*parent
)
55 struct object
*obj
= NULL
;
56 struct commit_list
*parents
= NULL
;
58 char *sign_commit
= NULL
; /* FIXME: cli users might want to sign again */
59 struct commit_extra_header
*extra
= NULL
;
60 struct strbuf msg
= STRBUF_INIT
;
61 const char *out_enc
= get_commit_output_encoding();
62 const char *message
= repo_logmsg_reencode(the_repository
, based_on
,
64 const char *orig_message
= NULL
;
65 const char *exclude_gpgsig
[] = { "gpgsig", NULL
};
67 commit_list_insert(parent
, &parents
);
68 extra
= read_commit_extra_headers(based_on
, exclude_gpgsig
);
69 find_commit_subject(message
, &orig_message
);
70 strbuf_addstr(&msg
, orig_message
);
71 author
= get_author(message
);
73 if (commit_tree_extended(msg
.buf
, msg
.len
, &tree
->object
.oid
, parents
,
74 &ret
, author
, NULL
, sign_commit
, extra
)) {
75 error(_("failed to write commit object"));
79 obj
= parse_object(the_repository
, &ret
);
82 free_commit_extra_headers(extra
);
83 free_commit_list(parents
);
86 return (struct commit
*)obj
;
91 struct strset positive_refs
;
92 struct strset negative_refs
;
93 int positive_refexprs
;
94 int negative_refexprs
;
97 static void get_ref_information(struct rev_cmdline_info
*cmd_info
,
98 struct ref_info
*ref_info
)
102 ref_info
->onto
= NULL
;
103 strset_init(&ref_info
->positive_refs
);
104 strset_init(&ref_info
->negative_refs
);
105 ref_info
->positive_refexprs
= 0;
106 ref_info
->negative_refexprs
= 0;
109 * When the user specifies e.g.
110 * git replay origin/main..mybranch
111 * git replay ^origin/next mybranch1 mybranch2
112 * we want to be able to determine where to replay the commits. In
113 * these examples, the branches are probably based on an old version
114 * of either origin/main or origin/next, so we want to replay on the
115 * newest version of that branch. In contrast we would want to error
117 * git replay ^origin/master ^origin/next mybranch
118 * git replay mybranch~2..mybranch
119 * the first of those because there's no unique base to choose, and
120 * the second because they'd likely just be replaying commits on top
121 * of the same commit and not making any difference.
123 for (i
= 0; i
< cmd_info
->nr
; i
++) {
124 struct rev_cmdline_entry
*e
= cmd_info
->rev
+ i
;
125 struct object_id oid
;
126 const char *refexpr
= e
->name
;
127 char *fullname
= NULL
;
128 int can_uniquely_dwim
= 1;
132 if (repo_dwim_ref(the_repository
, refexpr
, strlen(refexpr
), &oid
, &fullname
, 0) != 1)
133 can_uniquely_dwim
= 0;
135 if (e
->flags
& BOTTOM
) {
136 if (can_uniquely_dwim
)
137 strset_add(&ref_info
->negative_refs
, fullname
);
138 if (!ref_info
->negative_refexprs
)
139 ref_info
->onto
= lookup_commit_reference_gently(the_repository
,
141 ref_info
->negative_refexprs
++;
143 if (can_uniquely_dwim
)
144 strset_add(&ref_info
->positive_refs
, fullname
);
145 ref_info
->positive_refexprs
++;
152 static void determine_replay_mode(struct rev_cmdline_info
*cmd_info
,
153 const char *onto_name
,
155 struct commit
**onto
,
156 struct strset
**update_refs
)
158 struct ref_info rinfo
;
160 get_ref_information(cmd_info
, &rinfo
);
161 if (!rinfo
.positive_refexprs
)
162 die(_("need some commits to replay"));
163 if (onto_name
&& *advance_name
)
164 die(_("--onto and --advance are incompatible"));
165 else if (onto_name
) {
166 *onto
= peel_committish(onto_name
);
167 if (rinfo
.positive_refexprs
<
168 strset_get_size(&rinfo
.positive_refs
))
169 die(_("all positive revisions given must be references"));
170 } else if (*advance_name
) {
171 struct object_id oid
;
172 char *fullname
= NULL
;
174 *onto
= peel_committish(*advance_name
);
175 if (repo_dwim_ref(the_repository
, *advance_name
, strlen(*advance_name
),
176 &oid
, &fullname
, 0) == 1) {
178 *advance_name
= fullname
;
180 die(_("argument to --advance must be a reference"));
182 if (rinfo
.positive_refexprs
> 1)
183 die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
185 int positive_refs_complete
= (
186 rinfo
.positive_refexprs
==
187 strset_get_size(&rinfo
.positive_refs
));
188 int negative_refs_complete
= (
189 rinfo
.negative_refexprs
==
190 strset_get_size(&rinfo
.negative_refs
));
192 * We need either positive_refs_complete or
193 * negative_refs_complete, but not both.
195 if (rinfo
.negative_refexprs
> 0 &&
196 positive_refs_complete
== negative_refs_complete
)
197 die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
198 if (negative_refs_complete
) {
199 struct hashmap_iter iter
;
200 struct strmap_entry
*entry
;
201 const char *last_key
= NULL
;
203 if (rinfo
.negative_refexprs
== 0)
204 die(_("all positive revisions given must be references"));
205 else if (rinfo
.negative_refexprs
> 1)
206 die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
207 else if (rinfo
.positive_refexprs
> 1)
208 die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
210 /* Only one entry, but we have to loop to get it */
211 strset_for_each_entry(&rinfo
.negative_refs
,
213 last_key
= entry
->key
;
217 *advance_name
= xstrdup_or_null(last_key
);
218 } else { /* positive_refs_complete */
219 if (rinfo
.negative_refexprs
> 1)
220 die(_("cannot implicitly determine correct base for --onto"));
221 if (rinfo
.negative_refexprs
== 1)
225 if (!*advance_name
) {
226 *update_refs
= xcalloc(1, sizeof(**update_refs
));
227 **update_refs
= rinfo
.positive_refs
;
228 memset(&rinfo
.positive_refs
, 0, sizeof(**update_refs
));
230 strset_clear(&rinfo
.negative_refs
);
231 strset_clear(&rinfo
.positive_refs
);
234 static struct commit
*mapped_commit(kh_oid_map_t
*replayed_commits
,
235 struct commit
*commit
,
236 struct commit
*fallback
)
238 khint_t pos
= kh_get_oid_map(replayed_commits
, commit
->object
.oid
);
239 if (pos
== kh_end(replayed_commits
))
241 return kh_value(replayed_commits
, pos
);
244 static struct commit
*pick_regular_commit(struct commit
*pickme
,
245 kh_oid_map_t
*replayed_commits
,
247 struct merge_options
*merge_opt
,
248 struct merge_result
*result
)
250 struct commit
*base
, *replayed_base
;
251 struct tree
*pickme_tree
, *base_tree
;
253 base
= pickme
->parents
->item
;
254 replayed_base
= mapped_commit(replayed_commits
, base
, onto
);
256 result
->tree
= repo_get_commit_tree(the_repository
, replayed_base
);
257 pickme_tree
= repo_get_commit_tree(the_repository
, pickme
);
258 base_tree
= repo_get_commit_tree(the_repository
, base
);
260 merge_opt
->branch1
= short_commit_name(replayed_base
);
261 merge_opt
->branch2
= short_commit_name(pickme
);
262 merge_opt
->ancestor
= xstrfmt("parent of %s", merge_opt
->branch2
);
264 merge_incore_nonrecursive(merge_opt
,
270 free((char*)merge_opt
->ancestor
);
271 merge_opt
->ancestor
= NULL
;
274 return create_commit(result
->tree
, pickme
, replayed_base
);
277 int cmd_replay(int argc
, const char **argv
, const char *prefix
)
279 const char *advance_name_opt
= NULL
;
280 char *advance_name
= NULL
;
281 struct commit
*onto
= NULL
;
282 const char *onto_name
= NULL
;
285 struct rev_info revs
;
286 struct commit
*last_commit
= NULL
;
287 struct commit
*commit
;
288 struct merge_options merge_opt
;
289 struct merge_result result
;
290 struct strset
*update_refs
= NULL
;
291 kh_oid_map_t
*replayed_commits
;
294 const char * const replay_usage
[] = {
295 N_("(EXPERIMENTAL!) git replay "
296 "([--contained] --onto <newbase> | --advance <branch>) "
297 "<revision-range>..."),
300 struct option replay_options
[] = {
301 OPT_STRING(0, "advance", &advance_name_opt
,
303 N_("make replay advance given branch")),
304 OPT_STRING(0, "onto", &onto_name
,
306 N_("replay onto given commit")),
307 OPT_BOOL(0, "contained", &contained
,
308 N_("advance all branches contained in revision-range")),
312 argc
= parse_options(argc
, argv
, prefix
, replay_options
, replay_usage
,
313 PARSE_OPT_KEEP_ARGV0
| PARSE_OPT_KEEP_UNKNOWN_OPT
);
315 if (!onto_name
&& !advance_name_opt
) {
316 error(_("option --onto or --advance is mandatory"));
317 usage_with_options(replay_usage
, replay_options
);
320 if (advance_name_opt
&& contained
)
321 die(_("options '%s' and '%s' cannot be used together"),
322 "--advance", "--contained");
323 advance_name
= xstrdup_or_null(advance_name_opt
);
325 repo_init_revisions(the_repository
, &revs
, prefix
);
328 * Set desired values for rev walking options here. If they
329 * are changed by some user specified option in setup_revisions()
330 * below, we will detect that below and then warn.
332 * TODO: In the future we might want to either die(), or allow
333 * some options changing these values if we think they could
337 revs
.sort_order
= REV_SORT_IN_GRAPH_ORDER
;
339 revs
.simplify_history
= 0;
341 argc
= setup_revisions(argc
, argv
, &revs
, NULL
);
343 ret
= error(_("unrecognized argument: %s"), argv
[1]);
348 * Detect and warn if we override some user specified rev
351 if (revs
.reverse
!= 1) {
352 warning(_("some rev walking options will be overridden as "
353 "'%s' bit in 'struct rev_info' will be forced"),
357 if (revs
.sort_order
!= REV_SORT_IN_GRAPH_ORDER
) {
358 warning(_("some rev walking options will be overridden as "
359 "'%s' bit in 'struct rev_info' will be forced"),
361 revs
.sort_order
= REV_SORT_IN_GRAPH_ORDER
;
363 if (revs
.topo_order
!= 1) {
364 warning(_("some rev walking options will be overridden as "
365 "'%s' bit in 'struct rev_info' will be forced"),
369 if (revs
.simplify_history
!= 0) {
370 warning(_("some rev walking options will be overridden as "
371 "'%s' bit in 'struct rev_info' will be forced"),
373 revs
.simplify_history
= 0;
376 determine_replay_mode(&revs
.cmdline
, onto_name
, &advance_name
,
377 &onto
, &update_refs
);
379 if (!onto
) /* FIXME: Should handle replaying down to root commit */
380 die("Replaying down to root commit is not supported yet!");
382 if (prepare_revision_walk(&revs
) < 0) {
383 ret
= error(_("error preparing revisions"));
387 init_basic_merge_options(&merge_opt
, the_repository
);
388 memset(&result
, 0, sizeof(result
));
389 merge_opt
.show_rename_progress
= 0;
391 replayed_commits
= kh_init_oid_map();
392 while ((commit
= get_revision(&revs
))) {
393 const struct name_decoration
*decoration
;
397 if (!commit
->parents
)
398 die(_("replaying down to root commit is not supported yet!"));
399 if (commit
->parents
->next
)
400 die(_("replaying merge commits is not supported yet!"));
402 last_commit
= pick_regular_commit(commit
, replayed_commits
, onto
,
403 &merge_opt
, &result
);
407 /* Record commit -> last_commit mapping */
408 pos
= kh_put_oid_map(replayed_commits
, commit
->object
.oid
, &hr
);
410 BUG("Duplicate rewritten commit: %s\n",
411 oid_to_hex(&commit
->object
.oid
));
412 kh_value(replayed_commits
, pos
) = last_commit
;
414 /* Update any necessary branches */
417 decoration
= get_name_decoration(&commit
->object
);
421 if (decoration
->type
== DECORATION_REF_LOCAL
&&
422 (contained
|| strset_contains(update_refs
,
423 decoration
->name
))) {
424 printf("update %s %s %s\n",
426 oid_to_hex(&last_commit
->object
.oid
),
427 oid_to_hex(&commit
->object
.oid
));
429 decoration
= decoration
->next
;
433 /* In --advance mode, advance the target ref */
434 if (result
.clean
== 1 && advance_name
) {
435 printf("update %s %s %s\n",
437 oid_to_hex(&last_commit
->object
.oid
),
438 oid_to_hex(&onto
->object
.oid
));
441 merge_finalize(&merge_opt
, &result
);
442 kh_destroy_oid_map(replayed_commits
);
444 strset_clear(update_refs
);
450 release_revisions(&revs
);