forward is an update-ref command that behaves similarly to update, but takes an additional argument, <ancestor>, and verifies that the new value is a descendent of ancestor before updating. This is useful for fast-forwarding prefetched refs. --- I've found git fetch --prefetch to be useful in my workflow for asynchronously updating git repos, but I'd really like a nice way to update my remote tracking branches using the latest prefetched refs without querying the remote in case the remote is temporarily unavailable or I'm on a plane or something. I've settled on using $ git for-each-ref refs/prefetch/ --format='update refs/%(refname:lstrip=2) %(refname)' to prepare a transaction for git update-ref, but it has a defect: if I have fetched the remote more recently than the last prefetch my remote tracking branches will be sent into the past. "forward" is a convenience feature for update-ref that will save the trouble of looping through each ref and finding the merge base so it can be fast-forwarded. Documentation/git-update-ref.txt | 5 ++++ builtin/update-ref.c | 49 ++++++++++++++++++++++++++++++++ t/t1400-update-ref.sh | 29 +++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt index 48b6683071e6..138a478b437f 100644 --- a/Documentation/git-update-ref.txt +++ b/Documentation/git-update-ref.txt @@ -66,6 +66,7 @@ performs all modifications together. Specify commands of the form: delete SP <ref> [SP <oldvalue>] LF verify SP <ref> [SP <oldvalue>] LF option SP <opt> LF + forward SP <ref> SP <newvalue> SP <ancestor> [SP <oldvalue>] LF start LF prepare LF commit LF @@ -105,6 +106,10 @@ update:: after the update and/or a zero <oldvalue> to make sure the ref does not exist before the update. +forward:: + Set <ref> to <newvalue> after verifying <oldvalue> only if + <newvalue> is a descendent of <ancestor>. + create:: Create <ref> with <newvalue> after verifying it does not exist. The given <newvalue> may not be zero. diff --git a/builtin/update-ref.c b/builtin/update-ref.c index a84e7b47a206..cdffa52b2d93 100644 --- a/builtin/update-ref.c +++ b/builtin/update-ref.c @@ -1,5 +1,6 @@ #include "cache.h" #include "config.h" +#include "commit-reach.h" #include "refs.h" #include "builtin.h" #include "parse-options.h" @@ -86,6 +87,11 @@ static char *parse_refname(const char **next) */ #define PARSE_SHA1_ALLOW_EMPTY 0x02 +/* + * The value being parsed is <ancestor>, for forward updates. + */ +#define PARSE_SHA1_ANCESTOR 0x04 + /* * Parse an argument separator followed by the next argument, if any. * If there is an argument, convert it to a SHA-1, write it to sha1, @@ -157,12 +163,16 @@ static int parse_next_oid(const char **next, const char *end, invalid: die(flags & PARSE_SHA1_OLD ? "%s %s: invalid <oldvalue>: %s" : + flags & PARSE_SHA1_ANCESTOR ? + "%s %s: invalid <ancestor>: %s" : "%s %s: invalid <newvalue>: %s", command, refname, arg.buf); eof: die(flags & PARSE_SHA1_OLD ? "%s %s: unexpected end of input when reading <oldvalue>" : + flags & PARSE_SHA1_ANCESTOR ? + "%s %s: unexpected end of input when reading <ancestor>" : "%s %s: unexpected end of input when reading <newvalue>", command, refname); } @@ -211,6 +221,44 @@ static void parse_cmd_update(struct ref_transaction *transaction, strbuf_release(&err); } +static void parse_cmd_forward(struct ref_transaction *transaction, + const char *next, const char *end) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id new_oid, anc_oid, old_oid; + int have_old; + + refname = parse_refname(&next); + if (!refname) + die("forward: missing <ref>"); + + if (parse_next_oid(&next, end, &new_oid, "forward", refname, 0)) + die("forward %s: missing <newvalue>", refname); + + if (parse_next_oid(&next, end, &anc_oid, "forward", refname, PARSE_SHA1_ANCESTOR)) + die("forward %s: missing <ancestor>", refname); + + have_old = !parse_next_oid(&next, end, &old_oid, "forward", refname, + PARSE_SHA1_OLD); + + if (*next != line_termination) + die("forward %s: extra input: %s", refname, next); + + if (!ref_newer(&new_oid, &anc_oid)) + die("forward %s: not a fast-forward", refname); + + if (ref_transaction_update(transaction, refname, + &new_oid, have_old ? &old_oid : NULL, + update_flags | create_reflog_flag, + msg, &err)) + die("%s", err.buf); + + update_flags = default_flags; + free(refname); + strbuf_release(&err); +} + static void parse_cmd_create(struct ref_transaction *transaction, const char *next, const char *end) { @@ -378,6 +426,7 @@ static const struct parse_cmd { enum update_refs_state state; } command[] = { { "update", parse_cmd_update, 3, UPDATE_REFS_OPEN }, + { "forward", parse_cmd_forward, 4, UPDATE_REFS_OPEN }, { "create", parse_cmd_create, 2, UPDATE_REFS_OPEN }, { "delete", parse_cmd_delete, 2, UPDATE_REFS_OPEN }, { "verify", parse_cmd_verify, 2, UPDATE_REFS_OPEN }, diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index cf58cf025cd2..6ac90aca1917 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -785,6 +785,35 @@ test_expect_success 'stdin update ref works with right old value' ' test_cmp expect actual ' +test_expect_success 'stdin forward ref fails with wrong ancestor value' ' + echo "forward $c $m~1 $m" >stdin && + test_must_fail git update-ref --stdin <stdin 2>err && + grep "forward $c: not a fast-forward" err && + test_must_fail git rev-parse --verify -q $c +' + +test_expect_success 'stdin forward ref fails with bad ancestor value' ' + echo "forward $c $m does-not-exist" >stdin && + test_must_fail git update-ref --stdin <stdin 2>err && + grep "fatal: forward $c: invalid <ancestor>: does-not-exist" err && + test_must_fail git rev-parse --verify -q $c +' + +test_expect_success 'stdin forward ref fails with bad old value' ' + echo "forward $c $m $m~1 does-not-exist" >stdin && + test_must_fail git update-ref --stdin <stdin 2>err && + grep "fatal: forward $c: invalid <oldvalue>: does-not-exist" err && + test_must_fail git rev-parse --verify -q $c +' + +test_expect_success 'stdin forward ref works with right ancestor value' ' + echo "forward $b $m~1 $m~2" >stdin && + git update-ref --stdin <stdin && + git rev-parse $m~1 >expect && + git rev-parse $b >actual && + test_cmp expect actual +' + test_expect_success 'stdin delete ref fails with wrong old value' ' echo "delete $a $m~1" >stdin && test_must_fail git update-ref --stdin <stdin 2>err && -- 2.39.2