Skip to contentSkip to conversation

push: Provide situational hints for non-fast-forward errors

PATCH In review Christopher Tiwald 3 participants 9 messages Jun 15, 2016 Documentation

lore
Share & tools
lore
#0 lore raw

PATCH v3 1/1 push: Provide situational hints for non-fast-forward errors

Pushing a non-fast-forward update to a remote repository will result in an error, but the hint text doesn't provide the correct resolution in every case. Give better resolution advice in three push scenarios:

1) If you push your current branch and it triggers a non-fast-forward error, you should merge remote changes with 'git pull' before pushing again.

2) If you push to a shared repository others push to, and your local tracking branches are not kept up to date, the 'matching refs' default will generate non-fast-forward errors on outdated branches. If this is your workflow, the 'matching refs' default is not for you. Consider setting the 'push.default' configuration variable to 'current' or 'upstream' to ensure only your current branch is pushed.

3) If you explicitly specify a ref that is not your current branch or push matching branches with ':', you will generate a non-fast-forward error if any pushed branch tip is out of date. You should checkout the offending branch and merge remote changes before pushing again.

Teach transport.c to recognize these scenarios and configure push.c to hint for them. If 'git push's default behavior changes or we discover more scenarios, extension is easy. Standardize on the advice API and add three new advice variables, 'pushNonFFCurrent', 'pushNonFFDefault', and 'pushNonFFMatching'. Setting any of these to 'false' will disable their affiliated advice. Setting 'pushNonFastForward' to false will disable all three, thus preserving the config option for users who already set it, but guaranteeing new users won't disable push advice accidentally.

---
Changes since v2:
- Cleaned up commit message language, specifically in scenario one.
- Created one config variable per piece of non-ff push advice. Additionally, preserved 'pushNonFastForward' as a means of disabling all non-ff push advice. Users who set this config option should see no change to 'git push'.

 Documentation/config.txt |   19 +++++++++++++--
 advice.c                 |    6 +++++
 advice.h                 |    3 +++
 builtin/push.c           |   60 ++++++++++++++++++++++++++++++++++++++++++----
 cache.h                  |    8 +++++--
 environment.c            |    2 +-
 transport.c              |   13 ++++++++--
 7 files changed, 99 insertions(+), 12 deletions(-)
Documentation/config.txt +17 -2
index c081657..fb386ab 100644
@@ -138,8 +138,23 @@ advice.*::
 +
 --
 	pushNonFastForward::
-		Advice shown when linkgit:git-push[1] refuses
-		non-fast-forward refs.
+		Set this variable to 'false' if you want to disable
+		'pushNonFFCurrent', 'pushNonFFDefault', and
+		'pushNonFFMatching' simultaneously.
+	pushNonFFCurrent::
+		Advice shown when linkgit:git-push[1] fails due to a
+		non-fast-forward update to the current branch.
+	pushNonFFDefault::
+		Advice to set 'push.default' to 'upstream' or 'current'
+		when you ran linkgit:git-push[1] and pushed 'matching
+		refs' by default (i.e. you did not provide an explicit
+		refspec, and no 'push.default' configuration was set)
+		and it resulted in a non-fast-forward error.
+	pushNonFFMatching::
+		Advice shown when you ran linkgit:git-push[1] and pushed
+		'matching refs' explicitly (i.e. you used ':', or
+		specified a refspec that isn't your current branch) and
+		it resulted in a non-fast-forward error.
 	statusHints::
 		Directions on how to stage/unstage/add shown in the
 		output of linkgit:git-status[1] and the template shown
advice.c +6 -0
index 01130e5..a492eea 100644
@@ -1,6 +1,9 @@
 #include "cache.h"
 
 int advice_push_nonfastforward = 1;
+int advice_push_non_ff_current = 1;
+int advice_push_non_ff_default = 1;
+int advice_push_non_ff_matching = 1;
 int advice_status_hints = 1;
 int advice_commit_before_merge = 1;
 int advice_resolve_conflict = 1;
@@ -12,6 +15,9 @@ static struct {
 	int *preference;
 } advice_config[] = {
 	{ "pushnonfastforward", &advice_push_nonfastforward },
+	{ "pushnonffcurrent", &advice_push_non_ff_current },
+	{ "pushnonffdefault", &advice_push_non_ff_default },
+	{ "pushnonffmatching", &advice_push_non_ff_matching },
 	{ "statushints", &advice_status_hints },
 	{ "commitbeforemerge", &advice_commit_before_merge },
 	{ "resolveconflict", &advice_resolve_conflict },
advice.h +3 -0
index 7bda45b..f3cdbbf 100644
@@ -4,6 +4,9 @@
 #include "git-compat-util.h"
 
 extern int advice_push_nonfastforward;
+extern int advice_push_non_ff_current;
+extern int advice_push_non_ff_default;
+extern int advice_push_non_ff_matching;
 extern int advice_status_hints;
 extern int advice_commit_before_merge;
 extern int advice_resolve_conflict;
builtin/push.c +55 -5

87 lines not on this page · raw message

cache.h +6 -2
index e5e1aa4..427b600 100644
@@ -625,7 +625,8 @@ enum push_default_type {
 	PUSH_DEFAULT_NOTHING = 0,
 	PUSH_DEFAULT_MATCHING,
 	PUSH_DEFAULT_UPSTREAM,
-	PUSH_DEFAULT_CURRENT
+	PUSH_DEFAULT_CURRENT,
+	PUSH_DEFAULT_UNSPECIFIED
 };
 
 extern enum branch_track git_branch_track;
@@ -1008,7 +1009,6 @@ struct ref {
 	char *symref;
 	unsigned int force:1,
 		merge:1,
-		nonfastforward:1,
 		deletion:1;
 	enum {
 		REF_STATUS_NONE = 0,
@@ -1019,6 +1019,10 @@ struct ref {
 		REF_STATUS_REMOTE_REJECT,
 		REF_STATUS_EXPECTING_REPORT
 	} status;
+	enum {
+		NON_FF_HEAD = 1,
+		NON_FF_OTHER
+	} nonfastforward;
 	char *remote_status;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
environment.c +1 -1
index c93b8f4..d7e6c65 100644
@@ -52,7 +52,7 @@ enum safe_crlf safe_crlf = SAFE_CRLF_WARN;
 unsigned whitespace_rule_cfg = WS_DEFAULT_RULE;
 enum branch_track git_branch_track = BRANCH_TRACK_REMOTE;
 enum rebase_setup_type autorebase = AUTOREBASE_NEVER;
-enum push_default_type push_default = PUSH_DEFAULT_MATCHING;
+enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;
 #ifndef OBJECT_CREATION_MODE
 #define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS
 #endif
transport.c +11 -2
index 181f8f2..7864007 100644
@@ -721,6 +721,10 @@ void transport_print_push_status(const char *dest, struct ref *refs,
 {
 	struct ref *ref;
 	int n = 0;
+	unsigned char head_sha1[20];
+	char *head;
+
+	head = resolve_refdup("HEAD", head_sha1, 1, NULL);
 
 	if (verbose) {
 		for (ref = refs; ref; ref = ref->next)
@@ -738,8 +742,13 @@ void transport_print_push_status(const char *dest, struct ref *refs,
 		    ref->status != REF_STATUS_UPTODATE &&
 		    ref->status != REF_STATUS_OK)
 			n += print_one_push_status(ref, dest, n, porcelain);
-		if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD)
-			*nonfastforward = 1;
+		if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD &&
+		    *nonfastforward != NON_FF_HEAD) {
+			if (!strcmp(head, ref->name))
+				*nonfastforward = NON_FF_HEAD;
+			else
+				*nonfastforward = NON_FF_OTHER;
+		}
 	}
 }
 
Signature · 1 line
1.7.10.rc1.23.g2a051.dirty
#1 lore raw
Quoted 7 lines · Christopher Tiwald

Changes since v2:
- Cleaned up commit message language, specifically in scenario one.
- Created one config variable per piece of non-ff push advice. Additionally, preserved 'pushNonFastForward' as a means of disabling all non-ff push advice. Users who set this config option should see no change to 'git push'.

This one looks very sensible. Thanks.

#2 lore raw
In reply to Christopher Tiwald
Quoted 7 lines · Christopher Tiwald

Changes since v2:
- Cleaned up commit message language, specifically in scenario one.
- Created one config variable per piece of non-ff push advice. Additionally, preserved 'pushNonFastForward' as a means of disabling all non-ff push advice. Users who set this config option should see no change to 'git push'.

This version looks pretty good to me, but I have one question:

Quoted 9 lines
(quoted diff) +0 -1
@@ -1008,7 +1009,6 @@ struct ref {
 	char *symref;
 	unsigned int force:1,
 		merge:1,
-		nonfastforward:1,
 		deletion:1;
 	enum {
 		REF_STATUS_NONE = 0,

Review struct ref { · line 1028

Quoted 10 lines
(quoted diff) +4 -0
@@ -1019,6 +1019,10 @@ struct ref {
 		REF_STATUS_REMOTE_REJECT,
 		REF_STATUS_EXPECTING_REPORT
 	} status;
+	enum {
+		NON_FF_HEAD = 1,
+		NON_FF_OTHER
+	} nonfastforward;
 	char *remote_status;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */

Why is this enum stored inside the ref? It doesn't actually know whether it is a HEAD or not, and we never actually store that value there. We always just store a boolean (remote.c, ll. 1294-1298) and access it as one (remote.c, l. 1300; transport.c, l. 1259).

The only time we use the enum values is via the "int nonfastforward" passed to transport_push. I think it would be a lot clearer to leave nonfastforward as a single bit in the ref, and then define the enum elsewhere (or even just use #define if we are not going to use the enum type). Like this on top of your patch:

cache.h +1 -4
index 427b600..35f3075 100644
@@ -1009,6 +1009,7 @@ struct ref {
 	char *symref;
 	unsigned int force:1,
 		merge:1,
+		nonfastforward:1,
 		deletion:1;
 	enum {
 		REF_STATUS_NONE = 0,
@@ -1019,10 +1020,6 @@ struct ref {
 		REF_STATUS_REMOTE_REJECT,
 		REF_STATUS_EXPECTING_REPORT
 	} status;
-	enum {
-		NON_FF_HEAD = 1,
-		NON_FF_OTHER
-	} nonfastforward;
 	char *remote_status;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
transport.h +2 -0
index ce99ef8..1631a35 100644
@@ -138,6 +138,8 @@ int transport_set_option(struct transport *transport, const char *name,
 void transport_set_verbosity(struct transport *transport, int verbosity,
 	int force_progress);
 
+#define NON_FF_HEAD 1
+#define NON_FF_OTHER 2
 int transport_push(struct transport *connection,
 		   int refspec_nr, const char **refspec, int flags,
 		   int * nonfastforward);

I don't think your patch is buggy, because the enum is perfectly capable of being used as a single bit. But it's confusing to read, because ref->nonfastforward will never actually be set to the NON_FF_OTHER enum value.

-Peff

#3 lore raw
Quoted 5 lines · Jeff King

The only time we use the enum values is via the "int nonfastforward" passed to transport_push. I think it would be a lot clearer to leave nonfastforward as a single bit in the ref, and then define the enum elsewhere (or even just use #define if we are not going to use the enum type).

I used the REF_STATUS_* enum as a template for what I wanted to accomplish when authoring v1, but did notice there was no other place my new options made much sense (Junio helped me remove one other call between v1 and v2). I like the readability fixup, but it won't compile as both push.c and transport.c need to see these. Would something like the following work? It simply moves the define statements to cache.h, so that both push and transport can use them.

cache.h +4 -4
index 427b600..cb960c6 100644
@@ -1009,6 +1009,7 @@ struct ref {
 	char *symref;
 	unsigned int force:1,
 		merge:1,
+		nonfastforward:1,
 		deletion:1;
 	enum {
 		REF_STATUS_NONE = 0,
@@ -1019,15 +1020,14 @@ struct ref {
 		REF_STATUS_REMOTE_REJECT,
 		REF_STATUS_EXPECTING_REPORT
 	} status;
-	enum {
-		NON_FF_HEAD = 1,
-		NON_FF_OTHER
-	} nonfastforward;
 	char *remote_status;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
 };
 
+#define NON_FF_HEAD  1
+#define NON_FF_OTHER 2
+
 #define REF_NORMAL	(1u << 0)
 #define REF_HEADS	(1u << 1)
 #define REF_TAGS	(1u << 2)

It tests fine locally for me using the same test cases I've been using all along.

--
Christopher Tiwald

#4 lore raw
Quoted 7 lines · Christopher Tiwald

I used the REF_STATUS_* enum as a template for what I wanted to accomplish when authoring v1, but did notice there was no other place my new options made much sense (Junio helped me remove one other call between v1 and v2). I like the readability fixup, but it won't compile as both push.c and transport.c need to see these. Would something like the following work? It simply moves the define statements to cache.h, so that both push and transport can use them.

My suggestion put them in transport.h, which is included from both places. It compiles fine for me. Am I missing something?

Generally I would try to keep their definition near the function interface which uses them (i.e., transport_push). But I don't feel that strongly about it.

Your patch is already in 'next', so we will have to build on top rather than squashing. So here it is with an actual commit message:

-- >8 --
Subject: [PATCH] clean up struct ref's nonfastforward field

Each ref structure contains a "nonfastforward" field which is set during push to show whether the ref rewound history. Originally this was a single bit, but it was changed in f25950f (push: Provide situational hints for non-fast-forward errors) to an enum differentiating a non-ff of the current branch versus another branch.

However, we never actually set the member according to the enum values, nor did we ever read it expecting anything but a boolean value. But we did use the side effect of declaring the enum constants to store those values in a totally different integer variable. The code as-is isn't buggy, but the enum declaration inside "struct ref" is somewhat misleading.

Let's convert nonfastforward back into a single bit, and then define the NON_FF_* constants closer to where they would be used (they are returned via the "int *nonfastforward" parameter to transport_push, so we can define them there).

---
This is the least-invasive patch. You could also turn the "int *nonfastforward" into an enum, which might be even more readable.

 cache.h     |    5 +----
 transport.h |    2 ++
 2 files changed, 3 insertions(+), 4 deletions(-)
cache.h +1 -4
index 427b600..35f3075 100644
@@ -1009,6 +1009,7 @@ struct ref {
 	char *symref;
 	unsigned int force:1,
 		merge:1,
+		nonfastforward:1,
 		deletion:1;
 	enum {
 		REF_STATUS_NONE = 0,
@@ -1019,10 +1020,6 @@ struct ref {
 		REF_STATUS_REMOTE_REJECT,
 		REF_STATUS_EXPECTING_REPORT
 	} status;
-	enum {
-		NON_FF_HEAD = 1,
-		NON_FF_OTHER
-	} nonfastforward;
 	char *remote_status;
 	struct ref *peer_ref; /* when renaming */
 	char name[FLEX_ARRAY]; /* more */
transport.h +2 -0
index ce99ef8..1631a35 100644
@@ -138,6 +138,8 @@ int transport_set_option(struct transport *transport, const char *name,
 void transport_set_verbosity(struct transport *transport, int verbosity,
 	int force_progress);
 
+#define NON_FF_HEAD 1
+#define NON_FF_OTHER 2
 int transport_push(struct transport *connection,
 		   int refspec_nr, const char **refspec, int flags,
 		   int * nonfastforward);
Signature · 1 line
1.7.10.rc2.3.g0850
#5 lore raw
Quoted 10 lines · Jeff King
Quoted 7 lines · Christopher Tiwald

I used the REF_STATUS_* enum as a template for what I wanted to accomplish when authoring v1, but did notice there was no other place my new options made much sense (Junio helped me remove one other call between v1 and v2). I like the readability fixup, but it won't compile as both push.c and transport.c need to see these. Would something like the following work? It simply moves the define statements to cache.h, so that both push and transport can use them.

Quoted 2 lines

My suggestion put them in transport.h, which is included from both places. It compiles fine for me. Am I missing something?

Ah nope. That was me. Sorry about the noise. This otherwise makes sense to me.

--
Christopher Tiwald

#6 lore raw
Quoted 5 lines · Christopher Tiwald
Quoted 2 lines

My suggestion put them in transport.h, which is included from both places. It compiles fine for me. Am I missing something?

Ah nope. That was me. Sorry about the noise. This otherwise makes sense to me.

OK. Junio, can you throw the patch from the grandparent on top of ct/advise-push-default?

-Peff

#7 lore raw
In reply to Jeff King
Quoted 3 lines · Jeff King

Generally I would try to keep their definition near the function interface which uses them (i.e., transport_push). But I don't feel that strongly about it.

I think that advice makes sense.

Quoted 2 lines

Your patch is already in 'next', so we will have to build on top rather than squashing. So here it is with an actual commit message:

If the patch were already in 'next', we would have to build on top, but I thought I kept it out of 'next' because I knew this deserved a bit more review time. Perhaps I screwed up, or you are reading the history incorrectly?

	... goes and looks ...
Quoted 7 lines

-- >8 --
Subject: [PATCH] clean up struct ref's nonfastforward field

Each ref structure contains a "nonfastforward" field which is set during push to show whether the ref rewound history. Originally this was a single bit, but it was changed in f25950f (push: Provide situational hints for non-fast-forward

Whew. "git log remotes/ko/next..f25950f" says we are OK.

I'm however tempted to keep this follow-up patch as separate without squashing.

#8 lore raw
Quoted 9 lines · Junio C Hamano
Quoted 2 lines

Your patch is already in 'next', so we will have to build on top rather than squashing. So here it is with an actual commit message:

If the patch were already in 'next', we would have to build on top, but I thought I kept it out of 'next' because I knew this deserved a bit more review time. Perhaps I screwed up, or you are reading the history incorrectly?

	... goes and looks ...

Oops, you're right. I don't know why I thought it was, and obviously I should check before speaking next time. :)

Quoted 2 lines

I'm however tempted to keep this follow-up patch as separate without squashing.

Either way is fine with me.

BTW, I was on a semi-vacation when Christopher posted the patch, so I missed out on most of the timely review. But I really like how it ended up; it's exactly what I was hoping for when we discussed this a month or two ago. So thanks for working on it, Christopher.

-Peff