Merge branch 'cr/push-force-tag-update'

Require "-f" for push to update a tag, even if it is a fast-forward.

* cr/push-force-tag-update:
  push: allow already-exists advice to be disabled
  push: rename config variable for more general use
  push: cleanup push rules comment
  push: clarify rejection of update to non-commit-ish
  push: require force for annotated tags
  push: require force for refs under refs/tags/
  push: flag updates that require force
  push: keep track of "update" state separately
  push: add advice for rejected tag reference
  push: return reject reasons as a bitset
maint
Junio C Hamano 2013-01-05 23:41:34 -08:00
commit 9a2c83d24c
13 changed files with 188 additions and 60 deletions

View File

@ -140,10 +140,11 @@ advice.*::
can tell Git that you do not need help by setting these to 'false': can tell Git that you do not need help by setting these to 'false':
+ +
-- --
pushNonFastForward:: pushUpdateRejected::
Set this variable to 'false' if you want to disable Set this variable to 'false' if you want to disable
'pushNonFFCurrent', 'pushNonFFDefault', and 'pushNonFFCurrent', 'pushNonFFDefault',
'pushNonFFMatching' simultaneously. 'pushNonFFMatching', and 'pushAlreadyExists'
simultaneously.
pushNonFFCurrent:: pushNonFFCurrent::
Advice shown when linkgit:git-push[1] fails due to a Advice shown when linkgit:git-push[1] fails due to a
non-fast-forward update to the current branch. non-fast-forward update to the current branch.
@ -158,6 +159,9 @@ advice.*::
'matching refs' explicitly (i.e. you used ':', or 'matching refs' explicitly (i.e. you used ':', or
specified a refspec that isn't your current branch) and specified a refspec that isn't your current branch) and
it resulted in a non-fast-forward error. it resulted in a non-fast-forward error.
pushAlreadyExists::
Shown when linkgit:git-push[1] rejects an update that
does not qualify for fast-forwarding (e.g., a tag.)
statusHints:: statusHints::
Show directions on how to proceed from the current Show directions on how to proceed from the current
state in the output of linkgit:git-status[1], in state in the output of linkgit:git-status[1], in

View File

@ -51,10 +51,11 @@ be named. If `:`<dst> is omitted, the same ref as <src> will be
updated. updated.
+ +
The object referenced by <src> is used to update the <dst> reference The object referenced by <src> is used to update the <dst> reference
on the remote side, but by default this is only allowed if the on the remote side. By default this is only allowed if <dst> is not
update can fast-forward <dst>. By having the optional leading `+`, a tag (annotated or lightweight), and then only if it can fast-forward
you can tell git to update the <dst> ref even when the update is not a <dst>. By having the optional leading `+`, you can tell git to update
fast-forward. This does *not* attempt to merge <src> into <dst>. See the <dst> ref even if it is not allowed by default (e.g., it is not a
fast-forward.) This does *not* attempt to merge <src> into <dst>. See
EXAMPLES below for details. EXAMPLES below for details.
+ +
`tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`. `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`.

View File

@ -1,9 +1,10 @@
#include "cache.h" #include "cache.h"


int advice_push_nonfastforward = 1; int advice_push_update_rejected = 1;
int advice_push_non_ff_current = 1; int advice_push_non_ff_current = 1;
int advice_push_non_ff_default = 1; int advice_push_non_ff_default = 1;
int advice_push_non_ff_matching = 1; int advice_push_non_ff_matching = 1;
int advice_push_already_exists = 1;
int advice_status_hints = 1; int advice_status_hints = 1;
int advice_commit_before_merge = 1; int advice_commit_before_merge = 1;
int advice_resolve_conflict = 1; int advice_resolve_conflict = 1;
@ -14,15 +15,19 @@ static struct {
const char *name; const char *name;
int *preference; int *preference;
} advice_config[] = { } advice_config[] = {
{ "pushnonfastforward", &advice_push_nonfastforward }, { "pushupdaterejected", &advice_push_update_rejected },
{ "pushnonffcurrent", &advice_push_non_ff_current }, { "pushnonffcurrent", &advice_push_non_ff_current },
{ "pushnonffdefault", &advice_push_non_ff_default }, { "pushnonffdefault", &advice_push_non_ff_default },
{ "pushnonffmatching", &advice_push_non_ff_matching }, { "pushnonffmatching", &advice_push_non_ff_matching },
{ "pushalreadyexists", &advice_push_already_exists },
{ "statushints", &advice_status_hints }, { "statushints", &advice_status_hints },
{ "commitbeforemerge", &advice_commit_before_merge }, { "commitbeforemerge", &advice_commit_before_merge },
{ "resolveconflict", &advice_resolve_conflict }, { "resolveconflict", &advice_resolve_conflict },
{ "implicitidentity", &advice_implicit_identity }, { "implicitidentity", &advice_implicit_identity },
{ "detachedhead", &advice_detached_head }, { "detachedhead", &advice_detached_head },

/* make this an alias for backward compatibility */
{ "pushnonfastforward", &advice_push_update_rejected }
}; };


void advise(const char *advice, ...) void advise(const char *advice, ...)

View File

@ -3,10 +3,11 @@


#include "git-compat-util.h" #include "git-compat-util.h"


extern int advice_push_nonfastforward; extern int advice_push_update_rejected;
extern int advice_push_non_ff_current; extern int advice_push_non_ff_current;
extern int advice_push_non_ff_default; extern int advice_push_non_ff_default;
extern int advice_push_non_ff_matching; extern int advice_push_non_ff_matching;
extern int advice_push_already_exists;
extern int advice_status_hints; extern int advice_status_hints;
extern int advice_commit_before_merge; extern int advice_commit_before_merge;
extern int advice_resolve_conflict; extern int advice_resolve_conflict;

View File

@ -220,31 +220,42 @@ static const char message_advice_checkout_pull_push[] =
"(e.g. 'git pull') before pushing again.\n" "(e.g. 'git pull') before pushing again.\n"
"See the 'Note about fast-forwards' in 'git push --help' for details."); "See the 'Note about fast-forwards' in 'git push --help' for details.");


static const char message_advice_ref_already_exists[] =
N_("Updates were rejected because the destination reference already exists\n"
"in the remote.");

static void advise_pull_before_push(void) static void advise_pull_before_push(void)
{ {
if (!advice_push_non_ff_current || !advice_push_nonfastforward) if (!advice_push_non_ff_current || !advice_push_update_rejected)
return; return;
advise(_(message_advice_pull_before_push)); advise(_(message_advice_pull_before_push));
} }


static void advise_use_upstream(void) static void advise_use_upstream(void)
{ {
if (!advice_push_non_ff_default || !advice_push_nonfastforward) if (!advice_push_non_ff_default || !advice_push_update_rejected)
return; return;
advise(_(message_advice_use_upstream)); advise(_(message_advice_use_upstream));
} }


static void advise_checkout_pull_push(void) static void advise_checkout_pull_push(void)
{ {
if (!advice_push_non_ff_matching || !advice_push_nonfastforward) if (!advice_push_non_ff_matching || !advice_push_update_rejected)
return; return;
advise(_(message_advice_checkout_pull_push)); advise(_(message_advice_checkout_pull_push));
} }


static void advise_ref_already_exists(void)
{
if (!advice_push_already_exists || !advice_push_update_rejected)
return;
advise(_(message_advice_ref_already_exists));
}

static int push_with_options(struct transport *transport, int flags) static int push_with_options(struct transport *transport, int flags)
{ {
int err; int err;
int nonfastforward; unsigned int reject_reasons;


transport_set_verbosity(transport, verbosity, progress); transport_set_verbosity(transport, verbosity, progress);


@ -257,7 +268,7 @@ static int push_with_options(struct transport *transport, int flags)
if (verbosity > 0) if (verbosity > 0)
fprintf(stderr, _("Pushing to %s\n"), transport->url); fprintf(stderr, _("Pushing to %s\n"), transport->url);
err = transport_push(transport, refspec_nr, refspec, flags, err = transport_push(transport, refspec_nr, refspec, flags,
&nonfastforward); &reject_reasons);
if (err != 0) if (err != 0)
error(_("failed to push some refs to '%s'"), transport->url); error(_("failed to push some refs to '%s'"), transport->url);


@ -265,18 +276,15 @@ static int push_with_options(struct transport *transport, int flags)
if (!err) if (!err)
return 0; return 0;


switch (nonfastforward) { if (reject_reasons & REJECT_NON_FF_HEAD) {
default:
break;
case NON_FF_HEAD:
advise_pull_before_push(); advise_pull_before_push();
break; } else if (reject_reasons & REJECT_NON_FF_OTHER) {
case NON_FF_OTHER:
if (default_matching_used) if (default_matching_used)
advise_use_upstream(); advise_use_upstream();
else else
advise_checkout_pull_push(); advise_checkout_pull_push();
break; } else if (reject_reasons & REJECT_ALREADY_EXISTS) {
advise_ref_already_exists();
} }


return 1; return 1;

View File

@ -44,6 +44,11 @@ static void print_helper_status(struct ref *ref)
msg = "non-fast forward"; msg = "non-fast forward";
break; break;


case REF_STATUS_REJECT_ALREADY_EXISTS:
res = "error";
msg = "already exists";
break;

case REF_STATUS_REJECT_NODELETE: case REF_STATUS_REJECT_NODELETE:
case REF_STATUS_REMOTE_REJECT: case REF_STATUS_REMOTE_REJECT:
res = "error"; res = "error";
@ -85,7 +90,7 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
int send_all = 0; int send_all = 0;
const char *receivepack = "git-receive-pack"; const char *receivepack = "git-receive-pack";
int flags; int flags;
int nonfastforward = 0; unsigned int reject_reasons;
int progress = -1; int progress = -1;


argv++; argv++;
@ -223,7 +228,7 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix)
ret |= finish_connect(conn); ret |= finish_connect(conn);


if (!helper_status) if (!helper_status)
transport_print_push_status(dest, remote_refs, args.verbose, 0, &nonfastforward); transport_print_push_status(dest, remote_refs, args.verbose, 0, &reject_reasons);


if (!args.dry_run && remote) { if (!args.dry_run && remote) {
struct ref *ref; struct ref *ref;

View File

@ -1003,14 +1003,19 @@ struct ref {
unsigned char old_sha1[20]; unsigned char old_sha1[20];
unsigned char new_sha1[20]; unsigned char new_sha1[20];
char *symref; char *symref;
unsigned int force:1, unsigned int
force:1,
requires_force:1,
merge:1, merge:1,
nonfastforward:1, nonfastforward:1,
not_forwardable:1,
update:1,
deletion:1; deletion:1;
enum { enum {
REF_STATUS_NONE = 0, REF_STATUS_NONE = 0,
REF_STATUS_OK, REF_STATUS_OK,
REF_STATUS_REJECT_NONFASTFORWARD, REF_STATUS_REJECT_NONFASTFORWARD,
REF_STATUS_REJECT_ALREADY_EXISTS,
REF_STATUS_REJECT_NODELETE, REF_STATUS_REJECT_NODELETE,
REF_STATUS_UPTODATE, REF_STATUS_UPTODATE,
REF_STATUS_REMOTE_REJECT, REF_STATUS_REMOTE_REJECT,

View File

@ -1279,12 +1279,34 @@ int match_push_refs(struct ref *src, struct ref **dst,
return 0; return 0;
} }


static inline int is_forwardable(struct ref* ref)
{
struct object *o;

if (!prefixcmp(ref->name, "refs/tags/"))
return 0;

/* old object must be a commit */
o = parse_object(ref->old_sha1);
if (!o || o->type != OBJ_COMMIT)
return 0;

/* new object must be commit-ish */
o = deref_tag(parse_object(ref->new_sha1), NULL, 0);
if (!o || o->type != OBJ_COMMIT)
return 0;

return 1;
}

void set_ref_status_for_push(struct ref *remote_refs, int send_mirror, void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
int force_update) int force_update)
{ {
struct ref *ref; struct ref *ref;


for (ref = remote_refs; ref; ref = ref->next) { for (ref = remote_refs; ref; ref = ref->next) {
int force_ref_update = ref->force || force_update;

if (ref->peer_ref) if (ref->peer_ref)
hashcpy(ref->new_sha1, ref->peer_ref->new_sha1); hashcpy(ref->new_sha1, ref->peer_ref->new_sha1);
else if (!send_mirror) else if (!send_mirror)
@ -1297,37 +1319,58 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
continue; continue;
} }


/* This part determines what can overwrite what. /*
* The rules are: * The below logic determines whether an individual
* refspec A:B can be pushed. The push will succeed
* if any of the following are true:
* *
* (0) you can always use --force or +A:B notation to * (1) the remote reference B does not exist
* selectively force individual ref pairs.
* *
* (1) if the old thing does not exist, it is OK. * (2) the remote reference B is being removed (i.e.,
* pushing :B where no source is specified)
* *
* (2) if you do not have the old thing, you are not allowed * (3) the update meets all fast-forwarding criteria:
* to overwrite it; you would not know what you are losing
* otherwise.
* *
* (3) if both new and old are commit-ish, and new is a * (a) the destination is not under refs/tags/
* descendant of old, it is OK. * (b) the old is a commit
* (c) the new is a descendant of the old
* *
* (4) regardless of all of the above, removing :B is * NOTE: We must actually have the old object in
* always allowed. * order to overwrite it in the remote reference,
* and the new object must be commit-ish. These are
* implied by (b) and (c) respectively.
*
* (4) it is forced using the +A:B notation, or by
* passing the --force argument
*/ */


ref->nonfastforward = ref->not_forwardable = !is_forwardable(ref);
!ref->deletion &&
!is_null_sha1(ref->old_sha1) &&
(!has_sha1_file(ref->old_sha1)
|| !ref_newer(ref->new_sha1, ref->old_sha1));


if (ref->nonfastforward && !ref->force && !force_update) { ref->update =
!ref->deletion &&
!is_null_sha1(ref->old_sha1);

if (ref->update) {
ref->nonfastforward =
!has_sha1_file(ref->old_sha1)
|| !ref_newer(ref->new_sha1, ref->old_sha1);

if (ref->not_forwardable) {
ref->requires_force = 1;
if (!force_ref_update) {
ref->status = REF_STATUS_REJECT_ALREADY_EXISTS;
continue;
}
} else if (ref->nonfastforward) {
ref->requires_force = 1;
if (!force_ref_update) {
ref->status = REF_STATUS_REJECT_NONFASTFORWARD; ref->status = REF_STATUS_REJECT_NONFASTFORWARD;
continue; continue;
} }
} }
} }
}
}


struct branch *branch_get(const char *name) struct branch *branch_get(const char *name)
{ {

View File

@ -229,6 +229,7 @@ int send_pack(struct send_pack_args *args,
/* Check for statuses set by set_ref_status_for_push() */ /* Check for statuses set by set_ref_status_for_push() */
switch (ref->status) { switch (ref->status) {
case REF_STATUS_REJECT_NONFASTFORWARD: case REF_STATUS_REJECT_NONFASTFORWARD:
case REF_STATUS_REJECT_ALREADY_EXISTS:
case REF_STATUS_UPTODATE: case REF_STATUS_UPTODATE:
continue; continue;
default: default:

View File

@ -368,7 +368,7 @@ test_expect_success 'push with colon-less refspec (2)' '
git branch -D frotz git branch -D frotz
fi && fi &&
git tag -f frotz && git tag -f frotz &&
git push testrepo frotz && git push -f testrepo frotz &&
check_push_result $the_commit tags/frotz && check_push_result $the_commit tags/frotz &&
check_push_result $the_first_commit heads/frotz check_push_result $the_first_commit heads/frotz


@ -929,6 +929,48 @@ test_expect_success 'push into aliased refs (inconsistent)' '
) )
' '


test_expect_success 'push requires --force to update lightweight tag' '
mk_test heads/master &&
mk_child child1 &&
mk_child child2 &&
(
cd child1 &&
git tag Tag &&
git push ../child2 Tag &&
git push ../child2 Tag &&
>file1 &&
git add file1 &&
git commit -m "file1" &&
git tag -f Tag &&
test_must_fail git push ../child2 Tag &&
git push --force ../child2 Tag &&
git tag -f Tag &&
test_must_fail git push ../child2 Tag HEAD~ &&
git push --force ../child2 Tag
)
'

test_expect_success 'push requires --force to update annotated tag' '
mk_test heads/master &&
mk_child child1 &&
mk_child child2 &&
(
cd child1 &&
git tag -a -m "message 1" Tag &&
git push ../child2 Tag:refs/tmp/Tag &&
git push ../child2 Tag:refs/tmp/Tag &&
>file1 &&
git add file1 &&
git commit -m "file1" &&
git tag -f -a -m "message 2" Tag &&
test_must_fail git push ../child2 Tag:refs/tmp/Tag &&
git push --force ../child2 Tag:refs/tmp/Tag &&
git tag -f -a -m "message 3" Tag HEAD~ &&
test_must_fail git push ../child2 Tag:refs/tmp/Tag &&
git push --force ../child2 Tag:refs/tmp/Tag
)
'

test_expect_success 'push --porcelain' ' test_expect_success 'push --porcelain' '
mk_empty && mk_empty &&
echo >.git/foo "To testrepo" && echo >.git/foo "To testrepo" &&

View File

@ -661,6 +661,11 @@ static void push_update_ref_status(struct strbuf *buf,
free(msg); free(msg);
msg = NULL; msg = NULL;
} }
else if (!strcmp(msg, "already exists")) {
status = REF_STATUS_REJECT_ALREADY_EXISTS;
free(msg);
msg = NULL;
}
} }


if (*ref) if (*ref)
@ -720,6 +725,7 @@ static int push_refs_with_push(struct transport *transport,
/* Check for statuses set by set_ref_status_for_push() */ /* Check for statuses set by set_ref_status_for_push() */
switch (ref->status) { switch (ref->status) {
case REF_STATUS_REJECT_NONFASTFORWARD: case REF_STATUS_REJECT_NONFASTFORWARD:
case REF_STATUS_REJECT_ALREADY_EXISTS:
case REF_STATUS_UPTODATE: case REF_STATUS_UPTODATE:
continue; continue;
default: default:

View File

@ -659,7 +659,7 @@ static void print_ok_ref_status(struct ref *ref, int porcelain)
const char *msg; const char *msg;


strcpy(quickref, status_abbrev(ref->old_sha1)); strcpy(quickref, status_abbrev(ref->old_sha1));
if (ref->nonfastforward) { if (ref->requires_force) {
strcat(quickref, "..."); strcat(quickref, "...");
type = '+'; type = '+';
msg = "forced update"; msg = "forced update";
@ -695,6 +695,10 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count, i
print_ref_status('!', "[rejected]", ref, ref->peer_ref, print_ref_status('!', "[rejected]", ref, ref->peer_ref,
"non-fast-forward", porcelain); "non-fast-forward", porcelain);
break; break;
case REF_STATUS_REJECT_ALREADY_EXISTS:
print_ref_status('!', "[rejected]", ref, ref->peer_ref,
"already exists", porcelain);
break;
case REF_STATUS_REMOTE_REJECT: case REF_STATUS_REMOTE_REJECT:
print_ref_status('!', "[remote rejected]", ref, print_ref_status('!', "[remote rejected]", ref,
ref->deletion ? NULL : ref->peer_ref, ref->deletion ? NULL : ref->peer_ref,
@ -714,7 +718,7 @@ static int print_one_push_status(struct ref *ref, const char *dest, int count, i
} }


void transport_print_push_status(const char *dest, struct ref *refs, void transport_print_push_status(const char *dest, struct ref *refs,
int verbose, int porcelain, int *nonfastforward) int verbose, int porcelain, unsigned int *reject_reasons)
{ {
struct ref *ref; struct ref *ref;
int n = 0; int n = 0;
@ -733,18 +737,19 @@ void transport_print_push_status(const char *dest, struct ref *refs,
if (ref->status == REF_STATUS_OK) if (ref->status == REF_STATUS_OK)
n += print_one_push_status(ref, dest, n, porcelain); n += print_one_push_status(ref, dest, n, porcelain);


*nonfastforward = 0; *reject_reasons = 0;
for (ref = refs; ref; ref = ref->next) { for (ref = refs; ref; ref = ref->next) {
if (ref->status != REF_STATUS_NONE && if (ref->status != REF_STATUS_NONE &&
ref->status != REF_STATUS_UPTODATE && ref->status != REF_STATUS_UPTODATE &&
ref->status != REF_STATUS_OK) ref->status != REF_STATUS_OK)
n += print_one_push_status(ref, dest, n, porcelain); n += print_one_push_status(ref, dest, n, porcelain);
if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD && if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD) {
*nonfastforward != NON_FF_HEAD) {
if (!strcmp(head, ref->name)) if (!strcmp(head, ref->name))
*nonfastforward = NON_FF_HEAD; *reject_reasons |= REJECT_NON_FF_HEAD;
else else
*nonfastforward = NON_FF_OTHER; *reject_reasons |= REJECT_NON_FF_OTHER;
} else if (ref->status == REF_STATUS_REJECT_ALREADY_EXISTS) {
*reject_reasons |= REJECT_ALREADY_EXISTS;
} }
} }
} }
@ -1031,9 +1036,9 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)


int transport_push(struct transport *transport, int transport_push(struct transport *transport,
int refspec_nr, const char **refspec, int flags, int refspec_nr, const char **refspec, int flags,
int *nonfastforward) unsigned int *reject_reasons)
{ {
*nonfastforward = 0; *reject_reasons = 0;
transport_verify_remote_names(refspec_nr, refspec); transport_verify_remote_names(refspec_nr, refspec);


if (transport->push) { if (transport->push) {
@ -1099,7 +1104,7 @@ int transport_push(struct transport *transport,
if (!quiet || err) if (!quiet || err)
transport_print_push_status(transport->url, remote_refs, transport_print_push_status(transport->url, remote_refs,
verbose | porcelain, porcelain, verbose | porcelain, porcelain,
nonfastforward); reject_reasons);


if (flags & TRANSPORT_PUSH_SET_UPSTREAM) if (flags & TRANSPORT_PUSH_SET_UPSTREAM)
set_upstreams(transport, remote_refs, pretend); set_upstreams(transport, remote_refs, pretend);

View File

@ -140,11 +140,13 @@ int transport_set_option(struct transport *transport, const char *name,
void transport_set_verbosity(struct transport *transport, int verbosity, void transport_set_verbosity(struct transport *transport, int verbosity,
int force_progress); int force_progress);


#define NON_FF_HEAD 1 #define REJECT_NON_FF_HEAD 0x01
#define NON_FF_OTHER 2 #define REJECT_NON_FF_OTHER 0x02
#define REJECT_ALREADY_EXISTS 0x04

int transport_push(struct transport *connection, int transport_push(struct transport *connection,
int refspec_nr, const char **refspec, int flags, int refspec_nr, const char **refspec, int flags,
int * nonfastforward); unsigned int * reject_reasons);


const struct ref *transport_get_remote_refs(struct transport *transport); const struct ref *transport_get_remote_refs(struct transport *transport);


@ -170,7 +172,7 @@ void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int v
int transport_refs_pushed(struct ref *ref); int transport_refs_pushed(struct ref *ref);


void transport_print_push_status(const char *dest, struct ref *refs, void transport_print_push_status(const char *dest, struct ref *refs,
int verbose, int porcelain, int *nonfastforward); int verbose, int porcelain, unsigned int *reject_reasons);


typedef void alternate_ref_fn(const struct ref *, void *); typedef void alternate_ref_fn(const struct ref *, void *);
extern void for_each_alternate_ref(alternate_ref_fn, void *); extern void for_each_alternate_ref(alternate_ref_fn, void *);