Skip to content

Commit f8c5d3e

Browse files
clickyotomygitster
authored andcommitted
push: add reflog check for "--force-if-includes"
Add a check to verify if the remote-tracking ref of the local branch is reachable from one of its "reflog" entries. The check iterates through the local ref's reflog to see if there is an entry for the remote-tracking ref and collecting any commits that are seen, into a list; the iteration stops if an entry in the reflog matches the remote ref or if the entry timestamp is older the latest entry of the remote ref's "reflog". If there wasn't an entry found for the remote ref, "in_merge_bases_many()" is called to check if it is reachable from the list of collected commits. When a local branch that is based on a remote ref, has been rewound and is to be force pushed on the remote, "--force-if-includes" runs a check that ensures any updates to the remote-tracking ref that may have happened (by push from another repository) in-between the time of the last update to the local branch (via "git-pull", for instance) and right before the time of push, have been integrated locally before allowing a forced update. If the new option is passed without specifying "--force-with-lease", or specified along with "--force-with-lease=<refname>:<expect>" it is a "no-op". Calls to "in_merge_bases_many()" return different results depending on whether the "commit-graph" feature is enabled or not -- it is temporarily disabled when the check runs [1]. [1] https://lore.kernel.org/git/[email protected] Signed-off-by: Srinidhi Kaushik <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 6c430a6 commit f8c5d3e

File tree

6 files changed

+219
-8
lines changed

6 files changed

+219
-8
lines changed

builtin/send-pack.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ static void print_helper_status(struct ref *ref)
7171
msg = "stale info";
7272
break;
7373

74+
case REF_STATUS_REJECT_REMOTE_UPDATED:
75+
res = "error";
76+
msg = "remote ref updated since checkout";
77+
break;
78+
7479
case REF_STATUS_REJECT_ALREADY_EXISTS:
7580
res = "error";
7681
msg = "already exists";

remote.c

Lines changed: 191 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,12 +1471,23 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
14711471
* with the remote-tracking branch to find the value
14721472
* to expect, but we did not have such a tracking
14731473
* branch.
1474+
*
1475+
* If the tip of the remote-tracking ref is unreachable
1476+
* from any reflog entry of its local ref indicating a
1477+
* possible update since checkout; reject the push.
14741478
*/
14751479
if (ref->expect_old_sha1) {
14761480
if (!oideq(&ref->old_oid, &ref->old_oid_expect))
14771481
reject_reason = REF_STATUS_REJECT_STALE;
1482+
else if (ref->check_reachable && ref->unreachable)
1483+
reject_reason =
1484+
REF_STATUS_REJECT_REMOTE_UPDATED;
14781485
else
1479-
/* If the ref isn't stale then force the update. */
1486+
/*
1487+
* If the ref isn't stale, and is reachable
1488+
* from from one of the reflog entries of
1489+
* the local branch, force the update.
1490+
*/
14801491
force_ref_update = 1;
14811492
}
14821493

@@ -2251,12 +2262,13 @@ int is_empty_cas(const struct push_cas_option *cas)
22512262

22522263
/*
22532264
* Look at remote.fetch refspec and see if we have a remote
2254-
* tracking branch for the refname there. Fill its current
2255-
* value in sha1[].
2265+
* tracking branch for the refname there. Fill the name of
2266+
* the remote-tracking branch in *dst_refname, and the name
2267+
* of the commit object at its tip in oid[].
22562268
* If we cannot do so, return negative to signal an error.
22572269
*/
22582270
static int remote_tracking(struct remote *remote, const char *refname,
2259-
struct object_id *oid)
2271+
struct object_id *oid, char **dst_refname)
22602272
{
22612273
char *dst;
22622274

@@ -2265,9 +2277,164 @@ static int remote_tracking(struct remote *remote, const char *refname,
22652277
return -1; /* no tracking ref for refname at remote */
22662278
if (read_ref(dst, oid))
22672279
return -1; /* we know what the tracking ref is but we cannot read it */
2280+
2281+
*dst_refname = dst;
22682282
return 0;
22692283
}
22702284

2285+
/*
2286+
* The struct "reflog_commit_list" and related helper functions
2287+
* for list manipulation are used for collecting commits into a
2288+
* list during reflog traversals in "check_and_collect_until()".
2289+
*/
2290+
struct reflog_commit_list {
2291+
struct commit **item;
2292+
size_t nr, alloc;
2293+
};
2294+
2295+
/* Append a commit to the list. */
2296+
static void append_commit(struct reflog_commit_list *list,
2297+
struct commit *commit)
2298+
{
2299+
ALLOC_GROW(list->item, list->nr + 1, list->alloc);
2300+
list->item[list->nr++] = commit;
2301+
}
2302+
2303+
/* Free and reset the list. */
2304+
static void free_reflog_commit_list(struct reflog_commit_list *list)
2305+
{
2306+
FREE_AND_NULL(list->item);
2307+
list->nr = list->alloc = 0;
2308+
}
2309+
2310+
struct check_and_collect_until_cb_data {
2311+
struct commit *remote_commit;
2312+
struct reflog_commit_list *local_commits;
2313+
timestamp_t remote_reflog_timestamp;
2314+
};
2315+
2316+
/* Get the timestamp of the latest entry. */
2317+
static int peek_reflog(struct object_id *o_oid, struct object_id *n_oid,
2318+
const char *ident, timestamp_t timestamp,
2319+
int tz, const char *message, void *cb_data)
2320+
{
2321+
timestamp_t *ts = cb_data;
2322+
*ts = timestamp;
2323+
return 1;
2324+
}
2325+
2326+
static int check_and_collect_until(struct object_id *o_oid,
2327+
struct object_id *n_oid,
2328+
const char *ident, timestamp_t timestamp,
2329+
int tz, const char *message, void *cb_data)
2330+
{
2331+
struct commit *commit;
2332+
struct check_and_collect_until_cb_data *cb = cb_data;
2333+
2334+
/* An entry was found. */
2335+
if (oideq(n_oid, &cb->remote_commit->object.oid))
2336+
return 1;
2337+
2338+
if ((commit = lookup_commit_reference(the_repository, n_oid)))
2339+
append_commit(cb->local_commits, commit);
2340+
2341+
/*
2342+
* If the reflog entry timestamp is older than the remote ref's
2343+
* latest reflog entry, there is no need to check or collect
2344+
* entries older than this one.
2345+
*/
2346+
if (timestamp < cb->remote_reflog_timestamp)
2347+
return -1;
2348+
2349+
return 0;
2350+
}
2351+
2352+
#define MERGE_BASES_BATCH_SIZE 8
2353+
2354+
/*
2355+
* Iterate through the reflog of the local ref to check if there is an entry
2356+
* for the given remote-tracking ref; runs until the timestamp of an entry is
2357+
* older than latest timestamp of remote-tracking ref's reflog. Any commits
2358+
* are that seen along the way are collected into a list to check if the
2359+
* remote-tracking ref is reachable from any of them.
2360+
*/
2361+
static int is_reachable_in_reflog(const char *local, const struct ref *remote)
2362+
{
2363+
timestamp_t date;
2364+
struct commit *commit;
2365+
struct commit **chunk;
2366+
struct check_and_collect_until_cb_data cb;
2367+
struct reflog_commit_list list = { NULL, 0, 0 };
2368+
size_t size = 0;
2369+
int ret = 0;
2370+
2371+
commit = lookup_commit_reference(the_repository, &remote->old_oid);
2372+
if (!commit)
2373+
goto cleanup_return;
2374+
2375+
/*
2376+
* Get the timestamp from the latest entry
2377+
* of the remote-tracking ref's reflog.
2378+
*/
2379+
for_each_reflog_ent_reverse(remote->tracking_ref, peek_reflog, &date);
2380+
2381+
cb.remote_commit = commit;
2382+
cb.local_commits = &list;
2383+
cb.remote_reflog_timestamp = date;
2384+
ret = for_each_reflog_ent_reverse(local, check_and_collect_until, &cb);
2385+
2386+
/* We found an entry in the reflog. */
2387+
if (ret > 0)
2388+
goto cleanup_return;
2389+
2390+
/*
2391+
* Check if the remote commit is reachable from any
2392+
* of the commits in the collected list, in batches.
2393+
*/
2394+
for (chunk = list.item; chunk < list.item + list.nr; chunk += size) {
2395+
size = list.item + list.nr - chunk;
2396+
if (MERGE_BASES_BATCH_SIZE < size)
2397+
size = MERGE_BASES_BATCH_SIZE;
2398+
2399+
if ((ret = in_merge_bases_many(commit, size, chunk)))
2400+
break;
2401+
}
2402+
2403+
cleanup_return:
2404+
free_reflog_commit_list(&list);
2405+
return ret;
2406+
}
2407+
2408+
/* Toggle the "commit-graph" feature; return the previously set state. */
2409+
static int toggle_commit_graph(struct repository *repo, int disable) {
2410+
int prev = repo->commit_graph_disabled;
2411+
repo->commit_graph_disabled = disable;
2412+
return prev;
2413+
}
2414+
2415+
/*
2416+
* Check for reachability of a remote-tracking
2417+
* ref in the reflog entries of its local ref.
2418+
*/
2419+
static void check_if_includes_upstream(struct ref *remote)
2420+
{
2421+
int prev;
2422+
struct ref *local = get_local_ref(remote->name);
2423+
if (!local)
2424+
return;
2425+
2426+
/*
2427+
* TODO: Remove "toggle_commit_graph()" calls around the check.
2428+
* Depending on whether "commit-graph" enabled or not,
2429+
* "in_merge_bases_many()" returns different results;
2430+
* disable it temporarily when the check runs.
2431+
*/
2432+
prev = toggle_commit_graph(the_repository, 1);
2433+
if (is_reachable_in_reflog(local->name, remote) <= 0)
2434+
remote->unreachable = 1;
2435+
toggle_commit_graph(the_repository, prev);
2436+
}
2437+
22712438
static void apply_cas(struct push_cas_option *cas,
22722439
struct remote *remote,
22732440
struct ref *ref)
@@ -2282,8 +2449,12 @@ static void apply_cas(struct push_cas_option *cas,
22822449
ref->expect_old_sha1 = 1;
22832450
if (!entry->use_tracking)
22842451
oidcpy(&ref->old_oid_expect, &entry->expect);
2285-
else if (remote_tracking(remote, ref->name, &ref->old_oid_expect))
2452+
else if (remote_tracking(remote, ref->name,
2453+
&ref->old_oid_expect,
2454+
&ref->tracking_ref))
22862455
oidclr(&ref->old_oid_expect);
2456+
else
2457+
ref->check_reachable = cas->use_force_if_includes;
22872458
return;
22882459
}
22892460

@@ -2292,15 +2463,28 @@ static void apply_cas(struct push_cas_option *cas,
22922463
return;
22932464

22942465
ref->expect_old_sha1 = 1;
2295-
if (remote_tracking(remote, ref->name, &ref->old_oid_expect))
2466+
if (remote_tracking(remote, ref->name,
2467+
&ref->old_oid_expect,
2468+
&ref->tracking_ref))
22962469
oidclr(&ref->old_oid_expect);
2470+
else
2471+
ref->check_reachable = cas->use_force_if_includes;
22972472
}
22982473

22992474
void apply_push_cas(struct push_cas_option *cas,
23002475
struct remote *remote,
23012476
struct ref *remote_refs)
23022477
{
23032478
struct ref *ref;
2304-
for (ref = remote_refs; ref; ref = ref->next)
2479+
for (ref = remote_refs; ref; ref = ref->next) {
23052480
apply_cas(cas, remote, ref);
2481+
2482+
/*
2483+
* If "compare-and-swap" is in "use_tracking[_for_rest]"
2484+
* mode, and if "--force-if-includes" was specified, run
2485+
* the check.
2486+
*/
2487+
if (ref->check_reachable)
2488+
check_if_includes_upstream(ref);
2489+
}
23062490
}

remote.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,20 @@ struct ref {
107107
struct object_id new_oid;
108108
struct object_id old_oid_expect; /* used by expect-old */
109109
char *symref;
110+
char *tracking_ref;
110111
unsigned int
111112
force:1,
112113
forced_update:1,
113114
expect_old_sha1:1,
114115
exact_oid:1,
115-
deletion:1;
116+
deletion:1,
117+
/* Need to check if local reflog reaches the remote tip. */
118+
check_reachable:1,
119+
/*
120+
* Store the result of the check enabled by "check_reachable";
121+
* implies the local reflog does not reach the remote tip.
122+
*/
123+
unreachable:1;
116124

117125
enum {
118126
REF_NOT_MATCHED = 0, /* initial value */
@@ -142,6 +150,7 @@ struct ref {
142150
REF_STATUS_REJECT_NEEDS_FORCE,
143151
REF_STATUS_REJECT_STALE,
144152
REF_STATUS_REJECT_SHALLOW,
153+
REF_STATUS_REJECT_REMOTE_UPDATED,
145154
REF_STATUS_UPTODATE,
146155
REF_STATUS_REMOTE_REJECT,
147156
REF_STATUS_EXPECTING_REPORT,
@@ -341,6 +350,7 @@ struct ref *get_stale_heads(struct refspec *rs, struct ref *fetch_map);
341350

342351
struct push_cas_option {
343352
unsigned use_tracking_for_rest:1;
353+
unsigned use_force_if_includes:1;
344354
struct push_cas {
345355
struct object_id expect;
346356
unsigned use_tracking:1;

send-pack.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ static int check_to_send_update(const struct ref *ref, const struct send_pack_ar
299299
case REF_STATUS_REJECT_FETCH_FIRST:
300300
case REF_STATUS_REJECT_NEEDS_FORCE:
301301
case REF_STATUS_REJECT_STALE:
302+
case REF_STATUS_REJECT_REMOTE_UPDATED:
302303
case REF_STATUS_REJECT_NODELETE:
303304
return CHECK_REF_STATUS_REJECTED;
304305
case REF_STATUS_UPTODATE:

transport-helper.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,10 @@ static int push_update_ref_status(struct strbuf *buf,
827827
status = REF_STATUS_REJECT_STALE;
828828
FREE_AND_NULL(msg);
829829
}
830+
else if (!strcmp(msg, "remote ref updated since checkout")) {
831+
status = REF_STATUS_REJECT_REMOTE_UPDATED;
832+
FREE_AND_NULL(msg);
833+
}
830834
else if (!strcmp(msg, "forced update")) {
831835
forced = 1;
832836
FREE_AND_NULL(msg);
@@ -967,6 +971,7 @@ static int push_refs_with_push(struct transport *transport,
967971
case REF_STATUS_REJECT_NONFASTFORWARD:
968972
case REF_STATUS_REJECT_STALE:
969973
case REF_STATUS_REJECT_ALREADY_EXISTS:
974+
case REF_STATUS_REJECT_REMOTE_UPDATED:
970975
if (atomic) {
971976
reject_atomic_push(remote_refs, mirror);
972977
string_list_clear(&cas_options, 0);

transport.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,11 @@ static int print_one_push_report(struct ref *ref, const char *dest, int count,
633633
"stale info",
634634
report, porcelain, summary_width);
635635
break;
636+
case REF_STATUS_REJECT_REMOTE_UPDATED:
637+
print_ref_status('!', "[rejected]", ref, ref->peer_ref,
638+
"remote ref updated since checkout",
639+
report, porcelain, summary_width);
640+
break;
636641
case REF_STATUS_REJECT_SHALLOW:
637642
print_ref_status('!', "[rejected]", ref, ref->peer_ref,
638643
"new shallow roots not allowed",
@@ -1185,6 +1190,7 @@ static int run_pre_push_hook(struct transport *transport,
11851190
if (!r->peer_ref) continue;
11861191
if (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;
11871192
if (r->status == REF_STATUS_REJECT_STALE) continue;
1193+
if (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;
11881194
if (r->status == REF_STATUS_UPTODATE) continue;
11891195

11901196
strbuf_reset(&buf);

0 commit comments

Comments
 (0)