On Thu, Sep 08, 2016 at 07:49:12AM +0700, Duy Nguyen wrote:
> I got the message in the subject when pushing to github today. Yes I
> know it's github, not git. But according to stackoveflow [1] it's a
> local problem. Which makes me think, if we know exactly what this is
> (or at least roughly the problem area), maybe we could improve git to
> catch it locally in the first place (and because other git servers may
> not have the same protection as github). Jeff maybe you can reveal
> something about this "fatal error in commit_refs"? I'm sure it's not
> in git code. But I would understand if the answer is "no".
The short answer is that it's nothing to do with Git or the client; it's
GitHub-specific code running on the server that is outside of Git
entirely.
The long answer is that pushes to GitHub don't hit Git directly these
days. They hit a proxy layer that speaks just enough of the Git protocol
to relay to N separate receives spread across N replica servers[1]. Those
receive-packs take in the pack and verify it, but don't actually update
any refs[2]. Then the proxy layer runs its own set of policy hooks, and
speaks a commit-protocol to each of the replicas so that they all agree
on the new ref state. That last step is called "commit_refs" internally.
So this is really an internal failure at the ref-update stage. There
_should_ be a reasonable error message, but I think "fatal error in
commit_refs" is the generic last-ditch fallback. I'll pass this along to
people in charge of that code, as we should be generating a more useful
error message.
-Peff
[1] I glossed over a lot of details there. If you're interested:
http://githubengineering.com/introducing-dgit/
http://githubengineering.com/building-resilience-in-spokes/
[2] Initially the proxy just fed a set of temporary refs to
receive-pack, and it was completely stock. These days we do have
a trivial patch that skips the ref write. I haven't sent it upstream
because it's useless by itself (but it's below for reference).
I'm happy to polish it if somebody actually has a use for it.
---
Documentation/config.txt | 8 ++++++++
builtin/receive-pack.c | 13 +++++++++++--
t/t9944-receive-pack-nowrite.sh | 37 +++++++++++++++++++++++++++++++++++++
3 files changed, 56 insertions(+), 2 deletions(-)
create mode 100755 t/t9944-receive-pack-nowrite.sh
diff --git a/Documentation/config.txt b/Documentation/config.txt
index f8e6484..38cc1ac 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -2406,6 +2406,14 @@ receive.refUpdateNameLimit::
is a hard limit of 65520 bytes due to git's protocol, so this
value must be smaller than that.
+receive.writeRefs::
+ If set to `false`, `receive-pack` will perform all of the usual
+ ref consistency checks (checking for non-ff, etc), but _not_
+ actually write any ref changes to disk (nor even check that such
+ writes would succeed, as doing so atomically would require
+ taking individual ref locks to be of any value). The default is
+ `true`.
+
remote.pushDefault::
The remote to push to by default. Overrides
`branch.<name>.remote` for all branches, and is overridden by
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 94704e7..4a87365 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -87,6 +87,8 @@ static long nonce_stamp_slop;
static unsigned long nonce_stamp_slop_limit;
static struct ref_transaction *transaction;
+static int write_refs = 1;
+
static enum deny_action parse_deny_action(const char *var, const char *value)
{
if (value) {
@@ -244,6 +246,11 @@ static int receive_pack_config(const char *var, const char
*value, void *cb)
return 0;
}
+ if (strcmp(var, "receive.writerefs") == 0) {
+ write_refs = git_config_bool(var, value);
+ return 0;
+ }
+
return git_default_config(var, value, cb);
}
@@ -1060,7 +1067,8 @@ static const char *update(struct command *cmd, struct
shallow_info *si)
cmd->did_not_exist = 1;
}
}
- if (ref_transaction_delete(transaction,
+ if (write_refs &&
+ ref_transaction_delete(transaction,
namespaced_name,
old_sha1,
flags, "push", &err)) {
@@ -1077,7 +1085,8 @@ static const char *update(struct command *cmd, struct
shallow_info *si)
update_shallow_ref(cmd, si))
return "shallow error";
- if (ref_transaction_update(transaction,
+ if (write_refs &&
+ ref_transaction_update(transaction,
namespaced_name,
new_sha1, old_sha1,
flags, "push",
diff --git a/t/t9944-receive-pack-nowrite.sh b/t/t9944-receive-pack-nowrite.sh
new file mode 100755
index 0000000..7b27bc1
--- /dev/null
+++ b/t/t9944-receive-pack-nowrite.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+
+test_description='test no-write tweak to receive-pack'
+. ./test-lib.sh
+
+test_expect_success 'create a few commits' '
+ test_commit one &&
+ git update-ref refs/heads/a HEAD &&
+ test_commit two &&
+ git update-ref refs/heads/b HEAD &&
+ test_commit three &&
+ git update-ref refs/heads/c HEAD
+'
+
+# push just two; hold back "c" so we can push a creation later
+test_expect_success 'create destination repo' '
+ git init --bare dst.git &&
+ git for-each-ref refs/heads/a refs/heads/b >expect &&
+ git push dst.git a b &&
+ git -C dst.git for-each-ref >actual &&
+ test_cmp expect actual
+'
+
+# push an update, a deletion, and a creation
+test_expect_success 'push with no-write config set' '
+ git push --receive-pack="git -c \
+ receive.writeRefs=false \
+ receive-pack" \
+ dst.git b:a :b c:c
+'
+
+test_expect_success 'push did not touch real refs' '
+ git -C dst.git for-each-ref >actual &&
+ test_cmp expect actual
+'
+
+test_done
--
2.10.0.rc2.154.gb4a4b8b