diff options
-rw-r--r-- | Documentation/config.txt | 10 | ||||
-rw-r--r-- | Documentation/git-push.txt | 9 | ||||
-rw-r--r-- | advice.c | 9 | ||||
-rw-r--r-- | advice.h | 3 | ||||
-rw-r--r-- | builtin/push.c | 32 | ||||
-rw-r--r-- | builtin/send-pack.c | 9 | ||||
-rw-r--r-- | cache.h | 7 | ||||
-rw-r--r-- | remote.c | 83 | ||||
-rw-r--r-- | send-pack.c | 1 | ||||
-rwxr-xr-x | t/t5516-fetch-push.sh | 44 | ||||
-rw-r--r-- | transport-helper.c | 6 | ||||
-rw-r--r-- | transport.c | 25 | ||||
-rw-r--r-- | transport.h | 10 |
13 files changed, 188 insertions, 60 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt index bf8f911e1f..50a2288999 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -140,10 +140,11 @@ advice.*:: 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 - 'pushNonFFCurrent', 'pushNonFFDefault', and - 'pushNonFFMatching' simultaneously. + 'pushNonFFCurrent', 'pushNonFFDefault', + 'pushNonFFMatching', and 'pushAlreadyExists' + simultaneously. pushNonFFCurrent:: Advice shown when linkgit:git-push[1] fails due to a non-fast-forward update to the current branch. @@ -158,6 +159,9 @@ advice.*:: 'matching refs' explicitly (i.e. you used ':', or specified a refspec that isn't your current branch) and 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:: Show directions on how to proceed from the current state in the output of linkgit:git-status[1], in diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt index 8b637d339f..c964b796be 100644 --- a/Documentation/git-push.txt +++ b/Documentation/git-push.txt @@ -51,10 +51,11 @@ be named. If `:`<dst> is omitted, the same ref as <src> will be updated. + 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 -update can fast-forward <dst>. By having the optional leading `+`, -you can tell git to update the <dst> ref even when the update is not a -fast-forward. This does *not* attempt to merge <src> into <dst>. See +on the remote side. By default this is only allowed if <dst> is not +a tag (annotated or lightweight), and then only if it can fast-forward +<dst>. By having the optional leading `+`, you can tell git to update +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. + `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`. @@ -1,9 +1,10 @@ #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_default = 1; int advice_push_non_ff_matching = 1; +int advice_push_already_exists = 1; int advice_status_hints = 1; int advice_commit_before_merge = 1; int advice_resolve_conflict = 1; @@ -14,15 +15,19 @@ static struct { const char *name; int *preference; } advice_config[] = { - { "pushnonfastforward", &advice_push_nonfastforward }, + { "pushupdaterejected", &advice_push_update_rejected }, { "pushnonffcurrent", &advice_push_non_ff_current }, { "pushnonffdefault", &advice_push_non_ff_default }, { "pushnonffmatching", &advice_push_non_ff_matching }, + { "pushalreadyexists", &advice_push_already_exists }, { "statushints", &advice_status_hints }, { "commitbeforemerge", &advice_commit_before_merge }, { "resolveconflict", &advice_resolve_conflict }, { "implicitidentity", &advice_implicit_identity }, { "detachedhead", &advice_detached_head }, + + /* make this an alias for backward compatibility */ + { "pushnonfastforward", &advice_push_update_rejected } }; void advise(const char *advice, ...) @@ -3,10 +3,11 @@ #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_default; extern int advice_push_non_ff_matching; +extern int advice_push_already_exists; extern int advice_status_hints; extern int advice_commit_before_merge; extern int advice_resolve_conflict; diff --git a/builtin/push.c b/builtin/push.c index db9ba30b08..8491e431e4 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -220,31 +220,42 @@ static const char message_advice_checkout_pull_push[] = "(e.g. 'git pull') before pushing again.\n" "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) { - if (!advice_push_non_ff_current || !advice_push_nonfastforward) + if (!advice_push_non_ff_current || !advice_push_update_rejected) return; advise(_(message_advice_pull_before_push)); } 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; advise(_(message_advice_use_upstream)); } 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; 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) { int err; - int nonfastforward; + unsigned int reject_reasons; transport_set_verbosity(transport, verbosity, progress); @@ -257,7 +268,7 @@ static int push_with_options(struct transport *transport, int flags) if (verbosity > 0) fprintf(stderr, _("Pushing to %s\n"), transport->url); err = transport_push(transport, refspec_nr, refspec, flags, - &nonfastforward); + &reject_reasons); if (err != 0) 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) return 0; - switch (nonfastforward) { - default: - break; - case NON_FF_HEAD: + if (reject_reasons & REJECT_NON_FF_HEAD) { advise_pull_before_push(); - break; - case NON_FF_OTHER: + } else if (reject_reasons & REJECT_NON_FF_OTHER) { if (default_matching_used) advise_use_upstream(); else advise_checkout_pull_push(); - break; + } else if (reject_reasons & REJECT_ALREADY_EXISTS) { + advise_ref_already_exists(); } return 1; diff --git a/builtin/send-pack.c b/builtin/send-pack.c index d34201372d..f849e0a4a0 100644 --- a/builtin/send-pack.c +++ b/builtin/send-pack.c @@ -44,6 +44,11 @@ static void print_helper_status(struct ref *ref) msg = "non-fast forward"; break; + case REF_STATUS_REJECT_ALREADY_EXISTS: + res = "error"; + msg = "already exists"; + break; + case REF_STATUS_REJECT_NODELETE: case REF_STATUS_REMOTE_REJECT: res = "error"; @@ -85,7 +90,7 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix) int send_all = 0; const char *receivepack = "git-receive-pack"; int flags; - int nonfastforward = 0; + unsigned int reject_reasons; int progress = -1; argv++; @@ -223,7 +228,7 @@ int cmd_send_pack(int argc, const char **argv, const char *prefix) ret |= finish_connect(conn); 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) { struct ref *ref; @@ -1003,14 +1003,19 @@ struct ref { unsigned char old_sha1[20]; unsigned char new_sha1[20]; char *symref; - unsigned int force:1, + unsigned int + force:1, + requires_force:1, merge:1, nonfastforward:1, + not_forwardable:1, + update:1, deletion:1; enum { REF_STATUS_NONE = 0, REF_STATUS_OK, REF_STATUS_REJECT_NONFASTFORWARD, + REF_STATUS_REJECT_ALREADY_EXISTS, REF_STATUS_REJECT_NODELETE, REF_STATUS_UPTODATE, REF_STATUS_REMOTE_REJECT, @@ -1279,12 +1279,34 @@ int match_push_refs(struct ref *src, struct ref **dst, 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, int force_update) { struct ref *ref; for (ref = remote_refs; ref; ref = ref->next) { + int force_ref_update = ref->force || force_update; + if (ref->peer_ref) hashcpy(ref->new_sha1, ref->peer_ref->new_sha1); else if (!send_mirror) @@ -1297,34 +1319,55 @@ void set_ref_status_for_push(struct ref *remote_refs, int send_mirror, 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 - * selectively force individual ref pairs. + * (1) the remote reference B does not exist * - * (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 - * to overwrite it; you would not know what you are losing - * otherwise. + * (3) the update meets all fast-forwarding criteria: * - * (3) if both new and old are commit-ish, and new is a - * descendant of old, it is OK. + * (a) the destination is not under refs/tags/ + * (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 - * always allowed. + * NOTE: We must actually have the old object in + * 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->deletion && - !is_null_sha1(ref->old_sha1) && - (!has_sha1_file(ref->old_sha1) - || !ref_newer(ref->new_sha1, ref->old_sha1)); + ref->not_forwardable = !is_forwardable(ref); - if (ref->nonfastforward && !ref->force && !force_update) { - ref->status = REF_STATUS_REJECT_NONFASTFORWARD; - continue; + 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; + continue; + } + } } } } diff --git a/send-pack.c b/send-pack.c index f50dfd9f48..1c375f0a28 100644 --- a/send-pack.c +++ b/send-pack.c @@ -229,6 +229,7 @@ int send_pack(struct send_pack_args *args, /* Check for statuses set by set_ref_status_for_push() */ switch (ref->status) { case REF_STATUS_REJECT_NONFASTFORWARD: + case REF_STATUS_REJECT_ALREADY_EXISTS: case REF_STATUS_UPTODATE: continue; default: diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh index b5417cc951..60093728fe 100755 --- a/t/t5516-fetch-push.sh +++ b/t/t5516-fetch-push.sh @@ -368,7 +368,7 @@ test_expect_success 'push with colon-less refspec (2)' ' git branch -D frotz fi && git tag -f frotz && - git push testrepo frotz && + git push -f testrepo frotz && check_push_result $the_commit tags/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' ' mk_empty && echo >.git/foo "To testrepo" && diff --git a/transport-helper.c b/transport-helper.c index 4713b69302..965b778cb3 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -661,6 +661,11 @@ static void push_update_ref_status(struct strbuf *buf, free(msg); msg = NULL; } + else if (!strcmp(msg, "already exists")) { + status = REF_STATUS_REJECT_ALREADY_EXISTS; + free(msg); + msg = NULL; + } } 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() */ switch (ref->status) { case REF_STATUS_REJECT_NONFASTFORWARD: + case REF_STATUS_REJECT_ALREADY_EXISTS: case REF_STATUS_UPTODATE: continue; default: diff --git a/transport.c b/transport.c index 9932f402df..2673d273ff 100644 --- a/transport.c +++ b/transport.c @@ -659,7 +659,7 @@ static void print_ok_ref_status(struct ref *ref, int porcelain) const char *msg; strcpy(quickref, status_abbrev(ref->old_sha1)); - if (ref->nonfastforward) { + if (ref->requires_force) { strcat(quickref, "..."); type = '+'; 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, "non-fast-forward", porcelain); break; + case REF_STATUS_REJECT_ALREADY_EXISTS: + print_ref_status('!', "[rejected]", ref, ref->peer_ref, + "already exists", porcelain); + break; case REF_STATUS_REMOTE_REJECT: print_ref_status('!', "[remote rejected]", 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, - int verbose, int porcelain, int *nonfastforward) + int verbose, int porcelain, unsigned int *reject_reasons) { struct ref *ref; int n = 0; @@ -733,18 +737,19 @@ void transport_print_push_status(const char *dest, struct ref *refs, if (ref->status == REF_STATUS_OK) n += print_one_push_status(ref, dest, n, porcelain); - *nonfastforward = 0; + *reject_reasons = 0; for (ref = refs; ref; ref = ref->next) { if (ref->status != REF_STATUS_NONE && ref->status != REF_STATUS_UPTODATE && ref->status != REF_STATUS_OK) n += print_one_push_status(ref, dest, n, porcelain); - if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD && - *nonfastforward != NON_FF_HEAD) { + if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD) { if (!strcmp(head, ref->name)) - *nonfastforward = NON_FF_HEAD; + *reject_reasons |= REJECT_NON_FF_HEAD; 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 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); if (transport->push) { @@ -1099,7 +1104,7 @@ int transport_push(struct transport *transport, if (!quiet || err) transport_print_push_status(transport->url, remote_refs, verbose | porcelain, porcelain, - nonfastforward); + reject_reasons); if (flags & TRANSPORT_PUSH_SET_UPSTREAM) set_upstreams(transport, remote_refs, pretend); diff --git a/transport.h b/transport.h index 4a61c0c3f2..bfd2df5823 100644 --- a/transport.h +++ b/transport.h @@ -140,11 +140,13 @@ int transport_set_option(struct transport *transport, const char *name, void transport_set_verbosity(struct transport *transport, int verbosity, int force_progress); -#define NON_FF_HEAD 1 -#define NON_FF_OTHER 2 +#define REJECT_NON_FF_HEAD 0x01 +#define REJECT_NON_FF_OTHER 0x02 +#define REJECT_ALREADY_EXISTS 0x04 + int transport_push(struct transport *connection, int refspec_nr, const char **refspec, int flags, - int * nonfastforward); + unsigned int * reject_reasons); 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); 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 *); extern void for_each_alternate_ref(alternate_ref_fn, void *); |