threads / rfc / 11133

[RFC] Introduce .git/BRANCH to point to the current branch

Subject: [RFC] Introduce .git/BRANCH to point to the current branch

## tl;dr

14 messages between Dec 4, 2007 and Dec 6, 2007.

replies: 13people: 7as markdown or json

Salikh Zakirov· Dec 4, 2007, 20:08 UTC · lore

Before this change, .git/HEAD file played two roles: (a) point to the base commit of the working directory (b) point to current branch

This combination leads to the confusing user experience if the branch changes independently of the working directory. This can happen in following cases:

1) git clone repo repo2
   cd repo2
   ... edit something
   git commit -a
   git push
2) checking out the same branch in the another working directory
associated with the same repository, e.g. created by
contrib/workdir/git-new-workdir
   git new-workdir repo repo2
   cd repo2
   ... edit something
   git commit -a

In both cases original repository will report differences between index and HEAD, even if though particular working directory state has not been changed at all.

This patch separates the notion of the current branch from the base commit. HEAD will always be "detached", and will store the hash of the base commit. Newly introduced BRANCH reference will point to the current branch.

When new commit is created, it is first stored in the HEAD. Then HEAD is compared to the branch head (BRANCH), and branch head is updated only if it is fast forward. So, in all simple case there will be no difference in the workflow.

In the "irregular" use cases above, branch head may diverge from the base commit of the repository. This situation is detected by the updaters of the HEAD, and user is advised to run merge. ---

Unfortunately, I have not figured out how to set up an empty repository to use separate HEAD and BRANCH, so one needs to set up an existing repository manually in order to play with this change:

	git rev-parse HEAD > .git/HEAD.new
	mv .git/HEAD .git/BRANCH
	mv .git/HEAD.new .git/HEAD

The way branch head is updated after HEAD update in this patch is clearly unacceptable, as it causes too much code duplication. I wonder if it would be better to introduce a special case in git-update-ref?

 git-checkout.sh |    4 +++-
 git-commit.sh   |   16 ++++++++++++++++
 git-merge.sh    |   15 +++++++++++++++
 wt-status.c     |   11 +++++++----
 wt-status.h     |    1 +
 5 files changed, 42 insertions(+), 5 deletions(-)
diff --git a/git-checkout.sh b/git-checkout.sh
index f6d58ac..8e1a1ab 100755
--- a/git-checkout.sh
+++ b/git-checkout.sh
@@ -266,7 +266,9 @@ if [ "$?" -eq 0 ]; then
 	if test -n "$branch"
 	then
 		old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'`
-		GIT_DIR="$GIT_DIR" git symbolic-ref -m "checkout: moving from ${old_branch_name:-$old} to $branch" HEAD "refs/heads/$branch"
+		GIT_DIR="$GIT_DIR" git symbolic-ref -m "checkout: moving from ${old_branch_name:-$old} to $branch" BRANCH "refs/heads/$branch"
+		commit=$(git rev-parse --verify BRANCH 2>/dev/null) &&
+		git update-ref --no-deref -m "checkout: moving from ${old_branch_name:-$old} to $branch" HEAD "$commit"
 		if test -n "$quiet"
 		then
 			true	# nothing
diff --git a/git-commit.sh b/git-commit.sh
index 2c4a406..b7315c1 100755
--- a/git-commit.sh
+++ b/git-commit.sh
@@ -609,7 +609,23 @@ then
 		mv "$NEXT_INDEX" "$THIS_INDEX"
 	else
 		: ;# happy
+	fi && {
+	branchhead=$(git rev-parse --verify BRANCH 2>/dev/null)
+	if test -n "$branchhead"
+	then
+		if test -z "`git rev-list $commit..$branchhead`"
+		then
+			# fast-forward, update the branch pointer as well
+			git update-ref -m "$GIT_REFLOG_ACTION: $rlogm" BRANCH $commit "$branchhead"
+		else
+			name=$(git symbolic-ref BRANCH | sed 's,refs/heads/,,')
+			echo >&2 "* HEAD diverged from $name"
+			echo >&2 "* Consider 'git merge $name'"
+		fi
+	else
+		git-update-ref -m "$GIT_REFLOG_ACTION: $rlogm" BRANCH $commit "$branchhead"
 	fi
+	}
 else
 	echo >&2 "* no commit message?  aborting commit."
 	false
diff --git a/git-merge.sh b/git-merge.sh
index 1c123a3..3d4b31b 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -94,6 +94,21 @@ finish () {
 		*)
 			git update-ref -m "$rlogm" HEAD "$1" "$head" || exit 1
 			git gc --auto
+			branchhead=$(git rev-parse --verify BRANCH 2>/dev/null)
+			if test -n "$branchhead"
+			then
+				if test -z "`git rev-list $commit..$branchhead`"
+				then
+					# fast-forward, update the branch pointer as well
+					git update-ref -m "$rlogm" BRANCH "$1" "$branchhead" || exit 1
+				else
+					name=$(git symbolic-ref BRANCH | sed 's,refs/heads/,,')
+					echo >&2 "* HEAD diverged from $name"
+					echo >&2 "* Consider 'git merge $name'"
+				fi
+			else
+				git-update-ref -m "$GIT_REFLOG_ACTION: $rlogm" BRANCH $commit "$branchhead"
+			fi
 			;;
 		esac
 		;;
diff --git a/wt-status.c b/wt-status.c
index bf2fe8d..9fe5d10 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -44,12 +44,14 @@ static const char* color(int slot)
 
 void wt_status_prepare(struct wt_status *s)
 {
-	unsigned char sha1[20];
-	const char *head;
+	unsigned char sha1[20], sha2[20];
+	const char *head, *branch;
 
 	memset(s, 0, sizeof(*s));
 	head = resolve_ref("HEAD", sha1, 0, NULL);
-	s->branch = head ? xstrdup(head) : NULL;
+	branch = resolve_ref("BRANCH", sha2, 0, NULL);
+	s->diverged = hashcmp(sha1, sha2);
+	s->branch = branch ? xstrdup(branch) : NULL;
 	s->reference = "HEAD";
 	s->fp = stdout;
 	s->index_file = get_index_file();
@@ -309,6 +311,7 @@ void wt_status_print(struct wt_status *s)
 	if (s->branch) {
 		const char *on_what = "On branch ";
 		const char *branch_name = s->branch;
+		const char *diverged = (s->diverged ? ", diverged" : "");
 		if (!prefixcmp(branch_name, "refs/heads/"))
 			branch_name += 11;
 		else if (!strcmp(branch_name, "HEAD")) {
@@ -316,7 +319,7 @@ void wt_status_print(struct wt_status *s)
 			on_what = "Not currently on any branch.";
 		}
 		color_fprintf_ln(s->fp, color(WT_STATUS_HEADER),
-			"# %s%s", on_what, branch_name);
+			"# %s%s%s", on_what, branch_name, diverged);
 	}
 
 	if (s->is_initial) {
diff --git a/wt-status.h b/wt-status.h
index 7744932..db8129d 100644
--- a/wt-status.h
+++ b/wt-status.h
@@ -17,6 +17,7 @@ struct wt_status {
 	int verbose;
 	int amend;
 	int untracked;
+	int diverged;
 	/* These are computed during processing of the individual sections */
 	int commitable;
 	int workdir_dirty;
-- 
1.5.3.5.610.g3532b
Matthieu Moy· Dec 4, 2007, 20:19 UTC · re: Salikh Zakirov · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

Salikh Zakirov <salikh@gmail.com> writes:
> This combination leads to the confusing user experience
> if the branch changes independently of the working directory.
> This can happen in following cases:

There's another thing that your proposal could change: navigating back in history without loosing track of the branch you're on.

Currently, I can do:
# Oh, what did this look like two commits ago?
$ git checkout HEAD^^
# Ah, OK, let's go back to the tip
$ git checkout branch-name
               ^^^^^^^^^^^
But I have to remember and re-type the branch name.
I can imagine (not tested with your patch) doing just:

$ git checkout HEAD^^ $ git checkout BRANCH

(that's one point in favor of your change, but I'm not familiar enough with git's internal to say whether this is sufficient to justify the change).

-- 
Matthieu
Jakub Narebski· Dec 4, 2007, 20:42 UTC · re: Matthieu Moy · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

Matthieu Moy <Matthieu.Moy@imag.fr> writes:
Show 5 quoted lines
> Salikh Zakirov <salikh@gmail.com> writes:
> 
> > This combination leads to the confusing user experience
> > if the branch changes independently of the working directory.
> > This can happen in following cases:
All those cases are cases of not recommended workflows.

Please search the archives for idea of BASE extension to index (instead of your separate file under .git/refs), and why it is not in current git.

BTW. how in your proposal would you detach HEAD?
Show 11 quoted lines
> There's another thing that your proposal could change: navigating back
> in history without loosing track of the branch you're on.
> 
> Currently, I can do:
> 
> # Oh, what did this look like two commits ago?
> $ git checkout HEAD^^
> # Ah, OK, let's go back to the tip
> $ git checkout branch-name
>                ^^^^^^^^^^^
> But I have to remember and re-type the branch name.
No, you don't have. You can use
  $ git checkout ORIG_HEAD
or
  $ git checkout HEAD@{1}
-- 
Jakub Narebski
ShadeHawk on #git
Poland
Junio C Hamano· Dec 4, 2007, 20:46 UTC · re: Jakub Narebski · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

Jakub Narebski <jnareb@gmail.com> writes:
Show 13 quoted lines
>> Currently, I can do:
>> 
>> # Oh, what did this look like two commits ago?
>> $ git checkout HEAD^^
>> # Ah, OK, let's go back to the tip
>> $ git checkout branch-name
>>                ^^^^^^^^^^^
>> But I have to remember and re-type the branch name.
>
> No, you don't have. You can use
>   $ git checkout ORIG_HEAD
> or
>   $ git checkout HEAD@{1}

But the point is he wants to go back to the branch he came from. He does not want to detach HEAD at the original commit.

Having said that, I am not sympathetic to "I have to remember".
Matthieu Moy· Dec 4, 2007, 21:32 UTC · re: Junio C Hamano · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

Junio C Hamano <gitster@pobox.com> writes:
> Having said that, I am not sympathetic to "I have to remember".

That's definitely not an important point, but I find it conveinient, for example in hg, to be able to just "hg update" to get to the tip of a branch.

A use-case (which joins the one of Salikh actually) is to publish both a repository and a working tree (I do that for my collegues which may or (usually) may not use git). Making a hook to update the working tree after a push is a terrible hack in git, and just "hg update" in Mercurial.

(once again, that's one point in favor of base separate from current branch, but it doesn't cancel the possible drawbacks. It seems the thread "Reverting the whole index-base series" at http://thread.gmane.org/gmane.comp.version-control.git/44360/focus=44525 is interesting to read, I'll dig a bit more.

As you mention in the thread, detaching head when pushing to a checked-out branch would probably be an interesting option)

-- 
Matthieu
Junio C Hamano· Dec 4, 2007, 21:48 UTC · re: Matthieu Moy · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

Matthieu Moy <Matthieu.Moy@imag.fr> writes:
Show 13 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
>
>> Having said that, I am not sympathetic to "I have to remember".
>
> That's definitely not an important point, but I find it conveinient,
> for example in hg, to be able to just "hg update" to get to the tip of
> a branch.
>
> A use-case (which joins the one of Salikh actually) is to publish both
> a repository and a working tree (I do that for my collegues which may
> or (usually) may not use git). Making a hook to update the working
> tree after a push is a terrible hack in git, and just "hg update" in
> Mercurial.

That's a different issue. The reason push-into-a-live-tree works that way in Hg is because they have this implicit notion of unmerged heads that is merged with the next "hg update". We don't.

Instead, we are more explicit. If you want to emulate that workflow in git, you would prepare a reception branch, and have people push into it (see gitfaq and look for "sync out of a firewalled host". In the FAQ entry, pretend as if machineA is such a live work tree you are working in, and allow people to push into --- that B/master is the reception branch, although the FAQ entry is about your private use and written as if only you from a satellite machine can push into it, it can easily be a shared branch).

Instead of saying "hg update", you (the person working in such a live work tree that allows others to push into) merge that reception branch.

Nanako Shiraishi· Dec 4, 2007, 22:04 UTC · re: Junio C Hamano · lore

[PATCH] git-checkout --push/--pop

This introduces a branch-stack mechanism to record branch switching in $GIT_DIR/BRANCH_STACK file. If you are switching to another branch and plan to come back to the original branch soon, add '--push' option to record your current branch. When you want to come back, 'git checkout --pop' will switch back to the branch recorded at the top of the stack, while popping it.

Signed-off-by: しらいしななこ <nanako3@bluebottle.com>
---
 git-checkout.sh |   42 +++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 41 insertions(+), 1 deletions(-)
diff --git a/git-checkout.sh b/git-checkout.sh
index f6d58ac..b53aea6 100755
--- a/git-checkout.sh
+++ b/git-checkout.sh
@@ -8,8 +8,10 @@ b=          create a new branch started at <branch>
 l           create the new branchs reflog
 track       tells if the new branch should track the remote branch
 f           proceed even if the index or working tree is not HEAD
-m           performa  three-way merge on local modifications if needed
+m           perform a three-way merge on local modifications if needed
 q,quiet     be quiet
+push        push the current branch to branch stack
+pop         come back to the previous branch by poping from branch stack
 "
 SUBDIRECTORY_OK=Sometimes
 . git-sh-setup
@@ -27,6 +29,8 @@ newbranch=
 newbranch_log=
 merge=
 quiet=
+push=
+pop=
 v=-v
 LF='
 '
@@ -49,6 +53,12 @@ while test $# != 0; do
 	--track|--no-track)
 		track="$1"
 		;;
+	--push)
+		push=1
+		;;
+	--pop)
+		pop=1
+		;;
 	-f)
 		force=1
 		;;
@@ -70,6 +80,27 @@ while test $# != 0; do
 	shift
 done
 
+if test -n "$pop"
+then
+	if test $# != 0
+	then
+		die "git checkout: --pop is incompatible with branch name"
+	fi
+	if test -n "$push"
+	then
+		die "git checkout: --pop and --push are incompatible"
+	fi
+	last=$(tail -n 1 "$GIT_DIR/BRANCH_STACK")
+	if test -z "$last"
+	then
+		die "git checkout: empty branch stack"
+	fi
+	sed -e '$d' <"$GIT_DIR/BRANCH_STACK" >"$GIT_DIR/BRANCH_STACK.new"
+	mv "$GIT_DIR/BRANCH_STACK.new" "$GIT_DIR/BRANCH_STACK"
+	set x "$last"
+	shift
+fi
+
 arg="$1"
 if rev=$(git rev-parse --verify "$arg^0" 2>/dev/null)
 then
@@ -113,6 +144,11 @@ esac
 
 if test "$#" -ge 1
 then
+	# Checking out a path out of the index or a commit
+	if test -n "$push"
+	then
+		die "git checkout: --push while not switching branches"
+	fi
 	hint=
 	if test "$#" -eq 1
 	then
@@ -276,6 +312,10 @@ if [ "$?" -eq 0 ]; then
 		else
 			echo >&2 "Switched to${newbranch:+ a new} branch \"$branch\""
 		fi
+		if test -n "$push"
+		then
+			echo "$old_branch_name" >>"$GIT_DIR/BRANCH_STACK"
+		fi
 	elif test -n "$detached"
 	then
 		old_branch_name=`expr "z$oldbranch" : 'zrefs/heads/\(.*\)'`
-- 
1.5.3.6

-- 
Nanako Shiraishi
http://ivory.ap.teacup.com/nanako3/

----------------------------------------------------------------------
Get a free email account with anti spam protection.
http://www.bluebottle.com/tag/2
Junio C Hamano· Dec 5, 2007, 06:59 UTC · re: Nanako Shiraishi · lore

Re: [PATCH] git-checkout --push/--pop

Nanako Shiraishi <nanako3@bluebottle.com> writes:
> This introduces a branch-stack mechanism to record branch switching in $GIT_DIR/BRANCH_STACK file.  If you are switching to another branch and plan to come back to the original branch soon, add '--push' option to record your current branch.
> When you want to come back, 'git checkout --pop' will switch back to the branch recorded at the top of the stack, while popping it.
>
> Signed-off-by: しらいしななこ <nanako3@bluebottle.com>

Hmph, is this in response to my "I am not sympathtic to 'I have to remember'"?

Funnily enough, I often find myself almost typing pushd/popd when switching branches, so in that sense "git checkout" to switch branches does have some similarity to the notion of pushing and popping.

Matthieu, is this something that forgetful people would find useful?

Having said that, I think there are other push/pop people wanted from you. Hint, hint...

Matthieu Moy· Dec 5, 2007, 10:26 UTC · re: Junio C Hamano · lore

Re: [PATCH] git-checkout --push/--pop

Junio C Hamano <gitster@pobox.com> writes:
> Matthieu, is this something that forgetful people would find useful?

Not sure. That's obviously an interesting feature, but adding two more options to checkout (which is already a huge swiss-army knife) might not be worth the trouble.

And the issue with push/pop approaches is that I usually notice I have to use pop after not having used push (i.e. I use "cd -" all the time, but rarely "pushd"/"popd").

-- 
Matthieu
David Kågedal· Dec 5, 2007, 17:44 UTC · re: Matthieu Moy · lore

Re: [PATCH] git-checkout --push/--pop

Matthieu Moy <Matthieu.Moy@imag.fr> writes:
Show 11 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
>
>> Matthieu, is this something that forgetful people would find useful?
>
> Not sure. That's obviously an interesting feature, but adding two more
> options to checkout (which is already a huge swiss-army knife) might
> not be worth the trouble.
>
> And the issue with push/pop approaches is that I usually notice I have
> to use pop after not having used push (i.e. I use "cd -" all the time,
> but rarely "pushd"/"popd").

It is probably more common that you want to be able to switch back to the previous branch, than that you actually need a full stack.

So a "git checkout --previous" could be enough.  Or a set of aliases
[alias]
	co = !"git symbolic-ref HEAD | sed -ne 's!refs/heads/!!p' > .git/LAST ; git checkout"
        pop = !"git co $(cat .git/LAST)"
-- 
David Kågedal
Robin Rosenberg· Dec 6, 2007, 23:39 UTC · re: Junio C Hamano · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

tisdag 04 december 2007 skrev Junio C Hamano:
Show 20 quoted lines
> Jakub Narebski <jnareb@gmail.com> writes:
> 
> >> Currently, I can do:
> >> 
> >> # Oh, what did this look like two commits ago?
> >> $ git checkout HEAD^^
> >> # Ah, OK, let's go back to the tip
> >> $ git checkout branch-name
> >>                ^^^^^^^^^^^
> >> But I have to remember and re-type the branch name.
> >
> > No, you don't have. You can use
> >   $ git checkout ORIG_HEAD
> > or
> >   $ git checkout HEAD@{1}
> 
> But the point is he wants to go back to the branch he came from.  He
> does not want to detach HEAD at the original commit.
> 
> Having said that, I am not sympathetic to "I have to remember".

I abuse git bisect for this temporary switcing. It only gives me a one level memory, but otoh the git prompt tells me I'm on a discourse.

[me@lathund GIT (rr/abspath|BISECTING)]$ git checkout master Switched to branch "master"

[me@lathund GIT (master|BISECTING)]$ git checkout HEAD~2
Note: moving to "HEAD~2" which isn't a local branch
If you want to create a new branch from this checkout, you may do so
(now or later) by using -b with the checkout command again. Example:
  git checkout -b <new_branch_name>
HEAD is now at afcc4f7... Merge branch 'js/prune-expire'

[me@lathund GIT (afcc4f7...|BISECTING)]$ git bisect reset Previous HEAD position was afcc4f7... Merge branch 'js/prune-expire' Switched to branch "rr/abspath" [me@lathund GIT (rr/abspath)]$

-- robin
Salikh Zakirov· Dec 4, 2007, 21:57 UTC · re: Jakub Narebski · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

Jakub Narebski wrote:
Show 6 quoted lines
>> Salikh Zakirov <salikh@gmail.com> writes:
>>> This combination leads to the confusing user experience
>>> if the branch changes independently of the working directory.
>>> This can happen in following cases:
> 
> All those cases are cases of not recommended workflows.

I guess those cases are not recommended exactly because there are prone to causing confusion with current git.

I use multiple workdirs quite often, and by now learned not to check out the same branch in different workdirs, but it would very convenient if it would not be necessary to remember it.

> Please search the archives for idea of BASE extension to index
> (instead of your separate file under .git/refs), and why it is
> not in current git.

I never realized this idea has been already tried. Thanks for the pointer! I would try to use BASE extension.

> BTW. how in your proposal would you detach HEAD?

Deleting .git/BRANCH should be enough. But I cannot see the workflow that would need it. If one checks out older state of the branch, there is no harm in maintaining the BRANCH pointer, as commits done in this state need to be manually merged in order to get into the branch, in the same way as detached head works currently.

Junio C Hamano· Dec 4, 2007, 22:06 UTC · re: Salikh Zakirov · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

Salikh Zakirov <salikh@gmail.com> writes:
Show 8 quoted lines
> Jakub Narebski wrote:
> ...
>> Please search the archives for idea of BASE extension to index
>> (instead of your separate file under .git/refs), and why it is
>> not in current git.
>
> I never realized this idea has been already tried. Thanks for the pointer!
> I would try to use BASE extension.
Start reading from here.
http://thread.gmane.org/gmane.comp.version-control.git/44360/focus=44508

All of the articles in that thread is worth a read to understand why the idea did not quite pan out and the whole series was reverted.

052737e33a898af8eb4f1bd97be046e05ecb9bea reverts the series from 'next'. 309202782399878a2d7973e21e6e62ba3b9fde50 was the last of the index-base series.

Jakub Narebski· Dec 4, 2007, 22:08 UTC · re: Salikh Zakirov · lore

Re: [RFC] Introduce .git/BRANCH to point to the current branch

On Tue, 4 Dec 2007, Salikh Zakirov wrote:
Show 10 quoted lines
> Jakub Narebski wrote:
>>> Salikh Zakirov <salikh@gmail.com> writes:
>>>> This combination leads to the confusing user experience
>>>> if the branch changes independently of the working directory.
>>>> This can happen in following cases:
>> 
>> All those cases are cases of not recommended workflows.
> 
> I guess those cases are not recommended exactly because
> there are prone to causing confusion with current git.

BTW. I think for each such workflow there is alternative in git, which does not cause those problems.

> I use multiple workdirs quite often, and by now learned
> not to check out the same branch in different workdirs, 
> but it would very convenient if it would not be necessary
> to remember it. 
Those cases are not recommended because they would _always_ cause
confusion, whether the user is informed that somehow working copy
or current branch has changed while he/she was working or not.
Currently git tries to follow "merge when ready" (i.e. you
explicitely ask to merge or rebase), not "merge when needed"
(like in old, broken CVS workflow where 'cvs update' might have
_forced_ a merge before being allowed to save his/her finished
work).
 
>> BTW. how in your proposal would you detach HEAD?
> 
> Deleting .git/BRANCH should be enough.
Hmmm...
> But I cannot see the workflow that would need it.

Checking out tag. Checking out remote-tracking branch. You cannot commit on top of either (immovable) tag, or (externally controlled) remote-tracking branch.

Besides rebase now uses detached HEAD, and I guess that bisect would too.

-- 
Jakub Narebski
Poland

← back to recent threads