Subject: Re: [RFC] Detached-HEAD reminder on commit?

From: Junio C Hamano <[email-masked]>
Date: Fri, 05 Sep 2008 16:43:52 -0700

Junio C Hamano <[email-masked]> writes:

> "Avery Pennarun" <[email-masked]> writes:
> ...
>> 1) Checking out a remote branch "git checkout origin/master" detaches
>> my HEAD, which is kind of bad, since it's such a common thing to want
>> to do.
>
> I do not think it is bad at all.  The feature to detach HEAD was designed
> for that kind of usage.  Start sightseeing, possibly futz with the code,
> and even create some snapshot commits, and then:
>
>  * if it starts to take a usable shape, say "git checkout -b my-topic",
>    from there, to give your exploration a lasting home; or
>    
>  * if it doesn't pan out, just discard it with "git checkout -f master"
>    (or whatever you wanted to switch back to).
>
> One thing that might help for downstream people would be to be able to say
> "I am making 'my-topic' branch out of a detached HEAD, but it really is
> meant to be a fork of origin/master that I detached my HEAD from, so
> please set up tracking for that one".
>
> You could force people to say "git checkout -b my-topic origin/master"
> from the beginning, but that is very unreasonable and unworkable.  When
> you are exploring, you more often than not do not know where your quest
> would lead to until spending some time.  It is quite important to be able
> to delay the decision to create a local branch to keep what you did, and
> (more importantly) to be able to delay deciding what to name that topic.
>
> Perhaps "git checkout -b my-topic" from a detached HEAD should inspect the
> HEAD reflog to see which remote (or local) branch you came from, and give
> that to the --track logic.

So here is a patch for discussion, not heavily tested, but:

	$ git checkout origin/master
        $ git commit; hack hack hack ...
        $ git checkout --track -b mybranch

sequence should result in mybranch tracking the 'master' branch from the
'origin'.

The patch is just a proof of concept; doing this for HEAD reflog that is
several megabytes long might take nontrivial amount of time (at least from
performance standard of git); if we wanted to go this route, we should add
an API to read the reflog entries from more recent to older.

 branch.c |   59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 59 insertions(+), 0 deletions(-)

diff --git c/branch.c w/branch.c
index b1e59f2..2ec6418 100644
--- c/branch.c
+++ w/branch.c
@@ -48,6 +48,62 @@ static int should_setup_rebase(const struct tracking *tracking)
 }
 
 /*
+ * A branch is created out of "HEAD" and we would want tracking;
+ * go back the reflog to figure out where we really came from.
+ */
+static int refine_head_one(const char *name, size_t namelen,
+			   struct strbuf *found_ref)
+{
+	char *real_ref;
+	unsigned char sha1[20];
+	if (dwim_ref(name, namelen, sha1, &real_ref) != 1)
+		return 0;
+	strbuf_reset(found_ref);
+	strbuf_addstr(found_ref, real_ref);
+	return 0;
+}
+
+static int one_head_ent(unsigned char *osha1, unsigned char *nsha1,
+			const char *ident, unsigned long timestamp, int zone,
+			const char *message, void *cbdata)
+{
+	/*
+	 * Look for signs of HEAD coming from elsewhere.
+	 *
+	 * "checkout: moving from %*s to %s" done by "git checkout"
+	 * "%s: updating HEAD" done by "git reset"
+	 */
+	struct strbuf *found_ref = cbdata;
+	char *cp;
+	size_t len;
+
+	if (!prefixcmp(message, "checkout: moving from ")) {
+		cp = strstr(message, " to ");
+		if (!cp)
+			return 0;
+		cp += 4;
+		len = strlen(cp);
+		if (cp[len-1] == '\n')
+			len--;
+		return refine_head_one(cp, len, found_ref);
+	}
+
+	cp = strstr(message, ": updating HEAD");
+	if (cp && !cp[15])
+		return refine_head_one(message, cp - message, found_ref);
+	return 0;
+}
+
+static const char *refine_head_ref(void)
+{
+	struct strbuf found = STRBUF_INIT;
+
+	strbuf_addstr(&found, "HEAD");
+	for_each_reflog_ent("HEAD", one_head_ent, &found);
+	return strbuf_detach(&found, NULL);
+}
+
+/*
  * This is called when new_ref is branched off of orig_ref, and tries
  * to infer the settings for branch.<new_ref>.{remote,merge} from the
  * config.
@@ -58,6 +114,9 @@ static int setup_tracking(const char *new_ref, const char *orig_ref,
 	char key[1024];
 	struct tracking tracking;
 
+	if (!strcmp(orig_ref, "HEAD"))
+		orig_ref = refine_head_ref();
+
 	if (strlen(new_ref) > 1024 - 7 - 7 - 1)
 		return error("Tracking not set up: name too long: %s",
 				new_ref);