Skip to content

Commit 28f5d17

Browse files
committed
remote.c: add command line option parser for "--force-with-lease"
Update "git push" and "git send-pack" to parse this commnd line option. The intended sematics is: * "--force-with-lease" alone, without specifying the details, will protect _all_ remote refs that are going to be updated by requiring their current value to be the same as some reasonable default, unless otherwise specified; * "--force-with-lease=refname", without specifying the expected value, will protect that refname, if it is going to be updated, by requiring its current value to be the same as some reasonable default. * "--force-with-lease=refname:value" will protect that refname, if it is going to be updated, by requiring its current value to be the same as the specified value; and * "--no-force-with-lease" will cancel all the previous --force-with-lease on the command line. For now, "some reasonable default" is tentatively defined as "the value of the remote-tracking branch we have for the ref of the remote being updated", and it is an error if we do not have such a remote-tracking branch. But this is known to be fragile, its use is not yet recommended, and hopefully we will find more reasonable default as we gain experience with this feature. The manual marks the feature as experimental unless the expected value is specified explicitly for this reason. Because the command line options are parsed _before_ we know which remote we are pushing to, there needs further processing to the parsed data after we instantiate the transport object to: * expand "refname" given by the user to a full refname to be matched with the list of "struct ref" used in match_push_refs() and set_ref_status_for_push(); and * learning the actual local ref that is the remote-tracking branch for the specified remote ref. Further, some processing need to be deferred until we find the set of remote refs and match_push_refs() returns in order to find the ones that need to be checked after explicit ones have been processed for "--force-with-lease" (no specific details). These post-processing will be the topic of the next patch. This option was originally called "cas" (for "compare and swap"), the name which nobody liked because it was too technical. The second attempt called it "lockref" (because it is conceptually like pushing after taking a lock) but the word "lock" was hated because it implied that it may reject push by others, which is not the way this option works. This round calls it "force-with-lease". You assume you took the lease on the ref when you fetched to decide what the rebased history should be, and you can push back only if the lease has not been broken. Signed-off-by: Junio C Hamano <[email protected]>
1 parent ab22d2e commit 28f5d17

File tree

5 files changed

+168
-11
lines changed

5 files changed

+168
-11
lines changed

Diff for: Documentation/git-push.txt

+66-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ SYNOPSIS
1111
[verse]
1212
'git push' [--all | --mirror | --tags] [--follow-tags] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
1313
[--repo=<repository>] [-f | --force] [--prune] [-v | --verbose] [-u | --set-upstream]
14+
[--force-with-lease[=<refname>[:<expect>]]]
1415
[--no-verify] [<repository> [<refspec>...]]
1516

1617
DESCRIPTION
@@ -130,21 +131,75 @@ already exists on the remote side.
130131
repository over ssh, and you do not have the program in
131132
a directory on the default $PATH.
132133

134+
--[no-]force-with-lease::
135+
--force-with-lease=<refname>::
136+
--force-with-lease=<refname>:<expect>::
137+
Usually, "git push" refuses to update a remote ref that is
138+
not an ancestor of the local ref used to overwrite it.
139+
+
140+
This option bypasses the check, but instead requires that the
141+
current value of the ref to be the expected value. "git push"
142+
fails otherwise.
143+
+
144+
Imagine that you have to rebase what you have already published.
145+
You will have to bypass the "must fast-forward" rule in order to
146+
replace the history you originally published with the rebased history.
147+
If somebody else built on top of your original history while you are
148+
rebasing, the tip of the branch at the remote may advance with her
149+
commit, and blindly pushing with `--force` will lose her work.
150+
+
151+
This option allows you to say that you expect the history you are
152+
updating is what you rebased and want to replace. If the remote ref
153+
still points at the commit you specified, you can be sure that no
154+
other people did anything to the ref (it is like taking a "lease" on
155+
the ref without explicitly locking it, and you update the ref while
156+
making sure that your earlier "lease" is still valid).
157+
+
158+
`--force-with-lease` alone, without specifying the details, will protect
159+
all remote refs that are going to be updated by requiring their
160+
current value to be the same as the remote-tracking branch we have
161+
for them, unless specified with a `--force-with-lease=<refname>:<expect>`
162+
option that explicitly states what the expected value is.
163+
+
164+
`--force-with-lease=<refname>`, without specifying the expected value, will
165+
protect the named ref (alone), if it is going to be updated, by
166+
requiring its current value to be the same as the remote-tracking
167+
branch we have for it.
168+
+
169+
`--force-with-lease=<refname>:<expect>` will protect the named ref (alone),
170+
if it is going to be updated, by requiring its current value to be
171+
the same as the specified value <expect> (which is allowed to be
172+
different from the remote-tracking branch we have for the refname,
173+
or we do not even have to have such a remote-tracking branch when
174+
this form is used).
175+
+
176+
Note that all forms other than `--force-with-lease=<refname>:<expect>`
177+
that specifies the expected current value of the ref explicitly are
178+
still experimental and their semantics may change as we gain experience
179+
with this feature.
180+
+
181+
"--no-force-with-lease" will cancel all the previous --force-with-lease on the
182+
command line.
183+
133184
-f::
134185
--force::
135186
Usually, the command refuses to update a remote ref that is
136187
not an ancestor of the local ref used to overwrite it.
137-
This flag disables the check. This can cause the
138-
remote repository to lose commits; use it with care.
139-
Note that `--force` applies to all the refs that are pushed,
140-
hence using it with `push.default` set to `matching` or with
141-
multiple push destinations configured with `remote.*.push`
142-
may overwrite refs other than the current branch (including
143-
local refs that are strictly behind their remote counterpart).
144-
To force a push to only one branch, use a `+` in front of the
145-
refspec to push (e.g `git push origin +master` to force a push
146-
to the `master` branch). See the `<refspec>...` section above
147-
for details.
188+
Also, when `--force-with-lease` option is used, the command refuses
189+
to update a remote ref whose current value does not match
190+
what is expected.
191+
+
192+
This flag disables these checks, and can cause the remote repository
193+
to lose commits; use it with care.
194+
+
195+
Note that `--force` applies to all the refs that are pushed, hence
196+
using it with `push.default` set to `matching` or with multiple push
197+
destinations configured with `remote.*.push` may overwrite refs
198+
other than the current branch (including local refs that are
199+
strictly behind their remote counterpart). To force a push to only
200+
one branch, use a `+` in front of the refspec to push (e.g `git push
201+
origin +master` to force a push to the `master` branch). See the
202+
`<refspec>...` section above for details.
148203

149204
--repo=<repository>::
150205
This option is only relevant if no <repository> argument is

Diff for: builtin/push.c

+6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ static const char *receivepack;
2121
static int verbosity;
2222
static int progress = -1;
2323

24+
static struct push_cas_option cas;
25+
2426
static const char **refspec;
2527
static int refspec_nr;
2628
static int refspec_alloc;
@@ -432,6 +434,10 @@ int cmd_push(int argc, const char **argv, const char *prefix)
432434
OPT_BIT('n' , "dry-run", &flags, N_("dry run"), TRANSPORT_PUSH_DRY_RUN),
433435
OPT_BIT( 0, "porcelain", &flags, N_("machine-readable output"), TRANSPORT_PUSH_PORCELAIN),
434436
OPT_BIT('f', "force", &flags, N_("force updates"), TRANSPORT_PUSH_FORCE),
437+
{ OPTION_CALLBACK,
438+
0, CAS_OPT_NAME, &cas, N_("refname>:<expect"),
439+
N_("require old value of ref to be at this value"),
440+
PARSE_OPT_OPTARG, parseopt_push_cas_option },
435441
{ OPTION_CALLBACK, 0, "recurse-submodules", &flags, N_("check"),
436442
N_("control recursive pushing of submodules"),
437443
PARSE_OPT_OPTARG, option_parse_recurse_submodules },

Diff for: builtin/send-pack.c

+17
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
103103
int flags;
104104
unsigned int reject_reasons;
105105
int progress = -1;
106+
struct push_cas_option cas = {0};
106107

107108
argv++;
108109
for (i = 1; i < argc; i++, argv++) {
@@ -165,6 +166,22 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
165166
helper_status = 1;
166167
continue;
167168
}
169+
if (!strcmp(arg, "--" CAS_OPT_NAME)) {
170+
if (parse_push_cas_option(&cas, NULL, 0) < 0)
171+
exit(1);
172+
continue;
173+
}
174+
if (!strcmp(arg, "--no-" CAS_OPT_NAME)) {
175+
if (parse_push_cas_option(&cas, NULL, 1) < 0)
176+
exit(1);
177+
continue;
178+
}
179+
if (!prefixcmp(arg, "--" CAS_OPT_NAME "=")) {
180+
if (parse_push_cas_option(&cas,
181+
strchr(arg, '=') + 1, 1) < 0)
182+
exit(1);
183+
continue;
184+
}
168185
usage(send_pack_usage);
169186
}
170187
if (!dest) {

Diff for: remote.c

+57
Original file line numberDiff line numberDiff line change
@@ -1921,3 +1921,60 @@ struct ref *get_stale_heads(struct refspec *refs, int ref_count, struct ref *fet
19211921
string_list_clear(&ref_names, 0);
19221922
return stale_refs;
19231923
}
1924+
1925+
/*
1926+
* Compare-and-swap
1927+
*/
1928+
void clear_cas_option(struct push_cas_option *cas)
1929+
{
1930+
int i;
1931+
1932+
for (i = 0; i < cas->nr; i++)
1933+
free(cas->entry[i].refname);
1934+
free(cas->entry);
1935+
memset(cas, 0, sizeof(*cas));
1936+
}
1937+
1938+
static struct push_cas *add_cas_entry(struct push_cas_option *cas,
1939+
const char *refname,
1940+
size_t refnamelen)
1941+
{
1942+
struct push_cas *entry;
1943+
ALLOC_GROW(cas->entry, cas->nr + 1, cas->alloc);
1944+
entry = &cas->entry[cas->nr++];
1945+
memset(entry, 0, sizeof(*entry));
1946+
entry->refname = xmemdupz(refname, refnamelen);
1947+
return entry;
1948+
}
1949+
1950+
int parse_push_cas_option(struct push_cas_option *cas, const char *arg, int unset)
1951+
{
1952+
const char *colon;
1953+
struct push_cas *entry;
1954+
1955+
if (unset) {
1956+
/* "--no-<option>" */
1957+
clear_cas_option(cas);
1958+
return 0;
1959+
}
1960+
1961+
if (!arg) {
1962+
/* just "--<option>" */
1963+
cas->use_tracking_for_rest = 1;
1964+
return 0;
1965+
}
1966+
1967+
/* "--<option>=refname" or "--<option>=refname:value" */
1968+
colon = strchrnul(arg, ':');
1969+
entry = add_cas_entry(cas, arg, colon - arg);
1970+
if (!*colon)
1971+
entry->use_tracking = 1;
1972+
else if (get_sha1(colon + 1, entry->expect))
1973+
return error("cannot parse expected object name '%s'", colon + 1);
1974+
return 0;
1975+
}
1976+
1977+
int parseopt_push_cas_option(const struct option *opt, const char *arg, int unset)
1978+
{
1979+
return parse_push_cas_option(opt->value, arg, unset);
1980+
}

Diff for: remote.h

+22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#ifndef REMOTE_H
22
#define REMOTE_H
33

4+
#include "parse-options.h"
5+
46
enum {
57
REMOTE_CONFIG,
68
REMOTE_REMOTES,
@@ -226,4 +228,24 @@ struct ref *guess_remote_head(const struct ref *head,
226228
/* Return refs which no longer exist on remote */
227229
struct ref *get_stale_heads(struct refspec *refs, int ref_count, struct ref *fetch_map);
228230

231+
/*
232+
* Compare-and-swap
233+
*/
234+
#define CAS_OPT_NAME "force-with-lease"
235+
236+
struct push_cas_option {
237+
unsigned use_tracking_for_rest:1;
238+
struct push_cas {
239+
unsigned char expect[20];
240+
unsigned use_tracking:1;
241+
char *refname;
242+
} *entry;
243+
int nr;
244+
int alloc;
245+
};
246+
247+
extern int parseopt_push_cas_option(const struct option *, const char *arg, int unset);
248+
extern int parse_push_cas_option(struct push_cas_option *, const char *arg, int unset);
249+
extern void clear_cas_option(struct push_cas_option *);
250+
229251
#endif

0 commit comments

Comments
 (0)