From: Brad King <hidden> Date: 2016-06-15 22:58:33
Hi Folks,
While thinking about some how some server-side branch management
services might work, I came across a need to update multiple refs
locked with verified old values simultaneously. For example, to
transfer ownership of some commits by rewinding a branch and creating
a new branch at the original head, one must lock both refs.
Otherwise, depending on the order of updates another process could
create the new branch after we've rewound the original, or add commits
to the original after we've created the new branch.
This series teaches update-ref a new --stdin option to read update and
delete instructions from lines of standard input, lock all refs up
front with verified old values, and then perform the modifications.
This is still work in progress, but it is ready for comments and
feedback. The series is based on master as of v1.8.4.
Notable unfinished work:
* I propose a format for stdin lines in the last commit of the series
as a proof-of-concept but I invite suggestions of better formats.
The format must be able to specify updates and deletes with optional
old values and optional no-deref.
* No tests for new features, though existing tests pass for me.
* No check for duplicate refs in input. Currently a duplicate ref
will result in a failure message like:
fatal: Unable to create '....lock': File exists.
If no other git process is currently running, this probably means a
git process crashed in this repository earlier. Make sure no other git
process is running and remove the file manually to continue.
Instead we should reject duplicate ref names up front. I would
appreciate suggestions about an efficient data structure already
available in Git to perform this lookup.
I welcome feedback on the approach, interface, and implementation.
Thanks,
-Brad
Brad King (7):
reset: rename update_refs to reset_refs
refs: report ref type from lock_any_ref_for_update
refs: factor update_ref steps into helpers
refs: factor delete_ref loose ref step into a helper
refs: add function to repack without multiple refs
refs: add update_refs for multiple simultaneous updates
update-ref: support multiple simultaneous updates
Documentation/git-update-ref.txt | 19 ++++-
branch.c | 2 +-
builtin/commit.c | 2 +-
builtin/fetch.c | 2 +-
builtin/receive-pack.c | 2 +-
builtin/reflog.c | 2 +-
builtin/replace.c | 2 +-
builtin/reset.c | 4 +-
builtin/tag.c | 2 +-
builtin/update-ref.c | 93 ++++++++++++++++++++++-
fast-import.c | 2 +-
refs.c | 150 ++++++++++++++++++++++++++++++++------
refs.h | 13 +++-
sequencer.c | 2 +-
14 files changed, 262 insertions(+), 35 deletions(-)
--
1.7.10.4
From: Brad King <hidden> Date: 2016-06-15 22:58:33
Factor the lock and write steps and error handling into helper functions
update_ref_lock and update_ref_write to allow later use elsewhere.
Expose lock_any_ref_for_update's type_p to update_ref_lock callers.
Signed-off-by: Brad King <redacted>
---
refs.c | 28 +++++++++++++++++++++++-----
1 file changed, 23 insertions(+), 5 deletions(-)
@@ -137,7 +137,7 @@ extern struct ref_lock *lock_ref_sha1(const char *refname, const unsigned char *#define REF_NODEREF 0x01externstructref_lock*lock_any_ref_for_update(constchar*refname,constunsignedchar*old_sha1,-intflags);+intflags,int*type_p);/** Close the file descriptor owned by a lock and return the status */externintclose_ref(structref_lock*lock);
From: Brad King <hidden> Date: 2016-06-15 22:58:33
Generalize repack_without_ref as repack_without_refs to support a list
of refs and implement the former in terms of the latter.
Signed-off-by: Brad King <redacted>
---
refs.c | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
@@ -2414,25 +2414,35 @@ static int curate_packed_ref_fn(struct ref_entry *entry, void *cb_data)return0;}-staticintrepack_without_ref(constchar*refname)+staticintrepack_without_refs(constchar**refnames,intn){structref_dir*packed;structstring_listrefs_to_delete=STRING_LIST_INIT_DUP;structstring_list_item*ref_to_delete;+inti,removed=0;++/* Look for a packed ref: */+for(i=0;i<n;++i)+if(get_packed_ref(refnames[i]))+break;-if(!get_packed_ref(refname))-return0;/* refname does not exist in packed refs */+/* Avoid locking if we have nothing to do: */+if(i==n)+return0;/* no refname exists in packed refs */if(lock_packed_refs(0)){unable_to_lock_error(git_path("packed-refs"),errno);-returnerror("cannot delete '%s' from packed refs",refname);+returnerror("cannot delete '%s' from packed refs",refnames[i]);}packed=get_packed_refs(&ref_cache);-/* Remove refname from the cache: */-if(remove_entry(packed,refname)==-1){+/* Remove refnames from the cache: */+for(i=0;i<n;++i)+if(remove_entry(packed,refnames[i])!=-1)+removed=1;+if(!removed){/*-*Thepackedentrydisappearedwhilewewere+*Allpackedentriesdisappearedwhilewewere*acquiringthelock.*/rollback_packed_refs();
@@ -2450,6 +2460,11 @@ static int repack_without_ref(const char *refname)returncommit_packed_refs();}+staticintrepack_without_ref(constchar*refname)+{+returnrepack_without_refs(&refname,1);+}+staticintdelete_ref_loose(structref_lock*lock,inttype){interr,i,ret=0;
From: Brad King <hidden> Date: 2016-06-15 22:58:33
Get it out of the way for a future refs.h function.
Signed-off-by: Brad King <redacted>
---
builtin/reset.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -350,7 +350,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)if(!pathspec&&!unborn){/* Any resets without paths update HEAD to the head being*switchedto,savingthepreviousheadinORIG_HEADbefore.*/-update_ref_status=update_refs(rev,sha1);+update_ref_status=reset_refs(rev,sha1);if(reset_type==HARD&&!update_ref_status&&!quiet)print_new_head_line(lookup_commit_reference(sha1));
From: Brad King <hidden> Date: 2016-06-15 22:58:33
Factor loose ref deletion into helper function delete_ref_loose to allow
later use elsewhere. While at it, rename local names 'flag => type' and
'delopt => flags' for consistency with callers and called functions.
Signed-off-by: Brad King <redacted>
---
refs.c | 24 ++++++++++++++++--------
1 file changed, 16 insertions(+), 8 deletions(-)
@@ -2468,6 +2463,19 @@ int delete_ref(const char *refname, const unsigned char *sha1, int delopt)lock->lk->filename[i]='.';}+returnret;+}++intdelete_ref(constchar*refname,constunsignedchar*sha1,intflags)+{+structref_lock*lock;+intret=0,type=0;++lock=lock_ref_sha1_basic(refname,sha1,flags,&type);+if(!lock)+return1;+ret|=delete_ref_loose(lock,type);+/* removing the loose one could have resurrected an earlier*packedone.Also,ifitwasnotlooseweneedtorepack*withoutit.
From: Brad King <hidden> Date: 2016-06-15 22:58:33
Add 'struct ref_update' to encode the information needed to update or
delete a ref (name, new sha1, optional old sha1, no-deref flag). Add
function 'update_refs' accepting an array of updates to perform. First
acquire locks on all refs with verified old values. Then update or
delete all refs accordingly. Fail if any one lock cannot be obtained or
any one old value does not match.
Though the refs themeselves cannot be modified together in a single
atomic transaction, this function does enable some useful semantics.
For example, a caller may create a new branch starting from the head of
another branch and rewind the original branch at the same time. This
transfers ownership of commits between branches without risk of losing
commits added to the original branch by a concurrent process, or risk of
a concurrent process creating the new branch first.
Signed-off-by: Brad King <redacted>
---
refs.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
refs.h | 11 +++++++++++
2 files changed, 77 insertions(+)
@@ -3238,6 +3238,72 @@ int update_ref(const char *action, const char *refname,returnupdate_ref_write(action,refname,sha1,lock,onerr);}+intupdate_refs(constchar*action,structref_update*updates,+intn,enumaction_on_erronerr)+{+intret=0,delnum=0,i;+int*types;+structref_lock**locks;+constchar**delnames;++if(!updates||!n)+return0;++/* Allocate work space: */+types=xmalloc(sizeof(int)*n);+locks=xmalloc(sizeof(structref_lock*)*n);+delnames=xmalloc(sizeof(constchar*)*n);++/* Acquire all locks while verifying old values: */+for(i=0;i<n;++i){+locks[i]=update_ref_lock(updates[i].ref_name,+updates[i].old_sha1,+updates[i].flags,+&types[i],onerr);+if(!locks[i])+break;+}++/* Abort if we did not get all locks: */+if(i<n){+while(--i>=0)+unlock_ref(locks[i]);+free(types);+free(locks);+free(delnames);+return1;+}++/* Perform updates first so live commits remain referenced: */+for(i=0;i<n;++i)+if(!is_null_sha1(updates[i].new_sha1)){+ret|=update_ref_write(action,+updates[i].ref_name,+updates[i].new_sha1,+locks[i],onerr);+locks[i]=0;/* freed by update_ref_write */+}++/* Perform deletes now that updates are safely completed: */+for(i=0;i<n;++i)+if(locks[i]){+delnames[delnum++]=locks[i]->ref_name;+ret|=delete_ref_loose(locks[i],types[i]);+}+ret|=repack_without_refs(delnames,delnum);+for(i=0;i<delnum;++i)+unlink_or_warn(git_path("logs/%s",delnames[i]));+clear_loose_ref_cache(&ref_cache);+for(i=0;i<n;++i)+if(locks[i])+unlock_ref(locks[i]);++free(types);+free(locks);+free(delnames);+returnret;+}+structref*find_ref_by_name(conststructref*list,constchar*name){for(;list;list=list->next)
@@ -214,6 +214,17 @@ int update_ref(const char *action, const char *refname,constunsignedchar*sha1,constunsignedchar*oldval,intflags,enumaction_on_erronerr);+structref_update{+constchar*ref_name;+unsignedcharnew_sha1[20];+unsignedchar*old_sha1;+intflags;+};++/** lock all refs and then write all of them */+intupdate_refs(constchar*action,structref_update*updates,+intn,enumaction_on_erronerr);+externintparse_hide_refs_config(constchar*var,constchar*value,constchar*);externintref_is_hidden(constchar*);
@@ -58,6 +58,23 @@ archive by creating a symlink tree). With `-d` flag, it deletes the named <ref> after verifying it still contains <oldvalue>.+With `--stdin`, update-ref reads instructions from standard input+and performs all modifications together. Specify updates with+lines of the form:++ [ --no-deref SP ] <ref> SP <newvalue> [ SP <oldvalue> ] LF++and deletes with lines of the form:++ [ --no-deref SP ] -d SP <ref> [ SP <oldvalue> ] LF++or as updates with 40 "0" as <newvalue>. Blank lines are ignored.+Lines of any other format or a repeated <ref> produce an error.+If all <ref>s can be locked with matching <oldvalue>s+simultaneously all modifications are performed. Otherwise, no+modifications are performed. Note that while each individual+<ref> is updated or deleted atomically, a concurrent reader may+still see a subset of the modifications. Logging Updates ---------------
From: Martin Fick <hidden> Date: 2016-06-15 22:58:33
On Thursday, August 29, 2013 08:11:48 am Brad King wrote:
fatal: Unable to create '....lock': File exists.
If no other git process is currently running, this
probably means a git process crashed in this repository
earlier. Make sure no other git process is running and
remove the file manually to continue.
I don't believe git currently tries to do any form of stale
lock recovery since it is racy and unreliable (both single
server or on a multi-server shared repo),
-Martin
From: Brad King <hidden> Date: 2016-06-15 22:58:33
On 08/29/2013 11:32 AM, Martin Fick wrote:
On Thursday, August 29, 2013 08:11:48 am Brad King wrote:
quoted
fatal: Unable to create '....lock': File exists.
If no other git process is currently running, this
probably means a git process crashed in this repository
earlier. Make sure no other git process is running and
remove the file manually to continue.
I don't believe git currently tries to do any form of stale
lock recovery since it is racy and unreliable (both single
server or on a multi-server shared repo),
Nor should it in this case. I was saying that the front-end
needs to reject duplicate ref names from the stdin lines before
trying to lock the ref twice to avoid this message. I'm asking
for a suggestion for existing data structure capabilities in
Git's source to efficiently detect the duplicate name.
-Brad
From: Brad King <hidden> Date: 2016-06-15 22:58:34
Factor loose ref deletion into helper function delete_ref_loose to allow
later use elsewhere.
Signed-off-by: Brad King <redacted>
---
refs.c | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
@@ -2468,6 +2463,19 @@ int delete_ref(const char *refname, const unsigned char *sha1, int delopt)lock->lk->filename[i]='.';}+returnret;+}++intdelete_ref(constchar*refname,constunsignedchar*sha1,intdelopt)+{+structref_lock*lock;+intret=0,flag=0;++lock=lock_ref_sha1_basic(refname,sha1,delopt,&flag);+if(!lock)+return1;+ret|=delete_ref_loose(lock,flag);+/* removing the loose one could have resurrected an earlier*packedone.Also,ifitwasnotlooseweneedtorepack*withoutit.
From: Brad King <hidden> Date: 2016-06-15 22:58:34
The function resets refs rather than doing arbitrary updates.
Rename it to allow a future general-purpose update_refs function
to be added.
Signed-off-by: Brad King <redacted>
---
builtin/reset.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -350,7 +350,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)if(!pathspec&&!unborn){/* Any resets without paths update HEAD to the head being*switchedto,savingthepreviousheadinORIG_HEADbefore.*/-update_ref_status=update_refs(rev,sha1);+update_ref_status=reset_refs(rev,sha1);if(reset_type==HARD&&!update_ref_status&&!quiet)print_new_head_line(lookup_commit_reference(sha1));
From: Brad King <hidden> Date: 2016-06-15 22:58:34
Add 'struct ref_update' to encode the information needed to update or
delete a ref (name, new sha1, optional old sha1, no-deref flag). Add
function 'update_refs' accepting an array of updates to perform. First
sort the input array to order locks consistently everywhere and reject
multiple updates to the same ref. Then acquire locks on all refs with
verified old values. Then update or delete all refs accordingly. Fail
if any one lock cannot be obtained or any one old value does not match.
Though the refs themeselves cannot be modified together in a single
atomic transaction, this function does enable some useful semantics.
For example, a caller may create a new branch starting from the head of
another branch and rewind the original branch at the same time. This
transfers ownership of commits between branches without risk of losing
commits added to the original branch by a concurrent process, or risk of
a concurrent process creating the new branch first.
Signed-off-by: Brad King <redacted>
---
refs.c | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
refs.h | 14 ++++++++
2 files changed, 135 insertions(+)
@@ -3238,6 +3238,127 @@ int update_ref(const char *action, const char *refname,returnupdate_ref_write(action,refname,sha1,lock,onerr);}+staticintref_update_compare(constvoid*r1,constvoid*r2)+{+structref_update*u1=(structref_update*)(r1);+structref_update*u2=(structref_update*)(r2);+intret;+ret=strcmp(u1->ref_name,u2->ref_name);+if(ret)+returnret;+ret=hashcmp(u1->new_sha1,u2->new_sha1);+if(ret)+returnret;+ret=hashcmp(u1->old_sha1,u2->old_sha1);+if(ret)+returnret;+ret=u1->flags-u2->flags;+if(ret)+returnret;+returnu1->have_old-u2->have_old;+}++staticintref_update_reject_duplicates(structref_update*updates,intn,+enumaction_on_erronerr)+{+inti;+for(i=1;i<n;++i)+if(!strcmp(updates[i-1].ref_name,updates[i].ref_name))+break;+if(i<n){+constchar*str="Multiple updates for ref '%s' not allowed.";+switch(onerr){+caseMSG_ON_ERR:error(str,updates[i].ref_name);break;+caseDIE_ON_ERR:die(str,updates[i].ref_name);break;+caseQUIET_ON_ERR:break;+}+return1;+}+return0;+}++intupdate_refs(constchar*action,conststructref_update*updates_orig,+intn,enumaction_on_erronerr)+{+intret=0,delnum=0,i;+structref_update*updates;+int*types;+structref_lock**locks;+constchar**delnames;++if(!updates_orig||!n)+return0;++/* Allocate work space: */+updates=xmalloc(sizeof(structref_update)*n);+types=xmalloc(sizeof(int)*n);+locks=xmalloc(sizeof(structref_lock*)*n);+delnames=xmalloc(sizeof(constchar*)*n);++/* Copy, sort, and reject duplicate refs: */+memcpy(updates,updates_orig,sizeof(structref_update)*n);+qsort(updates,n,sizeof(structref_update),ref_update_compare);+if(ref_update_reject_duplicates(updates,n,onerr)){+free(updates);+free(types);+free(locks);+free(delnames);+return1;+}++/* Acquire all locks while verifying old values: */+for(i=0;i<n;++i){+locks[i]=update_ref_lock(updates[i].ref_name,+(updates[i].have_old?+updates[i].old_sha1:NULL),+updates[i].flags,+&types[i],onerr);+if(!locks[i])+break;+}++/* Abort if we did not get all locks: */+if(i<n){+while(--i>=0)+unlock_ref(locks[i]);+free(updates);+free(types);+free(locks);+free(delnames);+return1;+}++/* Perform updates first so live commits remain referenced: */+for(i=0;i<n;++i)+if(!is_null_sha1(updates[i].new_sha1)){+ret|=update_ref_write(action,+updates[i].ref_name,+updates[i].new_sha1,+locks[i],onerr);+locks[i]=0;/* freed by update_ref_write */+}++/* Perform deletes now that updates are safely completed: */+for(i=0;i<n;++i)+if(locks[i]){+delnames[delnum++]=locks[i]->ref_name;+ret|=delete_ref_loose(locks[i],types[i]);+}+ret|=repack_without_refs(delnames,delnum);+for(i=0;i<delnum;++i)+unlink_or_warn(git_path("logs/%s",delnames[i]));+clear_loose_ref_cache(&ref_cache);+for(i=0;i<n;++i)+if(locks[i])+unlock_ref(locks[i]);++free(updates);+free(types);+free(locks);+free(delnames);+returnret;+}+structref*find_ref_by_name(conststructref*list,constchar*name){for(;list;list=list->next)
@@ -302,4 +302,210 @@ test_expect_success \'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)'\'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'+a=refs/heads/a+b=refs/heads/b+c=refs/heads/c+z=0000000000000000000000000000000000000000+e="''"++test_expect_success'stdin works with no input''+rm-fstdin&&+touchstdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin fails with bad line lines''+echo" ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: no ref on line: "err&&+echo"--">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: no ref on line: --"err&&+echo"--bad-option">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown option --bad-option"err&&+echo"-\'"'"'$a$m" > stdin &&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown option -'"'"'" err &&+echo"~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format on line: ~a $m"err&&+echo"$a '"'"'master" > stdin &&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unterminated single-quote: '"'"'master" err &&+echo"$a \master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unquoted backslash not escaping single-quote: \\\\master"err&&+echo"$a$m$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: too many arguments on line: $a$m$m$m"err&&+echo"$a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: missing new value on line: $a"err+'++test_expect_success'stdin fails with duplicate refs''+echo"$a$m">stdin&&+echo"$b$m">>stdin&&+echo"$a$m">>stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin create ref works with no old value''+echo"$a$m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with zero old value''+echo"$b$m$z">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b&&+echo"$b$m$e">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref fails with wrong old value''+echo"$c$m$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad old value''+echo"$c$m does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid old value on line: $c$m does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad new value''+echo"$c does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid new value on line: $c does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref works with right old value''+echo"$b$m~1 $m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update ref fails with wrong old value''+echo"$b$m~1 $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$b'"'"'"err&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with wrong old value''+echo"$a$e$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+echo"--no-deref TESTSYMREF $a$b">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+echo"--no-deref TESTSYMREF $e$b">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref works with right old value''+echo"$b$e$m~1">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin create refs works with some old values''+echo"$a$m">stdin&&+echo"$b$m$z">>stdin&&+echo"$c$z$z">>stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with identity updates''+echo"">stdin&&# also test blank lines+echo"$a$m$m">>stdin&&+echo"">>stdin&&+echo" '"'"'$b'"'"' $m$m ">>stdin&&+echo"">>stdin&&+echo"-- $c$z$e ">>stdin&&+echo"">>stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs fails with wrong old value''+gitupdate-ref$c$m&&+echo"$a$m$m">stdin&&+echo"$b$m$m">>stdin&&+echo"$c$e$e">>stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+echo"$a$z$m">stdin&&+echo"$b$z$m">>stdin&&+echo"$c$e$m~1">>stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'+ test_done
From: Brad King <hidden> Date: 2016-06-15 22:58:34
Add a --stdin signature to read update instructions from standard input
and apply multiple ref updates together. Use an input format that
supports any update that could be specified via the command-line,
including object names like 'branch:path with space'.
Signed-off-by: Brad King <redacted>
---
Documentation/git-update-ref.txt | 21 ++++++-
builtin/update-ref.c | 121 +++++++++++++++++++++++++++++++++++++-
2 files changed, 140 insertions(+), 2 deletions(-)
@@ -58,6 +58,25 @@ archive by creating a symlink tree). With `-d` flag, it deletes the named <ref> after verifying it still contains <oldvalue>.+With `--stdin`, update-ref reads instructions from standard input and+performs all modifications together. Empty lines are ignored.+Each non-empty line is parsed as whitespace-separated arguments.+Use single-quotes to enclose whitespace and backslashes and an+unquoted backslash to escape a single quote. Specify updates with+lines of the form:++ [--no-deref] [--] <ref> <newvalue> [<oldvalue>]++Lines of any other format or a repeated <ref> produce an error.+Specify a zero <newvalue> to delete a ref and/or a zero <oldvalue>+to make sure that a ref not exist. Use either 40 "0" or the+empty string (written as '') to specify a zero value.++If all <ref>s can be locked with matching <oldvalue>s+simultaneously all modifications are performed. Otherwise, no+modifications are performed. Note that while each individual+<ref> is updated or deleted atomically, a concurrent reader may+still see a subset of the modifications. Logging Updates ---------------
@@ -6,19 +6,129 @@staticconstchar*constgit_update_ref_usage[]={N_("git update-ref [options] -d <refname> [<oldval>]"),N_("git update-ref [options] <refname> <newval> [<oldval>]"),+N_("git update-ref [options] --stdin"),NULL};+staticconstcharblank[]=" \t\r\n";++staticintupdates_size;+staticintupdates_count;+staticstructref_update*updates;++staticconstchar*update_refs_stdin_next_arg(constchar*next,+structstrbuf*arg)+{+/* Skip leading whitespace: */+while(isspace(*next))+++next;++/* Return NULL when no argument is found: */+if(!*next)+returnNULL;++/* Parse the argument: */+strbuf_reset(arg);+for(;;){+charc=*next;+if(!c||isspace(c))+break;+++next;+if(c=='\''){+size_tlen=strcspn(next,"'");+if(!next[len])+die("unterminated single-quote: '%s",next);+strbuf_add(arg,next,len);+next+=len+1;+continue;+}+if(c=='\\'){+if(*next=='\'')+c=*next++;+else+die("unquoted backslash not escaping "+"single-quote: \\%s",next);+}+strbuf_addch(arg,c);+}+returnnext;+}++staticvoidupdate_refs_stdin(constchar*line)+{+intoptions=1,flags=0,argc=0;+char*argv[3]={0,0,0};+structstrbufarg=STRBUF_INIT;+structref_update*update;+constchar*next=line;++/* Skip blank lines: */+if(!line[0])+return;++/* Parse arguments on this line: */+while((next=update_refs_stdin_next_arg(next,&arg))!=NULL){+if(options&&arg.buf[0]=='-')+if(!strcmp(arg.buf,"--no-deref"))+flags|=REF_NODEREF;+elseif(!strcmp(arg.buf,"--"))+options=0;+else+die("unknown option %s",arg.buf);+elseif(argc>=3)+die("too many arguments on line: %s",line);+else{+argv[argc++]=xstrdup(arg.buf);+options=0;+}+}+strbuf_release(&arg);++/* Allocate and zero-init a struct ref_update: */+if(updates_count==updates_size){+updates_size=updates_size?(updates_size*2):16;+updates=xrealloc(updates,sizeof(*updates)*updates_size);+memset(updates+updates_count,0,+sizeof(*updates)*(updates_size-updates_count));+}+update=&updates[updates_count++];+update->flags=flags;++/* Set the update ref_name: */+if(!argv[0])+die("no ref on line: %s",line);+if(check_refname_format(argv[0],REFNAME_ALLOW_ONELEVEL))+die("invalid ref format on line: %s",line);+update->ref_name=argv[0];+argv[0]=0;++/* Set the update new_sha1 and, if specified, old_sha1: */+if(!argv[1])+die("missing new value on line: %s",line);+if(*argv[1]&&get_sha1(argv[1],update->new_sha1))+die("invalid new value on line: %s",line);+if(argv[2]){+update->have_old=1;+if(*argv[2]&&get_sha1(argv[2],update->old_sha1))+die("invalid old value on line: %s",line);+}++while(argc>0)+free(argv[--argc]);+}+intcmd_update_ref(intargc,constchar**argv,constchar*prefix){constchar*refname,*oldval,*msg=NULL;unsignedcharsha1[20],oldsha1[20];-intdelete=0,no_deref=0,flags=0;+intdelete=0,no_deref=0,read_stdin=0,flags=0;+structstrbufline=STRBUF_INIT;structoptionoptions[]={OPT_STRING('m',NULL,&msg,N_("reason"),N_("reason of the update")),OPT_BOOLEAN('d',NULL,&delete,N_("delete the reference")),OPT_BOOLEAN(0,"no-deref",&no_deref,N_("update <refname> not the one it points to")),+OPT_BOOLEAN(0,"stdin",&read_stdin,N_("read updates from stdin")),OPT_END(),};
@@ -28,6 +138,15 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)if(msg&&!*msg)die("Refusing to perform update with empty message.");+if(read_stdin){+if(delete||no_deref||argc>0)+usage_with_options(git_update_ref_usage,options);+while(strbuf_getline(&line,stdin,'\n')!=EOF)+update_refs_stdin(line.buf);+strbuf_release(&line);+returnupdate_refs(msg,updates,updates_count,DIE_ON_ERR);+}+if(delete){if(argc<1||argc>2)usage_with_options(git_update_ref_usage,options);
From: Brad King <hidden> Date: 2016-06-15 22:58:34
Factor the lock and write steps and error handling into helper functions
update_ref_lock and update_ref_write to allow later use elsewhere.
Expose lock_any_ref_for_update's type_p to update_ref_lock callers.
Signed-off-by: Brad King <redacted>
---
refs.c | 28 +++++++++++++++++++++++-----
1 file changed, 23 insertions(+), 5 deletions(-)
From: Brad King <hidden> Date: 2016-06-15 22:58:34
Generalize repack_without_ref as repack_without_refs to support a list
of refs and implement the former in terms of the latter.
Signed-off-by: Brad King <redacted>
---
refs.c | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
@@ -2414,25 +2414,35 @@ static int curate_packed_ref_fn(struct ref_entry *entry, void *cb_data)return0;}-staticintrepack_without_ref(constchar*refname)+staticintrepack_without_refs(constchar**refnames,intn){structref_dir*packed;structstring_listrefs_to_delete=STRING_LIST_INIT_DUP;structstring_list_item*ref_to_delete;+inti,removed=0;++/* Look for a packed ref: */+for(i=0;i<n;++i)+if(get_packed_ref(refnames[i]))+break;-if(!get_packed_ref(refname))-return0;/* refname does not exist in packed refs */+/* Avoid locking if we have nothing to do: */+if(i==n)+return0;/* no refname exists in packed refs */if(lock_packed_refs(0)){unable_to_lock_error(git_path("packed-refs"),errno);-returnerror("cannot delete '%s' from packed refs",refname);+returnerror("cannot delete '%s' from packed refs",refnames[i]);}packed=get_packed_refs(&ref_cache);-/* Remove refname from the cache: */-if(remove_entry(packed,refname)==-1){+/* Remove refnames from the cache: */+for(i=0;i<n;++i)+if(remove_entry(packed,refnames[i])!=-1)+removed=1;+if(!removed){/*-*Thepackedentrydisappearedwhilewewere+*Allpackedentriesdisappearedwhilewewere*acquiringthelock.*/rollback_packed_refs();
@@ -2450,6 +2460,11 @@ static int repack_without_ref(const char *refname)returncommit_packed_refs();}+staticintrepack_without_ref(constchar*refname)+{+returnrepack_without_refs(&refname,1);+}+staticintdelete_ref_loose(structref_lock*lock,intflag){interr,i,ret=0;
From: Brad King <hidden> Date: 2016-06-15 22:58:35
Hi Folks,
Here is the second revision of a series to support locking multiple
refs at the same time to update all of them consistently. The first
series can be found at $gmane/233260. This revision is ready to
consider for integration.
Updates since the previous revision of the series:
* Incorporated style fixes and cleanups suggested by Junio.
* In patch 6, the new update_refs function now sorts the updates
so that locks are acquired in a consistent order by competing
processes. Then it uses a simple linear scan to reject input
containing duplicate refs (which are adjacent after sorting).
Also, struct ref_update now has a symmetric representation
for new_sha1 and old_sha1.
* In patch 7, I propose a new format for instructions read from
standard input that is much more robust and extensible.
* Patch 8 is new and adds test cases covering new features
and error cases.
-Brad
Brad King (8):
reset: rename update_refs to reset_refs
refs: report ref type from lock_any_ref_for_update
refs: factor update_ref steps into helpers
refs: factor delete_ref loose ref step into a helper
refs: add function to repack without multiple refs
refs: add update_refs for multiple simultaneous updates
update-ref: support multiple simultaneous updates
update-ref: add test cases covering --stdin signature
Documentation/git-update-ref.txt | 21 +++-
branch.c | 2 +-
builtin/commit.c | 2 +-
builtin/fetch.c | 3 +-
builtin/receive-pack.c | 3 +-
builtin/reflog.c | 2 +-
builtin/replace.c | 2 +-
builtin/reset.c | 4 +-
builtin/tag.c | 2 +-
builtin/update-ref.c | 121 +++++++++++++++++++++-
fast-import.c | 2 +-
refs.c | 203 +++++++++++++++++++++++++++++++++----
refs.h | 16 ++-
sequencer.c | 3 +-
t/t1400-update-ref.sh | 206 ++++++++++++++++++++++++++++++++++++++
15 files changed, 558 insertions(+), 34 deletions(-)
--
1.7.10.4
@@ -137,7 +137,7 @@ extern struct ref_lock *lock_ref_sha1(const char *refname, const unsigned char *#define REF_NODEREF 0x01externstructref_lock*lock_any_ref_for_update(constchar*refname,constunsignedchar*old_sha1,-intflags);+intflags,int*type_p);/** Close the file descriptor owned by a lock and return the status */externintclose_ref(structref_lock*lock);
From: Michael Haggerty <hidden> Date: 2016-06-15 22:58:35
On 08/30/2013 08:12 PM, Brad King wrote:
quoted hunk
Factor loose ref deletion into helper function delete_ref_loose to allow
later use elsewhere.
Signed-off-by: Brad King <redacted>
---
refs.c | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
@@ -2468,6 +2463,19 @@ int delete_ref(const char *refname, const unsigned char *sha1, int delopt)lock->lk->filename[i]='.';}+returnret;+}+
At first glance it is odd that delete_ref_loose() takes a (struct
ref_lock *) argument but only actually uses lock->lk->filename. But I
guess that the function is so specific to the contents of struct
ref_lock and indeed struct lock_file that it wouldn't make sense to pass
it only the filename attribute. So OK.
Given that ret is only returned, you could restore the filename before
the if statement and replace the ret variable with an immediate return
statement:
static int delete_ref_loose(struct ref_lock *lock, int flag)
{
if (!(flag & REF_ISPACKED) || flag & REF_ISSYMREF) {
/* loose */
int err, i = strlen(lock->lk->filename) - 5; /* .lock */
lock->lk->filename[i] = 0;
err = unlink_or_warn(lock->lk->filename);
lock->lk->filename[i] = '.';
if (err && errno != ENOENT)
return 1;
}
return 0;
}
+int delete_ref(const char *refname, const unsigned char *sha1, int delopt)
+{
+ struct ref_lock *lock;
+ int ret = 0, flag = 0;
+
+ lock = lock_ref_sha1_basic(refname, sha1, delopt, &flag);
+ if (!lock)
+ return 1;
+ ret |= delete_ref_loose(lock, flag);
+
/* removing the loose one could have resurrected an earlier
* packed one. Also, if it was not loose we need to repack
* without it.
From: Michael Haggerty <hidden> Date: 2016-06-15 22:58:35
On 08/30/2013 08:12 PM, Brad King wrote:
Add 'struct ref_update' to encode the information needed to update or
delete a ref (name, new sha1, optional old sha1, no-deref flag). Add
function 'update_refs' accepting an array of updates to perform. First
sort the input array to order locks consistently everywhere and reject
multiple updates to the same ref. Then acquire locks on all refs with
verified old values. Then update or delete all refs accordingly. Fail
if any one lock cannot be obtained or any one old value does not match.
Though the refs themeselves cannot be modified together in a single
s/themeselves/themselves/
quoted hunk
atomic transaction, this function does enable some useful semantics.
For example, a caller may create a new branch starting from the head of
another branch and rewind the original branch at the same time. This
transfers ownership of commits between branches without risk of losing
commits added to the original branch by a concurrent process, or risk of
a concurrent process creating the new branch first.
Signed-off-by: Brad King <redacted>
---
refs.c | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
refs.h | 14 ++++++++
2 files changed, 135 insertions(+)
If you declare u1 and u2 to be "const struct ref_update *" (i.e., add
"const"), then you have const correctness and don't need the explicit
casts. (And the parentheses around r1 and r2 are superfluous in any case.)
+ int ret;
Style: we usually put a blank line between variable declarations and the
first line of code.
+ ret = strcmp(u1->ref_name, u2->ref_name);
+ if (ret)
+ return ret;
+ ret = hashcmp(u1->new_sha1, u2->new_sha1);
+ if (ret)
+ return ret;
+ ret = hashcmp(u1->old_sha1, u2->old_sha1);
+ if (ret)
+ return ret;
+ ret = u1->flags - u2->flags;
+ if (ret)
+ return ret;
+ return u1->have_old - u2->have_old;
+}
Is there a need to compare more than ref_name? If two entries are found
with the same name, then ref_update_reject_duplicates() will error out
anyway. So the relative order among entries with the same name is
irrelevant. I think it would be OK to return 0 for any entries with the
same ref_name, even if they differ in other fields.
+
+static int ref_update_reject_duplicates(struct ref_update *updates, int n,
+ enum action_on_err onerr)
+{
+ int i;
+ for (i = 1; i < n; ++i)
+ if (!strcmp(updates[i - 1].ref_name, updates[i].ref_name))
+ break;
The error handling code could be right here instead of the "break"
statement, removing the need for the "if" conditional.
+ if (i < n) {
+ const char *str = "Multiple updates for ref '%s' not allowed.";
+ switch (onerr) {
+ case MSG_ON_ERR: error(str, updates[i].ref_name); break;
+ case DIE_ON_ERR: die(str, updates[i].ref_name); break;
+ case QUIET_ON_ERR: break;
+ }
+ return 1;
+ }
+ return 0;
+}
+
+int update_refs(const char *action, const struct ref_update *updates_orig,
+ int n, enum action_on_err onerr)
+{
+ int ret = 0, delnum = 0, i;
+ struct ref_update *updates;
+ int *types;
+ struct ref_lock **locks;
+ const char **delnames;
+
+ if (!updates_orig || !n)
+ return 0;
+
+ /* Allocate work space: */
+ updates = xmalloc(sizeof(struct ref_update) * n);
It seems preferred here to write
updates = xmalloc(sizeof(*updates) * n);
as this will continue to work if the type of updates is ever changed.
Similarly for the next lines.
An alternative to managing separate arrays to hold types and locks would
be to include the scratch space in struct ref_update and document it
"for internal use only; need not be initialized by caller". On the one
hand it's ugly to cruft up the "interface" with internal implementation
details; on the other hand there is precedent for this sort of thing
(e.g., ref_lock::force_write or lock_file::on_list) and it would
simplify the code.
You could save some space and memory shuffling (during memcpy() and
qsort()) if you would declare "updates" to be an array of pointers to
"struct ref_update" rather than an array of structs. Sorting could then
be done by moving pointers around instead of moving the structs. This
would also make it easier for update_refs() to pass information about
the references back to its caller, should that ever be needed.
But I suppose that n will usually be small, so this suggestion can be
considered optional.
+ if (ref_update_reject_duplicates(updates, n, onerr)) {
+ free(updates);
+ free(types);
+ free(locks);
+ free(delnames);
+ return 1;
+ }
+
+ /* Acquire all locks while verifying old values: */
+ for (i = 0; i < n; ++i) {
+ locks[i] = update_ref_lock(updates[i].ref_name,
+ (updates[i].have_old ?
+ updates[i].old_sha1 : NULL),
+ updates[i].flags,
+ &types[i], onerr);
+ if (!locks[i])
+ break;
The error handling code could go here in place of the "break",
especially if it's only "ret = 1; goto label;" as suggested below.
+ }
+
+ /* Abort if we did not get all locks: */
+ if (i < n) {
+ while (--i >= 0)
+ unlock_ref(locks[i]);
+ free(updates);
+ free(types);
+ free(locks);
+ free(delnames);
+ return 1;
+ }
+
+ /* Perform updates first so live commits remain referenced: */
+ for (i = 0; i < n; ++i)
+ if (!is_null_sha1(updates[i].new_sha1)) {
+ ret |= update_ref_write(action,
+ updates[i].ref_name,
+ updates[i].new_sha1,
+ locks[i], onerr);
+ locks[i] = 0; /* freed by update_ref_write */
+ }
+
Hmmm, if one of the calls to update_ref_write() fails, would it be safer
to abort the rest of the work (especially the reference deletions)?
+ /* Perform deletes now that updates are safely completed: */
+ for (i = 0; i < n; ++i)
+ if (locks[i]) {
+ delnames[delnum++] = locks[i]->ref_name;
+ ret |= delete_ref_loose(locks[i], types[i]);
+ }
+ ret |= repack_without_refs(delnames, delnum);
+ for (i = 0; i < delnum; ++i)
+ unlink_or_warn(git_path("logs/%s", delnames[i]));
+ clear_loose_ref_cache(&ref_cache);
+ for (i = 0; i < n; ++i)
+ if (locks[i])
+ unlock_ref(locks[i]);
+
+ free(updates);
+ free(types);
+ free(locks);
+ free(delnames);
+ return ret;
+}
There's a lot of duplicated cleanup code in the function. If you put a
label before the final for loop, and if you initialize the locks array
to zeros (e.g., by using xcalloc()), then the three exits could all
share the same code "ret = 1; goto cleanup;".
quoted hunk
+
struct ref *find_ref_by_name(const struct ref *list, const char *name)
{
for ( ; list; list = list->next)
Please document this structure, especially the relationship between
have_old and old_sha1.
+
+/**
+ * Lock all refs and then perform all modifications.
+ */
+int update_refs(const char *action, const struct ref_update *updates,
+ int n, enum action_on_err onerr);
+
extern int parse_hide_refs_config(const char *var, const char *value, const char *);
extern int ref_is_hidden(const char *);
Overall, thanks; it looks good. I think this change is useful by itself
and is also a good start towards implementing true reference
transactions (which I think will be necessary pretty soon, at least for
some applications). I will write another email discussing how your
changes are related to some changes that I have been working on lately.
Michael
--
Michael Haggerty
mhagger@alum.mit.edu
http://softwareswirl.blogspot.com/
From: Michael Haggerty <hidden> Date: 2016-06-15 22:58:35
On 08/30/2013 08:12 PM, Brad King wrote:
quoted hunk
Add a --stdin signature to read update instructions from standard input
and apply multiple ref updates together. Use an input format that
supports any update that could be specified via the command-line,
including object names like 'branch:path with space'.
Signed-off-by: Brad King <redacted>
---
Documentation/git-update-ref.txt | 21 ++++++-
builtin/update-ref.c | 121 +++++++++++++++++++++++++++++++++++++-
2 files changed, 140 insertions(+), 2 deletions(-)
@@ -58,6 +58,25 @@ archive by creating a symlink tree). With `-d` flag, it deletes the named <ref> after verifying it still contains <oldvalue>.+With `--stdin`, update-ref reads instructions from standard input and+performs all modifications together. Empty lines are ignored.+Each non-empty line is parsed as whitespace-separated arguments.+Use single-quotes to enclose whitespace and backslashes and an+unquoted backslash to escape a single quote. Specify updates with+lines of the form:++ [--no-deref] [--] <ref> <newvalue> [<oldvalue>]++Lines of any other format or a repeated <ref> produce an error.+Specify a zero <newvalue> to delete a ref and/or a zero <oldvalue>+to make sure that a ref not exist. Use either 40 "0" or the+empty string (written as '') to specify a zero value.++If all <ref>s can be locked with matching <oldvalue>s+simultaneously all modifications are performed. Otherwise, no
Comma after "simultaneously".
quoted hunk
+modifications are performed. Note that while each individual
+<ref> is updated or deleted atomically, a concurrent reader may
+still see a subset of the modifications.
Logging Updates
---------------
From: Michael Haggerty <hidden> Date: 2016-06-15 22:58:35
On 08/30/2013 08:11 PM, Brad King wrote:
Here is the second revision of a series to support locking multiple
refs at the same time to update all of them consistently. The first
series can be found at $gmane/233260. This revision is ready to
consider for integration.
I'm very interested in this area and I regret that I have been so
invisible lately. I definitely like the way your changes are going.
I have been doing some work in the same neighborhood and our work
overlaps somewhat. Namely, it is incorrect that delete_ref() currently
deletes the loose ref before rewriting packed-refs, because it can cause
a simultaneous reader to see the packed value for a moment in time, and
the packed value might not even point to a valid object anymore. In
fact, the current version is even worse: it deletes the loose reference
before even locking the packed-refs file, so if the packed refs file
cannot be rewritten (e.g., because it is locked by another process) then
the reference is permanently left in a corrupt state.
On the other hand, writing the packed-refs file first would also be
incorrect, because a pack-refs process could jump in before the loose
refs file was deleted, read the loose value, and write it to the new
packed-refs file. The result would be that the first process would
think that it had deleted the reference but it would still exist (not
such a catastrophe, but incorrect nevertheless).
The solution that I have been working on is to first lock *both* the
loose and packed refs files, then rewrite the packed-refs file *but
retain a lock on it*, then rewrite the loose-ref file and release its
lock, then release the lock on the packed-refs file. By retaining the
lock on the packed-refs file during the whole "transaction", we prevent
another process from trying to pack the refs before our reference is
completely deleted. This requires the file-locking API to be enhanced
to allow a file to be replaced by its new version while still retaining
a lock on the file.
Your code has the same bug as the original (it's not your fault!) so I
think it will eventually have to be fixed to look something like
acquire lock on packed-refs
for reference in ref_updates:
lock reference
if old sha1 known:
verify old sha1 is still current
for reference in ref_updates:
if reference should be created/modified:
modify reference
release lock on reference
delete references from packed-refs file and activate new
version of the file *but retain a lock on the
packed-refs file*
for reference in ref_updates:
if reference should be deleted:
delete loose version of reference
release lock on reference
release lock on packed-refs file
This is really all just for your information; there is certainly no
obligation for you to fix this pre-existing problem. And I'm working on
it anyway; if you happen to be interested you can view my current
work-in-progress on GitHub (though it still doesn't work!):
https://github.com/mhagger/git/tree/WIP-delete-ref-locking
Feedback would of course be welcome.
Michael
--
Michael Haggerty
mhagger@alum.mit.edu
http://softwareswirl.blogspot.com/
Despite the semantic relationship between all these cases, if there is
a regression in one case, the person reading the verbose output has to
study it carefully to determine the offending case. If you decompose
this monolith so that each case is in its own test_expect_success,
then the regressed case becomes immediately obvious.
From: Junio C Hamano <hidden> Date: 2016-06-15 22:58:35
Brad King [off-list ref] writes:
quoted hunk
Factor the lock and write steps and error handling into helper functions
update_ref_lock and update_ref_write to allow later use elsewhere.
Expose lock_any_ref_for_update's type_p to update_ref_lock callers.
Signed-off-by: Brad King <redacted>
---
refs.c | 28 +++++++++++++++++++++++-----
1 file changed, 23 insertions(+), 5 deletions(-)
@@ -3170,12 +3170,13 @@ int for_each_reflog(each_ref_fn fn, void *cb_data)returnretval;}-intupdate_ref(constchar*action,constchar*refname,-constunsignedchar*sha1,constunsignedchar*oldval,-intflags,enumaction_on_erronerr)+staticstructref_lock*update_ref_lock(constchar*refname,+constunsignedchar*oldval,+intflags,int*type_p,+enumaction_on_erronerr){staticstructref_lock*lock;
Not the fault of this patch, as the original update_ref() had it
this way, but it is not necessary to keep the value of this variable
across invocations. Let's drop "static" from here, and also the
corresponding variable in the new update_ref().
Will locally tweak while queuing.
From: Junio C Hamano <hidden> Date: 2016-06-15 22:58:35
Brad King [off-list ref] writes:
Add 'struct ref_update' to encode the information needed to update or
delete a ref (name, new sha1, optional old sha1, no-deref flag). Add
function 'update_refs' accepting an array of updates to perform. First
sort the input array to order locks consistently everywhere and reject
multiple updates to the same ref. Then acquire locks on all refs with
verified old values. Then update or delete all refs accordingly. Fail
if any one lock cannot be obtained or any one old value does not match.
OK. The code releases the locks it acquired so far when it fails,
which is good.
Though the refs themeselves cannot be modified together in a single
"themselves".
atomic transaction, this function does enable some useful semantics.
For example, a caller may create a new branch starting from the head of
another branch and rewind the original branch at the same time. This
transfers ownership of commits between branches without risk of losing
commits added to the original branch by a concurrent process, or risk of
a concurrent process creating the new branch first.
Let's have a blank line between the end of decls and the beginning
of the body here.
+ ret = strcmp(u1->ref_name, u2->ref_name);
+ if (ret)
+ return ret;
+ ret = hashcmp(u1->new_sha1, u2->new_sha1);
+ if (ret)
+ return ret;
+ ret = hashcmp(u1->old_sha1, u2->old_sha1);
+ if (ret)
+ return ret;
+ ret = u1->flags - u2->flags;
+ if (ret)
+ return ret;
+ return u1->have_old - u2->have_old;
+}
I notice that we are using an array of structures and letting qsort
swap 50~64 bytes of data, instead of sorting an array of pointers,
each element of which points at a structure. This may not matter
unless we are asked to update thousands at once, so I think it is OK
for now.
+static int ref_update_reject_duplicates(struct ref_update *updates, int n,
+ enum action_on_err onerr)
+{
+ int i;
+ for (i = 1; i < n; ++i)
+ if (!strcmp(updates[i - 1].ref_name, updates[i].ref_name))
+ break;
Optionally we could silently dedup multiple identical updates and
not fail it in ref-update-reject-duplicates. But that does not have
to be done until we find people's script would benefit from such a
nicety.
By the way, unless there is a strong reason not to do so,
post-increment "i++" (and pre-decrement "--i", if you use it) is the
norm around here. Especially in places like the third part of a
for(;;) loop where people are used to see "i++", breaking the idiom
makes readers wonder if there is something else going on.
+ /* Perform updates first so live commits remain referenced: */
+ for (i = 0; i < n; ++i)
+ if (!is_null_sha1(updates[i].new_sha1)) {
+ ret |= update_ref_write(action,
+ updates[i].ref_name,
+ updates[i].new_sha1,
+ locks[i], onerr);
+ locks[i] = 0; /* freed by update_ref_write */
I think what is assigned here is a NULL pointer.
Will locally tweak while queuing. Thanks.
From: Brad King <hidden> Date: 2016-06-15 22:58:36
On 09/01/2013 02:08 AM, Junio C Hamano wrote:
Brad King [off-list ref] writes:
quoted
static struct ref_lock *lock;
Not the fault of this patch, as the original update_ref() had it
this way, but it is not necessary to keep the value of this variable
across invocations. Let's drop "static" from here, and also the
corresponding variable in the new update_ref().
From: Brad King <hidden> Date: 2016-06-15 22:58:36
On 08/31/2013 12:30 PM, Michael Haggerty wrote:
Given that ret is only returned, you could restore the filename before
the if statement and replace the ret variable with an immediate return
statement:
From: Brad King <hidden> Date: 2016-06-15 22:58:36
On 09/01/2013 02:08 AM, Junio C Hamano wrote:
quoted
Though the refs themeselves cannot be modified together in a single
"themselves".
Fixed.
I notice that we are using an array of structures and letting qsort
swap 50~64 bytes of data
Michael suggested this too, so fixed.
Optionally we could silently dedup multiple identical updates and
not fail it in ref-update-reject-duplicates. But that does not have
to be done until we find people's script would benefit from such a
nicety.
We can always be less strict about input later, so I'd like to keep
the implementation simpler for now.
By the way, unless there is a strong reason not to do so,
post-increment "i++" (and pre-decrement "--i", if you use it) is the
norm around here.
If you declare u1 and u2 to be "const struct ref_update *" (i.e., add
"const"), then you have const correctness and don't need the explicit
casts. (And the parentheses around r1 and r2 are superfluous in any case.)
Fixed.
quoted
+ ret = strcmp(u1->ref_name, u2->ref_name);
Is there a need to compare more than ref_name? If two entries are found
with the same name, then ref_update_reject_duplicates() will error out
Junio mentioned possibility of auto-combining identical entries which would
need full ordering. I think that can be added later so for now we can sort
only by ref name. Thanks.
quoted
+ if (!strcmp(updates[i - 1].ref_name, updates[i].ref_name))
+ break;
The error handling code could be right here instead of the "break"
statement, removing the need for the "if" conditional.
An alternative to managing separate arrays to hold types and locks would
be to include the scratch space in struct ref_update and document it
"for internal use only; need not be initialized by caller". On the one
hand it's ugly to cruft up the "interface" with internal implementation
details; on the other hand there is precedent for this sort of thing
(e.g., ref_lock::force_write or lock_file::on_list) and it would
simplify the code.
I think the "goto cleanup" reorganization simplifies the code enough
to not need this. After changing "updates" to an array of pointers
it needs to be separate so we can sort. Also "delnames" needs to be
a separate array to pass to repack_without_refs.
You could save some space and memory shuffling (during memcpy() and
qsort()) if you would declare "updates" to be an array of pointers to
"struct ref_update" rather than an array of structs. Sorting could then
be done by moving pointers around instead of moving the structs. This
would also make it easier for update_refs() to pass information about
the references back to its caller, should that ever be needed.
There's a lot of duplicated cleanup code in the function. If you put a
label before the final for loop, and if you initialize the locks array
to zeros (e.g., by using xcalloc()), then the three exits could all
share the same code "ret = 1; goto cleanup;".
Done, thanks.
quoted
+struct ref_update {
Please document this structure, especially the relationship between
have_old and old_sha1.
Done. I also moved it to the top of the header just under ref_lock
so it can be used by other APIs later.
Thanks,
-Brad
From: Brad King <hidden> Date: 2016-06-15 22:58:36
On 08/31/2013 11:41 PM, Eric Sunshine wrote:
quoted
+ rm -f stdin &&
+ touch stdin &&
Unless the timestamp of 'stdin' has particular significance, modern
git tests avoid 'touch' in favor of creating the empty file like this
>stdin &&
Fixed.
quoted
+ git update-ref --stdin < stdin &&
Style: Git test scripts omit whitespace following <, >, <<, and >>.
Fixed.
quoted
+test_expect_success 'stdin fails with bad line lines' '
Despite the semantic relationship between all these cases, if there is
a regression in one case, the person reading the verbose output has to
study it carefully to determine the offending case. If you decompose
this monolith so that each case is in its own test_expect_success,
then the regressed case becomes immediately obvious.
Yes, of course. Fixed.
multi-line preparations of 'stdin' might be more readable with a heredoc:
cat >stdin <<-EOF &&
$a $m
$b $m
$a $m
EOF
From: Brad King <hidden> Date: 2016-06-15 22:58:36
The function resets refs rather than doing arbitrary updates.
Rename it to allow a future general-purpose update_refs function
to be added.
Signed-off-by: Brad King <redacted>
---
builtin/reset.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -350,7 +350,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)if(!pathspec&&!unborn){/* Any resets without paths update HEAD to the head being*switchedto,savingthepreviousheadinORIG_HEADbefore.*/-update_ref_status=update_refs(rev,sha1);+update_ref_status=reset_refs(rev,sha1);if(reset_type==HARD&&!update_ref_status&&!quiet)print_new_head_line(lookup_commit_reference(sha1));
@@ -302,4 +302,260 @@ test_expect_success \'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)'\'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'+a=refs/heads/a+b=refs/heads/b+c=refs/heads/c+z=0000000000000000000000000000000000000000+e='""'+pws='path with space'++test_expect_success'stdin test setup''+echo"$pws">"$pws"&&+gitadd--"$pws"&&+gitcommit-m"$pws"+'++test_expect_success'stdin works with no input''+>stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin fails on bad input line with only whitespace''+echo" ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: no ref on line: "err+'++test_expect_success'stdin fails on bad input line with only --''+echo"--">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: no ref on line: --"err+'++test_expect_success'stdin fails on bad input line with only --bad-option''+echo"--bad-option">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown option --bad-option"err+'++test_expect_success'stdin fails on bad ref name''+echo"~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format on line: ~a $m"err+'++test_expect_success'stdin fails on badly quoted input''+echo"$a \"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: badly quoted argument: \\\"master"err+'++test_expect_success'stdin fails on bad input line with too many arguments''+echo"$a$m$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: too many arguments on line: $a$m$m$m"err+'++test_expect_success'stdin fails on bad input line with too few arguments''+echo"$a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: missing new value on line: $a"err+'++test_expect_success'stdin fails with duplicate refs''+cat>stdin<<-EOF&&+$a$m+$b$m+$a$m+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin create ref works with no old value''+echo"$a$m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with zero old value''+echo"$b$m$z">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b&&+echo"$b$m$e">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with path with space to blob''+echo"refs/blobs/pws \"$m:$pws\"">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref fails with wrong old value''+echo"$c$m$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad old value''+echo"$c$m does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid old value on line: $c$m does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad new value''+echo"$c does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid new value on line: $c does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref works with right old value''+echo"$b$m~1 $m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update ref fails with wrong old value''+echo"$b$m~1 $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$b'"'"'"err&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with wrong old value''+echo"$a$e$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+echo"--no-deref TESTSYMREF $a$b">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+echo"--no-deref TESTSYMREF $e$b">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref works with right old value''+echo"$b$e$m~1">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin create refs works with some old values''+cat>stdin<<-EOF&&+$a$m+$b$m$z+$c$z$z+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with identity updates''+cat>stdin<<-EOF&&+$a$m$m+$b$m$m+$c$z$e+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with extra whitespace''+cat>stdin<<-EOF&&++$a$m$m++"$b"$m$m''++--$c$z$e''+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs fails with wrong old value''+gitupdate-ref$c$m&&+cat>stdin<<-EOF&&+$a$m$m+$b$m$m+$c$e$e+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+cat>stdin<<-EOF&&+$a$z$m+$b$z$m+$c$e$m~1+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'+ test_done
From: Brad King <hidden> Date: 2016-06-15 22:58:36
Factor loose ref deletion into helper function delete_ref_loose to allow
later use elsewhere.
Signed-off-by: Brad King <redacted>
---
refs.c | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)
@@ -2450,24 +2450,31 @@ static int repack_without_ref(const char *refname)returncommit_packed_refs();}+staticintdelete_ref_loose(structref_lock*lock,intflag)+{+if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){+/* loose */+interr,i=strlen(lock->lk->filename)-5;/* .lock */++lock->lk->filename[i]=0;+err=unlink_or_warn(lock->lk->filename);+lock->lk->filename[i]='.';+if(err&&errno!=ENOENT)+return1;+}+return0;+}+intdelete_ref(constchar*refname,constunsignedchar*sha1,intdelopt){structref_lock*lock;-interr,i=0,ret=0,flag=0;+intret=0,flag=0;lock=lock_ref_sha1_basic(refname,sha1,delopt,&flag);if(!lock)return1;-if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){-/* loose */-i=strlen(lock->lk->filename)-5;/* .lock */-lock->lk->filename[i]=0;-err=unlink_or_warn(lock->lk->filename);-if(err&&errno!=ENOENT)-ret=1;+ret|=delete_ref_loose(lock,flag);-lock->lk->filename[i]='.';-}/* removing the loose one could have resurrected an earlier*packedone.Also,ifitwasnotlooseweneedtorepack*withoutit.
From: Brad King <hidden> Date: 2016-06-15 22:58:36
Hi Folks,
Here is the third revision of a series to support locking multiple
refs at the same time to update all of them consistently. The
previous revisions of the series can be found at $gmane/233260 and
$gmane/233458.
Updates since the previous revision of the series:
* Incorporated style fixes suggested in patches 6-8.
* In patch 3, the local "lock" variables in update_ref_lock and
update_ref now drop the existing "static" declaration.
* In patch 4, delete_ref_loose internals have been cleaned up
as Michael suggested.
* In patch 6:
- struct ref_update has been documented
- update_refs now takes an array of pointers to struct ref_update
as Michael and Junio both suggested
- update_refs return cases were simplified with a label and goto
- update_refs now stops immediately if any ref write fails
- ref_update_compare now compares only the ref name
* In patch 7, another new input format is proposed. It now uses
quoting based on unquote_c_style.
* In patch 8, more new test cases have been added. Failure cases
are now covered in separate steps to simplify diagnosis.
-Brad
Brad King (8):
reset: rename update_refs to reset_refs
refs: report ref type from lock_any_ref_for_update
refs: factor update_ref steps into helpers
refs: factor delete_ref loose ref step into a helper
refs: add function to repack without multiple refs
refs: add update_refs for multiple simultaneous updates
update-ref: support multiple simultaneous updates
update-ref: add test cases covering --stdin signature
Documentation/git-update-ref.txt | 20 ++-
branch.c | 2 +-
builtin/commit.c | 2 +-
builtin/fetch.c | 3 +-
builtin/receive-pack.c | 3 +-
builtin/reflog.c | 2 +-
builtin/replace.c | 2 +-
builtin/reset.c | 4 +-
builtin/tag.c | 2 +-
builtin/update-ref.c | 103 ++++++++++++++-
fast-import.c | 2 +-
refs.c | 191 ++++++++++++++++++++++++----
refs.h | 22 +++-
sequencer.c | 3 +-
t/t1400-update-ref.sh | 256 ++++++++++++++++++++++++++++++++++++++
15 files changed, 578 insertions(+), 39 deletions(-)
--
1.7.10.4
From: Brad King <hidden> Date: 2016-06-15 22:58:36
Generalize repack_without_ref as repack_without_refs to support a list
of refs and implement the former in terms of the latter.
Signed-off-by: Brad King <redacted>
---
refs.c | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
@@ -2414,25 +2414,35 @@ static int curate_packed_ref_fn(struct ref_entry *entry, void *cb_data)return0;}-staticintrepack_without_ref(constchar*refname)+staticintrepack_without_refs(constchar**refnames,intn){structref_dir*packed;structstring_listrefs_to_delete=STRING_LIST_INIT_DUP;structstring_list_item*ref_to_delete;+inti,removed=0;++/* Look for a packed ref: */+for(i=0;i<n;i++)+if(get_packed_ref(refnames[i]))+break;-if(!get_packed_ref(refname))-return0;/* refname does not exist in packed refs */+/* Avoid locking if we have nothing to do: */+if(i==n)+return0;/* no refname exists in packed refs */if(lock_packed_refs(0)){unable_to_lock_error(git_path("packed-refs"),errno);-returnerror("cannot delete '%s' from packed refs",refname);+returnerror("cannot delete '%s' from packed refs",refnames[i]);}packed=get_packed_refs(&ref_cache);-/* Remove refname from the cache: */-if(remove_entry(packed,refname)==-1){+/* Remove refnames from the cache: */+for(i=0;i<n;i++)+if(remove_entry(packed,refnames[i])!=-1)+removed=1;+if(!removed){/*-*Thepackedentrydisappearedwhilewewere+*Allpackedentriesdisappearedwhilewewere*acquiringthelock.*/rollback_packed_refs();
@@ -2450,6 +2460,11 @@ static int repack_without_ref(const char *refname)returncommit_packed_refs();}+staticintrepack_without_ref(constchar*refname)+{+returnrepack_without_refs(&refname,1);+}+staticintdelete_ref_loose(structref_lock*lock,intflag){if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){
@@ -137,7 +137,7 @@ extern struct ref_lock *lock_ref_sha1(const char *refname, const unsigned char *#define REF_NODEREF 0x01externstructref_lock*lock_any_ref_for_update(constchar*refname,constunsignedchar*old_sha1,-intflags);+intflags,int*type_p);/** Close the file descriptor owned by a lock and return the status */externintclose_ref(structref_lock*lock);
From: Brad King <hidden> Date: 2016-06-15 22:58:36
Factor the lock and write steps and error handling into helper functions
update_ref_lock and update_ref_write to allow later use elsewhere.
Expose lock_any_ref_for_update's type_p to update_ref_lock callers.
While at it, drop "static" from the local "lock" variable as it is not
necessary to keep across invocations.
Signed-off-by: Brad King <redacted>
---
refs.c | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
From: Brad King <hidden> Date: 2016-06-15 22:58:36
Add a --stdin signature to read update instructions from standard input
and apply multiple ref updates together. Use an input format that
supports any update that could be specified via the command-line,
including object names like "branch:path with space".
Signed-off-by: Brad King <redacted>
---
Documentation/git-update-ref.txt | 20 +++++++-
builtin/update-ref.c | 103 +++++++++++++++++++++++++++++++++++++-
2 files changed, 121 insertions(+), 2 deletions(-)
@@ -58,6 +58,24 @@ archive by creating a symlink tree). With `-d` flag, it deletes the named <ref> after verifying it still contains <oldvalue>.+With `--stdin`, update-ref reads instructions from standard input and+performs all modifications together. Empty lines are ignored.+Each non-empty line is parsed as whitespace-separated arguments.+Quote arguments containing whitespace as if in C source code.+Specify updates with lines of the form:++ [--no-deref] [--] <ref> <newvalue> [<oldvalue>]++Lines of any other format or a repeated <ref> produce an error.+Specify a zero <newvalue> to delete a ref and/or a zero <oldvalue>+to make sure that a ref not exist. Use either 40 "0" or the+empty string (written as "") to specify a zero value.++If all <ref>s can be locked with matching <oldvalue>s+simultaneously, all modifications are performed. Otherwise, no+modifications are performed. Note that while each individual+<ref> is updated or deleted atomically, a concurrent reader may+still see a subset of the modifications. Logging Updates ---------------
@@ -2,23 +2,115 @@#include"refs.h"#include"builtin.h"#include"parse-options.h"+#include"quote.h"staticconstchar*constgit_update_ref_usage[]={N_("git update-ref [options] -d <refname> [<oldval>]"),N_("git update-ref [options] <refname> <newval> [<oldval>]"),+N_("git update-ref [options] --stdin"),NULL};+staticintupdates_alloc;+staticintupdates_count;+staticconststructref_update**updates;++staticconstchar*update_refs_stdin_next_arg(constchar*next,+structstrbuf*arg)+{+/* Skip leading whitespace: */+while(isspace(*next))+++next;++/* Return NULL when no argument is found: */+if(!*next)+returnNULL;++/* Parse the argument: */+strbuf_reset(arg);+if(*next=='"'){+if(unquote_c_style(arg,next,&next))+die("badly quoted argument: %s",next);+returnnext;+}+while(*next&&!isspace(*next))+strbuf_addch(arg,*next++);+returnnext;+}++staticvoidupdate_refs_stdin(constchar*line)+{+intoptions=1,flags=0,argc=0;+char*argv[3]={0,0,0};+structstrbufarg=STRBUF_INIT;+structref_update*update;+constchar*next=line;++/* Skip blank lines: */+if(!line[0])+return;++/* Parse arguments on this line: */+while((next=update_refs_stdin_next_arg(next,&arg))!=NULL){+if(options&&arg.buf[0]=='-')+if(!strcmp(arg.buf,"--no-deref"))+flags|=REF_NODEREF;+elseif(!strcmp(arg.buf,"--"))+options=0;+else+die("unknown option %s",arg.buf);+elseif(argc>=3)+die("too many arguments on line: %s",line);+else{+argv[argc++]=xstrdup(arg.buf);+options=0;+}+}+strbuf_release(&arg);++/* Allocate and zero-init a struct ref_update: */+update=xcalloc(1,sizeof(*update));+ALLOC_GROW(updates,updates_count+1,updates_alloc);+updates[updates_count++]=update;++/* Set the update ref_name: */+if(!argv[0])+die("no ref on line: %s",line);+if(check_refname_format(argv[0],REFNAME_ALLOW_ONELEVEL))+die("invalid ref format on line: %s",line);+update->ref_name=argv[0];+argv[0]=0;++/* Set the update new_sha1 and, if specified, old_sha1: */+if(!argv[1])+die("missing new value on line: %s",line);+if(*argv[1]&&get_sha1(argv[1],update->new_sha1))+die("invalid new value on line: %s",line);+if(argv[2]){+update->have_old=1;+if(*argv[2]&&get_sha1(argv[2],update->old_sha1))+die("invalid old value on line: %s",line);+}++/* Set the update flags: */+update->flags=flags;++while(argc>0)+free(argv[--argc]);+}+intcmd_update_ref(intargc,constchar**argv,constchar*prefix){constchar*refname,*oldval,*msg=NULL;unsignedcharsha1[20],oldsha1[20];-intdelete=0,no_deref=0,flags=0;+intdelete=0,no_deref=0,read_stdin=0,flags=0;+structstrbufline=STRBUF_INIT;structoptionoptions[]={OPT_STRING('m',NULL,&msg,N_("reason"),N_("reason of the update")),OPT_BOOLEAN('d',NULL,&delete,N_("delete the reference")),OPT_BOOLEAN(0,"no-deref",&no_deref,N_("update <refname> not the one it points to")),+OPT_BOOLEAN(0,"stdin",&read_stdin,N_("read updates from stdin")),OPT_END(),};
@@ -28,6 +120,15 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)if(msg&&!*msg)die("Refusing to perform update with empty message.");+if(read_stdin){+if(delete||no_deref||argc>0)+usage_with_options(git_update_ref_usage,options);+while(strbuf_getline(&line,stdin,'\n')!=EOF)+update_refs_stdin(line.buf);+strbuf_release(&line);+returnupdate_refs(msg,updates,updates_count,DIE_ON_ERR);+}+if(delete){if(argc<1||argc>2)usage_with_options(git_update_ref_usage,options);
From: Brad King <hidden> Date: 2016-06-15 22:58:36
Add 'struct ref_update' to encode the information needed to update or
delete a ref (name, new sha1, optional old sha1, no-deref flag). Add
function 'update_refs' accepting an array of updates to perform. First
sort the input array to order locks consistently everywhere and reject
multiple updates to the same ref. Then acquire locks on all refs with
verified old values. Then update or delete all refs accordingly. Fail
if any one lock cannot be obtained or any one old value does not match.
Though the refs themselves cannot be modified together in a single
atomic transaction, this function does enable some useful semantics.
For example, a caller may create a new branch starting from the head of
another branch and rewind the original branch at the same time. This
transfers ownership of commits between branches without risk of losing
commits added to the original branch by a concurrent process, or risk of
a concurrent process creating the new branch first.
Signed-off-by: Brad King <redacted>
---
refs.c | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
refs.h | 20 +++++++++++++
2 files changed, 120 insertions(+)
@@ -3237,6 +3237,106 @@ int update_ref(const char *action, const char *refname,returnupdate_ref_write(action,refname,sha1,lock,onerr);}+staticintref_update_compare(constvoid*r1,constvoid*r2)+{+conststructref_update*const*u1=r1;+conststructref_update*const*u2=r2;+returnstrcmp((*u1)->ref_name,(*u2)->ref_name);+}++staticintref_update_reject_duplicates(structref_update**updates,intn,+enumaction_on_erronerr)+{+inti;+for(i=1;i<n;i++)+if(!strcmp(updates[i-1]->ref_name,updates[i]->ref_name)){+constchar*str=+"Multiple updates for ref '%s' not allowed.";+switch(onerr){+caseMSG_ON_ERR:+error(str,updates[i]->ref_name);break;+caseDIE_ON_ERR:+die(str,updates[i]->ref_name);break;+caseQUIET_ON_ERR:+break;+}+return1;+}+return0;+}++intupdate_refs(constchar*action,conststructref_update**updates_orig,+intn,enumaction_on_erronerr)+{+intret=0,delnum=0,i;+structref_update**updates;+int*types;+structref_lock**locks;+constchar**delnames;++if(!updates_orig||!n)+return0;++/* Allocate work space: */+updates=xmalloc(sizeof(*updates)*n);+types=xmalloc(sizeof(*types)*n);+locks=xcalloc(n,sizeof(*locks));+delnames=xmalloc(sizeof(*delnames)*n);++/* Copy, sort, and reject duplicate refs: */+memcpy(updates,updates_orig,sizeof(*updates)*n);+qsort(updates,n,sizeof(*updates),ref_update_compare);+ret=ref_update_reject_duplicates(updates,n,onerr);+if(ret)+gotocleanup;++/* Acquire all locks while verifying old values: */+for(i=0;i<n;i++){+locks[i]=update_ref_lock(updates[i]->ref_name,+(updates[i]->have_old?+updates[i]->old_sha1:NULL),+updates[i]->flags,+&types[i],onerr);+if(!locks[i]){+ret=1;+gotocleanup;+}+}++/* Perform updates first so live commits remain referenced: */+for(i=0;i<n;i++)+if(!is_null_sha1(updates[i]->new_sha1)){+ret=update_ref_write(action,+updates[i]->ref_name,+updates[i]->new_sha1,+locks[i],onerr);+locks[i]=NULL;/* freed by update_ref_write */+if(ret)+gotocleanup;+}++/* Perform deletes now that updates are safely completed: */+for(i=0;i<n;i++)+if(locks[i]){+delnames[delnum++]=locks[i]->ref_name;+ret|=delete_ref_loose(locks[i],types[i]);+}+ret|=repack_without_refs(delnames,delnum);+for(i=0;i<delnum;i++)+unlink_or_warn(git_path("logs/%s",delnames[i]));+clear_loose_ref_cache(&ref_cache);++cleanup:+for(i=0;i<n;i++)+if(locks[i])+unlock_ref(locks[i]);+free(updates);+free(types);+free(locks);+free(delnames);+returnret;+}+structref*find_ref_by_name(conststructref*list,constchar*name){for(;list;list=list->next)
This quoting proposal was written in response to $gmane/233479:
On 08/30/2013 06:51 PM, Junio C Hamano wrote:
When we need to deal with arbitrary strings (like pathnames), other
parts of the system usually give the user two interfaces, --stdin
with and without -z, and the strings are C-quoted when run without
the -z option, and terminated with NUL when run with the -z option.
1. Do we want to allow arbitrary non-space characters in unquoted
arguments (while loop above) or reserve some syntax for future use?
2. Thinking about how the -z variation might work, I ran:
$ git grep '\[0\] == '"'"'"' -- '*.c'
builtin/check-attr.c: if (line_termination && buf.buf[0] == '"') {
builtin/check-ignore.c: if (line_termination && buf.buf[0] == '"') {
builtin/checkout-index.c: if (line_termination && buf.buf[0] == '"') {
builtin/hash-object.c: if (buf.buf[0] == '"') {
builtin/mktree.c: if (line_termination && path[0] == '"') {
builtin/update-index.c: if (line_termination && path_name[0] == '"') {
builtin/update-index.c: if (line_termination && buf.buf[0] == '"') {
All of these support quoting only in the non-z mode (the hash-object.c
line follows a getline using hard-coded '\n'). However, they are
all in cases looking for one value on a line or at the end of a line
so their -z option allows NUL-terminated lines containing LF.
What distinguishes the "update-ref --stdin" case is that we want to
represent multiple arguments on one line, each allowing arbitrary
characters or an empty string. From a brief search a couple places
I found that do something related are:
* apply: Read multiple paths from a diff header, using unquote_c_style
for quoted paths and separated by spaces. There is no -z input mode.
* config: Output keyword=value\n becomes keyword\nvalue\0 in -z mode.
This works because the first piece (keyword) cannot have a LF
and there is at most one value so all LFs belong to it.
* quote.c: sq_dequote_to_argv handles single quotes like a shell
would but allows only one space between arguments. No -z mode.
This is similar to my v2 proposal.
If we use unquote_c_style and spaces to divide LF-terminated lines,
how shall we divide arguments on NUL-terminated lines?
Thanks,
-Brad
From: Michael Haggerty <hidden> Date: 2016-06-15 22:58:36
On 09/02/2013 07:20 PM, Brad King wrote:
On 09/01/2013 02:08 AM, Junio C Hamano wrote:
quoted
quoted
Though the refs themeselves cannot be modified together in a single
"themselves".
Fixed.
quoted
I notice that we are using an array of structures and letting qsort
swap 50~64 bytes of data
Michael suggested this too, so fixed.
Hmmm, I see that you changed the signature of update_refs() to take an
array of pointers. My suggestion was unclear, but I didn't mean that
the function signature had to be changed. Rather, I meant that *within*
the function, you could have created an array of pointers to the
structures in the input array and thereafter accessed it via the pointers:
int update_refs(const char *action, const struct ref_update *updates_orig,
int n, enum action_on_err onerr)
{
[...]
struct ref_update **updates;
[...]
updates = xcalloc(n, sizeof(*updates));
for (i = 0; i < n; i++)
updates[i] = &updates_orig[i];
[...]
}
However, your approach is also fine. It will typically involve more
malloc()s but smaller memcpy()s (i.e., via ALLOC_GROW()) at the caller,
and since usually the number of ref_updates being done at one time will
be limited anyway, I don't see a reason to prefer one version over the
other.
Thanks for making the change.
Michael
--
Michael Haggerty
mhagger@alum.mit.edu
http://softwareswirl.blogspot.com/
@@ -302,4 +302,260 @@ test_expect_success \'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)'\'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'+a=refs/heads/a+b=refs/heads/b+c=refs/heads/c+z=0000000000000000000000000000000000000000+e='""'+pws='path with space'++test_expect_success'stdin fails on bad input line with only whitespace''+echo" ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: no ref on line: "err+'++test_expect_success'stdin fails on bad input line with only --''+echo"--">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: no ref on line: --"err+'++test_expect_success'stdin fails on bad input line with only --bad-option''+echo"--bad-option">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown option --bad-option"err+'
When you decomposed the monolithic test from v1 into individual tests,
you dropped a couple cases ("fatal: unknown option'" and "fatal:
unterminated single-quote"). Was this intentional?
The leading '-' on '-EOF' allows you to indent the content of the
heredoc and the terminating EOF, which makes the test read nicely:
test_expect_success 'stdin fails with duplicate refs' '
cat >stdin <<-EOF &&
$a $m
$b $m
$a $m
EOF
test_must_fail git update-ref ...
'
+
+test_expect_success 'stdin create ref works with no old value' '
+ echo "$a $m" >stdin &&
+ git update-ref --stdin <stdin &&
+ git rev-parse $m >expect &&
+ git rev-parse $a >actual &&
+ test_cmp expect actual
+'
From: Brad King <hidden> Date: 2016-06-15 22:58:36
On 09/03/2013 12:43 AM, Michael Haggerty wrote:
Hmmm, I see that you changed the signature of update_refs() to take an
array of pointers. My suggestion was unclear, but I didn't mean that
the function signature had to be changed.
From: Brad King <hidden> Date: 2016-06-15 22:58:36
On 09/03/2013 04:16 AM, Eric Sunshine wrote:
When you decomposed the monolithic test from v1 into individual tests,
you dropped a couple cases ("fatal: unknown option'" and "fatal:
unterminated single-quote"). Was this intentional?
Yes. The v3 patch 7 changed the set of error messages to be covered.
The leading '-' on '-EOF' allows you to indent the content of the
heredoc and the terminating EOF, which makes the test read nicely:
Very nice. Fixed for next iteration.
Thanks,
-Brad
@@ -302,4 +302,449 @@ test_expect_success \'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)'\'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'+a=refs/heads/a+b=refs/heads/b+c=refs/heads/c+E='""'+pws='path with space'++print_nul(){+whiletest$#-gt0;do+printf--"$1"&&+printf--"Q"|q_to_nul&&+shift||return+done+}++test_expect_success'-z fails without --stdin''+test_must_failgitupdate-ref-z$m$m$m2>err&&+grep"usage: git update-ref"err+'++test_expect_success'stdin test setup''+echo"$pws">"$pws"&&+gitadd--"$pws"&&+gitcommit-m"$pws"+'++test_expect_success'stdin works with no input''+>stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin works with whitespace-only input''+echo" ">stdin&&+gitupdate-ref--stdin<stdin2>err&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin fails on bad input line with only --''+echo"--">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: input line with no ref"err+'++test_expect_success'stdin fails on bad input line with only --bad-option''+echo"--bad-option">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown option --bad-option"err+'++test_expect_success'stdin fails on bad ref name''+echo"~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails on badly quoted input''+echo"$a \"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: badly quoted argument: \\\"master"err+'++test_expect_success'stdin fails on bad input line with too many arguments''+echo"$a$m$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: too many arguments for ref $a"err+'++test_expect_success'stdin fails on bad input line with too few arguments''+echo"$a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: missing new value for ref $a"err+'++test_expect_success'stdin fails with duplicate refs''+cat>stdin<<-EOF&&+$a$m+$b$m+$a$m+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin create ref works with no old value''+echo"$a$m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with zero old value''+echo"$b$m$Z">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin create ref works with empty old value''+echo"$b$m$E">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with path with space to blob''+echo"refs/blobs/pws \"$m:$pws\"">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin create ref fails with wrong old value''+echo"$c$m$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad old value''+echo"$c$m does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad new value''+echo"$c does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref works with right old value''+echo"$b$m~1 $m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update ref fails with wrong old value''+echo"$b$m~1 $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$b'"'"'"err&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with wrong old value''+echo"$a$E$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+echo"--no-deref TESTSYMREF $a$b">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+echo"--no-deref TESTSYMREF $E$b">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref works with right old value''+echo"$b$E$m~1">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin create refs works with some old values''+cat>stdin<<-EOF&&+$a$m+$b$m$Z+$c$Z$Z+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with identity updates''+cat>stdin<<-EOF&&+$a$m$m+$b$m$m+$c$Z$E+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with extra whitespace''+cat>stdin<<-EOF&&+''+$a$m$m+''+"$b"$m$m''+''+--$c$Z$E''+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs fails with wrong old value''+gitupdate-ref$c$m&&+cat>stdin<<-EOF&&+$a$m$m+$b$m$m+$c$E$E+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+cat>stdin<<-EOF&&+$a$Z$m+$b$Z$m+$c$E$m~1+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z works on empty input''+>stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin -z works on empty input sequence''+print_nul"\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin -z fails on unterminated input sequence''+print_nul"$a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unterminated -z input sequence"err+'++test_expect_success'stdin -z create ref works with no old value''+print_nul"$a""$m""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z create ref works with zero old value''+print_nul"$b""$m""$Z""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin -z create ref works with empty old value''+print_nul"$b""$m""""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z create ref works with path with space to blob''+print_nul"refs/blobs/pws""$m:$pws""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin -z create ref fails with wrong old value''+print_nul"$c""$m""$m~1""\n">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with bad old value''+print_nul"$c""$m""does-not-exist""\n">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with bad new value''+print_nul"$c""does-not-exist""\n">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update ref works with right old value''+print_nul"$b""$m~1""$m""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update ref fails with wrong old value''+print_nul"$b""$m~1""$m""\n">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$b'"'"'"err&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref fails with wrong old value''+print_nul"$a""""$m~1""\n">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+print_nul"--no-deref""TESTSYMREF""$a""$b""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete symref works with --no-deref''+gitsymbolic-refTESTSYMREF$b&&+print_nul"--no-deref""TESTSYMREF""""$b""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref works with right old value''+print_nul"$b""""$m~1""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin -z create refs works with some old values''+print_nul"$a""$m""\n""$b""$m""$Z""\n""$c""$Z""$Z""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs works with identity updates''+print_nul"$a""$m""$m""\n""$b""$m""$m""\n""$c""$Z""""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs fails with wrong old value''+gitupdate-ref$c$m&&+print_nul"$a""$m""$m""\n""$b""$m""$m""\n""$c""""""\n">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+print_nul"$a""$Z""$m""\n""$b""$Z""$m""\n""$c""""$m~1""\n">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'+ test_done
From: Brad King <hidden> Date: 2016-06-15 22:58:37
Factor loose ref deletion into helper function delete_ref_loose to allow
later use elsewhere.
Signed-off-by: Brad King <redacted>
---
refs.c | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)
@@ -2450,24 +2450,31 @@ static int repack_without_ref(const char *refname)returncommit_packed_refs();}+staticintdelete_ref_loose(structref_lock*lock,intflag)+{+if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){+/* loose */+interr,i=strlen(lock->lk->filename)-5;/* .lock */++lock->lk->filename[i]=0;+err=unlink_or_warn(lock->lk->filename);+lock->lk->filename[i]='.';+if(err&&errno!=ENOENT)+return1;+}+return0;+}+intdelete_ref(constchar*refname,constunsignedchar*sha1,intdelopt){structref_lock*lock;-interr,i=0,ret=0,flag=0;+intret=0,flag=0;lock=lock_ref_sha1_basic(refname,sha1,delopt,&flag);if(!lock)return1;-if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){-/* loose */-i=strlen(lock->lk->filename)-5;/* .lock */-lock->lk->filename[i]=0;-err=unlink_or_warn(lock->lk->filename);-if(err&&errno!=ENOENT)-ret=1;+ret|=delete_ref_loose(lock,flag);-lock->lk->filename[i]='.';-}/* removing the loose one could have resurrected an earlier*packedone.Also,ifitwasnotlooseweneedtorepack*withoutit.
From: Brad King <hidden> Date: 2016-06-15 22:58:37
Hi Folks,
Here is the fourth revision of a series to support locking multiple
refs at the same time to update all of them consistently. The
previous revisions of the series can be found at $gmane/233260,
$gmane/233458, and $gmane/233647.
Updates since the previous revision of the series:
* Patches 1-4 are identical
* Patches 5-7 no longer have ":" at the end of comments, a style I
"learned" from the context of patch 5 but that I saw Junio squash
out of patch 5 v2 when he queued it as 53237ae4.
* Patch 7 has a re-organized parser and now defines a -z format for
stdin that terminates arguments with NUL and lines with LF NUL:
... <ref> NUL <newvalue> NUL [ <oldvalue> NUL ] LF NUL
* Patch 8 now has test cases for -z mode and updated error cases for
the re-organized parser.
Note to maintainer:
* Patch 4 needs to be re-queued to replace c7c80f49 due to the
tweak suggested in $gmane/233521 and made in v3 and kept in v4.
Thanks,
-Brad
Brad King (8):
reset: rename update_refs to reset_refs
refs: report ref type from lock_any_ref_for_update
refs: factor update_ref steps into helpers
refs: factor delete_ref loose ref step into a helper
refs: add function to repack without multiple refs
refs: add update_refs for multiple simultaneous updates
update-ref: support multiple simultaneous updates
update-ref: add test cases covering --stdin signature
Documentation/git-update-ref.txt | 22 +-
branch.c | 2 +-
builtin/commit.c | 2 +-
builtin/fetch.c | 3 +-
builtin/receive-pack.c | 3 +-
builtin/reflog.c | 2 +-
builtin/replace.c | 2 +-
builtin/reset.c | 4 +-
builtin/tag.c | 2 +-
builtin/update-ref.c | 144 ++++++++++++-
fast-import.c | 2 +-
refs.c | 195 ++++++++++++++---
refs.h | 22 +-
sequencer.c | 3 +-
t/t1400-update-ref.sh | 445 +++++++++++++++++++++++++++++++++++++++
15 files changed, 812 insertions(+), 41 deletions(-)
--
1.8.4.rc3
From: Brad King <hidden> Date: 2016-06-15 22:58:37
Add 'struct ref_update' to encode the information needed to update or
delete a ref (name, new sha1, optional old sha1, no-deref flag). Add
function 'update_refs' accepting an array of updates to perform. First
sort the input array to order locks consistently everywhere and reject
multiple updates to the same ref. Then acquire locks on all refs with
verified old values. Then update or delete all refs accordingly. Fail
if any one lock cannot be obtained or any one old value does not match.
Though the refs themselves cannot be modified together in a single
atomic transaction, this function does enable some useful semantics.
For example, a caller may create a new branch starting from the head of
another branch and rewind the original branch at the same time. This
transfers ownership of commits between branches without risk of losing
commits added to the original branch by a concurrent process, or risk of
a concurrent process creating the new branch first.
Signed-off-by: Brad King <redacted>
---
refs.c | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
refs.h | 20 +++++++++++++
2 files changed, 120 insertions(+)
@@ -3237,6 +3237,106 @@ int update_ref(const char *action, const char *refname,returnupdate_ref_write(action,refname,sha1,lock,onerr);}+staticintref_update_compare(constvoid*r1,constvoid*r2)+{+conststructref_update*const*u1=r1;+conststructref_update*const*u2=r2;+returnstrcmp((*u1)->ref_name,(*u2)->ref_name);+}++staticintref_update_reject_duplicates(structref_update**updates,intn,+enumaction_on_erronerr)+{+inti;+for(i=1;i<n;i++)+if(!strcmp(updates[i-1]->ref_name,updates[i]->ref_name)){+constchar*str=+"Multiple updates for ref '%s' not allowed.";+switch(onerr){+caseMSG_ON_ERR:+error(str,updates[i]->ref_name);break;+caseDIE_ON_ERR:+die(str,updates[i]->ref_name);break;+caseQUIET_ON_ERR:+break;+}+return1;+}+return0;+}++intupdate_refs(constchar*action,conststructref_update**updates_orig,+intn,enumaction_on_erronerr)+{+intret=0,delnum=0,i;+structref_update**updates;+int*types;+structref_lock**locks;+constchar**delnames;++if(!updates_orig||!n)+return0;++/* Allocate work space */+updates=xmalloc(sizeof(*updates)*n);+types=xmalloc(sizeof(*types)*n);+locks=xcalloc(n,sizeof(*locks));+delnames=xmalloc(sizeof(*delnames)*n);++/* Copy, sort, and reject duplicate refs */+memcpy(updates,updates_orig,sizeof(*updates)*n);+qsort(updates,n,sizeof(*updates),ref_update_compare);+ret=ref_update_reject_duplicates(updates,n,onerr);+if(ret)+gotocleanup;++/* Acquire all locks while verifying old values */+for(i=0;i<n;i++){+locks[i]=update_ref_lock(updates[i]->ref_name,+(updates[i]->have_old?+updates[i]->old_sha1:NULL),+updates[i]->flags,+&types[i],onerr);+if(!locks[i]){+ret=1;+gotocleanup;+}+}++/* Perform updates first so live commits remain referenced */+for(i=0;i<n;i++)+if(!is_null_sha1(updates[i]->new_sha1)){+ret=update_ref_write(action,+updates[i]->ref_name,+updates[i]->new_sha1,+locks[i],onerr);+locks[i]=NULL;/* freed by update_ref_write */+if(ret)+gotocleanup;+}++/* Perform deletes now that updates are safely completed */+for(i=0;i<n;i++)+if(locks[i]){+delnames[delnum++]=locks[i]->ref_name;+ret|=delete_ref_loose(locks[i],types[i]);+}+ret|=repack_without_refs(delnames,delnum);+for(i=0;i<delnum;i++)+unlink_or_warn(git_path("logs/%s",delnames[i]));+clear_loose_ref_cache(&ref_cache);++cleanup:+for(i=0;i<n;i++)+if(locks[i])+unlock_ref(locks[i]);+free(updates);+free(types);+free(locks);+free(delnames);+returnret;+}+structref*find_ref_by_name(conststructref*list,constchar*name){for(;list;list=list->next)
From: Brad King <hidden> Date: 2016-06-15 22:58:37
Add a --stdin signature to read update instructions from standard input
and apply multiple ref updates together. Use an input format that
supports any update that could be specified via the command-line,
including object names like "branch:path with space".
Signed-off-by: Brad King <redacted>
---
Documentation/git-update-ref.txt | 22 +++++-
builtin/update-ref.c | 144 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 164 insertions(+), 2 deletions(-)
@@ -58,6 +58,26 @@ archive by creating a symlink tree). With `-d` flag, it deletes the named <ref> after verifying it still contains <oldvalue>.+With `--stdin`, update-ref reads instructions from standard input and+performs all modifications together. Empty lines are ignored.+Each non-empty line is parsed as whitespace-separated arguments.+Quote arguments containing whitespace as if in C source code.+Specify updates with lines of the form:++ [--no-deref] [--] <ref> <newvalue> [<oldvalue>]++Lines of any other format or a repeated <ref> produce an error.+Specify a zero <newvalue> to delete a ref and/or a zero <oldvalue>+to make sure that a ref not exist. Use either 40 "0" or the+empty string (written as "") to specify a zero value. Use `-z`+to specify input with no whitespace, quoting, or escaping, and+terminate each argument by NUL and each line by LF NUL.++If all <ref>s can be locked with matching <oldvalue>s+simultaneously, all modifications are performed. Otherwise, no+modifications are performed. Note that while each individual+<ref> is updated or deleted atomically, a concurrent reader may+still see a subset of the modifications. Logging Updates ---------------
@@ -2,23 +2,152 @@#include"refs.h"#include"builtin.h"#include"parse-options.h"+#include"quote.h"+#include"argv-array.h"staticconstchar*constgit_update_ref_usage[]={N_("git update-ref [options] -d <refname> [<oldval>]"),N_("git update-ref [options] <refname> <newval> [<oldval>]"),+N_("git update-ref [options] --stdin [-z]"),NULL};+staticintupdates_alloc;+staticintupdates_count;+staticconststructref_update**updates;++staticvoidupdate_refs_stdin(intargc,constchar**argv)+{+structref_update*update;++/* Skip blank lines */+if(!argc)+return;++/* Allocate and zero-init a struct ref_update */+update=xcalloc(1,sizeof(*update));+ALLOC_GROW(updates,updates_count+1,updates_alloc);+updates[updates_count++]=update;++/* Process options */+while(argc>0&&argv[0][0]=='-'){+constchar*arg=argv[0];+--argc;+++argv;+if(!strcmp(arg,"--no-deref"))+update->flags|=REF_NODEREF;+elseif(!strcmp(arg,"--"))+break;+else+die("unknown option %s",arg);+}++/* Set the update ref_name */+if(argc<1)+die("input line with no ref!");+if(check_refname_format(argv[0],REFNAME_ALLOW_ONELEVEL))+die("invalid ref format: %s",argv[0]);+update->ref_name=xstrdup(argv[0]);++/* Set the update new_sha1 and, if specified, old_sha1 */+if(argc<2)+die("missing new value for ref %s",update->ref_name);+if(*argv[1]&&get_sha1(argv[1],update->new_sha1))+die("invalid new value for ref %s: %s",+update->ref_name,argv[1]);+if(argc>=3){+update->have_old=1;+if(*argv[2]&&get_sha1(argv[2],update->old_sha1))+die("invalid old value for ref %s: %s",+update->ref_name,argv[2]);+}++if(argc>3)+die("too many arguments for ref %s",update->ref_name);+}++staticconstchar*update_refs_stdin_parse_arg(constchar*next,+structstrbuf*arg)+{+/* Skip leading whitespace */+while(isspace(*next))+++next;++/* Return NULL when no argument is found */+if(!*next)+returnNULL;++/* Parse the argument */+strbuf_reset(arg);+if(*next=='"'){+if(unquote_c_style(arg,next,&next))+die("badly quoted argument: %s",next);+returnnext;+}+while(*next&&!isspace(*next))+strbuf_addch(arg,*next++);+returnnext;+}++staticvoidupdate_refs_stdin_parse_line(constchar*next)+{+structstrbufarg=STRBUF_INIT;+staticstructargv_arrayargs=ARGV_ARRAY_INIT;++/* Parse arguments on this line */+while((next=update_refs_stdin_parse_arg(next,&arg))!=NULL)+argv_array_push(&args,arg.buf);++/* Process this command */+update_refs_stdin(args.argc,args.argv);++argv_array_clear(&args);+strbuf_release(&arg);+}++staticvoidupdate_refs_stdin_read_n()+{+structstrbufline=STRBUF_INIT;++while(strbuf_getline(&line,stdin,'\n')!=EOF)+update_refs_stdin_parse_line(line.buf);++strbuf_release(&line);+}++staticvoidupdate_refs_stdin_read_z()+{+structstrbufarg=STRBUF_INIT;+staticstructargv_arrayargs=ARGV_ARRAY_INIT;++/* Process NUL-terminated arguments with commands ending in LF */+while(strbuf_getline(&arg,stdin,'\0')!=EOF){+if(!strcmp(arg.buf,"\n")){+update_refs_stdin(args.argc,args.argv);+argv_array_clear(&args);+}else{+argv_array_push(&args,arg.buf);+}+}++if(args.argc>0)+die("unterminated -z input sequence");++strbuf_release(&arg);+}+intcmd_update_ref(intargc,constchar**argv,constchar*prefix){constchar*refname,*oldval,*msg=NULL;unsignedcharsha1[20],oldsha1[20];-intdelete=0,no_deref=0,flags=0;+intdelete=0,no_deref=0,read_stdin=0,end_null=0,flags=0;structoptionoptions[]={OPT_STRING('m',NULL,&msg,N_("reason"),N_("reason of the update")),OPT_BOOLEAN('d',NULL,&delete,N_("delete the reference")),+OPT_BOOLEAN('z',NULL,&end_null,N_("stdin has NUL-terminated arguments")),OPT_BOOLEAN(0,"no-deref",&no_deref,N_("update <refname> not the one it points to")),+OPT_BOOLEAN(0,"stdin",&read_stdin,N_("read updates from stdin")),OPT_END(),};
@@ -28,6 +157,19 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)if(msg&&!*msg)die("Refusing to perform update with empty message.");+if(read_stdin){+if(delete||no_deref||argc>0)+usage_with_options(git_update_ref_usage,options);+if(end_null)+update_refs_stdin_read_z();+else+update_refs_stdin_read_n();+returnupdate_refs(msg,updates,updates_count,DIE_ON_ERR);+}++if(end_null)+usage_with_options(git_update_ref_usage,options);+if(delete){if(argc<1||argc>2)usage_with_options(git_update_ref_usage,options);
From: Brad King <hidden> Date: 2016-06-15 22:58:37
Factor the lock and write steps and error handling into helper functions
update_ref_lock and update_ref_write to allow later use elsewhere.
Expose lock_any_ref_for_update's type_p to update_ref_lock callers.
While at it, drop "static" from the local "lock" variable as it is not
necessary to keep across invocations.
Signed-off-by: Brad King <redacted>
---
refs.c | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
@@ -137,7 +137,7 @@ extern struct ref_lock *lock_ref_sha1(const char *refname, const unsigned char *#define REF_NODEREF 0x01externstructref_lock*lock_any_ref_for_update(constchar*refname,constunsignedchar*old_sha1,-intflags);+intflags,int*type_p);/** Close the file descriptor owned by a lock and return the status */externintclose_ref(structref_lock*lock);
From: Brad King <hidden> Date: 2016-06-15 22:58:37
Generalize repack_without_ref as repack_without_refs to support a list
of refs and implement the former in terms of the latter.
Signed-off-by: Brad King <redacted>
---
refs.c | 33 ++++++++++++++++++++++++---------
1 file changed, 24 insertions(+), 9 deletions(-)
@@ -2414,42 +2414,57 @@ static int curate_packed_ref_fn(struct ref_entry *entry, void *cb_data)return0;}-staticintrepack_without_ref(constchar*refname)+staticintrepack_without_refs(constchar**refnames,intn){structref_dir*packed;structstring_listrefs_to_delete=STRING_LIST_INIT_DUP;structstring_list_item*ref_to_delete;+inti,removed=0;++/* Look for a packed ref */+for(i=0;i<n;i++)+if(get_packed_ref(refnames[i]))+break;-if(!get_packed_ref(refname))-return0;/* refname does not exist in packed refs */+/* Avoid locking if we have nothing to do */+if(i==n)+return0;/* no refname exists in packed refs */if(lock_packed_refs(0)){unable_to_lock_error(git_path("packed-refs"),errno);-returnerror("cannot delete '%s' from packed refs",refname);+returnerror("cannot delete '%s' from packed refs",refnames[i]);}packed=get_packed_refs(&ref_cache);-/* Remove refname from the cache: */-if(remove_entry(packed,refname)==-1){+/* Remove refnames from the cache */+for(i=0;i<n;i++)+if(remove_entry(packed,refnames[i])!=-1)+removed=1;+if(!removed){/*-*Thepackedentrydisappearedwhilewewere+*Allpackedentriesdisappearedwhilewewere*acquiringthelock.*/rollback_packed_refs();return0;}-/* Remove any other accumulated cruft: */+/* Remove any other accumulated cruft */do_for_each_entry_in_dir(packed,0,curate_packed_ref_fn,&refs_to_delete);for_each_string_list_item(ref_to_delete,&refs_to_delete){if(remove_entry(packed,ref_to_delete->string)==-1)die("internal error");}-/* Write what remains: */+/* Write what remains */returncommit_packed_refs();}+staticintrepack_without_ref(constchar*refname)+{+returnrepack_without_refs(&refname,1);+}+staticintdelete_ref_loose(structref_lock*lock,intflag){if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){
From: Brad King <hidden> Date: 2016-06-15 22:58:37
The function resets refs rather than doing arbitrary updates.
Rename it to allow a future general-purpose update_refs function
to be added.
Signed-off-by: Brad King <redacted>
---
builtin/reset.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -350,7 +350,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)if(!pathspec&&!unborn){/* Any resets without paths update HEAD to the head being*switchedto,savingthepreviousheadinORIG_HEADbefore.*/-update_ref_status=update_refs(rev,sha1);+update_ref_status=reset_refs(rev,sha1);if(reset_type==HARD&&!update_ref_status&&!quiet)print_new_head_line(lookup_commit_reference(sha1));
From: Brad King <hidden> Date: 2016-06-15 22:58:41
Hi Folks,
Here is the fifth revision of a series to support locking multiple
refs at the same time to update all of them consistently. The
previous revisions of the series can be found at $gmane/233260,
$gmane/233458, $gmane/233647, and $gmane/233840.
Updates since the previous revision of the series:
* Patches 1-6 are identical to v4 so are not re-sent here.
* Patch 7 and 8 now implement and test the input format proposed and
discussed at $gmane/233990.
-Brad
Brad King (2):
update-ref: support multiple simultaneous updates
update-ref: add test cases covering --stdin signature
Documentation/git-update-ref.txt | 54 +++-
builtin/update-ref.c | 252 ++++++++++++++-
t/t1400-update-ref.sh | 639 +++++++++++++++++++++++++++++++++++++++
3 files changed, 943 insertions(+), 2 deletions(-)
--
1.8.4.rc3
From: Brad King <hidden> Date: 2016-06-15 22:58:41
Add a --stdin signature to read update instructions from standard input
and apply multiple ref updates together. Use an input format that
supports any update that could be specified via the command-line,
including object names like "branch:path with space".
Signed-off-by: Brad King <redacted>
---
Documentation/git-update-ref.txt | 54 ++++++++-
builtin/update-ref.c | 252 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 304 insertions(+), 2 deletions(-)
@@ -58,6 +58,58 @@ archive by creating a symlink tree). With `-d` flag, it deletes the named <ref> after verifying it still contains <oldvalue>.+With `--stdin`, update-ref reads instructions from standard input and+performs all modifications together. Specify commands of the form:++ update SP <ref> SP <newvalue> [SP <oldvalue>] LF+ create SP <ref> SP <newvalue> LF+ delete SP <ref> [SP <oldvalue>] LF+ verify SP <ref> [SP <oldvalue>] LF+ option SP <opt> LF++Quote fields containing whitespace as if they were strings in C source+code. Alternatively, use `-z` to specify commands without quoting:++ update SP <ref> NUL <newvalue> NUL [<oldvalue>] NUL+ create SP <ref> NUL <newvalue> NUL+ delete SP <ref> NUL [<oldvalue>] NUL+ verify SP <ref> NUL [<oldvalue>] NUL+ option SP <opt> NUL++Lines of any other format or a repeated <ref> produce an error.+Command meanings are:++update::+ Set <ref> to <newvalue> after verifying <oldvalue>, if given.+ Specify a zero <newvalue> to ensure the ref does not exist+ after the update and/or a zero <oldvalue> to make sure the+ ref does not exist before the update.++create::+ Create <ref> with <newvalue> after verifying it does not+ exist. The given <newvalue> may not be zero.++delete::+ Delete <ref> after verifying it exists with <oldvalue>, if+ given. If given, <oldvalue> may not be zero.++verify::+ Verify <ref> against <oldvalue> but do not change it. If+ <oldvalue> zero or missing, the ref must not exist.++option::+ Modify behavior of the next command naming a <ref>.+ The only valid option is `no-deref` to avoid dereferencing+ a symbolic ref.++Use 40 "0" or the empty string to specify a zero value, except that+with `-z` an empty <oldvalue> is considered missing.++If all <ref>s can be locked with matching <oldvalue>s+simultaneously, all modifications are performed. Otherwise, no+modifications are performed. Note that while each individual+<ref> is updated or deleted atomically, a concurrent reader may+still see a subset of the modifications. Logging Updates ---------------
@@ -2,23 +2,261 @@#include"refs.h"#include"builtin.h"#include"parse-options.h"+#include"quote.h"+#include"argv-array.h"staticconstchar*constgit_update_ref_usage[]={N_("git update-ref [options] -d <refname> [<oldval>]"),N_("git update-ref [options] <refname> <newval> [<oldval>]"),+N_("git update-ref [options] --stdin [-z]"),NULL};+staticintupdates_alloc;+staticintupdates_count;+staticconststructref_update**updates;++staticcharline_termination='\n';+staticintupdate_flags;++staticstructref_update*update_alloc(void)+{+structref_update*update;++/* Allocate and zero-init a struct ref_update */+update=xcalloc(1,sizeof(*update));+ALLOC_GROW(updates,updates_count+1,updates_alloc);+updates[updates_count++]=update;++/* Store and reset accumulated options */+update->flags=update_flags;+update_flags=0;++returnupdate;+}++staticvoidupdate_store_ref_name(structref_update*update,+constchar*ref_name)+{+if(check_refname_format(ref_name,REFNAME_ALLOW_ONELEVEL))+die("invalid ref format: %s",ref_name);+update->ref_name=xstrdup(ref_name);+}++staticvoidupdate_store_new_sha1(structref_update*update,+constchar*newvalue)+{+if(*newvalue&&get_sha1(newvalue,update->new_sha1))+die("invalid new value for ref %s: %s",+update->ref_name,newvalue);+}++staticvoidupdate_store_old_sha1(structref_update*update,+constchar*oldvalue)+{+if(*oldvalue&&get_sha1(oldvalue,update->old_sha1))+die("invalid old value for ref %s: %s",+update->ref_name,oldvalue);++/* We have an old value if non-empty, or if empty without -z */+update->have_old=*oldvalue||line_termination;+}++staticconstchar*parse_arg(constchar*next,structstrbuf*arg)+{+/* Parse SP-terminated, possibly C-quoted argument */+if(*next!='"')+while(*next&&!isspace(*next))+strbuf_addch(arg,*next++);+elseif(unquote_c_style(arg,next,&next))+die("badly quoted argument: %s",next);++/* Return position after the argument */+returnnext;+}++staticconstchar*parse_first_arg(constchar*next,structstrbuf*arg)+{+/* Parse argument immediately after "command SP" */+strbuf_reset(arg);+if(line_termination){+/* Without -z, use the next argument */+next=parse_arg(next,arg);+}else{+/* With -z, use rest of first NUL-terminated line */+strbuf_addstr(arg,next);+next=next+arg->len;+}+returnnext;+}++staticconstchar*parse_next_arg(constchar*next,structstrbuf*arg)+{+/* Parse next SP-terminated or NUL-terminated argument, if any */+strbuf_reset(arg);+if(line_termination){+/* Without -z, consume SP and use next argument */+if(!*next)+returnNULL;+if(*next!=' ')+die("expected SP but got: %s",next);+next=parse_arg(next+1,arg);+}else{+/* With -z, read the next NUL-terminated line */+if(*next)+die("expected NUL but got: %s",next);+if(strbuf_getline(arg,stdin,'\0')==EOF)+returnNULL;+next=arg->buf+arg->len;+}+returnnext;+}++staticvoidparse_cmd_update(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufnewvalue=STRBUF_INIT;+structstrbufoldvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("update line missing <ref>");++if((next=parse_next_arg(next,&newvalue))!=NULL)+update_store_new_sha1(update,newvalue.buf);+else+die("update %s missing <newvalue>",ref.buf);++if((next=parse_next_arg(next,&oldvalue))!=NULL)+update_store_old_sha1(update,oldvalue.buf);+elseif(!line_termination)+die("update %s missing [<oldvalue>] NUL",ref.buf);++if(next&&*next)+die("update %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_create(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufnewvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("create line missing <ref>");++if((next=parse_next_arg(next,&newvalue))!=NULL)+update_store_new_sha1(update,newvalue.buf);+else+die("create %s missing <newvalue>",ref.buf);+if(is_null_sha1(update->new_sha1))+die("create %s given zero new value",ref.buf);++if(next&&*next)+die("create %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_delete(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufoldvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("delete line missing <ref>");++if((next=parse_next_arg(next,&oldvalue))!=NULL)+update_store_old_sha1(update,oldvalue.buf);+elseif(!line_termination)+die("delete %s missing [<oldvalue>] NUL",ref.buf);+if(update->have_old&&is_null_sha1(update->old_sha1))+die("delete %s given zero old value",ref.buf);++if(next&&*next)+die("delete %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_verify(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("verify line missing <ref>");++if((next=parse_next_arg(next,&value))!=NULL){+update_store_old_sha1(update,value.buf);+update_store_new_sha1(update,value.buf);+}elseif(!line_termination)+die("verify %s missing [<oldvalue>] NUL",ref.buf);++if(next&&*next)+die("verify %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_option(constchar*next)+{+if(!strcmp(next,"no-deref"))+update_flags|=REF_NODEREF;+else+die("option unknown: %s",next);+}++staticvoidupdate_refs_stdin(void)+{+structstrbufcmd=STRBUF_INIT;++/* Read each line dispatch its command */+while(strbuf_getline(&cmd,stdin,line_termination)!=EOF)+if(!cmd.buf[0])+die("empty command in input");+elseif(isspace(*cmd.buf))+die("whitespace before command: %s",cmd.buf);+elseif(!prefixcmp(cmd.buf,"update "))+parse_cmd_update(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"create "))+parse_cmd_create(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"delete "))+parse_cmd_delete(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"verify "))+parse_cmd_verify(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"option "))+parse_cmd_option(cmd.buf+7);+else+die("unknown command: %s",cmd.buf);++strbuf_release(&cmd);+}+intcmd_update_ref(intargc,constchar**argv,constchar*prefix){constchar*refname,*oldval,*msg=NULL;unsignedcharsha1[20],oldsha1[20];-intdelete=0,no_deref=0,flags=0;+intdelete=0,no_deref=0,read_stdin=0,end_null=0,flags=0;structoptionoptions[]={OPT_STRING('m',NULL,&msg,N_("reason"),N_("reason of the update")),OPT_BOOLEAN('d',NULL,&delete,N_("delete the reference")),+OPT_BOOLEAN('z',NULL,&end_null,N_("stdin has NUL-terminated arguments")),OPT_BOOLEAN(0,"no-deref",&no_deref,N_("update <refname> not the one it points to")),+OPT_BOOLEAN(0,"stdin",&read_stdin,N_("read updates from stdin")),OPT_END(),};
@@ -28,6 +266,18 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)if(msg&&!*msg)die("Refusing to perform update with empty message.");+if(read_stdin){+if(delete||no_deref||argc>0)+usage_with_options(git_update_ref_usage,options);+if(end_null)+line_termination='\0';+update_refs_stdin();+returnupdate_refs(msg,updates,updates_count,DIE_ON_ERR);+}++if(end_null)+usage_with_options(git_update_ref_usage,options);+if(delete){if(argc<1||argc>2)usage_with_options(git_update_ref_usage,options);
@@ -302,4 +302,643 @@ test_expect_success \'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)'\'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'+a=refs/heads/a+b=refs/heads/b+c=refs/heads/c+E='""'+pws='path with space'++print_nul(){+whiletest$#-gt0;do+printf--"$1"&&+printf--"Q"|q_to_nul&&+shift||return+done+}++test_expect_success'stdin test setup''+echo"$pws">"$pws"&&+gitadd--"$pws"&&+gitcommit-m"$pws"+'++test_expect_success'-z fails without --stdin''+test_must_failgitupdate-ref-z$m$m$m2>err&&+grep"usage: git update-ref"err+'++test_expect_success'stdin works with no input''+>stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin fails on empty line''+echo"">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: empty command in input"err+'++test_expect_success'stdin fails on only whitespace''+echo" ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin fails on leading whitespace''+echo" create $a$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: whitespace before command: create $a$m"err+'++test_expect_success'stdin fails on unknown command''+echo"unknown $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown command: unknown $a"err+'++test_expect_success'stdin fails on badly quoted input''+echo"create $a \"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: badly quoted argument: \\\"master"err+'++test_expect_success'stdin fails on arguments not separated by space''+echo"create \"$a\"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: expected SP but got: master"err+'++test_expect_success'stdin fails create with no ref''+echo"create ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create line missing <ref>"err+'++test_expect_success'stdin fails create with bad ref name''+echo"create ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails create with no new value''+echo"create $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $a missing <newvalue>"err+'++test_expect_success'stdin fails create with too many arguments''+echo"create $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $a has extra input: $m"err+'++test_expect_success'stdin fails update with no ref''+echo"update ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update line missing <ref>"err+'++test_expect_success'stdin fails update with bad ref name''+echo"update ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails update with no new value''+echo"update $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update $a missing <newvalue>"err+'++test_expect_success'stdin fails update with too many arguments''+echo"update $a$m$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update $a has extra input: $m"err+'++test_expect_success'stdin fails delete with no ref''+echo"delete ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete line missing <ref>"err+'++test_expect_success'stdin fails delete with bad ref name''+echo"delete ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails delete with too many arguments''+echo"delete $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete $a has extra input: $m"err+'++test_expect_success'stdin fails verify with too many arguments''+echo"verify $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: verify $a has extra input: $m"err+'++test_expect_success'stdin fails option with unknown name''+echo"option unknown">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: option unknown: unknown"err+'++test_expect_success'stdin fails with duplicate refs''+cat>stdin<<-EOF&&+create$a$m+create$b$m+create$a$m+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin create ref works''+echo"create $a$m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update ref creates with zero old value''+echo"update $b$m$Z">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin update ref creates with empty old value''+echo"update $b$m$E">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with path with space to blob''+echo"create refs/blobs/pws \"$m:$pws\"">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin update ref fails with wrong old value''+echo"update $c$m$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref fails with bad old value''+echo"update $c$m does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad new value''+echo"create $c does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with zero new value''+echo"create $c ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $c given zero new value"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref works with right old value''+echo"update $b$m~1 $m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with wrong old value''+echo"delete $a$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with zero old value''+echo"delete $a ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete $a given zero old value"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+cat>stdin<<-EOF&&+optionno-deref+updateTESTSYMREF$a$b+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+cat>stdin<<-EOF&&+optionno-deref+deleteTESTSYMREF$b+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref works with right old value''+echo"delete $b$m~1">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin update/create/verify combination works''+cat>stdin<<-EOF&&+update$a$m+create$b$m+verify$c+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with identity updates''+cat>stdin<<-EOF&&+update$a$m$m+update$b$m$m+update$c$Z$E+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs fails with wrong old value''+gitupdate-ref$c$m&&+cat>stdin<<-EOF&&+update$a$m$m+update$b$m$m+update$c''+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+cat>stdin<<-EOF&&+delete$a$m+update$b$Z$m+update$c$E$m~1+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z works on empty input''+>stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin -z fails on empty line''+echo"">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin -z fails on empty command''+print_nul"">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: empty command in input"err+'++test_expect_success'stdin -z fails on only whitespace''+print_nul" ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin -z fails on leading whitespace''+print_nul" create $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: create $a"err+'++test_expect_success'stdin -z fails on unknown command''+print_nul"unknown $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: unknown $a"err+'++test_expect_success'stdin -z fails create with no ref''+print_nul"create ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create line missing <ref>"err+'++test_expect_success'stdin -z fails create with bad ref name''+print_nul"create ~a ""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a "err+'++test_expect_success'stdin -z fails create with no new value''+print_nul"create $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create $a missing <newvalue>"err+'++test_expect_success'stdin -z fails create with too many arguments''+print_nul"create $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails update with no ref''+print_nul"update ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update line missing <ref>"err+'++test_expect_success'stdin -z fails update with bad ref name''+print_nul"update ~a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin -z fails update with no new value''+print_nul"update $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update $a missing <newvalue>"err+'++test_expect_success'stdin -z fails update with no old value''+print_nul"update $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails update with too many arguments''+print_nul"update $a""$m""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails delete with no ref''+print_nul"delete ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete line missing <ref>"err+'++test_expect_success'stdin -z fails delete with bad ref name''+print_nul"delete ~a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin -z fails delete with no old value''+print_nul"delete $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails delete with too many arguments''+print_nul"delete $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails verify with too many arguments''+print_nul"verify $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails verify with no old value''+print_nul"verify $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: verify $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails option with unknown name''+print_nul"option unknown">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: option unknown: unknown"err+'++test_expect_success'stdin -z fails with duplicate refs''+print_nul"create $a""$m""create $b""$m""create $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin -z create ref works''+print_nul"create $a""$m">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update ref creates with zero old value''+print_nul"update $b""$m""$Z">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin -z update ref creates with empty old value''+print_nul"update $b""$m""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z create ref works with path with space to blob''+print_nul"create refs/blobs/pws""$m:$pws">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin -z update ref fails with wrong old value''+print_nul"update $c""$m""$m~1">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update ref fails with bad old value''+print_nul"update $c""$m""does-not-exist">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with bad new value''+print_nul"create $c""does-not-exist">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with zero new value''+print_nul"create $c""">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create $c given zero new value"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update ref works with right old value''+print_nul"update $b""$m~1""$m">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref fails with wrong old value''+print_nul"delete $a""$m~1">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref fails with zero old value''+print_nul"delete $a""$Z">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete $a given zero old value"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+print_nul"option no-deref""update TESTSYMREF""$a""$b">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+print_nul"option no-deref""delete TESTSYMREF""$b">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref works with right old value''+print_nul"delete $b""$m~1">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin -z update/create/verify combination works''+print_nul"update $a""$m""""create $b""$m""verify $c""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs works with identity updates''+print_nul"update $a""$m""$m""update $b""$m""$m""update $c""$Z""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs fails with wrong old value''+gitupdate-ref$c$m&&+print_nul"update $a""$m""$m""update $b""$m""$m""update $c""""$Z">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+print_nul"delete $a""$m""update $b""$Z""$m""update $c""""$m~1">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'+ test_done
From: Brad King <hidden> Date: 2016-06-15 22:58:42
Hi Folks,
Here is the sixth revision of a series to support locking multiple
refs at the same time to update all of them consistently. The
previous revisions of the series can be found at $gmane/233260,
$gmane/233458, $gmane/233647, $gmane/233840, and $gmane/234324.
Updates since the previous revision of the series:
* The entire series was rebased on master at bb80ee09; it was
previously based on v1.8.4.
* A conflict in refs.c with 47a59185 was resolved by preserving
the elimination of find_ref_by_name while adding our new content.
* A conflict in builtin/update-ref.c with d5d09d47 (Replace deprecated
OPT_BOOLEAN by OPT_BOOL, 2013-08-03) was resolved by integrating
both changes. The new options added in patch 7 now use OPT_BOOL.
-Brad
Brad King (8):
reset: rename update_refs to reset_refs
refs: report ref type from lock_any_ref_for_update
refs: factor update_ref steps into helpers
refs: factor delete_ref loose ref step into a helper
refs: add function to repack without multiple refs
refs: add update_refs for multiple simultaneous updates
update-ref: support multiple simultaneous updates
update-ref: add test cases covering --stdin signature
Documentation/git-update-ref.txt | 54 +++-
branch.c | 2 +-
builtin/commit.c | 2 +-
builtin/fetch.c | 3 +-
builtin/receive-pack.c | 3 +-
builtin/reflog.c | 2 +-
builtin/replace.c | 2 +-
builtin/reset.c | 4 +-
builtin/tag.c | 2 +-
builtin/update-ref.c | 252 ++++++++++++++-
fast-import.c | 2 +-
refs.c | 195 ++++++++++--
refs.h | 22 +-
sequencer.c | 3 +-
t/t1400-update-ref.sh | 639 +++++++++++++++++++++++++++++++++++++++
15 files changed, 1146 insertions(+), 41 deletions(-)
--
1.8.4.rc3
@@ -302,4 +302,643 @@ test_expect_success \'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)'\'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'+a=refs/heads/a+b=refs/heads/b+c=refs/heads/c+E='""'+pws='path with space'++print_nul(){+whiletest$#-gt0;do+printf--"$1"&&+printf--"Q"|q_to_nul&&+shift||return+done+}++test_expect_success'stdin test setup''+echo"$pws">"$pws"&&+gitadd--"$pws"&&+gitcommit-m"$pws"+'++test_expect_success'-z fails without --stdin''+test_must_failgitupdate-ref-z$m$m$m2>err&&+grep"usage: git update-ref"err+'++test_expect_success'stdin works with no input''+>stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin fails on empty line''+echo"">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: empty command in input"err+'++test_expect_success'stdin fails on only whitespace''+echo" ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin fails on leading whitespace''+echo" create $a$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: whitespace before command: create $a$m"err+'++test_expect_success'stdin fails on unknown command''+echo"unknown $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown command: unknown $a"err+'++test_expect_success'stdin fails on badly quoted input''+echo"create $a \"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: badly quoted argument: \\\"master"err+'++test_expect_success'stdin fails on arguments not separated by space''+echo"create \"$a\"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: expected SP but got: master"err+'++test_expect_success'stdin fails create with no ref''+echo"create ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create line missing <ref>"err+'++test_expect_success'stdin fails create with bad ref name''+echo"create ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails create with no new value''+echo"create $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $a missing <newvalue>"err+'++test_expect_success'stdin fails create with too many arguments''+echo"create $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $a has extra input: $m"err+'++test_expect_success'stdin fails update with no ref''+echo"update ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update line missing <ref>"err+'++test_expect_success'stdin fails update with bad ref name''+echo"update ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails update with no new value''+echo"update $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update $a missing <newvalue>"err+'++test_expect_success'stdin fails update with too many arguments''+echo"update $a$m$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update $a has extra input: $m"err+'++test_expect_success'stdin fails delete with no ref''+echo"delete ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete line missing <ref>"err+'++test_expect_success'stdin fails delete with bad ref name''+echo"delete ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails delete with too many arguments''+echo"delete $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete $a has extra input: $m"err+'++test_expect_success'stdin fails verify with too many arguments''+echo"verify $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: verify $a has extra input: $m"err+'++test_expect_success'stdin fails option with unknown name''+echo"option unknown">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: option unknown: unknown"err+'++test_expect_success'stdin fails with duplicate refs''+cat>stdin<<-EOF&&+create$a$m+create$b$m+create$a$m+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin create ref works''+echo"create $a$m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update ref creates with zero old value''+echo"update $b$m$Z">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin update ref creates with empty old value''+echo"update $b$m$E">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with path with space to blob''+echo"create refs/blobs/pws \"$m:$pws\"">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin update ref fails with wrong old value''+echo"update $c$m$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref fails with bad old value''+echo"update $c$m does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad new value''+echo"create $c does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with zero new value''+echo"create $c ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $c given zero new value"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref works with right old value''+echo"update $b$m~1 $m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with wrong old value''+echo"delete $a$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with zero old value''+echo"delete $a ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete $a given zero old value"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+cat>stdin<<-EOF&&+optionno-deref+updateTESTSYMREF$a$b+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+cat>stdin<<-EOF&&+optionno-deref+deleteTESTSYMREF$b+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref works with right old value''+echo"delete $b$m~1">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin update/create/verify combination works''+cat>stdin<<-EOF&&+update$a$m+create$b$m+verify$c+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with identity updates''+cat>stdin<<-EOF&&+update$a$m$m+update$b$m$m+update$c$Z$E+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs fails with wrong old value''+gitupdate-ref$c$m&&+cat>stdin<<-EOF&&+update$a$m$m+update$b$m$m+update$c''+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+cat>stdin<<-EOF&&+delete$a$m+update$b$Z$m+update$c$E$m~1+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z works on empty input''+>stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin -z fails on empty line''+echo"">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin -z fails on empty command''+print_nul"">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: empty command in input"err+'++test_expect_success'stdin -z fails on only whitespace''+print_nul" ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin -z fails on leading whitespace''+print_nul" create $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: create $a"err+'++test_expect_success'stdin -z fails on unknown command''+print_nul"unknown $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: unknown $a"err+'++test_expect_success'stdin -z fails create with no ref''+print_nul"create ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create line missing <ref>"err+'++test_expect_success'stdin -z fails create with bad ref name''+print_nul"create ~a ""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a "err+'++test_expect_success'stdin -z fails create with no new value''+print_nul"create $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create $a missing <newvalue>"err+'++test_expect_success'stdin -z fails create with too many arguments''+print_nul"create $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails update with no ref''+print_nul"update ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update line missing <ref>"err+'++test_expect_success'stdin -z fails update with bad ref name''+print_nul"update ~a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin -z fails update with no new value''+print_nul"update $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update $a missing <newvalue>"err+'++test_expect_success'stdin -z fails update with no old value''+print_nul"update $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails update with too many arguments''+print_nul"update $a""$m""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails delete with no ref''+print_nul"delete ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete line missing <ref>"err+'++test_expect_success'stdin -z fails delete with bad ref name''+print_nul"delete ~a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin -z fails delete with no old value''+print_nul"delete $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails delete with too many arguments''+print_nul"delete $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails verify with too many arguments''+print_nul"verify $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails verify with no old value''+print_nul"verify $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: verify $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails option with unknown name''+print_nul"option unknown">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: option unknown: unknown"err+'++test_expect_success'stdin -z fails with duplicate refs''+print_nul"create $a""$m""create $b""$m""create $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin -z create ref works''+print_nul"create $a""$m">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update ref creates with zero old value''+print_nul"update $b""$m""$Z">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin -z update ref creates with empty old value''+print_nul"update $b""$m""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z create ref works with path with space to blob''+print_nul"create refs/blobs/pws""$m:$pws">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin -z update ref fails with wrong old value''+print_nul"update $c""$m""$m~1">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update ref fails with bad old value''+print_nul"update $c""$m""does-not-exist">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with bad new value''+print_nul"create $c""does-not-exist">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with zero new value''+print_nul"create $c""">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create $c given zero new value"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update ref works with right old value''+print_nul"update $b""$m~1""$m">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref fails with wrong old value''+print_nul"delete $a""$m~1">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref fails with zero old value''+print_nul"delete $a""$Z">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete $a given zero old value"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+print_nul"option no-deref""update TESTSYMREF""$a""$b">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+print_nul"option no-deref""delete TESTSYMREF""$b">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref works with right old value''+print_nul"delete $b""$m~1">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin -z update/create/verify combination works''+print_nul"update $a""$m""""create $b""$m""verify $c""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs works with identity updates''+print_nul"update $a""$m""$m""update $b""$m""$m""update $c""$Z""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs fails with wrong old value''+gitupdate-ref$c$m&&+print_nul"update $a""$m""$m""update $b""$m""$m""update $c""""$Z">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+print_nul"delete $a""$m""update $b""$Z""$m""update $c""""$m~1">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'+ test_done
From: Brad King <hidden> Date: 2016-06-15 22:58:42
The function resets refs rather than doing arbitrary updates.
Rename it to allow a future general-purpose update_refs function
to be added.
Signed-off-by: Brad King <redacted>
---
builtin/reset.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -357,7 +357,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)if(!pathspec.nr&&!unborn){/* Any resets without paths update HEAD to the head being*switchedto,savingthepreviousheadinORIG_HEADbefore.*/-update_ref_status=update_refs(rev,sha1);+update_ref_status=reset_refs(rev,sha1);if(reset_type==HARD&&!update_ref_status&&!quiet)print_new_head_line(lookup_commit_reference(sha1));
From: Brad King <hidden> Date: 2016-06-15 22:58:42
Factor the lock and write steps and error handling into helper functions
update_ref_lock and update_ref_write to allow later use elsewhere.
Expose lock_any_ref_for_update's type_p to update_ref_lock callers.
While at it, drop "static" from the local "lock" variable as it is not
necessary to keep across invocations.
Signed-off-by: Brad King <redacted>
---
refs.c | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
@@ -137,7 +137,7 @@ extern struct ref_lock *lock_ref_sha1(const char *refname, const unsigned char *#define REF_NODEREF 0x01externstructref_lock*lock_any_ref_for_update(constchar*refname,constunsignedchar*old_sha1,-intflags);+intflags,int*type_p);/** Close the file descriptor owned by a lock and return the status */externintclose_ref(structref_lock*lock);
From: Brad King <hidden> Date: 2016-06-15 22:58:42
Factor loose ref deletion into helper function delete_ref_loose to allow
later use elsewhere.
Signed-off-by: Brad King <redacted>
---
refs.c | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)
@@ -2450,24 +2450,31 @@ static int repack_without_ref(const char *refname)returncommit_packed_refs();}+staticintdelete_ref_loose(structref_lock*lock,intflag)+{+if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){+/* loose */+interr,i=strlen(lock->lk->filename)-5;/* .lock */++lock->lk->filename[i]=0;+err=unlink_or_warn(lock->lk->filename);+lock->lk->filename[i]='.';+if(err&&errno!=ENOENT)+return1;+}+return0;+}+intdelete_ref(constchar*refname,constunsignedchar*sha1,intdelopt){structref_lock*lock;-interr,i=0,ret=0,flag=0;+intret=0,flag=0;lock=lock_ref_sha1_basic(refname,sha1,delopt,&flag);if(!lock)return1;-if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){-/* loose */-i=strlen(lock->lk->filename)-5;/* .lock */-lock->lk->filename[i]=0;-err=unlink_or_warn(lock->lk->filename);-if(err&&errno!=ENOENT)-ret=1;+ret|=delete_ref_loose(lock,flag);-lock->lk->filename[i]='.';-}/* removing the loose one could have resurrected an earlier*packedone.Also,ifitwasnotlooseweneedtorepack*withoutit.
From: Brad King <hidden> Date: 2016-06-15 22:58:42
Add a --stdin signature to read update instructions from standard input
and apply multiple ref updates together. Use an input format that
supports any update that could be specified via the command-line,
including object names like "branch:path with space".
Signed-off-by: Brad King <redacted>
---
Documentation/git-update-ref.txt | 54 ++++++++-
builtin/update-ref.c | 252 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 304 insertions(+), 2 deletions(-)
@@ -58,6 +58,58 @@ archive by creating a symlink tree). With `-d` flag, it deletes the named <ref> after verifying it still contains <oldvalue>.+With `--stdin`, update-ref reads instructions from standard input and+performs all modifications together. Specify commands of the form:++ update SP <ref> SP <newvalue> [SP <oldvalue>] LF+ create SP <ref> SP <newvalue> LF+ delete SP <ref> [SP <oldvalue>] LF+ verify SP <ref> [SP <oldvalue>] LF+ option SP <opt> LF++Quote fields containing whitespace as if they were strings in C source+code. Alternatively, use `-z` to specify commands without quoting:++ update SP <ref> NUL <newvalue> NUL [<oldvalue>] NUL+ create SP <ref> NUL <newvalue> NUL+ delete SP <ref> NUL [<oldvalue>] NUL+ verify SP <ref> NUL [<oldvalue>] NUL+ option SP <opt> NUL++Lines of any other format or a repeated <ref> produce an error.+Command meanings are:++update::+ Set <ref> to <newvalue> after verifying <oldvalue>, if given.+ Specify a zero <newvalue> to ensure the ref does not exist+ after the update and/or a zero <oldvalue> to make sure the+ ref does not exist before the update.++create::+ Create <ref> with <newvalue> after verifying it does not+ exist. The given <newvalue> may not be zero.++delete::+ Delete <ref> after verifying it exists with <oldvalue>, if+ given. If given, <oldvalue> may not be zero.++verify::+ Verify <ref> against <oldvalue> but do not change it. If+ <oldvalue> zero or missing, the ref must not exist.++option::+ Modify behavior of the next command naming a <ref>.+ The only valid option is `no-deref` to avoid dereferencing+ a symbolic ref.++Use 40 "0" or the empty string to specify a zero value, except that+with `-z` an empty <oldvalue> is considered missing.++If all <ref>s can be locked with matching <oldvalue>s+simultaneously, all modifications are performed. Otherwise, no+modifications are performed. Note that while each individual+<ref> is updated or deleted atomically, a concurrent reader may+still see a subset of the modifications. Logging Updates ---------------
@@ -2,23 +2,261 @@#include"refs.h"#include"builtin.h"#include"parse-options.h"+#include"quote.h"+#include"argv-array.h"staticconstchar*constgit_update_ref_usage[]={N_("git update-ref [options] -d <refname> [<oldval>]"),N_("git update-ref [options] <refname> <newval> [<oldval>]"),+N_("git update-ref [options] --stdin [-z]"),NULL};+staticintupdates_alloc;+staticintupdates_count;+staticconststructref_update**updates;++staticcharline_termination='\n';+staticintupdate_flags;++staticstructref_update*update_alloc(void)+{+structref_update*update;++/* Allocate and zero-init a struct ref_update */+update=xcalloc(1,sizeof(*update));+ALLOC_GROW(updates,updates_count+1,updates_alloc);+updates[updates_count++]=update;++/* Store and reset accumulated options */+update->flags=update_flags;+update_flags=0;++returnupdate;+}++staticvoidupdate_store_ref_name(structref_update*update,+constchar*ref_name)+{+if(check_refname_format(ref_name,REFNAME_ALLOW_ONELEVEL))+die("invalid ref format: %s",ref_name);+update->ref_name=xstrdup(ref_name);+}++staticvoidupdate_store_new_sha1(structref_update*update,+constchar*newvalue)+{+if(*newvalue&&get_sha1(newvalue,update->new_sha1))+die("invalid new value for ref %s: %s",+update->ref_name,newvalue);+}++staticvoidupdate_store_old_sha1(structref_update*update,+constchar*oldvalue)+{+if(*oldvalue&&get_sha1(oldvalue,update->old_sha1))+die("invalid old value for ref %s: %s",+update->ref_name,oldvalue);++/* We have an old value if non-empty, or if empty without -z */+update->have_old=*oldvalue||line_termination;+}++staticconstchar*parse_arg(constchar*next,structstrbuf*arg)+{+/* Parse SP-terminated, possibly C-quoted argument */+if(*next!='"')+while(*next&&!isspace(*next))+strbuf_addch(arg,*next++);+elseif(unquote_c_style(arg,next,&next))+die("badly quoted argument: %s",next);++/* Return position after the argument */+returnnext;+}++staticconstchar*parse_first_arg(constchar*next,structstrbuf*arg)+{+/* Parse argument immediately after "command SP" */+strbuf_reset(arg);+if(line_termination){+/* Without -z, use the next argument */+next=parse_arg(next,arg);+}else{+/* With -z, use rest of first NUL-terminated line */+strbuf_addstr(arg,next);+next=next+arg->len;+}+returnnext;+}++staticconstchar*parse_next_arg(constchar*next,structstrbuf*arg)+{+/* Parse next SP-terminated or NUL-terminated argument, if any */+strbuf_reset(arg);+if(line_termination){+/* Without -z, consume SP and use next argument */+if(!*next)+returnNULL;+if(*next!=' ')+die("expected SP but got: %s",next);+next=parse_arg(next+1,arg);+}else{+/* With -z, read the next NUL-terminated line */+if(*next)+die("expected NUL but got: %s",next);+if(strbuf_getline(arg,stdin,'\0')==EOF)+returnNULL;+next=arg->buf+arg->len;+}+returnnext;+}++staticvoidparse_cmd_update(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufnewvalue=STRBUF_INIT;+structstrbufoldvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("update line missing <ref>");++if((next=parse_next_arg(next,&newvalue))!=NULL)+update_store_new_sha1(update,newvalue.buf);+else+die("update %s missing <newvalue>",ref.buf);++if((next=parse_next_arg(next,&oldvalue))!=NULL)+update_store_old_sha1(update,oldvalue.buf);+elseif(!line_termination)+die("update %s missing [<oldvalue>] NUL",ref.buf);++if(next&&*next)+die("update %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_create(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufnewvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("create line missing <ref>");++if((next=parse_next_arg(next,&newvalue))!=NULL)+update_store_new_sha1(update,newvalue.buf);+else+die("create %s missing <newvalue>",ref.buf);+if(is_null_sha1(update->new_sha1))+die("create %s given zero new value",ref.buf);++if(next&&*next)+die("create %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_delete(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufoldvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("delete line missing <ref>");++if((next=parse_next_arg(next,&oldvalue))!=NULL)+update_store_old_sha1(update,oldvalue.buf);+elseif(!line_termination)+die("delete %s missing [<oldvalue>] NUL",ref.buf);+if(update->have_old&&is_null_sha1(update->old_sha1))+die("delete %s given zero old value",ref.buf);++if(next&&*next)+die("delete %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_verify(constchar*next)+{+structstrbufref=STRBUF_INIT;+structstrbufvalue=STRBUF_INIT;+structref_update*update;++update=update_alloc();++if((next=parse_first_arg(next,&ref))!=NULL&&ref.buf[0])+update_store_ref_name(update,ref.buf);+else+die("verify line missing <ref>");++if((next=parse_next_arg(next,&value))!=NULL){+update_store_old_sha1(update,value.buf);+update_store_new_sha1(update,value.buf);+}elseif(!line_termination)+die("verify %s missing [<oldvalue>] NUL",ref.buf);++if(next&&*next)+die("verify %s has extra input: %s",ref.buf,next);+}++staticvoidparse_cmd_option(constchar*next)+{+if(!strcmp(next,"no-deref"))+update_flags|=REF_NODEREF;+else+die("option unknown: %s",next);+}++staticvoidupdate_refs_stdin(void)+{+structstrbufcmd=STRBUF_INIT;++/* Read each line dispatch its command */+while(strbuf_getline(&cmd,stdin,line_termination)!=EOF)+if(!cmd.buf[0])+die("empty command in input");+elseif(isspace(*cmd.buf))+die("whitespace before command: %s",cmd.buf);+elseif(!prefixcmp(cmd.buf,"update "))+parse_cmd_update(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"create "))+parse_cmd_create(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"delete "))+parse_cmd_delete(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"verify "))+parse_cmd_verify(cmd.buf+7);+elseif(!prefixcmp(cmd.buf,"option "))+parse_cmd_option(cmd.buf+7);+else+die("unknown command: %s",cmd.buf);++strbuf_release(&cmd);+}+intcmd_update_ref(intargc,constchar**argv,constchar*prefix){constchar*refname,*oldval,*msg=NULL;unsignedcharsha1[20],oldsha1[20];-intdelete=0,no_deref=0,flags=0;+intdelete=0,no_deref=0,read_stdin=0,end_null=0,flags=0;structoptionoptions[]={OPT_STRING('m',NULL,&msg,N_("reason"),N_("reason of the update")),OPT_BOOL('d',NULL,&delete,N_("delete the reference")),+OPT_BOOL('z',NULL,&end_null,N_("stdin has NUL-terminated arguments")),OPT_BOOL(0,"no-deref",&no_deref,N_("update <refname> not the one it points to")),+OPT_BOOL(0,"stdin",&read_stdin,N_("read updates from stdin")),OPT_END(),};
@@ -28,6 +266,18 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)if(msg&&!*msg)die("Refusing to perform update with empty message.");+if(read_stdin){+if(delete||no_deref||argc>0)+usage_with_options(git_update_ref_usage,options);+if(end_null)+line_termination='\0';+update_refs_stdin();+returnupdate_refs(msg,updates,updates_count,DIE_ON_ERR);+}++if(end_null)+usage_with_options(git_update_ref_usage,options);+if(delete){if(argc<1||argc>2)usage_with_options(git_update_ref_usage,options);
From: Brad King <hidden> Date: 2016-06-15 22:58:42
Add 'struct ref_update' to encode the information needed to update or
delete a ref (name, new sha1, optional old sha1, no-deref flag). Add
function 'update_refs' accepting an array of updates to perform. First
sort the input array to order locks consistently everywhere and reject
multiple updates to the same ref. Then acquire locks on all refs with
verified old values. Then update or delete all refs accordingly. Fail
if any one lock cannot be obtained or any one old value does not match.
Though the refs themselves cannot be modified together in a single
atomic transaction, this function does enable some useful semantics.
For example, a caller may create a new branch starting from the head of
another branch and rewind the original branch at the same time. This
transfers ownership of commits between branches without risk of losing
commits added to the original branch by a concurrent process, or risk of
a concurrent process creating the new branch first.
Signed-off-by: Brad King <redacted>
---
refs.c | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
refs.h | 20 +++++++++++++
2 files changed, 120 insertions(+)
@@ -3237,6 +3237,106 @@ int update_ref(const char *action, const char *refname,returnupdate_ref_write(action,refname,sha1,lock,onerr);}+staticintref_update_compare(constvoid*r1,constvoid*r2)+{+conststructref_update*const*u1=r1;+conststructref_update*const*u2=r2;+returnstrcmp((*u1)->ref_name,(*u2)->ref_name);+}++staticintref_update_reject_duplicates(structref_update**updates,intn,+enumaction_on_erronerr)+{+inti;+for(i=1;i<n;i++)+if(!strcmp(updates[i-1]->ref_name,updates[i]->ref_name)){+constchar*str=+"Multiple updates for ref '%s' not allowed.";+switch(onerr){+caseMSG_ON_ERR:+error(str,updates[i]->ref_name);break;+caseDIE_ON_ERR:+die(str,updates[i]->ref_name);break;+caseQUIET_ON_ERR:+break;+}+return1;+}+return0;+}++intupdate_refs(constchar*action,conststructref_update**updates_orig,+intn,enumaction_on_erronerr)+{+intret=0,delnum=0,i;+structref_update**updates;+int*types;+structref_lock**locks;+constchar**delnames;++if(!updates_orig||!n)+return0;++/* Allocate work space */+updates=xmalloc(sizeof(*updates)*n);+types=xmalloc(sizeof(*types)*n);+locks=xcalloc(n,sizeof(*locks));+delnames=xmalloc(sizeof(*delnames)*n);++/* Copy, sort, and reject duplicate refs */+memcpy(updates,updates_orig,sizeof(*updates)*n);+qsort(updates,n,sizeof(*updates),ref_update_compare);+ret=ref_update_reject_duplicates(updates,n,onerr);+if(ret)+gotocleanup;++/* Acquire all locks while verifying old values */+for(i=0;i<n;i++){+locks[i]=update_ref_lock(updates[i]->ref_name,+(updates[i]->have_old?+updates[i]->old_sha1:NULL),+updates[i]->flags,+&types[i],onerr);+if(!locks[i]){+ret=1;+gotocleanup;+}+}++/* Perform updates first so live commits remain referenced */+for(i=0;i<n;i++)+if(!is_null_sha1(updates[i]->new_sha1)){+ret=update_ref_write(action,+updates[i]->ref_name,+updates[i]->new_sha1,+locks[i],onerr);+locks[i]=NULL;/* freed by update_ref_write */+if(ret)+gotocleanup;+}++/* Perform deletes now that updates are safely completed */+for(i=0;i<n;i++)+if(locks[i]){+delnames[delnum++]=locks[i]->ref_name;+ret|=delete_ref_loose(locks[i],types[i]);+}+ret|=repack_without_refs(delnames,delnum);+for(i=0;i<delnum;i++)+unlink_or_warn(git_path("logs/%s",delnames[i]));+clear_loose_ref_cache(&ref_cache);++cleanup:+for(i=0;i<n;i++)+if(locks[i])+unlock_ref(locks[i]);+free(updates);+free(types);+free(locks);+free(delnames);+returnret;+}+/**generateaformatsuitableforscanffromaref_rev_parse_rules*rule,thatisreplacethe"%.*s"specwitha"%s"spec
From: Brad King <hidden> Date: 2016-06-15 22:58:42
Generalize repack_without_ref as repack_without_refs to support a list
of refs and implement the former in terms of the latter.
Signed-off-by: Brad King <redacted>
---
refs.c | 33 ++++++++++++++++++++++++---------
1 file changed, 24 insertions(+), 9 deletions(-)
@@ -2414,42 +2414,57 @@ static int curate_packed_ref_fn(struct ref_entry *entry, void *cb_data)return0;}-staticintrepack_without_ref(constchar*refname)+staticintrepack_without_refs(constchar**refnames,intn){structref_dir*packed;structstring_listrefs_to_delete=STRING_LIST_INIT_DUP;structstring_list_item*ref_to_delete;+inti,removed=0;++/* Look for a packed ref */+for(i=0;i<n;i++)+if(get_packed_ref(refnames[i]))+break;-if(!get_packed_ref(refname))-return0;/* refname does not exist in packed refs */+/* Avoid locking if we have nothing to do */+if(i==n)+return0;/* no refname exists in packed refs */if(lock_packed_refs(0)){unable_to_lock_error(git_path("packed-refs"),errno);-returnerror("cannot delete '%s' from packed refs",refname);+returnerror("cannot delete '%s' from packed refs",refnames[i]);}packed=get_packed_refs(&ref_cache);-/* Remove refname from the cache: */-if(remove_entry(packed,refname)==-1){+/* Remove refnames from the cache */+for(i=0;i<n;i++)+if(remove_entry(packed,refnames[i])!=-1)+removed=1;+if(!removed){/*-*Thepackedentrydisappearedwhilewewere+*Allpackedentriesdisappearedwhilewewere*acquiringthelock.*/rollback_packed_refs();return0;}-/* Remove any other accumulated cruft: */+/* Remove any other accumulated cruft */do_for_each_entry_in_dir(packed,0,curate_packed_ref_fn,&refs_to_delete);for_each_string_list_item(ref_to_delete,&refs_to_delete){if(remove_entry(packed,ref_to_delete->string)==-1)die("internal error");}-/* Write what remains: */+/* Write what remains */returncommit_packed_refs();}+staticintrepack_without_ref(constchar*refname)+{+returnrepack_without_refs(&refname,1);+}+staticintdelete_ref_loose(structref_lock*lock,intflag){if(!(flag&REF_ISPACKED)||flag&REF_ISSYMREF){
I believe that current fashion in git test scripts is to add a space
before () in the function declaration. Likewise, the 'do' should be on
the line following 'while' and aligned with 'while' (and drop the
semicolon).
The '--' option to printf is not likely portable. POSIX [1] certainly
does not mention it.
You can get printf to emit a NUL more naturally via \0, so q_to_nul is
unnecessary.
Finally, printf reuses its 'format' argument as many times as needed
to output all arguments, so the while loop is unneeded.
Thus printf provides all the functionality you require, and
print_nul() function can be dropped. So:
printf '%s\0' foo bar baz
is equivalent to:
print_null foo bar baz
[1]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html
@@ -58,6 +58,58 @@ archive by creating a symlink tree).+option::+ Modify behavior of the next command naming a <ref>.+ The only valid option is `no-deref` to avoid dereferencing+ a symbolic ref.++Use 40 "0" or the empty string to specify a zero value, except that
Did you want an 's' after the "0"?
0's
"0"s
"0"'s
zeros
zeroes
+with `-z` an empty <oldvalue> is considered missing.
+
+If all <ref>s can be locked with matching <oldvalue>s
+simultaneously, all modifications are performed. Otherwise, no
+modifications are performed. Note that while each individual
+<ref> is updated or deleted atomically, a concurrent reader may
+still see a subset of the modifications.
From: Brad King <hidden> Date: 2016-06-15 22:58:43
On 09/10/2013 06:51 PM, Eric Sunshine wrote:
On Mon, Sep 9, 2013 at 8:57 PM, Brad King [off-list ref] wrote:
quoted
+Use 40 "0" or the empty string to specify a zero value, except that
Did you want an 's' after the "0"?
The same description without 's' already appears in git-update-ref.txt
above this location in the existing documentation of the command-line
option behavior. I see 0{40} in git-receive-pack.txt and also in
howto/update-hook-example.txt. Perhaps a follow-up change can be made
to choose a consistent way to describe 40 0s.
-Brad
From: Brad King <hidden> Date: 2016-06-15 22:58:43
Extend t/t1400-update-ref.sh to cover cases using the --stdin option.
Signed-off-by: Brad King <redacted>
---
On 09/10/2013 06:46 PM, Eric Sunshine wrote:
Thus printf provides all the functionality you require, and
print_nul() function can be dropped. So:
printf '%s\0' foo bar baz
Wonderful, thanks! The single-quotes do not fit easily inside
test code blocks that are themselves single-quoted, so I packaged
the format up in a variable. Here is a revised patch.
-Brad
t/t1400-update-ref.sh | 632 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 632 insertions(+)
@@ -302,4 +302,636 @@ test_expect_success \'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)'\'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'+a=refs/heads/a+b=refs/heads/b+c=refs/heads/c+E='""'+F='%s\0'+pws='path with space'++test_expect_success'stdin test setup''+echo"$pws">"$pws"&&+gitadd--"$pws"&&+gitcommit-m"$pws"+'++test_expect_success'-z fails without --stdin''+test_must_failgitupdate-ref-z$m$m$m2>err&&+grep"usage: git update-ref"err+'++test_expect_success'stdin works with no input''+>stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin fails on empty line''+echo"">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: empty command in input"err+'++test_expect_success'stdin fails on only whitespace''+echo" ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin fails on leading whitespace''+echo" create $a$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: whitespace before command: create $a$m"err+'++test_expect_success'stdin fails on unknown command''+echo"unknown $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: unknown command: unknown $a"err+'++test_expect_success'stdin fails on badly quoted input''+echo"create $a \"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: badly quoted argument: \\\"master"err+'++test_expect_success'stdin fails on arguments not separated by space''+echo"create \"$a\"master">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: expected SP but got: master"err+'++test_expect_success'stdin fails create with no ref''+echo"create ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create line missing <ref>"err+'++test_expect_success'stdin fails create with bad ref name''+echo"create ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails create with no new value''+echo"create $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $a missing <newvalue>"err+'++test_expect_success'stdin fails create with too many arguments''+echo"create $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $a has extra input: $m"err+'++test_expect_success'stdin fails update with no ref''+echo"update ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update line missing <ref>"err+'++test_expect_success'stdin fails update with bad ref name''+echo"update ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails update with no new value''+echo"update $a">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update $a missing <newvalue>"err+'++test_expect_success'stdin fails update with too many arguments''+echo"update $a$m$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: update $a has extra input: $m"err+'++test_expect_success'stdin fails delete with no ref''+echo"delete ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete line missing <ref>"err+'++test_expect_success'stdin fails delete with bad ref name''+echo"delete ~a $m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin fails delete with too many arguments''+echo"delete $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete $a has extra input: $m"err+'++test_expect_success'stdin fails verify with too many arguments''+echo"verify $a$m$m">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: verify $a has extra input: $m"err+'++test_expect_success'stdin fails option with unknown name''+echo"option unknown">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: option unknown: unknown"err+'++test_expect_success'stdin fails with duplicate refs''+cat>stdin<<-EOF&&+create$a$m+create$b$m+create$a$m+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin create ref works''+echo"create $a$m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update ref creates with zero old value''+echo"update $b$m$Z">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin update ref creates with empty old value''+echo"update $b$m$E">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin create ref works with path with space to blob''+echo"create refs/blobs/pws \"$m:$pws\"">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin update ref fails with wrong old value''+echo"update $c$m$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref fails with bad old value''+echo"update $c$m does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with bad new value''+echo"create $c does-not-exist">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin create ref fails with zero new value''+echo"create $c ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: create $c given zero new value"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update ref works with right old value''+echo"update $b$m~1 $m">stdin&&+gitupdate-ref--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with wrong old value''+echo"delete $a$m~1">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref fails with zero old value''+echo"delete $a ">stdin&&+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: delete $a given zero old value"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin update symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+cat>stdin<<-EOF&&+optionno-deref+updateTESTSYMREF$a$b+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+cat>stdin<<-EOF&&+optionno-deref+deleteTESTSYMREF$b+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete ref works with right old value''+echo"delete $b$m~1">stdin&&+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin update/create/verify combination works''+cat>stdin<<-EOF&&+update$a$m+create$b$m+verify$c+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs works with identity updates''+cat>stdin<<-EOF&&+update$a$m$m+update$b$m$m+update$c$Z$E+EOF+gitupdate-ref--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin update refs fails with wrong old value''+gitupdate-ref$c$m&&+cat>stdin<<-EOF&&+update$a$m$m+update$b$m$m+update$c''+EOF+test_must_failgitupdate-ref--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+cat>stdin<<-EOF&&+delete$a$m+update$b$Z$m+update$c$E$m~1+EOF+gitupdate-ref--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z works on empty input''+>stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse--verify-q$m+'++test_expect_success'stdin -z fails on empty line''+echo"">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin -z fails on empty command''+printf$F"">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: empty command in input"err+'++test_expect_success'stdin -z fails on only whitespace''+printf$F" ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: "err+'++test_expect_success'stdin -z fails on leading whitespace''+printf$F" create $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: whitespace before command: create $a"err+'++test_expect_success'stdin -z fails on unknown command''+printf$F"unknown $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: unknown $a"err+'++test_expect_success'stdin -z fails create with no ref''+printf$F"create ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create line missing <ref>"err+'++test_expect_success'stdin -z fails create with bad ref name''+printf$F"create ~a ""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a "err+'++test_expect_success'stdin -z fails create with no new value''+printf$F"create $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create $a missing <newvalue>"err+'++test_expect_success'stdin -z fails create with too many arguments''+printf$F"create $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails update with no ref''+printf$F"update ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update line missing <ref>"err+'++test_expect_success'stdin -z fails update with bad ref name''+printf$F"update ~a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin -z fails update with no new value''+printf$F"update $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update $a missing <newvalue>"err+'++test_expect_success'stdin -z fails update with no old value''+printf$F"update $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: update $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails update with too many arguments''+printf$F"update $a""$m""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails delete with no ref''+printf$F"delete ">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete line missing <ref>"err+'++test_expect_success'stdin -z fails delete with bad ref name''+printf$F"delete ~a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid ref format: ~a"err+'++test_expect_success'stdin -z fails delete with no old value''+printf$F"delete $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails delete with too many arguments''+printf$F"delete $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails verify with too many arguments''+printf$F"verify $a""$m""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: unknown command: $m"err+'++test_expect_success'stdin -z fails verify with no old value''+printf$F"verify $a">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: verify $a missing \\[<oldvalue>\\] NUL"err+'++test_expect_success'stdin -z fails option with unknown name''+printf$F"option unknown">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: option unknown: unknown"err+'++test_expect_success'stdin -z fails with duplicate refs''+printf$F"create $a""$m""create $b""$m""create $a""$m">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Multiple updates for ref '"'"'$a'"'"' not allowed."err+'++test_expect_success'stdin -z create ref works''+printf$F"create $a""$m">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update ref creates with zero old value''+printf$F"update $b""$m""$Z">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitupdate-ref-d$b+'++test_expect_success'stdin -z update ref creates with empty old value''+printf$F"update $b""$m""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z create ref works with path with space to blob''+printf$F"create refs/blobs/pws""$m:$pws">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse"$m:$pws">expect&&+gitrev-parserefs/blobs/pws>actual&&+test_cmpexpectactual&&+gitupdate-ref-drefs/blobs/pws+'++test_expect_success'stdin -z update ref fails with wrong old value''+printf$F"update $c""$m""$m~1">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update ref fails with bad old value''+printf$F"update $c""$m""does-not-exist">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid old value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with bad new value''+printf$F"create $c""does-not-exist">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: invalid new value for ref $c: does-not-exist"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z create ref fails with zero new value''+printf$F"create $c""">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: create $c given zero new value"err&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update ref works with right old value''+printf$F"update $b""$m~1""$m">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref fails with wrong old value''+printf$F"delete $a""$m~1">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$a'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref fails with zero old value''+printf$F"delete $a""$Z">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: delete $a given zero old value"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z update symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+printf$F"option no-deref""update TESTSYMREF""$a""$b">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parseTESTSYMREF>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete symref works option no-deref''+gitsymbolic-refTESTSYMREF$b&&+printf$F"option no-deref""delete TESTSYMREF""$b">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-qTESTSYMREF&&+gitrev-parse$m~1>expect&&+gitrev-parse$b>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete ref works with right old value''+printf$F"delete $b""$m~1">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$b+'++test_expect_success'stdin -z update/create/verify combination works''+printf$F"update $a""$m""""create $b""$m""verify $c""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs works with identity updates''+printf$F"update $a""$m""$m""update $b""$m""$m""update $c""$Z""">stdin&&+gitupdate-ref-z--stdin<stdin&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+test_must_failgitrev-parse--verify-q$c+'++test_expect_success'stdin -z update refs fails with wrong old value''+gitupdate-ref$c$m&&+printf$F"update $a""$m""$m""update $b""$m""$m""update $c""""$Z">stdin&&+test_must_failgitupdate-ref-z--stdin<stdin2>err&&+grep"fatal: Cannot lock the ref '"'"'$c'"'"'"err&&+gitrev-parse$m>expect&&+gitrev-parse$a>actual&&+test_cmpexpectactual&&+gitrev-parse$b>actual&&+test_cmpexpectactual&&+gitrev-parse$c>actual&&+test_cmpexpectactual+'++test_expect_success'stdin -z delete refs works with packed and loose refs''+gitpack-refs--all&&+gitupdate-ref$c$m~1&&+printf$F"delete $a""$m""update $b""$Z""$m""update $c""""$m~1">stdin&&+gitupdate-ref-z--stdin<stdin&&+test_must_failgitrev-parse--verify-q$a&&+test_must_failgitrev-parse--verify-q$b&&+test_must_failgitrev-parse--verify-q$c+'+ test_done
From: Eric Sunshine <hidden> Date: 2016-06-15 22:58:43
On Wed, Sep 11, 2013 at 8:36 AM, Brad King [off-list ref] wrote:
On 09/10/2013 06:51 PM, Eric Sunshine wrote:
quoted
On Mon, Sep 9, 2013 at 8:57 PM, Brad King [off-list ref] wrote:
quoted
+Use 40 "0" or the empty string to specify a zero value, except that
Did you want an 's' after the "0"?
The same description without 's' already appears in git-update-ref.txt
above this location in the existing documentation of the command-line
Thanks for the explanation. (I could have checked the surrounding text
but didn't think to do so.)
option behavior. I see 0{40} in git-receive-pack.txt and also in
howto/update-hook-example.txt. Perhaps a follow-up change can be made
to choose a consistent way to describe 40 0s.
-Brad