Re: [PATCH] push: make `--force-with-lease[=<ref>]` safer
- From
Johannes Schindelin <johannes.schindelin@gmx.de>
- Date
- Sep 7, 2020, 19:45 UTC
- Message-ID
- <nycvar.QRO.7.76.6.2009072119350.56@tvgsbejvaqbjf.bet>
- In-Reply-To
- <20200904185147.77439-1-shrinidhi.kaushik@gmail.com>
Hi Srinidhi,
On Sat, 5 Sep 2020, Srinidhi Kaushik wrote:
Show 40 quoted lines
> The `--force-with-lease` option in `git-push`, makes sure that > refs on remote aren't clobbered by unexpected changes when the > "<expect>" ref value is explicitly specified. > > For other cases (i.e., `--force-with-lease[=<ref>]`) where the tip > of the remote tracking branch is populated as the "<expect>" value, > there is a possibility of allowing unwanted overwrites on the remote > side when some tools that implicitly fetch remote-tracking refs in > the background are used with the repository. If a remote-tracking ref > was updated when a rewrite is happening locally and if those changes > are pushed by omitting the "<expect>" value in `--force-with-lease`, > any new changes from the updated tip will be lost locally and will > be overwritten on the remote. > > This problem can be addressed by checking the `reflog` of the branch > that is being pushed and verify if there in a entry with the remote > tracking ref. By running this check, we can ensure that refs being > are fetched in the background while a "lease" is being held are not > overlooked before a push, and any new changes can be acknowledged > and (if necessary) integrated locally. > > The new check will cause `git-push` to fail if it detects the presence > of any updated refs that we do not have locally and reject the push > stating `implicit fetch` as the reason. > > An experimental configuration setting: `push.rejectImplicitFetch` > which defaults to `true` (when `features.experimental` is enabled) > has been added, to allow `git-push` to reject a push if the check > fails. > > Signed-off-by: Srinidhi Kaushik <shrinidhi.kaushik@gmail.com> > --- > > Hello, > I picked this up from #leftoverbits over at GitHub [1] from the open > issues list. This idea [2], for a safer `--force-with-lease` was > originally proposed by Johannes on the mailing list. > > [1]: https://github.com/gitgitgadget/git/issues/640 > [2]: https://lore.kernel.org/git/nycvar.QRO.7.76.6.1808272306271.73@tvgsbejvaqbjf.bet/
First of all: thank you for picking this up! The contribution is pleasantly well-written, thank you also for that.
Now, to be honest, I thought that this mode would merit a new option rather than piggy-backing on top of `--force-with-lease`. The reason is that `--force-with-lease` targets a slightly different use case than mine: it makes sure that we do not overwrite remote refs unless we already had a chance to inspect them.
In contrast, my workflow uses `git pull --rebase` in two or more separate worktrees, e.g. when developing a patch on two different Operating Systems, I frequently forget to pull (to my public repository) on one side, and I want to avoid force-pushing in that case, even if VS Code (or I, via `git remote update`) fetched the ref (but failing to rebase the local branch on top of it).
However, in other scenarios I very much do _not_ want to incorporate the remote ref. For example, I often fetch https://github.com/git-for-windows/git.wiki.git to check for the occasional bogus change. Whenever I see such a bogus change, and it is at the tip of the branch, I want to force-push _without_ incorporating the bogus change into the local branch, yet I _do_ want to use `--force-with-lease` because an independent change could have come in via the Wiki in the meantime.
So I think that the original `--force-with-lease` and the mode you implemented target subtly different use cases that are both valid, and therefore I would like to request a separate option for the latter.
However, I have to admit that I could not think of a good name for that option. "Implicit fetch" seems a bit too vague here, because the local branch was not fetched, and certainly not implicitly, yet the logic revolves around the local branch having been rebased to the remote-tracking ref at some stage.
Even if we went with the config option to modify `--force-with-lease`'s behavior, I would recommend separating out the `feature.experimental` changes into their own patch, so that they can be reverted easily in case the experimental feature is made the default.
A couple more comments:
Show 12 quoted lines
> @@ -1471,16 +1489,21 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror, > * If the remote ref has moved and is now different > * from what we expect, reject any push. > * > - * It also is an error if the user told us to check > - * with the remote-tracking branch to find the value > - * to expect, but we did not have such a tracking > - * branch. > + * It also is an error if the user told us to check with the > + * remote-tracking branch to find the value to expect, but we > + * did not have such a tracking branch, or we have one that > + * has new changes.
If I were you, I would try to keep the original formatting, so that it becomes more obvious that the part ", or we have [...]" was appended.
Show 9 quoted lines
> if (ref->expect_old_sha1) {
> if (!oideq(&ref->old_oid, &ref->old_oid_expect))
> reject_reason = REF_STATUS_REJECT_STALE;
> + else if (reject_implicit_fetch() && ref->implicit_fetch)
> + reject_reason = REF_STATUS_REJECT_IMPLICIT_FETCH;
> else
> - /* If the ref isn't stale then force the update. */
> + /*
> + * If the ref isn't stale, or there was noShould this "or" not be an "and" instead?
Show 30 quoted lines
> + * implicit fetch, force the update.
> + */
> force_ref_update = 1;
> }
> [...]
> static void apply_cas(struct push_cas_option *cas,
> struct remote *remote,
> struct ref *ref)
> {
> - int i;
> + int i, do_reflog_check = 0;
> + struct object_id oid;
> + struct ref *local_ref = get_local_ref(ref->name);
>
> /* Find an explicit --<option>=<name>[:<value>] entry */
> for (i = 0; i < cas->nr; i++) {
> struct push_cas *entry = &cas->entry[i];
> if (!refname_match(entry->refname, ref->name))
> continue;
> +
> ref->expect_old_sha1 = 1;
> if (!entry->use_tracking)
> oidcpy(&ref->old_oid_expect, &entry->expect);
> else if (remote_tracking(remote, ref->name, &ref->old_oid_expect))
> oidclr(&ref->old_oid_expect);
> - return;
> + else
> + do_reflog_check = 1;
> +
> + goto reflog_check;Hmm. I do not condemn `goto` statements in general, but this one makes the flow harder to follow. I would prefer something like this:
-- snip -- else if (remote_tracking(remote, ref->name, &ref->old_oid_expect)) oidclr(&ref->old_oid_expect); + else if (local_ref && !read_ref(local_ref->name, &oid)) + ref->implicit_fetch = + !remote_ref_in_reflog(&ref->old_oid, &oid, + local_ref->name); return; -- snap --
Again, thank you so much for working on this!
Ciao, Dscho