git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
From: Patrick Steinhardt <ps@pks.im>
To: git@vger.kernel.org
Cc: Junio C Hamano <gitster@pobox.com>,
	Christian Couder <christian.couder@gmail.com>
Subject: [PATCH v2 0/4] fetch: implement support for atomic reference updates
Date: Fri, 8 Jan 2021 13:11:10 +0100	[thread overview]
Message-ID: <cover.1610107599.git.ps@pks.im> (raw)
In-Reply-To: <cover.1610027375.git.ps@pks.im>

[-- Attachment #1: Type: text/plain, Size: 13320 bytes --]

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 <ps@pks.im>
     
      ## 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


[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

  parent reply	other threads:[~2021-01-08 12:14 UTC|newest]

Thread overview: 41+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-01-07 13:51 [PATCH 0/2] fetch: implement support for atomic reference updates Patrick Steinhardt
2021-01-07 13:51 ` [PATCH 1/2] fetch: allow passing a transaction to `s_update_ref()` Patrick Steinhardt
2021-01-07 22:59   ` Junio C Hamano
2021-01-08  0:45     ` Christian Couder
2021-01-08  7:18       ` Junio C Hamano
2021-01-07 13:51 ` [PATCH 2/2] fetch: implement support for atomic reference updates Patrick Steinhardt
2021-01-08  0:19   ` Junio C Hamano
2021-01-08 12:11 ` Patrick Steinhardt [this message]
2021-01-08 12:11   ` [PATCH v2 1/4] fetch: extract writing to FETCH_HEAD Patrick Steinhardt
2021-01-08 23:40     ` Junio C Hamano
2021-01-11 10:26       ` Patrick Steinhardt
2021-01-11 19:24         ` Junio C Hamano
2021-01-08 12:11   ` [PATCH v2 2/4] fetch: refactor `s_update_ref` to use common exit path Patrick Steinhardt
2021-01-08 23:50     ` Junio C Hamano
2021-01-11 10:28       ` Patrick Steinhardt
2021-01-08 12:11   ` [PATCH v2 3/4] fetch: allow passing a transaction to `s_update_ref()` Patrick Steinhardt
2021-01-08 23:53     ` Junio C Hamano
2021-01-08 12:11   ` [PATCH v2 4/4] fetch: implement support for atomic reference updates Patrick Steinhardt
2021-01-09  0:05     ` Junio C Hamano
2021-01-11 10:42       ` Patrick Steinhardt
2021-01-11 19:47         ` Junio C Hamano
2021-01-12 12:22           ` Patrick Steinhardt
2021-01-12 12:29             ` Patrick Steinhardt
2021-01-12 19:19             ` Junio C Hamano
2021-01-11 11:05 ` [PATCH v3 0/5] " Patrick Steinhardt
2021-01-11 11:05   ` [PATCH v3 1/5] fetch: extract writing to FETCH_HEAD Patrick Steinhardt
2021-01-11 23:16     ` Junio C Hamano
2021-01-11 11:05   ` [PATCH v3 2/5] fetch: use strbuf to format FETCH_HEAD updates Patrick Steinhardt
2021-01-11 11:10     ` Patrick Steinhardt
2021-01-11 11:17     ` Christian Couder
2021-01-11 23:21     ` Junio C Hamano
2021-01-11 11:05   ` [PATCH v3 3/5] fetch: refactor `s_update_ref` to use common exit path Patrick Steinhardt
2021-01-11 11:05   ` [PATCH v3 4/5] fetch: allow passing a transaction to `s_update_ref()` Patrick Steinhardt
2021-01-11 11:05   ` [PATCH v3 5/5] fetch: implement support for atomic reference updates Patrick Steinhardt
2021-01-12  0:04   ` [PATCH v3 0/5] " Junio C Hamano
2021-01-12 12:27 ` [PATCH v4 " Patrick Steinhardt
2021-01-12 12:27   ` [PATCH v4 1/5] fetch: extract writing to FETCH_HEAD Patrick Steinhardt
2021-01-12 12:27   ` [PATCH v4 2/5] fetch: use strbuf to format FETCH_HEAD updates Patrick Steinhardt
2021-01-12 12:27   ` [PATCH v4 3/5] fetch: refactor `s_update_ref` to use common exit path Patrick Steinhardt
2021-01-12 12:27   ` [PATCH v4 4/5] fetch: allow passing a transaction to `s_update_ref()` Patrick Steinhardt
2021-01-12 12:27   ` [PATCH v4 5/5] fetch: implement support for atomic reference updates Patrick Steinhardt

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=cover.1610107599.git.ps@pks.im \
    --to=ps@pks.im \
    --cc=christian.couder@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).