From: Patrick Steinhardt <hidden> Date: 2021-01-07 13:52:53
Hi,
this is a short patch series to implement support for atomic reference
updates for git-fetch(1). It's similar to `git push --atomic`, only that
it applies to the local side. That is the fetch will either succeed and
update all remote references or it will fail and update none.
Patrick
Patrick Steinhardt (2):
fetch: allow passing a transaction to `s_update_ref()`
fetch: implement support for atomic reference updates
Documentation/fetch-options.txt | 4 +
builtin/fetch.c | 72 ++++++++++++-----
t/t5510-fetch.sh | 139 ++++++++++++++++++++++++++++++++
3 files changed, 197 insertions(+), 18 deletions(-)
--
2.30.0
From: Patrick Steinhardt <hidden> Date: 2021-01-07 13:52:53
The handling of ref updates is completely handled by `s_update_ref()`,
which will manage the complete lifecycle of the reference transaction.
This is fine right now given that git-fetch(1) does not support atomic
fetches, so each reference gets its own transaction. It is quite
inflexible though, as `s_update_ref()` only knows about a single
reference update at a time, so it doesn't allow us to alter the
strategy.
This commit prepares `s_update_ref()` and its only caller
`update_local_ref()` to allow passing an external transaction. If none
is given, then the existing behaviour is triggered which creates a new
transaction and directly commits it. Otherwise, if the caller provides a
transaction, then we only queue the update but don't commit it. This
optionally allows the caller to manage when a transaction will be
committed.
Given that `update_local_ref()` is always called with a `NULL`
transaction for now, no change in behaviour is expected from this
change.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 48 +++++++++++++++++++++++++++++++-----------------
1 file changed, 31 insertions(+), 17 deletions(-)
@@ -799,7 +813,7 @@ static int update_local_ref(struct ref *ref,starts_with(ref->name,"refs/tags/")){if(force||ref->force){intr;-r=s_update_ref("updating tag",ref,0);+r=s_update_ref("updating tag",ref,transaction,0);format_display(display,r?'!':'t',_("[tag update]"),r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -836,7 +850,7 @@ static int update_local_ref(struct ref *ref,what=_("[new ref]");}-r=s_update_ref(msg,ref,0);+r=s_update_ref(msg,ref,transaction,0);format_display(display,r?'!':'*',what,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -858,7 +872,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"..");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("fast-forward",ref,1);+r=s_update_ref("fast-forward",ref,transaction,1);format_display(display,r?'!':' ',quickref.buf,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -870,7 +884,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"...");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("forced-update",ref,1);+r=s_update_ref("forced-update",ref,transaction,1);format_display(display,r?'!':'+',quickref.buf,r?_("unable to update local ref"):_("forced update"),remote,pretty_ref,summary_width);
From: Patrick Steinhardt <hidden> Date: 2021-01-07 13:53:21
When executing a fetch, then git will currently allocate one reference
transaction per reference update and directly commit it. This means that
fetches are non-atomic: even if some of the reference updates fail,
others may still succeed and modify local references.
This is fine in many scenarios, but this strategy has its downsides.
- The view of remote references may be inconsistent and may show a
bastardized state of the remote repository.
- Batching together updates may improve performance in certain
scenarios. While the impact probably isn't as pronounced with loose
references, the upcoming reftable backend may benefit as it needs to
write less files in case the update is batched.
- The reference-update hook is currently being executed twice per
updated reference. While this doesn't matter when there is no such
hook, we have seen severe performance regressions when doing a
git-fetch(1) with reference-transaction hook when the remote
repository has hundreds of thousands of references.
Similar to `git push --atomic`, this commit thus introduces atomic
fetches. Instead of allocating one reference transaction per updated
reference, it causes us to only allocate a single transaction and commit
it as soon as all updates were received. If locking of any reference
fails, then we abort the complete transaction and don't update any
reference, which gives us an all-or-nothing fetch.
Note that this may not completely fix the first of above downsides, as
the consistent view also depends on the server-side. If the server
doesn't have a consistent view of its own references during the
reference negotiation phase, then the client would get the same
inconsistent view the server has. This is a separate problem though and,
if it actually exists, can be fixed at a later point.
Signed-off-by: Patrick Steinhardt <redacted>
---
Documentation/fetch-options.txt | 4 +
builtin/fetch.c | 26 +++++-
t/t5510-fetch.sh | 139 ++++++++++++++++++++++++++++++++
3 files changed, 167 insertions(+), 2 deletions(-)
@@ -7,6 +7,10 @@ existing contents of `.git/FETCH_HEAD`. Without this option old data in `.git/FETCH_HEAD` will be overwritten.+--atomic::+ Use an atomic transaction to update local refs. Either all refs are+ updated, or on error, no refs are updated.+ --depth=<depth>:: Limit fetching to the specified number of commits from the tip of each remote branch history. If fetching to a 'shallow' repository
@@ -63,6 +63,7 @@ static int enable_auto_gc = 1;staticinttags=TAGS_DEFAULT,unshallow,update_shallow,deepen;staticintmax_jobs=-1,submodule_fetch_jobs_config=-1;staticintfetch_parallel_config=1;+staticintatomic_fetch;staticenumtransport_familyfamily;staticconstchar*depth;staticconstchar*deepen_since;
@@ -144,6 +145,8 @@ static struct option builtin_fetch_options[] = {N_("set upstream for git pull/fetch")),OPT_BOOL('a',"append",&append,N_("append to .git/FETCH_HEAD instead of overwriting")),+OPT_BOOL(0,"atomic",&atomic_fetch,+N_("use atomic transaction to update references")),OPT_STRING(0,"upload-pack",&upload_pack,N_("path"),N_("path to upload pack on remote end")),OPT__FORCE(&force,N_("force overwrite of local reference"),0),
@@ -1074,6 +1086,14 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,}}+if(!rc&&transaction){+rc=ref_transaction_commit(transaction,&err);+if(rc){+error("%s",err.buf);+gotoabort;+}+}+if(rc&STORE_REF_ERROR_DF_CONFLICT)error(_("some local refs could not be updated; try running\n"" 'git remote prune %s' to remove any old, conflicting "
@@ -176,6 +176,145 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'gitrev-parsesometag'+test_expect_success'fetch --atomic works with a single branch''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch&&+gitrev-parseatomic-branch>expected&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parseorigin/atomic-branch>actual&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic works with multiple branches''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch-1&&+gitbranchatomic-branch-2&&+gitbranchatomic-branch-3&&+gitrev-parserefs/heads/atomic-branch-1refs/heads/atomic-branch-2refs/heads/atomic-branch-3>actual&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-branch-1refs/remotes/origin/atomic-branch-2refs/remotes/origin/atomic-branch-3>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic works with mixed branches and tags''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-mixed-branch&&+gittagatomic-mixed-tag&&+gitrev-parserefs/heads/atomic-mixed-branchrefs/tags/atomic-mixed-tag>actual&&++git-Catomicfetch--tags--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-mixed-branchrefs/tags/atomic-mixed-tag>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic prunes references''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-prune-delete&&+gitclone.atomic&&+gitbranch--deleteatomic-prune-delete&&+gitbranchatomic-prune-create&&+gitrev-parserefs/heads/atomic-prune-create>actual&&++git-Catomicfetch--prune--atomicorigin&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-prune-delete&&+git-Catomicrev-parserefs/remotes/origin/atomic-prune-create>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic aborts with non-fast-forward update''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-non-ff&&+gitclone.atomic&&+gitrev-parseHEAD>actual&&++gitbranchatomic-new-branch&&+parent_commit=$(gitrev-parseatomic-non-ff~)&&+gitupdate-refrefs/heads/atomic-non-ff$parent_commit&&++test_must_failgit-Catomicfetch--atomicoriginrefs/heads/*:refs/remotes/origin/*&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-new-branch&&+git-Catomicrev-parserefs/remotes/origin/atomic-non-ff>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic executes a single reference transaction only''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-1&&+gitbranchatomic-hooks-2&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+committed+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+EOF++git-Catomicfetch--atomicorigin&&+test_cmpexpectedatomic/actual+'++test_expect_success'fetch --atomic aborts all reference updates if hook aborts''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-abort-1&&+gitbranchatomic-hooks-abort-2&&+gitbranchatomic-hooks-abort-3&&+gittagatomic-hooks-abort&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+aborted+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+exit1+EOF++git-Catomicfor-each-ref>expected-refs&&+test_must_failgit-Catomicfetch--tags--atomicorigin&&+git-Catomicfor-each-ref>actual-refs&&+test_cmpexpected-refsactual-refs+'+ test_expect_success'--refmap="" ignores configured refspec''cd"$TRASH_DIRECTORY"&&gitclone"$D"remote-refs&&
From: Patrick Steinhardt <hidden> Date: 2021-01-08 12:12:47
Hi,
this is the second version of my patch series to implement support for
atomic reference updates for git-fetch(1). It's similar to `git push
--atomic`, only that it applies to the local side. That is the fetch
will either succeed and update all remote references or it will fail and
update none.
Changes compared to v1:
- In v1, we still wrote to FETCH_HEAD even if the fetch failed. I've
fixed this now by pulling out logic to write to FETCH_HEAD in 1/4
so that we can now easily buffer updates and commit them only if
the reference transaction succeeds. There's some additional tests
in 4/4 to test it works as expected.
- As suggested by Christian, I've unified the exit path in
`s_update_ref()` in 2/4.
- I've dropped the `commit` variable and renamed
`transaction_to_free` to `our_transaction` as suggested by Junio.
Patrick
Patrick Steinhardt (4):
fetch: extract writing to FETCH_HEAD
fetch: refactor `s_update_ref` to use common exit path
fetch: allow passing a transaction to `s_update_ref()`
fetch: implement support for atomic reference updates
Documentation/fetch-options.txt | 4 +
builtin/fetch.c | 198 ++++++++++++++++++++++++--------
t/t5510-fetch.sh | 168 +++++++++++++++++++++++++++
3 files changed, 322 insertions(+), 48 deletions(-)
Range-diff against v1:
-: ---------- > 1: d80dbc5a9c fetch: extract writing to FETCH_HEAD
-: ---------- > 2: 718a8bf5d7 fetch: refactor `s_update_ref` to use common exit path
1: e627e729e5 ! 3: 4162d10fcb fetch: allow passing a transaction to `s_update_ref()`
@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
char *msg;
char *rla = getenv("GIT_REFLOG_ACTION");
- struct ref_transaction *transaction;
-+ struct ref_transaction *transaction_to_free = NULL;
++ struct ref_transaction *our_transaction = NULL;
struct strbuf err = STRBUF_INIT;
-- int ret, df_conflict = 0;
-+ int ret, df_conflict = 0, commit = 0;
+ int ret;
- if (dry_run)
- return 0;
@@ builtin/fetch.c: static int s_update_ref(const char *action,
rla = default_rla.buf;
msg = xstrfmt("%s: %s", rla, action);
- transaction = ref_transaction_begin(&err);
-- if (!transaction ||
-- ref_transaction_update(transaction, ref->name,
+ /*
+ * If no transaction was passed to us, we manage the transaction
+ * ourselves. Otherwise, we trust the caller to handle the transaction
+ * lifecycle.
+ */
-+ if (!transaction) {
-+ transaction = transaction_to_free = ref_transaction_begin(&err);
-+ if (!transaction)
-+ goto fail;
-+ commit = 1;
-+ }
-+
-+ if (ref_transaction_update(transaction, ref->name,
- &ref->new_oid,
- check_old ? &ref->old_oid : NULL,
- 0, msg, &err))
- goto fail;
-
-- ret = ref_transaction_commit(transaction, &err);
-- if (ret) {
-- df_conflict = (ret == TRANSACTION_NAME_CONFLICT);
-- goto fail;
-+ if (commit) {
-+ ret = ref_transaction_commit(transaction, &err);
-+ if (ret) {
-+ df_conflict = (ret == TRANSACTION_NAME_CONFLICT);
-+ goto fail;
+ if (!transaction) {
+- ret = STORE_REF_ERROR_OTHER;
+- goto out;
++ transaction = our_transaction = ref_transaction_begin(&err);
++ if (!transaction) {
++ ret = STORE_REF_ERROR_OTHER;
++ goto out;
+ }
}
+ ret = ref_transaction_update(transaction, ref->name, &ref->new_oid,
+@@ builtin/fetch.c: static int s_update_ref(const char *action,
+ goto out;
+ }
+
+- ret = ref_transaction_commit(transaction, &err);
+- if (ret) {
+- ret = (ret == TRANSACTION_NAME_CONFLICT) ? STORE_REF_ERROR_DF_CONFLICT
+- : STORE_REF_ERROR_OTHER;
+- goto out;
++ if (our_transaction) {
++ ret = ref_transaction_commit(our_transaction, &err);
++ if (ret) {
++ ret = (ret == TRANSACTION_NAME_CONFLICT) ? STORE_REF_ERROR_DF_CONFLICT
++ : STORE_REF_ERROR_OTHER;
++ goto out;
++ }
+ }
+
+ out:
- ref_transaction_free(transaction);
-+ ref_transaction_free(transaction_to_free);
++ ref_transaction_free(our_transaction);
+ if (ret)
+ error("%s", err.buf);
strbuf_release(&err);
- free(msg);
- return 0;
- fail:
-- ref_transaction_free(transaction);
-+ ref_transaction_free(transaction_to_free);
- error("%s", err.buf);
- strbuf_release(&err);
- free(msg);
@@ builtin/fetch.c: static void format_display(struct strbuf *display, char code,
}
2: 4807344e92 ! 4: 53705281b6 fetch: implement support for atomic reference updates
@@ Commit message
inconsistent view the server has. This is a separate problem though and,
if it actually exists, can be fixed at a later point.
+ This commit also changes the way we write FETCH_HEAD in case `--atomic`
+ is passed. Instead of writing changes as we go, we need to accumulate
+ all changes first and only commit them at the end when we know that all
+ reference updates succeeded. Ideally, we'd just do so via a temporary
+ file so that we don't need to carry all updates in-memory. This isn't
+ trivially doable though considering the `--append` mode, where we do not
+ truncate the file but simply append to it. And given that we support
+ concurrent processes appending to FETCH_HEAD at the same time without
+ any loss of data, seeding the temporary file with current contents of
+ FETCH_HEAD initially and then doing a rename wouldn't work either. So
+ this commit implements the simple strategy of buffering all changes and
+ appending them to the file on commit.
+
Signed-off-by: Patrick Steinhardt [off-list ref]
## Documentation/fetch-options.txt ##
@@ builtin/fetch.c: static struct option builtin_fetch_options[] = {
OPT_STRING(0, "upload-pack", &upload_pack, N_("path"),
N_("path to upload pack on remote end")),
OPT__FORCE(&force, N_("force overwrite of local reference"), 0),
-@@ builtin/fetch.c: static int store_updated_refs(const char *raw_url, const char *remote_name,
+@@ builtin/fetch.c: static int iterate_ref_map(void *cb_data, struct object_id *oid)
+
+ struct fetch_head {
FILE *fp;
++ struct strbuf buf;
+ };
+
+ static int open_fetch_head(struct fetch_head *fetch_head)
+@@ builtin/fetch.c: static int open_fetch_head(struct fetch_head *fetch_head)
+ if (!write_fetch_head)
+ return 0;
+
++ strbuf_init(&fetch_head->buf, 0);
+ fetch_head->fp = fopen(filename, "a");
+ if (!fetch_head->fp)
+ return error_errno(_("cannot open %s"), filename);
+@@ builtin/fetch.c: static void append_fetch_head(struct fetch_head *fetch_head, const char *old_oid
+ if (!write_fetch_head)
+ return;
+
+- fprintf(fetch_head->fp, "%s\t%s\t%s",
+- old_oid, merge_status_marker, note);
+- for (i = 0; i < url_len; ++i)
+- if ('\n' == url[i])
+- fputs("\\n", fetch_head->fp);
+- else
+- fputc(url[i], fetch_head->fp);
+- fputc('\n', fetch_head->fp);
++ /*
++ * When using an atomic fetch, we do not want to update FETCH_HEAD if
++ * any of the reference updates fails. We thus have to write all
++ * updates to a buffer first and only commit it as soon as all
++ * references have been successfully updated.
++ */
++ if (atomic_fetch) {
++ strbuf_addf(&fetch_head->buf, "%s\t%s\t%s",
++ old_oid, merge_status_marker, note);
++ strbuf_add(&fetch_head->buf, url, url_len);
++ strbuf_addch(&fetch_head->buf, '\n');
++ } else {
++ fprintf(fetch_head->fp, "%s\t%s\t%s",
++ old_oid, merge_status_marker, note);
++ for (i = 0; i < url_len; ++i)
++ if ('\n' == url[i])
++ fputs("\\n", fetch_head->fp);
++ else
++ fputc(url[i], fetch_head->fp);
++ fputc('\n', fetch_head->fp);
++ }
+ }
+
+ static void commit_fetch_head(struct fetch_head *fetch_head)
+ {
+- /* Nothing to commit yet. */
++ if (!write_fetch_head || !atomic_fetch)
++ return;
++ strbuf_write(&fetch_head->buf, fetch_head->fp);
+ }
+
+ static void close_fetch_head(struct fetch_head *fetch_head)
+@@ builtin/fetch.c: static void close_fetch_head(struct fetch_head *fetch_head)
+ return;
+
+ fclose(fetch_head->fp);
++ strbuf_release(&fetch_head->buf);
+ }
+
+ static const char warn_show_forced_updates[] =
+@@ builtin/fetch.c: static int store_updated_refs(const char *raw_url, const char *remote_name,
+ struct fetch_head fetch_head;
struct commit *commit;
int url_len, i, rc = 0;
- struct strbuf note = STRBUF_INIT;
@@ builtin/fetch.c: static int store_updated_refs(const char *raw_url, const char *
+ }
+ }
+
- if (rc & STORE_REF_ERROR_DF_CONFLICT)
- error(_("some local refs could not be updated; try running\n"
- " 'git remote prune %s' to remove any old, conflicting "
+ if (!rc)
+ commit_fetch_head(&fetch_head);
+
@@ builtin/fetch.c: static int store_updated_refs(const char *raw_url, const char *remote_name,
abort:
@@ builtin/fetch.c: static int store_updated_refs(const char *raw_url, const char *
+ strbuf_release(&err);
+ ref_transaction_free(transaction);
free(url);
- fclose(fp);
+ close_fetch_head(&fetch_head);
return rc;
+@@ builtin/fetch.c: int cmd_fetch(int argc, const char **argv, const char *prefix)
+ die(_("--filter can only be used with the remote "
+ "configured in extensions.partialclone"));
+
++ if (atomic_fetch)
++ die(_("--atomic can only be used when fetching "
++ "from one remote"));
++
+ if (stdin_refspecs)
+ die(_("--stdin can only be used when fetching "
+ "from one remote"));
## t/t5510-fetch.sh ##
@@ t/t5510-fetch.sh: test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
@@ t/t5510-fetch.sh: test_expect_success 'fetch --prune --tags with refspec prunes
+ cd "$D" &&
+ git clone . atomic &&
+ git branch atomic-branch &&
-+ git rev-parse atomic-branch >expected &&
++ oid=$(git rev-parse atomic-branch) &&
++ echo "$oid" >expected &&
+
+ git -C atomic fetch --atomic origin &&
+ git -C atomic rev-parse origin/atomic-branch >actual &&
-+ test_cmp expected actual
++ test_cmp expected actual &&
++ test $oid = "$(git -C atomic rev-parse --verify FETCH_HEAD)"
+'
+
+test_expect_success 'fetch --atomic works with multiple branches' '
@@ t/t5510-fetch.sh: test_expect_success 'fetch --prune --tags with refspec prunes
+ test_must_fail git -C atomic fetch --atomic origin refs/heads/*:refs/remotes/origin/* &&
+ test_must_fail git -C atomic rev-parse refs/remotes/origin/atomic-new-branch &&
+ git -C atomic rev-parse refs/remotes/origin/atomic-non-ff >expected &&
-+ test_cmp expected actual
++ test_cmp expected actual &&
++ test_must_be_empty atomic/.git/FETCH_HEAD
+'
+
+test_expect_success 'fetch --atomic executes a single reference transaction only' '
@@ t/t5510-fetch.sh: test_expect_success 'fetch --prune --tags with refspec prunes
+ git -C atomic for-each-ref >expected-refs &&
+ test_must_fail git -C atomic fetch --tags --atomic origin &&
+ git -C atomic for-each-ref >actual-refs &&
-+ test_cmp expected-refs actual-refs
++ test_cmp expected-refs actual-refs &&
++ test_must_be_empty atomic/.git/FETCH_HEAD
++'
++
++test_expect_success 'fetch --atomic --append appends to FETCH_HEAD' '
++ test_when_finished "rm -rf \"$D\"/atomic" &&
++
++ cd "$D" &&
++ git clone . atomic &&
++ oid=$(git rev-parse HEAD) &&
++
++ git branch atomic-fetch-head-1 &&
++ git -C atomic fetch --atomic origin atomic-fetch-head-1 &&
++ test_line_count = 1 atomic/.git/FETCH_HEAD &&
++
++ git branch atomic-fetch-head-2 &&
++ git -C atomic fetch --atomic --append origin atomic-fetch-head-2 &&
++ test_line_count = 2 atomic/.git/FETCH_HEAD &&
++ cp atomic/.git/FETCH_HEAD expected &&
++
++ write_script atomic/.git/hooks/reference-transaction <<-\EOF &&
++ exit 1
++ EOF
++
++ git branch atomic-fetch-head-3 &&
++ test_must_fail git -C atomic fetch --atomic --append origin atomic-fetch-head-3 &&
++ test_cmp expected atomic/.git/FETCH_HEAD
+'
+
test_expect_success '--refmap="" ignores configured refspec' '
--
2.30.0
From: Patrick Steinhardt <hidden> Date: 2021-01-08 12:12:14
When performing a fetch with the default `--write-fetch-head` option, we
write all updated references to FETCH_HEAD while the updates are
performed. Given that updates are not performed atomically, it means
that we we write to FETCH_HEAD even if some or all of the reference
updates fail.
Given that we simply update FETCH_HEAD ad-hoc with each reference, the
logic is completely contained in `store_update_refs` and thus quite hard
to extend. This can already be seen by the way we skip writing to the
FETCH_HEAD: instead of having a conditional which simply skips writing,
we instead open "/dev/null" and needlessly write all updates there.
We are about to extend git-fetch(1) to accept an `--atomic` flag which
will make the fetch an all-or-nothing operation with regards to the
reference updates. This will also require us to make the updates to
FETCH_HEAD an all-or-nothing operation, but as explained doing so is not
easy with the current layout. This commit thus refactors the wa we write
to FETCH_HEAD and pulls out the logic to open, append to, commit and
close the file. While this may seem rather over-the top at first,
pulling out this logic will make it a lot easier to update the code in a
subsequent commit. It also allows us to easily skip writing completely
in case `--no-write-fetch-head` was passed.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 80 ++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 62 insertions(+), 18 deletions(-)
@@ -897,6 +897,56 @@ static int iterate_ref_map(void *cb_data, struct object_id *oid)return0;}+structfetch_head{+FILE*fp;+};++staticintopen_fetch_head(structfetch_head*fetch_head)+{+constchar*filename=git_path_fetch_head(the_repository);++if(!write_fetch_head)+return0;++fetch_head->fp=fopen(filename,"a");+if(!fetch_head->fp)+returnerror_errno(_("cannot open %s"),filename);++return0;+}++staticvoidappend_fetch_head(structfetch_head*fetch_head,constchar*old_oid,+constchar*merge_status_marker,constchar*note,+constchar*url,size_turl_len)+{+size_ti;++if(!write_fetch_head)+return;++fprintf(fetch_head->fp,"%s\t%s\t%s",+old_oid,merge_status_marker,note);+for(i=0;i<url_len;++i)+if('\n'==url[i])+fputs("\\n",fetch_head->fp);+else+fputc(url[i],fetch_head->fp);+fputc('\n',fetch_head->fp);+}++staticvoidcommit_fetch_head(structfetch_head*fetch_head)+{+/* Nothing to commit yet. */+}++staticvoidclose_fetch_head(structfetch_head*fetch_head)+{+if(!write_fetch_head)+return;++fclose(fetch_head->fp);+}+staticconstcharwarn_show_forced_updates[]=N_("Fetch normally indicates which branches had a forced update,\n""but that check has been disabled. To re-enable, use '--show-forced-updates'\n"
@@ -909,22 +959,19 @@ N_("It took %.2f seconds to check forced updates. You can use\n"staticintstore_updated_refs(constchar*raw_url,constchar*remote_name,intconnectivity_checked,structref*ref_map){-FILE*fp;+structfetch_headfetch_head;structcommit*commit;inturl_len,i,rc=0;structstrbufnote=STRBUF_INIT;constchar*what,*kind;structref*rm;char*url;-constchar*filename=(!write_fetch_head-?"/dev/null"-:git_path_fetch_head(the_repository));intwant_status;intsummary_width=transport_summary_width(ref_map);-fp=fopen(filename,"a");-if(!fp)-returnerror_errno(_("cannot open %s"),filename);+rc=open_fetch_head(&fetch_head);+if(rc)+return-1;if(raw_url)url=transport_anonymize_url(raw_url);
@@ -1016,16 +1063,10 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,merge_status_marker="not-for-merge";/* fall-through */caseFETCH_HEAD_MERGE:-fprintf(fp,"%s\t%s\t%s",-oid_to_hex(&rm->old_oid),-merge_status_marker,-note.buf);-for(i=0;i<url_len;++i)-if('\n'==url[i])-fputs("\\n",fp);-else-fputc(url[i],fp);-fputc('\n',fp);+append_fetch_head(&fetch_head,+oid_to_hex(&rm->old_oid),+merge_status_marker,+note.buf,url,url_len);break;default:/* do not write anything to FETCH_HEAD */
@@ -1060,6 +1101,9 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,}}+if(!rc)+commit_fetch_head(&fetch_head);+if(rc&STORE_REF_ERROR_DF_CONFLICT)error(_("some local refs could not be updated; try running\n"" 'git remote prune %s' to remove any old, conflicting "
From: Patrick Steinhardt <hidden> Date: 2021-01-08 12:12:14
The handling of ref updates is completely handled by `s_update_ref()`,
which will manage the complete lifecycle of the reference transaction.
This is fine right now given that git-fetch(1) does not support atomic
fetches, so each reference gets its own transaction. It is quite
inflexible though, as `s_update_ref()` only knows about a single
reference update at a time, so it doesn't allow us to alter the
strategy.
This commit prepares `s_update_ref()` and its only caller
`update_local_ref()` to allow passing an external transaction. If none
is given, then the existing behaviour is triggered which creates a new
transaction and directly commits it. Otherwise, if the caller provides a
transaction, then we only queue the update but don't commit it. This
optionally allows the caller to manage when a transaction will be
committed.
Given that `update_local_ref()` is always called with a `NULL`
transaction for now, no change in behaviour is expected from this
change.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 43 +++++++++++++++++++++++++++----------------
1 file changed, 27 insertions(+), 16 deletions(-)
@@ -802,7 +813,7 @@ static int update_local_ref(struct ref *ref,starts_with(ref->name,"refs/tags/")){if(force||ref->force){intr;-r=s_update_ref("updating tag",ref,0);+r=s_update_ref("updating tag",ref,transaction,0);format_display(display,r?'!':'t',_("[tag update]"),r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -839,7 +850,7 @@ static int update_local_ref(struct ref *ref,what=_("[new ref]");}-r=s_update_ref(msg,ref,0);+r=s_update_ref(msg,ref,transaction,0);format_display(display,r?'!':'*',what,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -861,7 +872,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"..");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("fast-forward",ref,1);+r=s_update_ref("fast-forward",ref,transaction,1);format_display(display,r?'!':' ',quickref.buf,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -873,7 +884,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"...");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("forced-update",ref,1);+r=s_update_ref("forced-update",ref,transaction,1);format_display(display,r?'!':'+',quickref.buf,r?_("unable to update local ref"):_("forced update"),remote,pretty_ref,summary_width);
From: Patrick Steinhardt <hidden> Date: 2021-01-08 12:12:46
The cleanup code in `s_update_ref()` is currently duplicated for both
succesful and erroneous exit paths. This commit refactors the function
to have a shared exit path for both cases to remove the duplication.
Suggested-by: Christian Couder <redacted>
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 37 ++++++++++++++++++++-----------------
1 file changed, 20 insertions(+), 17 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2021-01-08 12:12:46
When executing a fetch, then git will currently allocate one reference
transaction per reference update and directly commit it. This means that
fetches are non-atomic: even if some of the reference updates fail,
others may still succeed and modify local references.
This is fine in many scenarios, but this strategy has its downsides.
- The view of remote references may be inconsistent and may show a
bastardized state of the remote repository.
- Batching together updates may improve performance in certain
scenarios. While the impact probably isn't as pronounced with loose
references, the upcoming reftable backend may benefit as it needs to
write less files in case the update is batched.
- The reference-update hook is currently being executed twice per
updated reference. While this doesn't matter when there is no such
hook, we have seen severe performance regressions when doing a
git-fetch(1) with reference-transaction hook when the remote
repository has hundreds of thousands of references.
Similar to `git push --atomic`, this commit thus introduces atomic
fetches. Instead of allocating one reference transaction per updated
reference, it causes us to only allocate a single transaction and commit
it as soon as all updates were received. If locking of any reference
fails, then we abort the complete transaction and don't update any
reference, which gives us an all-or-nothing fetch.
Note that this may not completely fix the first of above downsides, as
the consistent view also depends on the server-side. If the server
doesn't have a consistent view of its own references during the
reference negotiation phase, then the client would get the same
inconsistent view the server has. This is a separate problem though and,
if it actually exists, can be fixed at a later point.
This commit also changes the way we write FETCH_HEAD in case `--atomic`
is passed. Instead of writing changes as we go, we need to accumulate
all changes first and only commit them at the end when we know that all
reference updates succeeded. Ideally, we'd just do so via a temporary
file so that we don't need to carry all updates in-memory. This isn't
trivially doable though considering the `--append` mode, where we do not
truncate the file but simply append to it. And given that we support
concurrent processes appending to FETCH_HEAD at the same time without
any loss of data, seeding the temporary file with current contents of
FETCH_HEAD initially and then doing a rename wouldn't work either. So
this commit implements the simple strategy of buffering all changes and
appending them to the file on commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
Documentation/fetch-options.txt | 4 +
builtin/fetch.c | 66 ++++++++++---
t/t5510-fetch.sh | 168 ++++++++++++++++++++++++++++++++
3 files changed, 227 insertions(+), 11 deletions(-)
@@ -7,6 +7,10 @@ existing contents of `.git/FETCH_HEAD`. Without this option old data in `.git/FETCH_HEAD` will be overwritten.+--atomic::+ Use an atomic transaction to update local refs. Either all refs are+ updated, or on error, no refs are updated.+ --depth=<depth>:: Limit fetching to the specified number of commits from the tip of each remote branch history. If fetching to a 'shallow' repository
@@ -63,6 +63,7 @@ static int enable_auto_gc = 1;staticinttags=TAGS_DEFAULT,unshallow,update_shallow,deepen;staticintmax_jobs=-1,submodule_fetch_jobs_config=-1;staticintfetch_parallel_config=1;+staticintatomic_fetch;staticenumtransport_familyfamily;staticconstchar*depth;staticconstchar*deepen_since;
@@ -144,6 +145,8 @@ static struct option builtin_fetch_options[] = {N_("set upstream for git pull/fetch")),OPT_BOOL('a',"append",&append,N_("append to .git/FETCH_HEAD instead of overwriting")),+OPT_BOOL(0,"atomic",&atomic_fetch,+N_("use atomic transaction to update references")),OPT_STRING(0,"upload-pack",&upload_pack,N_("path"),N_("path to upload pack on remote end")),OPT__FORCE(&force,N_("force overwrite of local reference"),0),
@@ -1945,6 +1985,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)die(_("--filter can only be used with the remote ""configured in extensions.partialclone"));+if(atomic_fetch)+die(_("--atomic can only be used when fetching "+"from one remote"));+if(stdin_refspecs)die(_("--stdin can only be used when fetching ""from one remote"));
@@ -176,6 +176,174 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'gitrev-parsesometag'+test_expect_success'fetch --atomic works with a single branch''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch&&+oid=$(gitrev-parseatomic-branch)&&+echo"$oid">expected&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parseorigin/atomic-branch>actual&&+test_cmpexpectedactual&&+test$oid="$(git-Catomicrev-parse--verifyFETCH_HEAD)"+'++test_expect_success'fetch --atomic works with multiple branches''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch-1&&+gitbranchatomic-branch-2&&+gitbranchatomic-branch-3&&+gitrev-parserefs/heads/atomic-branch-1refs/heads/atomic-branch-2refs/heads/atomic-branch-3>actual&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-branch-1refs/remotes/origin/atomic-branch-2refs/remotes/origin/atomic-branch-3>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic works with mixed branches and tags''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-mixed-branch&&+gittagatomic-mixed-tag&&+gitrev-parserefs/heads/atomic-mixed-branchrefs/tags/atomic-mixed-tag>actual&&++git-Catomicfetch--tags--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-mixed-branchrefs/tags/atomic-mixed-tag>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic prunes references''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-prune-delete&&+gitclone.atomic&&+gitbranch--deleteatomic-prune-delete&&+gitbranchatomic-prune-create&&+gitrev-parserefs/heads/atomic-prune-create>actual&&++git-Catomicfetch--prune--atomicorigin&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-prune-delete&&+git-Catomicrev-parserefs/remotes/origin/atomic-prune-create>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic aborts with non-fast-forward update''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-non-ff&&+gitclone.atomic&&+gitrev-parseHEAD>actual&&++gitbranchatomic-new-branch&&+parent_commit=$(gitrev-parseatomic-non-ff~)&&+gitupdate-refrefs/heads/atomic-non-ff$parent_commit&&++test_must_failgit-Catomicfetch--atomicoriginrefs/heads/*:refs/remotes/origin/*&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-new-branch&&+git-Catomicrev-parserefs/remotes/origin/atomic-non-ff>expected&&+test_cmpexpectedactual&&+test_must_be_emptyatomic/.git/FETCH_HEAD+'++test_expect_success'fetch --atomic executes a single reference transaction only''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-1&&+gitbranchatomic-hooks-2&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+committed+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+EOF++git-Catomicfetch--atomicorigin&&+test_cmpexpectedatomic/actual+'++test_expect_success'fetch --atomic aborts all reference updates if hook aborts''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-abort-1&&+gitbranchatomic-hooks-abort-2&&+gitbranchatomic-hooks-abort-3&&+gittagatomic-hooks-abort&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+aborted+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+exit1+EOF++git-Catomicfor-each-ref>expected-refs&&+test_must_failgit-Catomicfetch--tags--atomicorigin&&+git-Catomicfor-each-ref>actual-refs&&+test_cmpexpected-refsactual-refs&&+test_must_be_emptyatomic/.git/FETCH_HEAD+'++test_expect_success'fetch --atomic --append appends to FETCH_HEAD''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+oid=$(gitrev-parseHEAD)&&++gitbranchatomic-fetch-head-1&&+git-Catomicfetch--atomicoriginatomic-fetch-head-1&&+test_line_count=1atomic/.git/FETCH_HEAD&&++gitbranchatomic-fetch-head-2&&+git-Catomicfetch--atomic--appendoriginatomic-fetch-head-2&&+test_line_count=2atomic/.git/FETCH_HEAD&&+cpatomic/.git/FETCH_HEADexpected&&++write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+exit1+EOF++gitbranchatomic-fetch-head-3&&+test_must_failgit-Catomicfetch--atomic--appendoriginatomic-fetch-head-3&&+test_cmpexpectedatomic/.git/FETCH_HEAD+'+ test_expect_success'--refmap="" ignores configured refspec''cd"$TRASH_DIRECTORY"&&gitclone"$D"remote-refs&&
From: Patrick Steinhardt <hidden> Date: 2021-01-11 11:06:06
When performing a fetch with the default `--write-fetch-head` option, we
write all updated references to FETCH_HEAD while the updates are
performed. Given that updates are not performed atomically, it means
that we we write to FETCH_HEAD even if some or all of the reference
updates fail.
Given that we simply update FETCH_HEAD ad-hoc with each reference, the
logic is completely contained in `store_update_refs` and thus quite hard
to extend. This can already be seen by the way we skip writing to the
FETCH_HEAD: instead of having a conditional which simply skips writing,
we instead open "/dev/null" and needlessly write all updates there.
We are about to extend git-fetch(1) to accept an `--atomic` flag which
will make the fetch an all-or-nothing operation with regards to the
reference updates. This will also require us to make the updates to
FETCH_HEAD an all-or-nothing operation, but as explained doing so is not
easy with the current layout. This commit thus refactors the wa we write
to FETCH_HEAD and pulls out the logic to open, append to, commit and
close the file. While this may seem rather over-the top at first,
pulling out this logic will make it a lot easier to update the code in a
subsequent commit. It also allows us to easily skip writing completely
in case `--no-write-fetch-head` was passed.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 108 +++++++++++++++++++++++++++++++++++-------------
remote.h | 2 +-
2 files changed, 80 insertions(+), 30 deletions(-)
@@ -897,6 +897,73 @@ static int iterate_ref_map(void *cb_data, struct object_id *oid)return0;}+structfetch_head{+FILE*fp;+};++staticintopen_fetch_head(structfetch_head*fetch_head)+{+constchar*filename=git_path_fetch_head(the_repository);++if(write_fetch_head){+fetch_head->fp=fopen(filename,"a");+if(!fetch_head->fp)+returnerror_errno(_("cannot open %s"),filename);+}else{+fetch_head->fp=NULL;+}++return0;+}++staticvoidappend_fetch_head(structfetch_head*fetch_head,+conststructobject_id*old_oid,+enumfetch_head_statusfetch_head_status,+constchar*note,+constchar*url,size_turl_len)+{+charold_oid_hex[GIT_MAX_HEXSZ+1];+constchar*merge_status_marker;+size_ti;++if(!fetch_head->fp)+return;++switch(fetch_head_status){+caseFETCH_HEAD_NOT_FOR_MERGE:+merge_status_marker="not-for-merge";+break;+caseFETCH_HEAD_MERGE:+merge_status_marker="";+break;+default:+/* do not write anything to FETCH_HEAD */+return;+}++fprintf(fetch_head->fp,"%s\t%s\t%s",+oid_to_hex_r(old_oid_hex,old_oid),merge_status_marker,note);+for(i=0;i<url_len;++i)+if('\n'==url[i])+fputs("\\n",fetch_head->fp);+else+fputc(url[i],fetch_head->fp);+fputc('\n',fetch_head->fp);+}++staticvoidcommit_fetch_head(structfetch_head*fetch_head)+{+/* Nothing to commit yet. */+}++staticvoidclose_fetch_head(structfetch_head*fetch_head)+{+if(!fetch_head->fp)+return;++fclose(fetch_head->fp);+}+staticconstcharwarn_show_forced_updates[]=N_("Fetch normally indicates which branches had a forced update,\n""but that check has been disabled. To re-enable, use '--show-forced-updates'\n"
@@ -909,22 +976,19 @@ N_("It took %.2f seconds to check forced updates. You can use\n"staticintstore_updated_refs(constchar*raw_url,constchar*remote_name,intconnectivity_checked,structref*ref_map){-FILE*fp;+structfetch_headfetch_head;structcommit*commit;inturl_len,i,rc=0;structstrbufnote=STRBUF_INIT;constchar*what,*kind;structref*rm;char*url;-constchar*filename=(!write_fetch_head-?"/dev/null"-:git_path_fetch_head(the_repository));intwant_status;intsummary_width=transport_summary_width(ref_map);-fp=fopen(filename,"a");-if(!fp)-returnerror_errno(_("cannot open %s"),filename);+rc=open_fetch_head(&fetch_head);+if(rc)+return-1;if(raw_url)url=transport_anonymize_url(raw_url);
@@ -1011,26 +1074,10 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,strbuf_addf(¬e,"%s ",kind);strbuf_addf(¬e,"'%s' of ",what);}-switch(rm->fetch_head_status){-caseFETCH_HEAD_NOT_FOR_MERGE:-merge_status_marker="not-for-merge";-/* fall-through */-caseFETCH_HEAD_MERGE:-fprintf(fp,"%s\t%s\t%s",-oid_to_hex(&rm->old_oid),-merge_status_marker,-note.buf);-for(i=0;i<url_len;++i)-if('\n'==url[i])-fputs("\\n",fp);-else-fputc(url[i],fp);-fputc('\n',fp);-break;-default:-/* do not write anything to FETCH_HEAD */-break;-}++append_fetch_head(&fetch_head,&rm->old_oid,+rm->fetch_head_status,+note.buf,url,url_len);strbuf_reset(¬e);if(ref){
@@ -1060,6 +1107,9 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,}}+if(!rc)+commit_fetch_head(&fetch_head);+if(rc&STORE_REF_ERROR_DF_CONFLICT)error(_("some local refs could not be updated; try running\n"" 'git remote prune %s' to remove any old, conflicting "
From: Patrick Steinhardt <hidden> Date: 2021-01-11 11:06:27
This commit refactors `append_fetch_head()` to use a `struct strbuf` for
formatting the update which we're about to append to the FETCH_HEAD
file. While the refactoring doesn't have much of a benefit right now, it
servers as a preparatory step to implement atomic fetches where we need
to buffer all updates to FETCH_HEAD and only flush them out if all
reference updates succeeded.
No change in behaviour is expected from this commit.
---
builtin/fetch.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2021-01-11 11:11:35
On Mon, Jan 11, 2021 at 12:05:20PM +0100, Patrick Steinhardt wrote:
This commit refactors `append_fetch_head()` to use a `struct strbuf` for
formatting the update which we're about to append to the FETCH_HEAD
file. While the refactoring doesn't have much of a benefit right now, it
servers as a preparatory step to implement atomic fetches where we need
to buffer all updates to FETCH_HEAD and only flush them out if all
reference updates succeeded.
No change in behaviour is expected from this commit.
Forgot to add my
Signed-off-by: Patrick Steinhardt <redacted>
Will amend in v4.
Patrick
From: Christian Couder <hidden> Date: 2021-01-11 11:18:28
On Mon, Jan 11, 2021 at 12:05 PM Patrick Steinhardt [off-list ref] wrote:
This commit refactors `append_fetch_head()` to use a `struct strbuf` for
formatting the update which we're about to append to the FETCH_HEAD
file. While the refactoring doesn't have much of a benefit right now, it
servers as a preparatory step to implement atomic fetches where we need
s/servers/serves/
to buffer all updates to FETCH_HEAD and only flush them out if all
reference updates succeeded.
From: Patrick Steinhardt <hidden> Date: 2021-01-11 11:06:27
The handling of ref updates is completely handled by `s_update_ref()`,
which will manage the complete lifecycle of the reference transaction.
This is fine right now given that git-fetch(1) does not support atomic
fetches, so each reference gets its own transaction. It is quite
inflexible though, as `s_update_ref()` only knows about a single
reference update at a time, so it doesn't allow us to alter the
strategy.
This commit prepares `s_update_ref()` and its only caller
`update_local_ref()` to allow passing an external transaction. If none
is given, then the existing behaviour is triggered which creates a new
transaction and directly commits it. Otherwise, if the caller provides a
transaction, then we only queue the update but don't commit it. This
optionally allows the caller to manage when a transaction will be
committed.
Given that `update_local_ref()` is always called with a `NULL`
transaction for now, no change in behaviour is expected from this
change.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 51 ++++++++++++++++++++++++++++++-------------------
1 file changed, 31 insertions(+), 20 deletions(-)
@@ -806,7 +817,7 @@ static int update_local_ref(struct ref *ref,starts_with(ref->name,"refs/tags/")){if(force||ref->force){intr;-r=s_update_ref("updating tag",ref,0);+r=s_update_ref("updating tag",ref,transaction,0);format_display(display,r?'!':'t',_("[tag update]"),r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -843,7 +854,7 @@ static int update_local_ref(struct ref *ref,what=_("[new ref]");}-r=s_update_ref(msg,ref,0);+r=s_update_ref(msg,ref,transaction,0);format_display(display,r?'!':'*',what,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -865,7 +876,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"..");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("fast-forward",ref,1);+r=s_update_ref("fast-forward",ref,transaction,1);format_display(display,r?'!':' ',quickref.buf,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -877,7 +888,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"...");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("forced-update",ref,1);+r=s_update_ref("forced-update",ref,transaction,1);format_display(display,r?'!':'+',quickref.buf,r?_("unable to update local ref"):_("forced update"),remote,pretty_ref,summary_width);
From: Patrick Steinhardt <hidden> Date: 2021-01-11 11:06:34
The cleanup code in `s_update_ref()` is currently duplicated for both
succesful and erroneous exit paths. This commit refactors the function
to have a shared exit path for both cases to remove the duplication.
Suggested-by: Christian Couder <redacted>
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 47 +++++++++++++++++++++++++++--------------------
1 file changed, 27 insertions(+), 20 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2021-01-11 11:07:28
When executing a fetch, then git will currently allocate one reference
transaction per reference update and directly commit it. This means that
fetches are non-atomic: even if some of the reference updates fail,
others may still succeed and modify local references.
This is fine in many scenarios, but this strategy has its downsides.
- The view of remote references may be inconsistent and may show a
bastardized state of the remote repository.
- Batching together updates may improve performance in certain
scenarios. While the impact probably isn't as pronounced with loose
references, the upcoming reftable backend may benefit as it needs to
write less files in case the update is batched.
- The reference-update hook is currently being executed twice per
updated reference. While this doesn't matter when there is no such
hook, we have seen severe performance regressions when doing a
git-fetch(1) with reference-transaction hook when the remote
repository has hundreds of thousands of references.
Similar to `git push --atomic`, this commit thus introduces atomic
fetches. Instead of allocating one reference transaction per updated
reference, it causes us to only allocate a single transaction and commit
it as soon as all updates were received. If locking of any reference
fails, then we abort the complete transaction and don't update any
reference, which gives us an all-or-nothing fetch.
Note that this may not completely fix the first of above downsides, as
the consistent view also depends on the server-side. If the server
doesn't have a consistent view of its own references during the
reference negotiation phase, then the client would get the same
inconsistent view the server has. This is a separate problem though and,
if it actually exists, can be fixed at a later point.
This commit also changes the way we write FETCH_HEAD in case `--atomic`
is passed. Instead of writing changes as we go, we need to accumulate
all changes first and only commit them at the end when we know that all
reference updates succeeded. Ideally, we'd just do so via a temporary
file so that we don't need to carry all updates in-memory. This isn't
trivially doable though considering the `--append` mode, where we do not
truncate the file but simply append to it. And given that we support
concurrent processes appending to FETCH_HEAD at the same time without
any loss of data, seeding the temporary file with current contents of
FETCH_HEAD initially and then doing a rename wouldn't work either. So
this commit implements the simple strategy of buffering all changes and
appending them to the file on commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
Documentation/fetch-options.txt | 4 +
builtin/fetch.c | 46 ++++++++-
t/t5510-fetch.sh | 168 ++++++++++++++++++++++++++++++++
3 files changed, 213 insertions(+), 5 deletions(-)
@@ -7,6 +7,10 @@ existing contents of `.git/FETCH_HEAD`. Without this option old data in `.git/FETCH_HEAD` will be overwritten.+--atomic::+ Use an atomic transaction to update local refs. Either all refs are+ updated, or on error, no refs are updated.+ --depth=<depth>:: Limit fetching to the specified number of commits from the tip of each remote branch history. If fetching to a 'shallow' repository
@@ -63,6 +63,7 @@ static int enable_auto_gc = 1;staticinttags=TAGS_DEFAULT,unshallow,update_shallow,deepen;staticintmax_jobs=-1,submodule_fetch_jobs_config=-1;staticintfetch_parallel_config=1;+staticintatomic_fetch;staticenumtransport_familyfamily;staticconstchar*depth;staticconstchar*deepen_since;
@@ -144,6 +145,8 @@ static struct option builtin_fetch_options[] = {N_("set upstream for git pull/fetch")),OPT_BOOL('a',"append",&append,N_("append to .git/FETCH_HEAD instead of overwriting")),+OPT_BOOL(0,"atomic",&atomic_fetch,+N_("use atomic transaction to update references")),OPT_STRING(0,"upload-pack",&upload_pack,N_("path"),N_("path to upload pack on remote end")),OPT__FORCE(&force,N_("force overwrite of local reference"),0),
@@ -1961,6 +1993,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)die(_("--filter can only be used with the remote ""configured in extensions.partialclone"));+if(atomic_fetch)+die(_("--atomic can only be used when fetching "+"from one remote"));+if(stdin_refspecs)die(_("--stdin can only be used when fetching ""from one remote"));
@@ -176,6 +176,174 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'gitrev-parsesometag'+test_expect_success'fetch --atomic works with a single branch''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch&&+oid=$(gitrev-parseatomic-branch)&&+echo"$oid">expected&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parseorigin/atomic-branch>actual&&+test_cmpexpectedactual&&+test$oid="$(git-Catomicrev-parse--verifyFETCH_HEAD)"+'++test_expect_success'fetch --atomic works with multiple branches''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch-1&&+gitbranchatomic-branch-2&&+gitbranchatomic-branch-3&&+gitrev-parserefs/heads/atomic-branch-1refs/heads/atomic-branch-2refs/heads/atomic-branch-3>actual&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-branch-1refs/remotes/origin/atomic-branch-2refs/remotes/origin/atomic-branch-3>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic works with mixed branches and tags''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-mixed-branch&&+gittagatomic-mixed-tag&&+gitrev-parserefs/heads/atomic-mixed-branchrefs/tags/atomic-mixed-tag>actual&&++git-Catomicfetch--tags--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-mixed-branchrefs/tags/atomic-mixed-tag>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic prunes references''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-prune-delete&&+gitclone.atomic&&+gitbranch--deleteatomic-prune-delete&&+gitbranchatomic-prune-create&&+gitrev-parserefs/heads/atomic-prune-create>actual&&++git-Catomicfetch--prune--atomicorigin&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-prune-delete&&+git-Catomicrev-parserefs/remotes/origin/atomic-prune-create>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic aborts with non-fast-forward update''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-non-ff&&+gitclone.atomic&&+gitrev-parseHEAD>actual&&++gitbranchatomic-new-branch&&+parent_commit=$(gitrev-parseatomic-non-ff~)&&+gitupdate-refrefs/heads/atomic-non-ff$parent_commit&&++test_must_failgit-Catomicfetch--atomicoriginrefs/heads/*:refs/remotes/origin/*&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-new-branch&&+git-Catomicrev-parserefs/remotes/origin/atomic-non-ff>expected&&+test_cmpexpectedactual&&+test_must_be_emptyatomic/.git/FETCH_HEAD+'++test_expect_success'fetch --atomic executes a single reference transaction only''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-1&&+gitbranchatomic-hooks-2&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+committed+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+EOF++git-Catomicfetch--atomicorigin&&+test_cmpexpectedatomic/actual+'++test_expect_success'fetch --atomic aborts all reference updates if hook aborts''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-abort-1&&+gitbranchatomic-hooks-abort-2&&+gitbranchatomic-hooks-abort-3&&+gittagatomic-hooks-abort&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+aborted+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+exit1+EOF++git-Catomicfor-each-ref>expected-refs&&+test_must_failgit-Catomicfetch--tags--atomicorigin&&+git-Catomicfor-each-ref>actual-refs&&+test_cmpexpected-refsactual-refs&&+test_must_be_emptyatomic/.git/FETCH_HEAD+'++test_expect_success'fetch --atomic --append appends to FETCH_HEAD''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+oid=$(gitrev-parseHEAD)&&++gitbranchatomic-fetch-head-1&&+git-Catomicfetch--atomicoriginatomic-fetch-head-1&&+test_line_count=1atomic/.git/FETCH_HEAD&&++gitbranchatomic-fetch-head-2&&+git-Catomicfetch--atomic--appendoriginatomic-fetch-head-2&&+test_line_count=2atomic/.git/FETCH_HEAD&&+cpatomic/.git/FETCH_HEADexpected&&++write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+exit1+EOF++gitbranchatomic-fetch-head-3&&+test_must_failgit-Catomicfetch--atomic--appendoriginatomic-fetch-head-3&&+test_cmpexpectedatomic/.git/FETCH_HEAD+'+ test_expect_success'--refmap="" ignores configured refspec''cd"$TRASH_DIRECTORY"&&gitclone"$D"remote-refs&&
From: Patrick Steinhardt <hidden> Date: 2021-01-12 12:29:03
Hi,
this is the fourth version of my patch series to implement support for
atomic reference updates for git-fetch(1). It's similar to `git push
--atomic`, only that it applies to the local side. That is the fetch
will either succeed and update all remote references or it will fail and
update none.
Changes compared to v3:
- Fixed indentation of the switch statement in 1/5.
- Added my missing SOB to 2/5 and fixed a typo in the commit
message.
Please see the attached range-diff for more details.
Patrick
Patrick Steinhardt (5):
fetch: extract writing to FETCH_HEAD
fetch: use strbuf to format FETCH_HEAD updates
fetch: refactor `s_update_ref` to use common exit path
fetch: allow passing a transaction to `s_update_ref()`
fetch: implement support for atomic reference updates
Documentation/fetch-options.txt | 4 +
builtin/fetch.c | 228 +++++++++++++++++++++++---------
remote.h | 2 +-
t/t5510-fetch.sh | 168 +++++++++++++++++++++++
4 files changed, 342 insertions(+), 60 deletions(-)
Range-diff against v3:
1: 61dc19a1ca ! 1: 9fcc8b54de fetch: extract writing to FETCH_HEAD
@@ builtin/fetch.c: static int iterate_ref_map(void *cb_data, struct object_id *oid
+ return;
+
+ switch (fetch_head_status) {
-+ case FETCH_HEAD_NOT_FOR_MERGE:
-+ merge_status_marker = "not-for-merge";
-+ break;
-+ case FETCH_HEAD_MERGE:
-+ merge_status_marker = "";
-+ break;
-+ default:
-+ /* do not write anything to FETCH_HEAD */
-+ return;
++ case FETCH_HEAD_NOT_FOR_MERGE:
++ merge_status_marker = "not-for-merge";
++ break;
++ case FETCH_HEAD_MERGE:
++ merge_status_marker = "";
++ break;
++ default:
++ /* do not write anything to FETCH_HEAD */
++ return;
+ }
+
+ fprintf(fetch_head->fp, "%s\t%s\t%s",
2: a19762690e ! 2: fb8542270a fetch: use strbuf to format FETCH_HEAD updates
@@ Commit message
This commit refactors `append_fetch_head()` to use a `struct strbuf` for
formatting the update which we're about to append to the FETCH_HEAD
file. While the refactoring doesn't have much of a benefit right now, it
- servers as a preparatory step to implement atomic fetches where we need
+ serves as a preparatory step to implement atomic fetches where we need
to buffer all updates to FETCH_HEAD and only flush them out if all
reference updates succeeded.
No change in behaviour is expected from this commit.
+ Signed-off-by: Patrick Steinhardt [off-list ref]
+
## builtin/fetch.c ##
@@ builtin/fetch.c: static int iterate_ref_map(void *cb_data, struct object_id *oid)
@@ builtin/fetch.c: static int open_fetch_head(struct fetch_head *fetch_head)
fetch_head->fp = NULL;
}
@@ builtin/fetch.c: static void append_fetch_head(struct fetch_head *fetch_head,
- return;
+ return;
}
- fprintf(fetch_head->fp, "%s\t%s\t%s",
3: c411f30e09 = 3: ba6908aa8c fetch: refactor `s_update_ref` to use common exit path
4: 865d357ba7 = 4: 7f820f6f83 fetch: allow passing a transaction to `s_update_ref()`
5: 6a79e7adcc = 5: 0b57d7a651 fetch: implement support for atomic reference updates
--
2.30.0
From: Patrick Steinhardt <hidden> Date: 2021-01-12 12:28:38
The cleanup code in `s_update_ref()` is currently duplicated for both
succesful and erroneous exit paths. This commit refactors the function
to have a shared exit path for both cases to remove the duplication.
Suggested-by: Christian Couder <redacted>
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 47 +++++++++++++++++++++++++++--------------------
1 file changed, 27 insertions(+), 20 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2021-01-12 12:28:38
When performing a fetch with the default `--write-fetch-head` option, we
write all updated references to FETCH_HEAD while the updates are
performed. Given that updates are not performed atomically, it means
that we we write to FETCH_HEAD even if some or all of the reference
updates fail.
Given that we simply update FETCH_HEAD ad-hoc with each reference, the
logic is completely contained in `store_update_refs` and thus quite hard
to extend. This can already be seen by the way we skip writing to the
FETCH_HEAD: instead of having a conditional which simply skips writing,
we instead open "/dev/null" and needlessly write all updates there.
We are about to extend git-fetch(1) to accept an `--atomic` flag which
will make the fetch an all-or-nothing operation with regards to the
reference updates. This will also require us to make the updates to
FETCH_HEAD an all-or-nothing operation, but as explained doing so is not
easy with the current layout. This commit thus refactors the wa we write
to FETCH_HEAD and pulls out the logic to open, append to, commit and
close the file. While this may seem rather over-the top at first,
pulling out this logic will make it a lot easier to update the code in a
subsequent commit. It also allows us to easily skip writing completely
in case `--no-write-fetch-head` was passed.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 108 +++++++++++++++++++++++++++++++++++-------------
remote.h | 2 +-
2 files changed, 80 insertions(+), 30 deletions(-)
@@ -897,6 +897,73 @@ static int iterate_ref_map(void *cb_data, struct object_id *oid)return0;}+structfetch_head{+FILE*fp;+};++staticintopen_fetch_head(structfetch_head*fetch_head)+{+constchar*filename=git_path_fetch_head(the_repository);++if(write_fetch_head){+fetch_head->fp=fopen(filename,"a");+if(!fetch_head->fp)+returnerror_errno(_("cannot open %s"),filename);+}else{+fetch_head->fp=NULL;+}++return0;+}++staticvoidappend_fetch_head(structfetch_head*fetch_head,+conststructobject_id*old_oid,+enumfetch_head_statusfetch_head_status,+constchar*note,+constchar*url,size_turl_len)+{+charold_oid_hex[GIT_MAX_HEXSZ+1];+constchar*merge_status_marker;+size_ti;++if(!fetch_head->fp)+return;++switch(fetch_head_status){+caseFETCH_HEAD_NOT_FOR_MERGE:+merge_status_marker="not-for-merge";+break;+caseFETCH_HEAD_MERGE:+merge_status_marker="";+break;+default:+/* do not write anything to FETCH_HEAD */+return;+}++fprintf(fetch_head->fp,"%s\t%s\t%s",+oid_to_hex_r(old_oid_hex,old_oid),merge_status_marker,note);+for(i=0;i<url_len;++i)+if('\n'==url[i])+fputs("\\n",fetch_head->fp);+else+fputc(url[i],fetch_head->fp);+fputc('\n',fetch_head->fp);+}++staticvoidcommit_fetch_head(structfetch_head*fetch_head)+{+/* Nothing to commit yet. */+}++staticvoidclose_fetch_head(structfetch_head*fetch_head)+{+if(!fetch_head->fp)+return;++fclose(fetch_head->fp);+}+staticconstcharwarn_show_forced_updates[]=N_("Fetch normally indicates which branches had a forced update,\n""but that check has been disabled. To re-enable, use '--show-forced-updates'\n"
@@ -909,22 +976,19 @@ N_("It took %.2f seconds to check forced updates. You can use\n"staticintstore_updated_refs(constchar*raw_url,constchar*remote_name,intconnectivity_checked,structref*ref_map){-FILE*fp;+structfetch_headfetch_head;structcommit*commit;inturl_len,i,rc=0;structstrbufnote=STRBUF_INIT;constchar*what,*kind;structref*rm;char*url;-constchar*filename=(!write_fetch_head-?"/dev/null"-:git_path_fetch_head(the_repository));intwant_status;intsummary_width=transport_summary_width(ref_map);-fp=fopen(filename,"a");-if(!fp)-returnerror_errno(_("cannot open %s"),filename);+rc=open_fetch_head(&fetch_head);+if(rc)+return-1;if(raw_url)url=transport_anonymize_url(raw_url);
@@ -1011,26 +1074,10 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,strbuf_addf(¬e,"%s ",kind);strbuf_addf(¬e,"'%s' of ",what);}-switch(rm->fetch_head_status){-caseFETCH_HEAD_NOT_FOR_MERGE:-merge_status_marker="not-for-merge";-/* fall-through */-caseFETCH_HEAD_MERGE:-fprintf(fp,"%s\t%s\t%s",-oid_to_hex(&rm->old_oid),-merge_status_marker,-note.buf);-for(i=0;i<url_len;++i)-if('\n'==url[i])-fputs("\\n",fp);-else-fputc(url[i],fp);-fputc('\n',fp);-break;-default:-/* do not write anything to FETCH_HEAD */-break;-}++append_fetch_head(&fetch_head,&rm->old_oid,+rm->fetch_head_status,+note.buf,url,url_len);strbuf_reset(¬e);if(ref){
@@ -1060,6 +1107,9 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,}}+if(!rc)+commit_fetch_head(&fetch_head);+if(rc&STORE_REF_ERROR_DF_CONFLICT)error(_("some local refs could not be updated; try running\n"" 'git remote prune %s' to remove any old, conflicting "
From: Patrick Steinhardt <hidden> Date: 2021-01-12 12:28:43
This commit refactors `append_fetch_head()` to use a `struct strbuf` for
formatting the update which we're about to append to the FETCH_HEAD
file. While the refactoring doesn't have much of a benefit right now, it
serves as a preparatory step to implement atomic fetches where we need
to buffer all updates to FETCH_HEAD and only flush them out if all
reference updates succeeded.
No change in behaviour is expected from this commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
From: Patrick Steinhardt <hidden> Date: 2021-01-12 12:29:41
The handling of ref updates is completely handled by `s_update_ref()`,
which will manage the complete lifecycle of the reference transaction.
This is fine right now given that git-fetch(1) does not support atomic
fetches, so each reference gets its own transaction. It is quite
inflexible though, as `s_update_ref()` only knows about a single
reference update at a time, so it doesn't allow us to alter the
strategy.
This commit prepares `s_update_ref()` and its only caller
`update_local_ref()` to allow passing an external transaction. If none
is given, then the existing behaviour is triggered which creates a new
transaction and directly commits it. Otherwise, if the caller provides a
transaction, then we only queue the update but don't commit it. This
optionally allows the caller to manage when a transaction will be
committed.
Given that `update_local_ref()` is always called with a `NULL`
transaction for now, no change in behaviour is expected from this
change.
Signed-off-by: Patrick Steinhardt <redacted>
---
builtin/fetch.c | 51 ++++++++++++++++++++++++++++++-------------------
1 file changed, 31 insertions(+), 20 deletions(-)
@@ -806,7 +817,7 @@ static int update_local_ref(struct ref *ref,starts_with(ref->name,"refs/tags/")){if(force||ref->force){intr;-r=s_update_ref("updating tag",ref,0);+r=s_update_ref("updating tag",ref,transaction,0);format_display(display,r?'!':'t',_("[tag update]"),r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -843,7 +854,7 @@ static int update_local_ref(struct ref *ref,what=_("[new ref]");}-r=s_update_ref(msg,ref,0);+r=s_update_ref(msg,ref,transaction,0);format_display(display,r?'!':'*',what,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -865,7 +876,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"..");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("fast-forward",ref,1);+r=s_update_ref("fast-forward",ref,transaction,1);format_display(display,r?'!':' ',quickref.buf,r?_("unable to update local ref"):NULL,remote,pretty_ref,summary_width);
@@ -877,7 +888,7 @@ static int update_local_ref(struct ref *ref,strbuf_add_unique_abbrev(&quickref,¤t->object.oid,DEFAULT_ABBREV);strbuf_addstr(&quickref,"...");strbuf_add_unique_abbrev(&quickref,&ref->new_oid,DEFAULT_ABBREV);-r=s_update_ref("forced-update",ref,1);+r=s_update_ref("forced-update",ref,transaction,1);format_display(display,r?'!':'+',quickref.buf,r?_("unable to update local ref"):_("forced update"),remote,pretty_ref,summary_width);
From: Patrick Steinhardt <hidden> Date: 2021-01-12 12:29:41
When executing a fetch, then git will currently allocate one reference
transaction per reference update and directly commit it. This means that
fetches are non-atomic: even if some of the reference updates fail,
others may still succeed and modify local references.
This is fine in many scenarios, but this strategy has its downsides.
- The view of remote references may be inconsistent and may show a
bastardized state of the remote repository.
- Batching together updates may improve performance in certain
scenarios. While the impact probably isn't as pronounced with loose
references, the upcoming reftable backend may benefit as it needs to
write less files in case the update is batched.
- The reference-update hook is currently being executed twice per
updated reference. While this doesn't matter when there is no such
hook, we have seen severe performance regressions when doing a
git-fetch(1) with reference-transaction hook when the remote
repository has hundreds of thousands of references.
Similar to `git push --atomic`, this commit thus introduces atomic
fetches. Instead of allocating one reference transaction per updated
reference, it causes us to only allocate a single transaction and commit
it as soon as all updates were received. If locking of any reference
fails, then we abort the complete transaction and don't update any
reference, which gives us an all-or-nothing fetch.
Note that this may not completely fix the first of above downsides, as
the consistent view also depends on the server-side. If the server
doesn't have a consistent view of its own references during the
reference negotiation phase, then the client would get the same
inconsistent view the server has. This is a separate problem though and,
if it actually exists, can be fixed at a later point.
This commit also changes the way we write FETCH_HEAD in case `--atomic`
is passed. Instead of writing changes as we go, we need to accumulate
all changes first and only commit them at the end when we know that all
reference updates succeeded. Ideally, we'd just do so via a temporary
file so that we don't need to carry all updates in-memory. This isn't
trivially doable though considering the `--append` mode, where we do not
truncate the file but simply append to it. And given that we support
concurrent processes appending to FETCH_HEAD at the same time without
any loss of data, seeding the temporary file with current contents of
FETCH_HEAD initially and then doing a rename wouldn't work either. So
this commit implements the simple strategy of buffering all changes and
appending them to the file on commit.
Signed-off-by: Patrick Steinhardt <redacted>
---
Documentation/fetch-options.txt | 4 +
builtin/fetch.c | 46 ++++++++-
t/t5510-fetch.sh | 168 ++++++++++++++++++++++++++++++++
3 files changed, 213 insertions(+), 5 deletions(-)
@@ -7,6 +7,10 @@ existing contents of `.git/FETCH_HEAD`. Without this option old data in `.git/FETCH_HEAD` will be overwritten.+--atomic::+ Use an atomic transaction to update local refs. Either all refs are+ updated, or on error, no refs are updated.+ --depth=<depth>:: Limit fetching to the specified number of commits from the tip of each remote branch history. If fetching to a 'shallow' repository
@@ -63,6 +63,7 @@ static int enable_auto_gc = 1;staticinttags=TAGS_DEFAULT,unshallow,update_shallow,deepen;staticintmax_jobs=-1,submodule_fetch_jobs_config=-1;staticintfetch_parallel_config=1;+staticintatomic_fetch;staticenumtransport_familyfamily;staticconstchar*depth;staticconstchar*deepen_since;
@@ -144,6 +145,8 @@ static struct option builtin_fetch_options[] = {N_("set upstream for git pull/fetch")),OPT_BOOL('a',"append",&append,N_("append to .git/FETCH_HEAD instead of overwriting")),+OPT_BOOL(0,"atomic",&atomic_fetch,+N_("use atomic transaction to update references")),OPT_STRING(0,"upload-pack",&upload_pack,N_("path"),N_("path to upload pack on remote end")),OPT__FORCE(&force,N_("force overwrite of local reference"),0),
@@ -1961,6 +1993,10 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)die(_("--filter can only be used with the remote ""configured in extensions.partialclone"));+if(atomic_fetch)+die(_("--atomic can only be used when fetching "+"from one remote"));+if(stdin_refspecs)die(_("--stdin can only be used when fetching ""from one remote"));
@@ -176,6 +176,174 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'gitrev-parsesometag'+test_expect_success'fetch --atomic works with a single branch''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch&&+oid=$(gitrev-parseatomic-branch)&&+echo"$oid">expected&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parseorigin/atomic-branch>actual&&+test_cmpexpectedactual&&+test$oid="$(git-Catomicrev-parse--verifyFETCH_HEAD)"+'++test_expect_success'fetch --atomic works with multiple branches''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-branch-1&&+gitbranchatomic-branch-2&&+gitbranchatomic-branch-3&&+gitrev-parserefs/heads/atomic-branch-1refs/heads/atomic-branch-2refs/heads/atomic-branch-3>actual&&++git-Catomicfetch--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-branch-1refs/remotes/origin/atomic-branch-2refs/remotes/origin/atomic-branch-3>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic works with mixed branches and tags''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-mixed-branch&&+gittagatomic-mixed-tag&&+gitrev-parserefs/heads/atomic-mixed-branchrefs/tags/atomic-mixed-tag>actual&&++git-Catomicfetch--tags--atomicorigin&&+git-Catomicrev-parserefs/remotes/origin/atomic-mixed-branchrefs/tags/atomic-mixed-tag>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic prunes references''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-prune-delete&&+gitclone.atomic&&+gitbranch--deleteatomic-prune-delete&&+gitbranchatomic-prune-create&&+gitrev-parserefs/heads/atomic-prune-create>actual&&++git-Catomicfetch--prune--atomicorigin&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-prune-delete&&+git-Catomicrev-parserefs/remotes/origin/atomic-prune-create>expected&&+test_cmpexpectedactual+'++test_expect_success'fetch --atomic aborts with non-fast-forward update''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitbranchatomic-non-ff&&+gitclone.atomic&&+gitrev-parseHEAD>actual&&++gitbranchatomic-new-branch&&+parent_commit=$(gitrev-parseatomic-non-ff~)&&+gitupdate-refrefs/heads/atomic-non-ff$parent_commit&&++test_must_failgit-Catomicfetch--atomicoriginrefs/heads/*:refs/remotes/origin/*&&+test_must_failgit-Catomicrev-parserefs/remotes/origin/atomic-new-branch&&+git-Catomicrev-parserefs/remotes/origin/atomic-non-ff>expected&&+test_cmpexpectedactual&&+test_must_be_emptyatomic/.git/FETCH_HEAD+'++test_expect_success'fetch --atomic executes a single reference transaction only''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-1&&+gitbranchatomic-hooks-2&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+committed+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-2+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+EOF++git-Catomicfetch--atomicorigin&&+test_cmpexpectedatomic/actual+'++test_expect_success'fetch --atomic aborts all reference updates if hook aborts''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+gitbranchatomic-hooks-abort-1&&+gitbranchatomic-hooks-abort-2&&+gitbranchatomic-hooks-abort-3&&+gittagatomic-hooks-abort&&+head_oid=$(gitrev-parseHEAD)&&++cat>expected<<-EOF&&+prepared+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+aborted+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-1+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-2+$ZERO_OID$head_oidrefs/remotes/origin/atomic-hooks-abort-3+$ZERO_OID$head_oidrefs/tags/atomic-hooks-abort+EOF++rm-fatomic/actual&&+write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+(echo"$*"&&cat)>>actual+exit1+EOF++git-Catomicfor-each-ref>expected-refs&&+test_must_failgit-Catomicfetch--tags--atomicorigin&&+git-Catomicfor-each-ref>actual-refs&&+test_cmpexpected-refsactual-refs&&+test_must_be_emptyatomic/.git/FETCH_HEAD+'++test_expect_success'fetch --atomic --append appends to FETCH_HEAD''+test_when_finished"rm -rf \"$D\"/atomic"&&++cd"$D"&&+gitclone.atomic&&+oid=$(gitrev-parseHEAD)&&++gitbranchatomic-fetch-head-1&&+git-Catomicfetch--atomicoriginatomic-fetch-head-1&&+test_line_count=1atomic/.git/FETCH_HEAD&&++gitbranchatomic-fetch-head-2&&+git-Catomicfetch--atomic--appendoriginatomic-fetch-head-2&&+test_line_count=2atomic/.git/FETCH_HEAD&&+cpatomic/.git/FETCH_HEADexpected&&++write_scriptatomic/.git/hooks/reference-transaction<<-\EOF&&+exit1+EOF++gitbranchatomic-fetch-head-3&&+test_must_failgit-Catomicfetch--atomic--appendoriginatomic-fetch-head-3&&+test_cmpexpectedatomic/.git/FETCH_HEAD+'+ test_expect_success'--refmap="" ignores configured refspec''cd"$TRASH_DIRECTORY"&&gitclone"$D"remote-refs&&