threads / rfc / 17221

RFC v3, 6 partssha1_name: implement @{-N} syntax for N-th last checked out

Subject: [PATCH/RFC v3 5/6] sha1_name: implement @{-N} syntax for N-th last checked out

## tl;dr

56 messages between Jan 17, 2009 and Jan 25, 2009. Diffs are folded; open one to read it.

replies: 55people: 3as markdown or json

Thomas Rast· Jan 17, 2009, 03:30 UTC · lore

[PATCH/RFC v3 0/6] N-th last checked out branch

Junio C Hamano <gitster@pobox.com> writes:
> "cd -" is a very good analogy why your "-" shortcut is a short-sighted
> convenience feature that is too narrow and not well designed.  "cd -" can
> go back, but you cannot say "ls -" to list the contents of the previous
> directory.
True.
> Another reason is the one level limitation.
It shares that limitation with my mind ;-)

But well, since you all seem to want a more general solution, here's a draft.

I started poking around in refs.c for the backwards search, but the two reflog functions had a lot of functionality implemented very differently. So 1-4 are just cleanup and adding that backwards iterator. Note that 3/6 changes for_each_reflog_ent() to die() when it finds a format error, unlike the previous behaviour. No tests seemed to care, but maybe someone out there relies on having broken reflogs? ;-)

>  * The code read the reflog twice, first to count how many branch
>    switching there are and then to locate the N-th entry we are interested
>    in, because I was lazy.  We may want an API to enumerate reflog entries
>    in reverse.
So this might be settled.
>  * The reflog parser only parses "checkout" and not rebase action.  It
>    also does not notice "git checkout HEAD^" is not switching to a real
>    branch.

I don't handle this, but I'm not sure how much of a problem it is. It can correctly detach and re-attach even in cases such as HEAD^.

Ok, now that you mention it, it will probably put you on the parent of whatever you happened to be on, instead of the earlier value of HEAD^.

>      $ git checkout @{-1}

One problem with the syntax is that the assumption that there can only be a single @{} construct is quite entrenched in sha1_name.c. So if we want to support @{-1}@{1}, that'll need some extra work.

@{-1}~2 and similar work fine though.
Other things of note:

I changed the semantics to really only look at checkouts that "did something". If you keep saying 'git checkout master' over and over, those will not count towards the N.

I changed the parser to read the 'checkout: moving from $old' instead of the 'to $new'. The above semantics introduced too many extra conditions for my taste if we wanted to support the pre-1.5.3 reflog comment format. (You'd have to check if the $to was changed from the last [newer] entry, but then there's another border case when you do 'git init; git checkout -b side; git checkout -' because you never moved to master.)

The @{date} syntax will be marginally slower after the refactoring because the old parser carefully avoided parsing numbers where it could. (I guess it could actually be done as a bisection for an extra order of magnitude.)

It's far too early here to be sending mail.
Interactive rebase needs a "move hunk" feature.
Thomas Rast (6):
  reflog: refactor parsing and checking
  reflog: refactor log open+mmap
  reflog: make for_each_reflog_ent use mmap
  reflog: add backwards iterator
  sha1_name: implement @{-N} syntax for N-th last checked out
  checkout: implement '@{-N}' and '-' special abbreviations
 Documentation/git-checkout.txt  |    4 +
 Documentation/git-rev-parse.txt |    3 +
 builtin-checkout.c              |   15 ++-
 cache.h                         |    1 +
 refs.c                          |  285 +++++++++++++++++++++++----------------
 refs.h                          |    1 +
 sha1_name.c                     |   79 +++++++++++-
 t/t1505-rev-parse-last.sh       |   71 ++++++++++
 t/t2012-checkout-last.sh        |   50 +++++++
 9 files changed, 387 insertions(+), 122 deletions(-)
 create mode 100755 t/t1505-rev-parse-last.sh
 create mode 100755 t/t2012-checkout-last.sh
Thomas Rast· Jan 17, 2009, 03:30 UTC · re: Thomas Rast · lore

[PATCH/RFC v3 1/6] reflog: refactor parsing and checking

read_ref_at() and for_each_reflog_ent() both had parsing and error checking routines. Refactor into a separate function that fully parses a single entry. Note that this switches for_each_reflog_ent() from silently ignoring errors to die().

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 refs.c |  197 ++++++++++++++++++++++++++++++++--------------------------------
 1 files changed, 98 insertions(+), 99 deletions(-)
Show changes to refs.c +98 −99
diff --git a/refs.c b/refs.c
index 33ced65..4571fac 100644
--- a/refs.c
+++ b/refs.c
@@ -1337,24 +1337,68 @@ int create_symref(const char *ref_target, const char *refs_heads_master,
 	return 0;
 }
 
-static char *ref_msg(const char *line, const char *endp)
+/*
+ * Check and parse a single reflog line.  Assumes that there is only
+ * one newline in the range buf[0]..buf[len-1] (but does check that it
+ * is at buf[len-1]).
+ */
+static void parse_reflog_line(const char *buf, int len,
+			      unsigned char *osha1, unsigned char *nsha1,
+			      char **email,
+			      unsigned long *timestamp, int *tz,
+			      char **message,
+			      const char *logname)
 {
-	const char *ep;
-	line += 82;
-	ep = memchr(line, '\n', endp - line);
-	if (!ep)
-		ep = endp;
-	return xmemdupz(line, ep - line);
+	static char *retbuf = NULL;
+	static int retbufsz = 0;
+	char *tzstr, *email_end;
+
+	if (len < 83 || buf[len-1] != '\n')
+		die("Log %s is corrupt (entry too short or unterminated).", logname);
+
+	if (get_sha1_hex(buf, osha1) || buf[40] != ' ')
+		die("Log %s is corrupt (malformed old sha1).", logname);
+
+	if (get_sha1_hex(buf + 41, nsha1) || buf[81] != ' ')
+		die("Log %s is corrupt (malformed new sha1).", logname);
+
+	ALLOC_GROW(retbuf, len-82+1, retbufsz);
+	memcpy(retbuf, buf+82, len-82);
+	retbuf[len-82] = '\0';
+
+	email_end = strchr(retbuf, '>');
+	if (!email_end || email_end[1] != ' ')
+		die("Log %s is corrupt (malformed email field).", logname);
+
+	*email = retbuf;
+	email_end[1] = '\0';
+
+	*timestamp = strtoul(email_end + 2, &tzstr, 10);
+	if (!(*timestamp) || !tzstr || tzstr[0] != ' ' ||
+	    (tzstr[1] != '+' && tzstr[1] != '-') ||
+	    !isdigit(tzstr[2]) || !isdigit(tzstr[3]) ||
+	    !isdigit(tzstr[4]) || !isdigit(tzstr[5]))
+		die("Log %s is corrupt (malformed timezone).", logname);
+	if (!(tzstr[6] == '\t' || tzstr[6] == '\n'))
+		die("Log %s is corrupt (bad message field separator).", logname);
+	*tz = strtoul(tzstr, NULL, 10);
+
+	if (tzstr[6] == '\t')
+		*message = tzstr+7;
+	else
+		*message = tzstr+6;
 }
 
 int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1, char **msg, unsigned long *cutoff_time, int *cutoff_tz, int *cutoff_cnt)
 {
-	const char *logfile, *logdata, *logend, *rec, *lastgt, *lastrec;
-	char *tz_c;
+	const char *logfile, *logdata, *logend, *rec, *start;
+	char *email, *message;
 	int logfd, tz, reccnt = 0;
 	struct stat st;
 	unsigned long date;
-	unsigned char logged_sha1[20];
+	unsigned char new_sha1[20];
+	unsigned char old_sha1[20];
+	unsigned char next_sha1[20];
 	void *log_mapped;
 	size_t mapsz;
 
@@ -1370,86 +1414,55 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
 	logdata = log_mapped;
 	close(logfd);
 
-	lastrec = NULL;
 	rec = logend = logdata + st.st_size;
+	if (logdata < rec && *(rec-1) == '\n')
+		rec--;
 	while (logdata < rec) {
-		reccnt++;
-		if (logdata < rec && *(rec-1) == '\n')
-			rec--;
-		lastgt = NULL;
-		while (logdata < rec && *(rec-1) != '\n') {
-			rec--;
-			if (*rec == '>')
-				lastgt = rec;
-		}
-		if (!lastgt)
-			die("Log %s is corrupt.", logfile);
-		date = strtoul(lastgt + 1, &tz_c, 10);
+		start = memrchr(logdata, '\n', rec-logdata);
+		if (start)
+			start++;
+		else
+			start = logdata;
+		parse_reflog_line(start, rec-start+1,
+				  old_sha1, new_sha1,
+				  &email, &date, &tz, &message,
+				  logfile);
+
+		if (cutoff_time)
+			*cutoff_time = date;
+		if (cutoff_tz)
+			*cutoff_tz = tz;
+		if (cutoff_cnt)
+			*cutoff_cnt = reccnt;
+		if (msg)
+			*msg = message;
+
 		if (date <= at_time || cnt == 0) {
-			tz = strtoul(tz_c, NULL, 10);
-			if (msg)
-				*msg = ref_msg(rec, logend);
-			if (cutoff_time)
-				*cutoff_time = date;
-			if (cutoff_tz)
-				*cutoff_tz = tz;
-			if (cutoff_cnt)
-				*cutoff_cnt = reccnt - 1;
-			if (lastrec) {
-				if (get_sha1_hex(lastrec, logged_sha1))
-					die("Log %s is corrupt.", logfile);
-				if (get_sha1_hex(rec + 41, sha1))
-					die("Log %s is corrupt.", logfile);
-				if (hashcmp(logged_sha1, sha1)) {
-					fprintf(stderr,
-						"warning: Log %s has gap after %s.\n",
-						logfile, show_date(date, tz, DATE_RFC2822));
-				}
-			}
-			else if (date == at_time) {
-				if (get_sha1_hex(rec + 41, sha1))
-					die("Log %s is corrupt.", logfile);
-			}
-			else {
-				if (get_sha1_hex(rec + 41, logged_sha1))
-					die("Log %s is corrupt.", logfile);
-				if (hashcmp(logged_sha1, sha1)) {
-					fprintf(stderr,
-						"warning: Log %s unexpectedly ended on %s.\n",
-						logfile, show_date(date, tz, DATE_RFC2822));
-				}
-			}
+			if (reccnt && hashcmp(new_sha1, next_sha1))
+				fprintf(stderr,
+					"warning: Log %s has gap after %s.\n",
+					logfile, show_date(date, tz, DATE_RFC2822));
+			if (!reccnt && date < at_time && hashcmp(new_sha1, next_sha1))
+				fprintf(stderr,
+					"warning: Log %s unexpectedly ended on %s.\n",
+					logfile, show_date(date, tz, DATE_RFC2822));
+				/* leave caller's sha1 untouched */
+			else
+				hashcpy(sha1, new_sha1);
 			munmap(log_mapped, mapsz);
 			return 0;
 		}
-		lastrec = rec;
+
+		hashcpy(next_sha1, old_sha1);
+		rec = start-1;
 		if (cnt > 0)
 			cnt--;
+		reccnt++;
 	}
 
-	rec = logdata;
-	while (rec < logend && *rec != '>' && *rec != '\n')
-		rec++;
-	if (rec == logend || *rec == '\n')
-		die("Log %s is corrupt.", logfile);
-	date = strtoul(rec + 1, &tz_c, 10);
-	tz = strtoul(tz_c, NULL, 10);
-	if (get_sha1_hex(logdata, sha1))
-		die("Log %s is corrupt.", logfile);
-	if (is_null_sha1(sha1)) {
-		if (get_sha1_hex(logdata + 41, sha1))
-			die("Log %s is corrupt.", logfile);
-	}
-	if (msg)
-		*msg = ref_msg(logdata, logend);
-	munmap(log_mapped, mapsz);
+	hashcpy(sha1, new_sha1);
 
-	if (cutoff_time)
-		*cutoff_time = date;
-	if (cutoff_tz)
-		*cutoff_tz = tz;
-	if (cutoff_cnt)
-		*cutoff_cnt = reccnt;
+	munmap(log_mapped, mapsz);
 	return 1;
 }
 
@@ -1466,30 +1479,16 @@ int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
 		return -1;
 	while (fgets(buf, sizeof(buf), logfp)) {
 		unsigned char osha1[20], nsha1[20];
-		char *email_end, *message;
+		char *email, *message;
 		unsigned long timestamp;
 		int len, tz;
 
-		/* old SP new SP name <email> SP time TAB msg LF */
 		len = strlen(buf);
-		if (len < 83 || buf[len-1] != '\n' ||
-		    get_sha1_hex(buf, osha1) || buf[40] != ' ' ||
-		    get_sha1_hex(buf + 41, nsha1) || buf[81] != ' ' ||
-		    !(email_end = strchr(buf + 82, '>')) ||
-		    email_end[1] != ' ' ||
-		    !(timestamp = strtoul(email_end + 2, &message, 10)) ||
-		    !message || message[0] != ' ' ||
-		    (message[1] != '+' && message[1] != '-') ||
-		    !isdigit(message[2]) || !isdigit(message[3]) ||
-		    !isdigit(message[4]) || !isdigit(message[5]))
-			continue; /* corrupt? */
-		email_end[1] = '\0';
-		tz = strtol(message + 1, NULL, 10);
-		if (message[6] != '\t')
-			message += 6;
-		else
-			message += 7;
-		ret = fn(osha1, nsha1, buf+82, timestamp, tz, message, cb_data);
+		parse_reflog_line(buf, len,
+				  osha1, nsha1,
+				  &email, &timestamp, &tz, &message,
+				  logfile);
+		ret = fn(osha1, nsha1, email, timestamp, tz, message, cb_data);
 		if (ret)
 			break;
 	}
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 03:30 UTC · re: Thomas Rast · lore

[PATCH/RFC v3 2/6] reflog: refactor log open+mmap

Move the open+mmap code from read_ref_at() to a separate function for the next patch.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 refs.c |   54 ++++++++++++++++++++++++++++++++----------------------
 1 files changed, 32 insertions(+), 22 deletions(-)
Show changes to refs.c +32 −22
diff --git a/refs.c b/refs.c
index 4571fac..0a57896 100644
--- a/refs.c
+++ b/refs.c
@@ -1389,12 +1389,31 @@ static void parse_reflog_line(const char *buf, int len,
 		*message = tzstr+6;
 }
 
+static char *open_reflog(const char *ref, size_t *mapsz, const char **logfile)
+{
+	struct stat st;
+	int logfd;
+	char *map;
+
+	*logfile = git_path("logs/%s", ref);
+	logfd = open(*logfile, O_RDONLY, 0);
+	if (logfd < 0)
+		return NULL;
+	if (fstat(logfd, &st))
+		return NULL;
+
+	*mapsz = xsize_t(st.st_size);
+	map = xmmap(NULL, *mapsz, PROT_READ, MAP_PRIVATE, logfd, 0);
+	close(logfd);
+	return map;
+}
+
+
 int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1, char **msg, unsigned long *cutoff_time, int *cutoff_tz, int *cutoff_cnt)
 {
-	const char *logfile, *logdata, *logend, *rec, *start;
+	const char *logfile, *logdata, *logend, *rec, *end;
 	char *email, *message;
-	int logfd, tz, reccnt = 0;
-	struct stat st;
+	int tz, reccnt = 0;
 	unsigned long date;
 	unsigned char new_sha1[20];
 	unsigned char old_sha1[20];
@@ -1402,28 +1421,20 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
 	void *log_mapped;
 	size_t mapsz;
 
-	logfile = git_path("logs/%s", ref);
-	logfd = open(logfile, O_RDONLY, 0);
-	if (logfd < 0)
+	logdata = log_mapped = open_reflog(ref, &mapsz, &logfile);
+	if (!logdata)
 		die("Unable to read log %s: %s", logfile, strerror(errno));
-	fstat(logfd, &st);
-	if (!st.st_size)
+	if (!mapsz)
 		die("Log %s is empty.", logfile);
-	mapsz = xsize_t(st.st_size);
-	log_mapped = xmmap(NULL, mapsz, PROT_READ, MAP_PRIVATE, logfd, 0);
-	logdata = log_mapped;
-	close(logfd);
 
-	rec = logend = logdata + st.st_size;
-	if (logdata < rec && *(rec-1) == '\n')
-		rec--;
+	rec = logend = logdata + mapsz;
 	while (logdata < rec) {
-		start = memrchr(logdata, '\n', rec-logdata);
-		if (start)
-			start++;
-		else
-			start = logdata;
-		parse_reflog_line(start, rec-start+1,
+		if (logdata < rec && rec[-1] == '\n')
+			rec--;
+		end = rec;
+		while (logdata < rec && rec[-1] != '\n')
+			rec--;
+		parse_reflog_line(rec, end-rec+1,
 				  old_sha1, new_sha1,
 				  &email, &date, &tz, &message,
 				  logfile);
@@ -1454,7 +1465,6 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
 		}
 
 		hashcpy(next_sha1, old_sha1);
-		rec = start-1;
 		if (cnt > 0)
 			cnt--;
 		reccnt++;
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 03:30 UTC · re: Thomas Rast · lore

[PATCH/RFC v3 3/6] reflog: make for_each_reflog_ent use mmap

Now that all building blocks have been refactored, we can implement for_each_reflog_ent in terms of them, so that it uses mmap.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 refs.c |   25 ++++++++++++++++---------
 1 files changed, 16 insertions(+), 9 deletions(-)
Show changes to refs.c +16 −9
diff --git a/refs.c b/refs.c
index 0a57896..3848aa0 100644
--- a/refs.c
+++ b/refs.c
@@ -1478,22 +1478,28 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
 
 int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
 {
-	const char *logfile;
-	FILE *logfp;
-	char buf[1024];
+	const char *log_mapped, *buf, *log_end, *logfile;
+	size_t mapsz;
 	int ret = 0;
 
-	logfile = git_path("logs/%s", ref);
-	logfp = fopen(logfile, "r");
-	if (!logfp)
+	buf = log_mapped = open_reflog(ref, &mapsz, &logfile);
+	if (!log_mapped)
 		return -1;
-	while (fgets(buf, sizeof(buf), logfp)) {
+	log_end = buf + mapsz;
+
+	while (buf < log_end) {
 		unsigned char osha1[20], nsha1[20];
 		char *email, *message;
+		const char *next;
 		unsigned long timestamp;
 		int len, tz;
 
-		len = strlen(buf);
+		next = memchr(buf, '\n', log_end - buf);
+		if (next)
+			next++;
+		else
+			next = log_end;
+		len = next - buf;
 		parse_reflog_line(buf, len,
 				  osha1, nsha1,
 				  &email, &timestamp, &tz, &message,
@@ -1501,8 +1507,9 @@ int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
 		ret = fn(osha1, nsha1, email, timestamp, tz, message, cb_data);
 		if (ret)
 			break;
+		buf = next;
 	}
-	fclose(logfp);
+	munmap((void*) log_mapped, mapsz);
 	return ret;
 }
 
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 03:30 UTC · re: Thomas Rast · lore

[PATCH/RFC v3 4/6] reflog: add backwards iterator

Add for_each_reflog_ent_backward() which does the same as for_each_reflog_ent(), except it traverses the reflog in backwards (newest to oldest) order.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 refs.c |   35 +++++++++++++++++++++++++++++++++++
 refs.h |    1 +
 2 files changed, 36 insertions(+), 0 deletions(-)
Show changes to 2 files +36 −0

refs.c, refs.h

diff --git a/refs.c b/refs.c
index 3848aa0..cc78f63 100644
--- a/refs.c
+++ b/refs.c
@@ -1476,6 +1476,41 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
 	return 1;
 }
 
+int for_each_reflog_ent_backward(const char *ref, each_reflog_ent_fn fn, void *cb_data)
+{
+	const char *log_mapped, *buf, *log_end, *logfile;
+	size_t mapsz;
+	int ret = 0;
+
+	log_mapped = open_reflog(ref, &mapsz, &logfile);
+	if (!log_mapped)
+		return -1;
+	buf = log_end = log_mapped + mapsz;
+
+	while (buf > log_mapped) {
+		unsigned char osha1[20], nsha1[20];
+		char *email, *message;
+		const char *end;
+		unsigned long timestamp;
+		int tz;
+
+		while (buf > log_mapped && buf[-1] == '\n')
+			buf--;
+		end = buf;
+		while (buf > log_mapped && buf[-1] != '\n')
+			buf--;
+		parse_reflog_line(buf, end-buf+1,
+				  osha1, nsha1,
+				  &email, &timestamp, &tz, &message,
+				  logfile);
+		ret = fn(osha1, nsha1, email, timestamp, tz, message, cb_data);
+		if (ret)
+			break;
+	}
+	munmap((void*) log_mapped, mapsz);
+	return ret;
+}
+
 int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
 {
 	const char *log_mapped, *buf, *log_end, *logfile;
diff --git a/refs.h b/refs.h
index 06ad260..723bddc 100644
--- a/refs.h
+++ b/refs.h
@@ -60,6 +60,7 @@ struct ref_lock {
 /* iterate over reflog entries */
 typedef int each_reflog_ent_fn(unsigned char *osha1, unsigned char *nsha1, const char *, unsigned long, int, const char *, void *);
 int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data);
+int for_each_reflog_ent_backward(const char *ref, each_reflog_ent_fn fn, void *cb_data);
 
 /*
  * Calls the specified function for each reflog file until it returns nonzero,
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 03:30 UTC · re: Thomas Rast · lore

Implements a new syntax @{-N} that parses the reflog for the N-th last interesting 'checkout: moving from $branch to $new' entry and substitutes $branch. Here, "interesting" is defined as $branch != $new. We then substitute the real branch name for the parse.

For example:
  git checkout foo
  git checkout bar
  git checkout master
  git checkout master   # did not move, so doesn't count
  git rev-parse @{-1}   # same as bar
  git rev-parse @{-2}   # same as foo
  git rev-parse @{-2}~3 # same as foo~3
Thanks to Junio for much of the code.
Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 Documentation/git-rev-parse.txt |    3 +
 cache.h                         |    1 +
 sha1_name.c                     |   79 +++++++++++++++++++++++++++++++++++++-
 t/t1505-rev-parse-last.sh       |   71 +++++++++++++++++++++++++++++++++++
 4 files changed, 151 insertions(+), 3 deletions(-)
 create mode 100755 t/t1505-rev-parse-last.sh
Show changes to 4 files +151 −3

Documentation/git-rev-parse.txt, cache.h, sha1_name.c, t/t1505-rev-parse-last.sh

diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 2921da3..3ccef2f 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -212,6 +212,9 @@ when you run 'git-merge'.
   reflog of the current branch. For example, if you are on the
   branch 'blabla', then '@\{1\}' means the same as 'blabla@\{1\}'.
 
+* The special construct '@\{-<n>\}' means the <n>th branch checked out
+  before the current one.
+
 * A suffix '{caret}' to a revision parameter means the first parent of
   that commit object.  '{caret}<n>' means the <n>th parent (i.e.
   'rev{caret}'
diff --git a/cache.h b/cache.h
index 8e1af26..0dd9168 100644
--- a/cache.h
+++ b/cache.h
@@ -663,6 +663,7 @@ static inline unsigned int hexval(unsigned char c)
 extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *);
 extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
 extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
+extern int interpret_nth_last_branch(const char *str, struct strbuf *);
 
 extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
 extern const char *ref_rev_parse_rules[];
diff --git a/sha1_name.c b/sha1_name.c
index 159c2ab..b2dd302 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -297,6 +297,8 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
 	return logs_found;
 }
 
+static int get_sha1_1(const char *name, int len, unsigned char *sha1);
+
 static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 {
 	static const char *warning = "warning: refname '%.*s' is ambiguous.\n";
@@ -307,7 +309,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	if (len == 40 && !get_sha1_hex(str, sha1))
 		return 0;
 
-	/* basic@{time or number} format to query ref-log */
+	/* basic@{time or number or -number} format to query ref-log */
 	reflog_len = at = 0;
 	if (str[len-1] == '}') {
 		for (at = 0; at < len - 1; at++) {
@@ -324,6 +326,14 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 		return -1;
 
 	if (!len && reflog_len) {
+		struct strbuf buf = STRBUF_INIT;
+		int ret;
+		/* try the @{-N} syntax for n-th checkout */
+		ret = interpret_nth_last_branch(str+at, &buf);
+		if (ret > 0) {
+			/* substitute this branch name and restart */
+			return get_sha1_1(buf.buf, buf.len, sha1);
+		}
 		/* allow "@{...}" to mean the current branch reflog */
 		refs_found = dwim_ref("HEAD", 4, sha1, &real_ref);
 	} else if (reflog_len)
@@ -379,8 +389,6 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	return 0;
 }
 
-static int get_sha1_1(const char *name, int len, unsigned char *sha1);
-
 static int get_parent(const char *name, int len,
 		      unsigned char *result, int idx)
 {
@@ -674,6 +682,71 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
 	return retval;
 }
 
+struct grab_nth_branch_switch_cbdata {
+	int nth;
+	struct strbuf *buf;
+};
+
+static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+				  const char *email, unsigned long timestamp, int tz,
+				  const char *message, void *cb_data)
+{
+	struct grab_nth_branch_switch_cbdata *cb = cb_data;
+	const char *match = NULL, *target = NULL;
+	size_t len;
+
+	if (!prefixcmp(message, "checkout: moving from ")) {
+		match = message + strlen("checkout: moving from ");
+		if ((target = strstr(match, " to ")) != NULL)
+			target += 4;
+	}
+
+	if (!match)
+		return 0;
+
+	len = target - match - 4;
+	if (target[len] == '\n' && !strncmp(match, target, len))
+		return 0;
+
+	if (--cb->nth <= 0) {
+		strbuf_reset(cb->buf);
+		strbuf_add(cb->buf, match, len);
+		return 1;
+	}
+	return 0;
+}
+
+/*
+ * This reads "@{-N}" syntax, finds the name of the Nth previous
+ * branch we were on, and places the name of the branch in the given
+ * buf and returns 0 if successful.
+ *
+ * If the input is not of the accepted format, it returns a negative
+ * number to signal an error.
+ */
+int interpret_nth_last_branch(const char *name, struct strbuf *buf)
+{
+	int nth;
+	struct grab_nth_branch_switch_cbdata cb;
+	const char *brace;
+	char *num_end;
+
+	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
+		return -1;
+	brace = strchr(name, '}');
+	if (!brace)
+		return -1;
+	nth = strtol(name+3, &num_end, 10);
+	if (num_end != brace)
+		return -1;
+
+	cb.nth = nth;
+	cb.buf = buf;
+	for_each_reflog_ent_backward("HEAD", grab_nth_branch_switch, &cb);
+
+	return brace-name+1;
+}
+
 /*
  * This is like "get_sha1_basic()", except it allows "sha1 expressions",
  * notably "xyz^" for "parent of xyz"
diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
new file mode 100755
index 0000000..1e49dd2
--- /dev/null
+++ b/t/t1505-rev-parse-last.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+test_description='test @{-N} syntax'
+
+. ./test-lib.sh
+
+
+make_commit () {
+	echo "$1" > "$1" &&
+	git add "$1" &&
+	git commit -m "$1"
+}
+
+
+test_expect_success 'setup' '
+
+	make_commit 1 &&
+	git branch side &&
+	make_commit 2 &&
+	make_commit 3 &&
+	git checkout side &&
+	make_commit 4 &&
+	git merge master &&
+	git checkout master
+
+'
+
+# 1 -- 2 -- 3 master
+#  \         \
+#   \         \
+#    --- 4 --- 5 side
+#
+# and 'side' should be the last branch
+
+git log --graph --all --pretty=oneline --decorate
+
+test_rev_equivalent () {
+
+	git rev-parse "$1" > expect &&
+	git rev-parse "$2" > output &&
+	test_cmp expect output
+
+}
+
+test_expect_success '@{-1} works' '
+	test_rev_equivalent side @{-1}
+'
+
+test_expect_success '@{-1}~2 works' '
+	test_rev_equivalent side~2 @{-1}~2
+'
+
+test_expect_success '@{-1}^2 works' '
+	test_rev_equivalent side^2 @{-1}^2
+'
+
+test_expect_failure '@{-1}@{1} works' '
+	test_rev_equivalent side@{1} @{-1}@{1}
+'
+
+test_expect_success '@{-2} works' '
+	test_rev_equivalent master @{-2}
+'
+
+test_expect_success '@{-3} fails' '
+	test_must_fail git rev-parse @{-3}
+'
+
+test_done
+
+
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 03:30 UTC · re: Thomas Rast · lore

[PATCH/RFC v3 6/6] checkout: implement '@{-N}' and '-' special abbreviations

Checks if the branch to be checked out is either '@{-N}' or the special shorthand '-' for '@{-1}' (i.e. the last checked out branch). If so, we take it to mean the branch name, not the corresponding SHA, so that we check out an attached HEAD on that branch.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 Documentation/git-checkout.txt |    4 +++
 builtin-checkout.c             |   15 ++++++++++-
 t/t2012-checkout-last.sh       |   50 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 67 insertions(+), 2 deletions(-)
 create mode 100755 t/t2012-checkout-last.sh
Show changes to 3 files +67 −2

Documentation/git-checkout.txt, builtin-checkout.c, t/t2012-checkout-last.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 9cd5151..3bccffa 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -133,6 +133,10 @@ the conflicted merge in the specified paths.
 +
 When this parameter names a non-branch (but still a valid commit object),
 your HEAD becomes 'detached'.
++
+As a special case, the "`@\{-N\}`" syntax for the N-th last branch
+checks out the branch (instead of detaching).  You may also specify
+"`-`" which is synonymous with "`@\{-1\}`".
 
 
 Detached HEAD
diff --git a/builtin-checkout.c b/builtin-checkout.c
index b5dd9c0..b0a101b 100644
--- a/builtin-checkout.c
+++ b/builtin-checkout.c
@@ -361,8 +361,16 @@ struct branch_info {
 static void setup_branch_path(struct branch_info *branch)
 {
 	struct strbuf buf = STRBUF_INIT;
-	strbuf_addstr(&buf, "refs/heads/");
-	strbuf_addstr(&buf, branch->name);
+	int ret;
+
+	if ((ret = interpret_nth_last_branch(branch->name, &buf))
+	    && ret == strlen(branch->name)) {
+		branch->name = xstrdup(buf.buf);
+		strbuf_splice(&buf, 0, 0, "refs/heads/", 11);
+	} else {
+		strbuf_addstr(&buf, "refs/heads/");
+		strbuf_addstr(&buf, branch->name);
+	}
 	branch->path = strbuf_detach(&buf, NULL);
 }
 
@@ -671,6 +679,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 		arg = argv[0];
 		has_dash_dash = (argc > 1) && !strcmp(argv[1], "--");
 
+		if (!strcmp(arg, "-"))
+			arg = "@{-1}";
+
 		if (get_sha1(arg, rev)) {
 			if (has_dash_dash)          /* case (1) */
 				die("invalid reference: %s", arg);
diff --git a/t/t2012-checkout-last.sh b/t/t2012-checkout-last.sh
new file mode 100755
index 0000000..320f6eb
--- /dev/null
+++ b/t/t2012-checkout-last.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+test_description='checkout can switch to last branch'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	echo hello >world &&
+	git add world &&
+	git commit -m initial &&
+	git branch other &&
+	echo "hello again" >>world &&
+	git add world &&
+	git commit -m second
+'
+
+test_expect_success '"checkout -" does not work initially' '
+	test_must_fail git checkout -
+'
+
+test_expect_success 'first branch switch' '
+	git checkout other
+'
+
+test_expect_success '"checkout -" switches back' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/master"
+'
+
+test_expect_success '"checkout -" switches forth' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/other"
+'
+
+test_expect_success 'detach HEAD' '
+	git checkout $(git rev-parse HEAD)
+'
+
+test_expect_success '"checkout -" attaches again' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/other"
+'
+
+test_expect_success '"checkout -" detaches again' '
+	git checkout - &&
+	test "z$(git rev-parse HEAD)" = "z$(git rev-parse other)" &&
+	test_must_fail git symbolic-ref HEAD
+'
+
+test_done
-- 
1.6.1.315.g92577
Johannes Schindelin· Jan 17, 2009, 05:40 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v3 2/6] reflog: refactor log open+mmap

Hi,
On Sat, 17 Jan 2009, Thomas Rast wrote:
Show 7 quoted lines
> +static char *open_reflog(const char *ref, size_t *mapsz, const char **logfile)
> +{
> +	struct stat st;
> +	int logfd;
> +	char *map;
> +
> +	*logfile = git_path("logs/%s", ref);

That is dangerous. git_path() returns a pointer to a static buffer. Before your patch, logfile was a local variable, and one could be relatively sure that git_path() was not called during the lifetime. Now the lifetime of logfile is no longer as clear-cut, and it is much easier to overlook that git_path() must not be called while logfile holds a reference to its static buffer.

Ciao, Dscho

Johannes Schindelin· Jan 17, 2009, 05:35 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v3 1/6] reflog: refactor parsing and checking

Hi,
On Sat, 17 Jan 2009, Thomas Rast wrote:
> Note that this switches for_each_reflog_ent() from silently ignoring 
> errors to die().
I am not convinced this is necessary, and neither that it is good.

You could return int from parse_reflog_line() just like the other non-dying functions.

And keep in mind that reflogs could be corrupted for all kinds of reasons; with your patch, reflog handling would fail gracelessly, even if a lot could still be salvaged (by ignoring the error with a warning).

Ciao, Dscho

Johannes Schindelin· Jan 17, 2009, 05:52 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v3 0/6] N-th last checked out branch

Hi,
On Sat, 17 Jan 2009, Thomas Rast wrote:
Show 12 quoted lines
>  Documentation/git-checkout.txt  |    4 +
>  Documentation/git-rev-parse.txt |    3 +
>  builtin-checkout.c              |   15 ++-
>  cache.h                         |    1 +
>  refs.c                          |  285 +++++++++++++++++++++++----------------
>  refs.h                          |    1 +
>  sha1_name.c                     |   79 +++++++++++-
>  t/t1505-rev-parse-last.sh       |   71 ++++++++++
>  t/t2012-checkout-last.sh        |   50 +++++++
>  9 files changed, 387 insertions(+), 122 deletions(-)
>  create mode 100755 t/t1505-rev-parse-last.sh
>  create mode 100755 t/t2012-checkout-last.sh
Let's quickly compare that to what Junio sent:
 builtin-checkout.c |   10 +++++-
 cache.h            |    1 +
 sha1_name.c        |   78 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 87 insertions(+), 2 deletions(-)

Granted, the documentation and tests are nice, but why that big change in refs.c? After all, the more you change in one go, the more you can break.

Let's take one step back. The two things that Junio's patch left to be desired (in addition to documentation and tests) are

- reflogs are traversed twice.  While this is not a showstopper, it is 
  easily improved by building a string_list and then picking the one entry 
  we're interested in, and
- the '-' handling you seem to want.

I really have to ask: why did you not work on top of Junio's patch, just adding docs, tests, and checkout -? And then -- maybe -- the string_list...

Although I have to admit that I am not _that_ convinced the string_list is worth it: reflogs are not evaluated all the time, so it is definitely not performance critical.

Ciao, Dscho

Thomas Rast· Jan 17, 2009, 13:38 UTC · re: Johannes Schindelin · lore

Re: [PATCH/RFC v3 0/6] N-th last checked out branch

Johannes Schindelin wrote:
Show 9 quoted lines
> On Sat, 17 Jan 2009, Thomas Rast wrote:
> >  9 files changed, 387 insertions(+), 122 deletions(-)
> 
> Let's quickly compare that to what Junio sent:
> 
>  builtin-checkout.c |   10 +++++-
>  cache.h            |    1 +
>  sha1_name.c        |   78 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 87 insertions(+), 2 deletions(-)
[...]
> - the '-' handling you seem to want.
Let's be clear that the handling *I* wanted is this big:
 builtin-checkout.c                     |   27 ++++++++++++++++-

In any case, I'll follow up with a version that still traverses the logs twice and thus doesn't need the first two.

Show 7 quoted lines
> I really have to ask: why did you not work on top of Junio's patch, just 
> adding docs, tests, and checkout -?  And then -- maybe -- the 
> string_list...
> 
> Although I have to admit that I am not _that_ convinced the string_list is 
> worth it: reflogs are not evaluated all the time, so it is definitely not 
> performance critical.

I take it you have some idea where and how string_list fits into this topic?

-- 
Thomas Rast
trast@{inf,student}.ethz.ch
Thomas Rast· Jan 17, 2009, 13:40 UTC · re: Thomas Rast · lore

[PATCH/RFC v3bis 1/2] sha1_name: implement @{-N} syntax for N-th last checked out

Implements a new syntax @{-N} that parses the reflog for the N-th last interesting 'checkout: moving from $branch to $new' entry and substitutes $branch. Here, "interesting" is defined as $branch != $new. We then substitute the real branch name for the parse.

For example:
  git checkout foo
  git checkout bar
  git checkout master
  git checkout master   # did not move, so doesn't count
  git rev-parse @{-1}   # same as bar
  git rev-parse @{-2}   # same as foo
  git rev-parse @{-2}~3 # same as foo~3
Thanks to Junio for much of the code.
Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 Documentation/git-rev-parse.txt |    3 +
 cache.h                         |    1 +
 sha1_name.c                     |   99 +++++++++++++++++++++++++++++++++++++-
 t/t1505-rev-parse-last.sh       |   71 ++++++++++++++++++++++++++++
 4 files changed, 171 insertions(+), 3 deletions(-)
 create mode 100755 t/t1505-rev-parse-last.sh
Show changes to 4 files +171 −3

Documentation/git-rev-parse.txt, cache.h, sha1_name.c, t/t1505-rev-parse-last.sh

diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 2921da3..3ccef2f 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -212,6 +212,9 @@ when you run 'git-merge'.
   reflog of the current branch. For example, if you are on the
   branch 'blabla', then '@\{1\}' means the same as 'blabla@\{1\}'.
 
+* The special construct '@\{-<n>\}' means the <n>th branch checked out
+  before the current one.
+
 * A suffix '{caret}' to a revision parameter means the first parent of
   that commit object.  '{caret}<n>' means the <n>th parent (i.e.
   'rev{caret}'
diff --git a/cache.h b/cache.h
index 8e1af26..0dd9168 100644
--- a/cache.h
+++ b/cache.h
@@ -663,6 +663,7 @@ static inline unsigned int hexval(unsigned char c)
 extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *);
 extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
 extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
+extern int interpret_nth_last_branch(const char *str, struct strbuf *);
 
 extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
 extern const char *ref_rev_parse_rules[];
diff --git a/sha1_name.c b/sha1_name.c
index 159c2ab..9e1538e 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -297,6 +297,8 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
 	return logs_found;
 }
 
+static int get_sha1_1(const char *name, int len, unsigned char *sha1);
+
 static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 {
 	static const char *warning = "warning: refname '%.*s' is ambiguous.\n";
@@ -307,7 +309,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	if (len == 40 && !get_sha1_hex(str, sha1))
 		return 0;
 
-	/* basic@{time or number} format to query ref-log */
+	/* basic@{time or number or -number} format to query ref-log */
 	reflog_len = at = 0;
 	if (str[len-1] == '}') {
 		for (at = 0; at < len - 1; at++) {
@@ -324,6 +326,16 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 		return -1;
 
 	if (!len && reflog_len) {
+		struct strbuf buf = STRBUF_INIT;
+		int ret;
+		/* try the @{-N} syntax for n-th checkout */
+		ret = interpret_nth_last_branch(str+at, &buf);
+		if (ret > 0) {
+			/* substitute this branch name and restart */
+			return get_sha1_1(buf.buf, buf.len, sha1);
+		} else if (ret == 0) {
+			return -1;
+		}
 		/* allow "@{...}" to mean the current branch reflog */
 		refs_found = dwim_ref("HEAD", 4, sha1, &real_ref);
 	} else if (reflog_len)
@@ -379,8 +391,6 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	return 0;
 }
 
-static int get_sha1_1(const char *name, int len, unsigned char *sha1);
-
 static int get_parent(const char *name, int len,
 		      unsigned char *result, int idx)
 {
@@ -674,6 +684,89 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
 	return retval;
 }
 
+struct grab_nth_branch_switch_cbdata {
+	int counting;
+	int nth;
+	struct strbuf *buf;
+};
+
+static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+				  const char *email, unsigned long timestamp, int tz,
+				  const char *message, void *cb_data)
+{
+	struct grab_nth_branch_switch_cbdata *cb = cb_data;
+	const char *match = NULL, *target = NULL;
+	size_t len;
+
+	if (!prefixcmp(message, "checkout: moving from ")) {
+		match = message + strlen("checkout: moving from ");
+		if ((target = strstr(match, " to ")) != NULL)
+			target += 4;
+	}
+
+	if (!match)
+		return 0;
+
+	len = target - match - 4;
+	if (target[len] == '\n' && !strncmp(match, target, len))
+		return 0;
+
+	if (cb->counting) {
+		cb->nth++;
+		return 0;
+	}
+
+	if (cb->nth-- <= 0) {
+		strbuf_reset(cb->buf);
+		strbuf_add(cb->buf, match, len);
+		return 1;
+	}
+	return 0;
+}
+
+/*
+ * This reads "@{-N}" syntax, finds the name of the Nth previous
+ * branch we were on, and places the name of the branch in the given
+ * buf and returns the number of characters parsed if successful.
+ *
+ * If the input is not of the accepted format, it returns a negative
+ * number to signal an error.
+ *
+ * If the input was ok but there are not N branch switches in the
+ * reflog, it returns 0.
+ */
+int interpret_nth_last_branch(const char *name, struct strbuf *buf)
+{
+	int nth;
+	struct grab_nth_branch_switch_cbdata cb;
+	const char *brace;
+	char *num_end;
+
+	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
+		return -1;
+	brace = strchr(name, '}');
+	if (!brace)
+		return -1;
+	nth = strtol(name+3, &num_end, 10);
+	if (num_end != brace)
+		return -1;
+
+	cb.counting = 1;
+	cb.nth = 0;
+	cb.buf = buf;
+	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+
+	if (cb.nth < nth)
+		return 0;
+
+	cb.counting = 0;
+	cb.nth -= nth;
+	cb.buf = buf;
+	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+
+	return brace-name+1;
+}
+
 /*
  * This is like "get_sha1_basic()", except it allows "sha1 expressions",
  * notably "xyz^" for "parent of xyz"
diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
new file mode 100755
index 0000000..1e49dd2
--- /dev/null
+++ b/t/t1505-rev-parse-last.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+test_description='test @{-N} syntax'
+
+. ./test-lib.sh
+
+
+make_commit () {
+	echo "$1" > "$1" &&
+	git add "$1" &&
+	git commit -m "$1"
+}
+
+
+test_expect_success 'setup' '
+
+	make_commit 1 &&
+	git branch side &&
+	make_commit 2 &&
+	make_commit 3 &&
+	git checkout side &&
+	make_commit 4 &&
+	git merge master &&
+	git checkout master
+
+'
+
+# 1 -- 2 -- 3 master
+#  \         \
+#   \         \
+#    --- 4 --- 5 side
+#
+# and 'side' should be the last branch
+
+git log --graph --all --pretty=oneline --decorate
+
+test_rev_equivalent () {
+
+	git rev-parse "$1" > expect &&
+	git rev-parse "$2" > output &&
+	test_cmp expect output
+
+}
+
+test_expect_success '@{-1} works' '
+	test_rev_equivalent side @{-1}
+'
+
+test_expect_success '@{-1}~2 works' '
+	test_rev_equivalent side~2 @{-1}~2
+'
+
+test_expect_success '@{-1}^2 works' '
+	test_rev_equivalent side^2 @{-1}^2
+'
+
+test_expect_failure '@{-1}@{1} works' '
+	test_rev_equivalent side@{1} @{-1}@{1}
+'
+
+test_expect_success '@{-2} works' '
+	test_rev_equivalent master @{-2}
+'
+
+test_expect_success '@{-3} fails' '
+	test_must_fail git rev-parse @{-3}
+'
+
+test_done
+
+
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 13:40 UTC · re: Thomas Rast · lore

[PATCH/RFC v3bis 2/2] checkout: implement '@{-N}' and '-' special abbreviations

Checks if the branch to be checked out is either '@{-N}' or the special shorthand '-' for '@{-1}' (i.e. the last checked out branch). If so, we take it to mean the branch name, not the corresponding SHA, so that we check out an attached HEAD on that branch.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 Documentation/git-checkout.txt |    4 +++
 builtin-checkout.c             |   15 ++++++++++-
 t/t2012-checkout-last.sh       |   50 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 67 insertions(+), 2 deletions(-)
 create mode 100755 t/t2012-checkout-last.sh
Show changes to 3 files +67 −2

Documentation/git-checkout.txt, builtin-checkout.c, t/t2012-checkout-last.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 9cd5151..3bccffa 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -133,6 +133,10 @@ the conflicted merge in the specified paths.
 +
 When this parameter names a non-branch (but still a valid commit object),
 your HEAD becomes 'detached'.
++
+As a special case, the "`@\{-N\}`" syntax for the N-th last branch
+checks out the branch (instead of detaching).  You may also specify
+"`-`" which is synonymous with "`@\{-1\}`".
 
 
 Detached HEAD
diff --git a/builtin-checkout.c b/builtin-checkout.c
index b5dd9c0..b0a101b 100644
--- a/builtin-checkout.c
+++ b/builtin-checkout.c
@@ -361,8 +361,16 @@ struct branch_info {
 static void setup_branch_path(struct branch_info *branch)
 {
 	struct strbuf buf = STRBUF_INIT;
-	strbuf_addstr(&buf, "refs/heads/");
-	strbuf_addstr(&buf, branch->name);
+	int ret;
+
+	if ((ret = interpret_nth_last_branch(branch->name, &buf))
+	    && ret == strlen(branch->name)) {
+		branch->name = xstrdup(buf.buf);
+		strbuf_splice(&buf, 0, 0, "refs/heads/", 11);
+	} else {
+		strbuf_addstr(&buf, "refs/heads/");
+		strbuf_addstr(&buf, branch->name);
+	}
 	branch->path = strbuf_detach(&buf, NULL);
 }
 
@@ -671,6 +679,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 		arg = argv[0];
 		has_dash_dash = (argc > 1) && !strcmp(argv[1], "--");
 
+		if (!strcmp(arg, "-"))
+			arg = "@{-1}";
+
 		if (get_sha1(arg, rev)) {
 			if (has_dash_dash)          /* case (1) */
 				die("invalid reference: %s", arg);
diff --git a/t/t2012-checkout-last.sh b/t/t2012-checkout-last.sh
new file mode 100755
index 0000000..320f6eb
--- /dev/null
+++ b/t/t2012-checkout-last.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+test_description='checkout can switch to last branch'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	echo hello >world &&
+	git add world &&
+	git commit -m initial &&
+	git branch other &&
+	echo "hello again" >>world &&
+	git add world &&
+	git commit -m second
+'
+
+test_expect_success '"checkout -" does not work initially' '
+	test_must_fail git checkout -
+'
+
+test_expect_success 'first branch switch' '
+	git checkout other
+'
+
+test_expect_success '"checkout -" switches back' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/master"
+'
+
+test_expect_success '"checkout -" switches forth' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/other"
+'
+
+test_expect_success 'detach HEAD' '
+	git checkout $(git rev-parse HEAD)
+'
+
+test_expect_success '"checkout -" attaches again' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/other"
+'
+
+test_expect_success '"checkout -" detaches again' '
+	git checkout - &&
+	test "z$(git rev-parse HEAD)" = "z$(git rev-parse other)" &&
+	test_must_fail git symbolic-ref HEAD
+'
+
+test_done
-- 
1.6.1.315.g92577
Johannes Schindelin· Jan 17, 2009, 15:04 UTC · re: Thomas Rast · lore

[PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Instead of traversing them twice, we just build a list of branch switches, pick the one we're interested in, and free the list again.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
	On Sat, 17 Jan 2009, Thomas Rast wrote:
	> Johannes Schindelin wrote:
	> 
	> > I really have to ask: why did you not work on top of Junio's 
	> > patch, just adding docs, tests, and checkout -?  And then -- maybe -- 
	> > the string_list...
	Of course, I meant the patch as-is, with Junio as author.  But 
	hey, if he does not care...
	> > Although I have to admit that I am not _that_ convinced the 
	> > string_list is worth it: reflogs are not evaluated all the time, so 
	> > it is definitely not performance critical.
	>
	> I take it you have some idea where and how string_list fits into 
	> this topic?
	Indeed.
	This patch was generated using --collapse-non-alnums for readability.
	Did not really help much.
 sha1_name.c |   72 ++++++++++++++++++++++++----------------------------------
 1 files changed, 30 insertions(+), 42 deletions(-)
Show changes to sha1_name.c +30 −42
diff --git a/sha1_name.c b/sha1_name.c
index 2bbc5f1..306d04b 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -6,6 +6,7 @@
 #include "tree-walk.h"
 #include "refs.h"
 #include "cache-tree.h"
+#include "string-list.h"
 
 static int find_short_object_filename(int len, const char *name, unsigned char *sha1)
 {
@@ -691,43 +692,31 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
 	return retval;
 }
 
-struct grab_nth_branch_switch_cbdata {
-	int counting;
-	int nth;
-	struct strbuf *buf;
-};
-
-static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+static int add_one_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 				  const char *email, unsigned long timestamp, int tz,
 				  const char *message, void *cb_data)
 {
-	struct grab_nth_branch_switch_cbdata *cb = cb_data;
+	struct string_list *list = cb_data;
 	const char *match = NULL, *target = NULL;
 	size_t len;
-
-	if (!prefixcmp(message, "checkout: moving from ")) {
-		match = message + strlen("checkout: moving from ");
-		if ((target = strstr(match, " to ")) != NULL)
-			target += 4;
-	}
-
-	if (!match)
+
+	if (prefixcmp(message, "checkout: moving from "))
 		return 0;
 
-	len = target - match - 4;
-	if (target[len] == '\n' && !strncmp(match, target, len))
-		return 0;
-
-	if (cb->counting) {
-		cb->nth++;
-		return 0;
-	}
-
-	if (cb->nth-- <= 0) {
-		strbuf_reset(cb->buf);
-		strbuf_add(cb->buf, match, len);
-		return 1;
-	}
+	match = message + strlen("checkout: moving from ");
+
+	/* Is it "moving" from a branch to itself?  Then ignore it. */
+	if ((target = strstr(match, " to ")) != NULL) {
+		target += 4;
+		len = target - match - 4;
+		if (target[len] == '\n' && !strncmp(match, target, len))
+			return 0;
+	}
+	else
+		len = strchrnul(match, ' ') - match;
+
+	string_list_append(xstrndup(match, len), list);
+
 	return 0;
 }
 
@@ -745,7 +734,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 {
 	int nth;
-	struct grab_nth_branch_switch_cbdata cb;
+	struct string_list branch_list = { NULL, 0, 0, 0 };
 	const char *brace;
 	char *num_end;
 
@@ -758,18 +747,17 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 	if (num_end != brace)
 		return -1;
 
-	cb.counting = 1;
-	cb.nth = 0;
-	cb.buf = buf;
-	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
-
-	if (cb.nth < nth)
+	for_each_reflog_ent("HEAD", add_one_branch_switch, &branch_list);
+
+	if (branch_list.nr < nth)
 		return 0;
-
-	cb.counting = 0;
-	cb.nth -= nth;
-	cb.buf = buf;
-	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+
+	strbuf_reset(buf);
+	strbuf_addstr(buf, branch_list.items[branch_list.nr - nth].string);
+
+	/* force free()ing the items */
+	branch_list.strdup_strings = 1;
+	string_list_clear(&branch_list, 0);
 
 	return brace-name+1;
 }
-- 
1.6.1.325.g062d4
Thomas Rast· Jan 17, 2009, 16:09 UTC · re: Johannes Schindelin · lore

[PATCH/RFC v4 0/5] N-th last checked out branch

Johannes Schindelin wrote:
Show 17 quoted lines
> 
> Instead of traversing them twice, we just build a list of branch switches,
> pick the one we're interested in, and free the list again.
> 
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> 
> On Sat, 17 Jan 2009, Thomas Rast wrote:
> 
> > Johannes Schindelin wrote:
> > 
> > > I really have to ask: why did you not work on top of Junio's 
> > > patch, just adding docs, tests, and checkout -?  And then -- maybe -- 
> > > the string_list...
> 
> Of course, I meant the patch as-is, with Junio as author.  But 
> hey, if he does not care...
*shrug*
For one thing there was no commit message.

I made up a message, split the changes I made into smaller patches, and added a fixed up version of your patch on top, since it had some context that is not in any version I have.

Johannes Schindelin (1):
  interpret_nth_last_branch(): avoid traversing the reflogs twice
Junio C Hamano (1):
  checkout: implement "@{-N}" shortcut name for N-th last branch
Thomas Rast (3):
  sha1_name: tweak @{-N} lookup
  sha1_name: support @{-N} syntax in get_sha1()
  checkout: implement "-" abbreviation, add docs and tests
 Documentation/git-checkout.txt  |    4 ++
 Documentation/git-rev-parse.txt |    3 +
 builtin-checkout.c              |   15 ++++++-
 cache.h                         |    1 +
 sha1_name.c                     |   88 +++++++++++++++++++++++++++++++++++++-
 t/t1505-rev-parse-last.sh       |   71 +++++++++++++++++++++++++++++++
 t/t2012-checkout-last.sh        |   50 ++++++++++++++++++++++
 7 files changed, 227 insertions(+), 5 deletions(-)
 create mode 100755 t/t1505-rev-parse-last.sh
 create mode 100755 t/t2012-checkout-last.sh
Thomas Rast· Jan 17, 2009, 16:09 UTC · re: Thomas Rast · lore

[PATCH/RFC v4 1/5] checkout: implement "@{-N}" shortcut name for N-th last branch

From: Junio C Hamano <gitster@pobox.com>
Implement a shortcut @{-N} for the N-th last branch checked out, that
works by parsing the reflog for the message added by previous
git-checkout invocations.  We expand the @{-N} to the branch name, so
that you end up on an attached HEAD on that branch.
---
 builtin-checkout.c |   10 +++++-
 cache.h            |    1 +
 sha1_name.c        |   78 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 87 insertions(+), 2 deletions(-)
Show changes to 3 files +87 −2

builtin-checkout.c, cache.h, sha1_name.c

diff --git a/builtin-checkout.c b/builtin-checkout.c
index b5dd9c0..a3b69d6 100644
--- a/builtin-checkout.c
+++ b/builtin-checkout.c
@@ -361,8 +361,14 @@ struct branch_info {
 static void setup_branch_path(struct branch_info *branch)
 {
 	struct strbuf buf = STRBUF_INIT;
-	strbuf_addstr(&buf, "refs/heads/");
-	strbuf_addstr(&buf, branch->name);
+
+	if (!interpret_nth_last_branch(branch->name, &buf)) {
+		branch->name = xstrdup(buf.buf);
+		strbuf_splice(&buf, 0, 0, "refs/heads/", 11);
+	} else {
+		strbuf_addstr(&buf, "refs/heads/");
+		strbuf_addstr(&buf, branch->name);
+	}
 	branch->path = strbuf_detach(&buf, NULL);
 }
 
diff --git a/cache.h b/cache.h
index 8e1af26..0dd9168 100644
--- a/cache.h
+++ b/cache.h
@@ -663,6 +663,7 @@ static inline unsigned int hexval(unsigned char c)
 extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *);
 extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
 extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
+extern int interpret_nth_last_branch(const char *str, struct strbuf *);
 
 extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
 extern const char *ref_rev_parse_rules[];
diff --git a/sha1_name.c b/sha1_name.c
index 159c2ab..6377264 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -674,6 +674,84 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
 	return retval;
 }
 
+struct grab_nth_branch_switch_cbdata {
+	int counting;
+	int nth;
+	struct strbuf *buf;
+};
+
+static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+				  const char *email, unsigned long timestamp, int tz,
+				  const char *message, void *cb_data)
+{
+	struct grab_nth_branch_switch_cbdata *cb = cb_data;
+	const char *match = NULL;
+
+	if (!prefixcmp(message, "checkout: moving to "))
+		match = message + strlen("checkout: moving to ");
+	else if (!prefixcmp(message, "checkout: moving from ")) {
+		const char *cp = message + strlen("checkout: moving from ");
+		if ((cp = strstr(cp, " to ")) != NULL) {
+			match = cp + 4;
+		}
+	}
+
+	if (!match)
+		return 0;
+
+	if (cb->counting) {
+		cb->nth++;
+		return 0;
+	}
+
+	if (--cb->nth <= 0) {
+		size_t len = strlen(match);
+		while (match[len-1] == '\n')
+			len--;
+		strbuf_reset(cb->buf);
+		strbuf_add(cb->buf, match, len);
+		return 1;
+	}
+	return 0;
+}
+
+/*
+ * This reads "@{-N}" syntax, finds the name of the Nth previous
+ * branch we were on, and places the name of the branch in the given
+ * buf and returns 0 if successful.
+ *
+ * If the input is not of the accepted format, it returns a negative
+ * number to signal an error.
+ */
+int interpret_nth_last_branch(const char *name, struct strbuf *buf)
+{
+	int nth, i;
+	struct grab_nth_branch_switch_cbdata cb;
+
+	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
+		return -1;
+	for (i = 3, nth = 0; name[i] && name[i] != '}'; i++) {
+		char ch = name[i];
+		if ('0' <= ch && ch <= '9')
+			nth = nth * 10 + ch - '0';
+		else
+			return -1;
+	}
+	if (nth < 0 || 10 <= nth)
+		return -1;
+
+	cb.counting = 1;
+	cb.nth = 0;
+	cb.buf = buf;
+	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+
+	cb.counting = 0;
+	cb.nth -= nth;
+	cb.buf = buf;
+	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+	return 0;
+}
+
 /*
  * This is like "get_sha1_basic()", except it allows "sha1 expressions",
  * notably "xyz^" for "parent of xyz"
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 16:09 UTC · re: Thomas Rast · lore

[PATCH/RFC v4 2/5] sha1_name: tweak @{-N} lookup

Have the lookup only look at "interesting" checkouts, meaning those that tell you "Already on ..." don't count even though they also cause a reflog entry.

Let interpret_nth_last_branch() return the number of characters parsed, so that git-checkout can verify that the branch spec was @{-N}, not @{-1}^2 or something like that. (The latter will be added later.)

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 builtin-checkout.c |    4 ++-
 sha1_name.c        |   53 ++++++++++++++++++++++++++++-----------------------
 2 files changed, 32 insertions(+), 25 deletions(-)
Show changes to 2 files +32 −25

builtin-checkout.c, sha1_name.c

diff --git a/builtin-checkout.c b/builtin-checkout.c
index a3b69d6..dc1de06 100644
--- a/builtin-checkout.c
+++ b/builtin-checkout.c
@@ -361,8 +361,10 @@ struct branch_info {
 static void setup_branch_path(struct branch_info *branch)
 {
 	struct strbuf buf = STRBUF_INIT;
+	int ret;
 
-	if (!interpret_nth_last_branch(branch->name, &buf)) {
+	if ((ret = interpret_nth_last_branch(branch->name, &buf))
+	    && ret == strlen(branch->name)) {
 		branch->name = xstrdup(buf.buf);
 		strbuf_splice(&buf, 0, 0, "refs/heads/", 11);
 	} else {
diff --git a/sha1_name.c b/sha1_name.c
index 6377264..34e39db 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -685,29 +685,28 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 				  const char *message, void *cb_data)
 {
 	struct grab_nth_branch_switch_cbdata *cb = cb_data;
-	const char *match = NULL;
-
-	if (!prefixcmp(message, "checkout: moving to "))
-		match = message + strlen("checkout: moving to ");
-	else if (!prefixcmp(message, "checkout: moving from ")) {
-		const char *cp = message + strlen("checkout: moving from ");
-		if ((cp = strstr(cp, " to ")) != NULL) {
-			match = cp + 4;
-		}
+	const char *match = NULL, *target = NULL;
+	size_t len;
+
+	if (!prefixcmp(message, "checkout: moving from ")) {
+		match = message + strlen("checkout: moving from ");
+		if ((target = strstr(match, " to ")) != NULL)
+			target += 4;
 	}
 
 	if (!match)
 		return 0;
 
+	len = target - match - 4;
+	if (target[len] == '\n' && !strncmp(match, target, len))
+		return 0;
+
 	if (cb->counting) {
 		cb->nth++;
 		return 0;
 	}
 
-	if (--cb->nth <= 0) {
-		size_t len = strlen(match);
-		while (match[len-1] == '\n')
-			len--;
+	if (cb->nth-- <= 0) {
 		strbuf_reset(cb->buf);
 		strbuf_add(cb->buf, match, len);
 		return 1;
@@ -718,26 +717,28 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 /*
  * This reads "@{-N}" syntax, finds the name of the Nth previous
  * branch we were on, and places the name of the branch in the given
- * buf and returns 0 if successful.
+ * buf and returns the number of characters parsed if successful.
  *
  * If the input is not of the accepted format, it returns a negative
  * number to signal an error.
+ *
+ * If the input was ok but there are not N branch switches in the
+ * reflog, it returns 0.
  */
 int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 {
-	int nth, i;
+	int nth;
 	struct grab_nth_branch_switch_cbdata cb;
+	const char *brace;
+	char *num_end;
 
 	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
 		return -1;
-	for (i = 3, nth = 0; name[i] && name[i] != '}'; i++) {
-		char ch = name[i];
-		if ('0' <= ch && ch <= '9')
-			nth = nth * 10 + ch - '0';
-		else
-			return -1;
-	}
-	if (nth < 0 || 10 <= nth)
+	brace = strchr(name, '}');
+	if (!brace)
+		return -1;
+	nth = strtol(name+3, &num_end, 10);
+	if (num_end != brace)
 		return -1;
 
 	cb.counting = 1;
@@ -745,11 +746,15 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 	cb.buf = buf;
 	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
 
+	if (cb.nth < nth)
+		return 0;
+
 	cb.counting = 0;
 	cb.nth -= nth;
 	cb.buf = buf;
 	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
-	return 0;
+
+	return brace-name+1;
 }
 
 /*
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 16:09 UTC · re: Thomas Rast · lore

[PATCH/RFC v4 3/5] sha1_name: support @{-N} syntax in get_sha1()

Let get_sha1() parse the @{-N} syntax, with docs and tests.

Note that while @{-1}^2, @{-2}~5 and such are supported, @{-1}@{1} is currently not allowed.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 Documentation/git-rev-parse.txt |    3 ++
 sha1_name.c                     |   16 +++++++--
 t/t1505-rev-parse-last.sh       |   71 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 87 insertions(+), 3 deletions(-)
 create mode 100755 t/t1505-rev-parse-last.sh
Show changes to 3 files +87 −3

Documentation/git-rev-parse.txt, sha1_name.c, t/t1505-rev-parse-last.sh

diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 2921da3..3ccef2f 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -212,6 +212,9 @@ when you run 'git-merge'.
   reflog of the current branch. For example, if you are on the
   branch 'blabla', then '@\{1\}' means the same as 'blabla@\{1\}'.
 
+* The special construct '@\{-<n>\}' means the <n>th branch checked out
+  before the current one.
+
 * A suffix '{caret}' to a revision parameter means the first parent of
   that commit object.  '{caret}<n>' means the <n>th parent (i.e.
   'rev{caret}'
diff --git a/sha1_name.c b/sha1_name.c
index 34e39db..9e1538e 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -297,6 +297,8 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
 	return logs_found;
 }
 
+static int get_sha1_1(const char *name, int len, unsigned char *sha1);
+
 static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 {
 	static const char *warning = "warning: refname '%.*s' is ambiguous.\n";
@@ -307,7 +309,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	if (len == 40 && !get_sha1_hex(str, sha1))
 		return 0;
 
-	/* basic@{time or number} format to query ref-log */
+	/* basic@{time or number or -number} format to query ref-log */
 	reflog_len = at = 0;
 	if (str[len-1] == '}') {
 		for (at = 0; at < len - 1; at++) {
@@ -324,6 +326,16 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 		return -1;
 
 	if (!len && reflog_len) {
+		struct strbuf buf = STRBUF_INIT;
+		int ret;
+		/* try the @{-N} syntax for n-th checkout */
+		ret = interpret_nth_last_branch(str+at, &buf);
+		if (ret > 0) {
+			/* substitute this branch name and restart */
+			return get_sha1_1(buf.buf, buf.len, sha1);
+		} else if (ret == 0) {
+			return -1;
+		}
 		/* allow "@{...}" to mean the current branch reflog */
 		refs_found = dwim_ref("HEAD", 4, sha1, &real_ref);
 	} else if (reflog_len)
@@ -379,8 +391,6 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	return 0;
 }
 
-static int get_sha1_1(const char *name, int len, unsigned char *sha1);
-
 static int get_parent(const char *name, int len,
 		      unsigned char *result, int idx)
 {
diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
new file mode 100755
index 0000000..1e49dd2
--- /dev/null
+++ b/t/t1505-rev-parse-last.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+test_description='test @{-N} syntax'
+
+. ./test-lib.sh
+
+
+make_commit () {
+	echo "$1" > "$1" &&
+	git add "$1" &&
+	git commit -m "$1"
+}
+
+
+test_expect_success 'setup' '
+
+	make_commit 1 &&
+	git branch side &&
+	make_commit 2 &&
+	make_commit 3 &&
+	git checkout side &&
+	make_commit 4 &&
+	git merge master &&
+	git checkout master
+
+'
+
+# 1 -- 2 -- 3 master
+#  \         \
+#   \         \
+#    --- 4 --- 5 side
+#
+# and 'side' should be the last branch
+
+git log --graph --all --pretty=oneline --decorate
+
+test_rev_equivalent () {
+
+	git rev-parse "$1" > expect &&
+	git rev-parse "$2" > output &&
+	test_cmp expect output
+
+}
+
+test_expect_success '@{-1} works' '
+	test_rev_equivalent side @{-1}
+'
+
+test_expect_success '@{-1}~2 works' '
+	test_rev_equivalent side~2 @{-1}~2
+'
+
+test_expect_success '@{-1}^2 works' '
+	test_rev_equivalent side^2 @{-1}^2
+'
+
+test_expect_failure '@{-1}@{1} works' '
+	test_rev_equivalent side@{1} @{-1}@{1}
+'
+
+test_expect_success '@{-2} works' '
+	test_rev_equivalent master @{-2}
+'
+
+test_expect_success '@{-3} fails' '
+	test_must_fail git rev-parse @{-3}
+'
+
+test_done
+
+
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 16:09 UTC · re: Thomas Rast · lore

[PATCH/RFC v4 4/5] checkout: implement "-" abbreviation, add docs and tests

Have '-' mean the same as '@{-1}', i.e., the last branch we were on.
Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 Documentation/git-checkout.txt |    4 +++
 builtin-checkout.c             |    3 ++
 t/t2012-checkout-last.sh       |   50 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 57 insertions(+), 0 deletions(-)
 create mode 100755 t/t2012-checkout-last.sh
Show changes to 3 files +57 −0

Documentation/git-checkout.txt, builtin-checkout.c, t/t2012-checkout-last.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 9cd5151..3bccffa 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -133,6 +133,10 @@ the conflicted merge in the specified paths.
 +
 When this parameter names a non-branch (but still a valid commit object),
 your HEAD becomes 'detached'.
++
+As a special case, the "`@\{-N\}`" syntax for the N-th last branch
+checks out the branch (instead of detaching).  You may also specify
+"`-`" which is synonymous with "`@\{-1\}`".
 
 
 Detached HEAD
diff --git a/builtin-checkout.c b/builtin-checkout.c
index dc1de06..b0a101b 100644
--- a/builtin-checkout.c
+++ b/builtin-checkout.c
@@ -679,6 +679,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 		arg = argv[0];
 		has_dash_dash = (argc > 1) && !strcmp(argv[1], "--");
 
+		if (!strcmp(arg, "-"))
+			arg = "@{-1}";
+
 		if (get_sha1(arg, rev)) {
 			if (has_dash_dash)          /* case (1) */
 				die("invalid reference: %s", arg);
diff --git a/t/t2012-checkout-last.sh b/t/t2012-checkout-last.sh
new file mode 100755
index 0000000..320f6eb
--- /dev/null
+++ b/t/t2012-checkout-last.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+test_description='checkout can switch to last branch'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	echo hello >world &&
+	git add world &&
+	git commit -m initial &&
+	git branch other &&
+	echo "hello again" >>world &&
+	git add world &&
+	git commit -m second
+'
+
+test_expect_success '"checkout -" does not work initially' '
+	test_must_fail git checkout -
+'
+
+test_expect_success 'first branch switch' '
+	git checkout other
+'
+
+test_expect_success '"checkout -" switches back' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/master"
+'
+
+test_expect_success '"checkout -" switches forth' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/other"
+'
+
+test_expect_success 'detach HEAD' '
+	git checkout $(git rev-parse HEAD)
+'
+
+test_expect_success '"checkout -" attaches again' '
+	git checkout - &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/other"
+'
+
+test_expect_success '"checkout -" detaches again' '
+	git checkout - &&
+	test "z$(git rev-parse HEAD)" = "z$(git rev-parse other)" &&
+	test_must_fail git symbolic-ref HEAD
+'
+
+test_done
-- 
1.6.1.315.g92577
Thomas Rast· Jan 17, 2009, 16:09 UTC · re: Thomas Rast · lore

[PATCH/RFC v4 5/5] interpret_nth_last_branch(): avoid traversing the reflogs twice

From: Johannes Schindelin <Johannes.Schindelin@gmx.de>

Instead of traversing them twice, we just build a list of branch switches, pick the one we're interested in, and free the list again.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sha1_name.c |   61 ++++++++++++++++++++++++----------------------------------
 1 files changed, 25 insertions(+), 36 deletions(-)
Show changes to sha1_name.c +25 −36
diff --git a/sha1_name.c b/sha1_name.c
index 9e1538e..b21a1f0 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -5,6 +5,8 @@
 #include "blob.h"
 #include "tree-walk.h"
 #include "refs.h"
+#include "cache-tree.h"
+#include "string-list.h"
 
 static int find_short_object_filename(int len, const char *name, unsigned char *sha1)
 {
@@ -684,43 +686,31 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
 	return retval;
 }
 
-struct grab_nth_branch_switch_cbdata {
-	int counting;
-	int nth;
-	struct strbuf *buf;
-};
-
-static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+static int add_one_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 				  const char *email, unsigned long timestamp, int tz,
 				  const char *message, void *cb_data)
 {
-	struct grab_nth_branch_switch_cbdata *cb = cb_data;
+	struct string_list *list = cb_data;
 	const char *match = NULL, *target = NULL;
 	size_t len;
 
-	if (!prefixcmp(message, "checkout: moving from ")) {
-		match = message + strlen("checkout: moving from ");
-		if ((target = strstr(match, " to ")) != NULL)
-			target += 4;
-	}
-
-	if (!match)
+	if (prefixcmp(message, "checkout: moving from "))
 		return 0;
 
-	len = target - match - 4;
-	if (target[len] == '\n' && !strncmp(match, target, len))
-		return 0;
+	match = message + strlen("checkout: moving from ");
 
-	if (cb->counting) {
-		cb->nth++;
-		return 0;
+	/* Is it "moving" from a branch to itself?  Then ignore it. */
+	if ((target = strstr(match, " to ")) != NULL) {
+		target += 4;
+		len = target - match - 4;
+		if (target[len] == '\n' && !strncmp(match, target, len))
+			return 0;
 	}
+	else
+		len = strchrnul(match, ' ') - match;
+
+	string_list_append(xstrndup(match, len), list);
 
-	if (cb->nth-- <= 0) {
-		strbuf_reset(cb->buf);
-		strbuf_add(cb->buf, match, len);
-		return 1;
-	}
 	return 0;
 }
 
@@ -738,7 +728,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 {
 	int nth;
-	struct grab_nth_branch_switch_cbdata cb;
+	struct string_list branch_list = { NULL, 0, 0, 0 };
 	const char *brace;
 	char *num_end;
 
@@ -751,18 +741,17 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 	if (num_end != brace)
 		return -1;
 
-	cb.counting = 1;
-	cb.nth = 0;
-	cb.buf = buf;
-	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+	for_each_reflog_ent("HEAD", add_one_branch_switch, &branch_list);
 
-	if (cb.nth < nth)
+	if (branch_list.nr < nth)
 		return 0;
 
-	cb.counting = 0;
-	cb.nth -= nth;
-	cb.buf = buf;
-	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+	strbuf_reset(buf);
+	strbuf_addstr(buf, branch_list.items[branch_list.nr - nth].string);
+
+	/* force free()ing the items */
+	branch_list.strdup_strings = 1;
+	string_list_clear(&branch_list, 0);
 
 	return brace-name+1;
 }
-- 
1.6.1.315.g92577
Johannes Schindelin· Jan 17, 2009, 18:08 UTC · re: Thomas Rast · lore

[PATCH 6/5] Fix parsing of @{-1}@{1}

To do that, Git no longer looks forward for the '@{' corresponding to the closing '}' but backward, and dwim_ref() as well as dwim_log() learnt about the @{-<N>} notation.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
	The modifications of dwim_ref() and dwim_log() are probably
	more important than the issue I tried to fix...
 sha1_name.c               |   25 ++++++++++++++++++++++++-
 t/t1505-rev-parse-last.sh |    2 +-
 2 files changed, 25 insertions(+), 2 deletions(-)
Show changes to 2 files +25 −2

sha1_name.c, t/t1505-rev-parse-last.sh

diff --git a/sha1_name.c b/sha1_name.c
index 306d04b..ee0c456 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -240,8 +240,28 @@ static int ambiguous_path(const char *path, int len)
 	return slash;
 }
 
+/*
+ * *string and *len will only be substituted, and *string returned (for
+ * later free()ing) if the string passed in is of the form @{-<n>}.
+ */
+static char *substitute_nth_last_branch(const char **string, int *len)
+{
+	struct strbuf buf = STRBUF_INIT;
+	int ret = interpret_nth_last_branch(*string, &buf);
+
+	if (ret == *len) {
+		size_t size;
+		*string = strbuf_detach(&buf, &size);
+		*len = size;
+		return (char *)*string;
+	}
+
+	return NULL;
+}
+
 int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref)
 {
+	char *last_branch = substitute_nth_last_branch(&str, &len);
 	const char **p, *r;
 	int refs_found = 0;
 
@@ -261,11 +281,13 @@ int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref)
 				break;
 		}
 	}
+	free(last_branch);
 	return refs_found;
 }
 
 int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
 {
+	char *last_branch = substitute_nth_last_branch(&str, &len);
 	const char **p;
 	int logs_found = 0;
 
@@ -296,6 +318,7 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
 		if (!warn_ambiguous_refs)
 			break;
 	}
+	free(last_branch);
 	return logs_found;
 }
 
@@ -314,7 +337,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	/* basic@{time or number or -number} format to query ref-log */
 	reflog_len = at = 0;
 	if (str[len-1] == '}') {
-		for (at = 0; at < len - 1; at++) {
+		for (at = len-2; at >= 0; at--) {
 			if (str[at] == '@' && str[at+1] == '{') {
 				reflog_len = (len-1) - (at+2);
 				len = at;
diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
index 72e8322..2d6b31e 100755
--- a/t/t1505-rev-parse-last.sh
+++ b/t/t1505-rev-parse-last.sh
@@ -58,7 +58,7 @@ test_expect_success '@{-1}^2 works' '
 	test_rev_equivalent side^2 @{-1}^2
 '
 
-test_expect_failure '@{-1}@{1} works' '
+test_expect_success '@{-1}@{1} works' '
 	test_rev_equivalent side@{1} @{-1}@{1}
 '
 
-- 
1.6.1.332.g9a59d
Junio C Hamano· Jan 17, 2009, 20:02 UTC · re: Johannes Schindelin · lore

Re: [PATCH 6/5] Fix parsing of @{-1}@{1}

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
Show 9 quoted lines
> To do that, Git no longer looks forward for the '@{' corresponding to the
> closing '}' but backward, and dwim_ref() as well as dwim_log() learnt
> about the @{-<N>} notation.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>
> 	The modifications of dwim_ref() and dwim_log() are probably
> 	more important than the issue I tried to fix...
Good, so we can say things like:
	git log -g @{-1}
        git show-branch -g @{-1}
with this?

By the way, I noticed that without these patch series we show something when "git rev-parse --verify @{-1}" is asked for. What is it trying to show?

Johannes Schindelin· Jan 17, 2009, 21:22 UTC · re: Junio C Hamano · lore

Re: [PATCH 6/5] Fix parsing of @{-1}@{1}

Hi,
On Sat, 17 Jan 2009, Junio C Hamano wrote:
Show 18 quoted lines
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > To do that, Git no longer looks forward for the '@{' corresponding to the
> > closing '}' but backward, and dwim_ref() as well as dwim_log() learnt
> > about the @{-<N>} notation.
> >
> > Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> > ---
> >
> > 	The modifications of dwim_ref() and dwim_log() are probably
> > 	more important than the issue I tried to fix...
> 
> Good, so we can say things like:
> 
> 	git log -g @{-1}
>         git show-branch -g @{-1}
> 
> with this?
I _hope_ :-)
> By the way, I noticed that without these patch series we show something 
> when "git rev-parse --verify @{-1}" is asked for.  What is it trying to 
> show?
Apparently the same as @{1.Jan}: in get_sha1_basic(), we have this code:
                /* Is it asking for N-th entry, or approxidate? */
                for (i = nth = 0; 0 <= nth && i < reflog_len; i++) {
                        char ch = str[at+2+i];
                        if ('0' <= ch && ch <= '9')
                                nth = nth * 10 + ch - '0';
                        else
                                nth = -1;
                }
                if (100000000 <= nth) {
                        at_time = nth;
                        nth = -1;
                } else if (0 <= nth)
                        at_time = 0;
                else {
                        char *tmp = xstrndup(str + at + 2, reflog_len);
                        at_time = approxidate(tmp);
                        free(tmp);
                }

So in the loop, nth is set to -1 because of a non-digit, and later, approxidate is called for nth == -1, which does this:

	$ ./test-date now
	now -> bad -> Thu Jan  1 01:00:00 1970
	now -> Sat Jan 17 22:21:20 2009

Ciao, Dscho

Junio C Hamano· Jan 17, 2009, 19:57 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v4 4/5] checkout: implement "-" abbreviation, add docs and tests

Thomas Rast <trast@student.ethz.ch> writes:
Show 8 quoted lines
> @@ -133,6 +133,10 @@ the conflicted merge in the specified paths.
>  +
>  When this parameter names a non-branch (but still a valid commit object),
>  your HEAD becomes 'detached'.
> ++
> +As a special case, the "`@\{-N\}`" syntax for the N-th last branch
> +checks out the branch (instead of detaching).  You may also specify
> +"`-`" which is synonymous with "`@\{-1\}`".
I mildly disagree with this wording.

The new syntax is supposed to be a new way to name a branch, not a random non-branch committish that is special cased by "git checkout". I would further suggest that we should teach "git rev-parse --symbolic-full-name" and "git rev-parse --symbolic" about the new syntax, so that scripts can use the syntax to find out the same information.

The "-" thing deserves a mention here in the documentation. That _is_ a special case that only applies to the "git checkout" command.

Show 11 quoted lines
> diff --git a/builtin-checkout.c b/builtin-checkout.c
> index dc1de06..b0a101b 100644
> --- a/builtin-checkout.c
> +++ b/builtin-checkout.c
> @@ -679,6 +679,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
>  		arg = argv[0];
>  		has_dash_dash = (argc > 1) && !strcmp(argv[1], "--");
>  
> +		if (!strcmp(arg, "-"))
> +			arg = "@{-1}";
> +

This is not quite nice as it could be, but it probably is Ok. If the interpretation of @{-1} errors out, the user won't see an error message that talks about "-" but instead the user will see "@{-1}".

Also it will look somewhat inconsistent to the end user who does not know the internals for "-" claim to be a synonym for @{-1} but it really isn't. For example, "git checkout -^0" does not work as "git checkout @{-1}^0".

To avoid such confusion, we could instead make "git checkout - <ENTER>" a synonym for "git checkout @{-1} <ENTER>", without claiming to make "-" a synonym for "@{-1}". In other words, "git checkout -" can become a very narrow, focused special case that does not allow anything else, such as pathspecs, "--" separator, nor --force and other options.

Johannes Schindelin· Jan 17, 2009, 17:55 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v4 3/5] sha1_name: support @{-N} syntax in get_sha1()

Hi,
On Sat, 17 Jan 2009, Thomas Rast wrote:
Show 42 quoted lines
> diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
> new file mode 100755
> index 0000000..1e49dd2
> --- /dev/null
> +++ b/t/t1505-rev-parse-last.sh
> @@ -0,0 +1,71 @@
> +#!/bin/sh
> +
> +test_description='test @{-N} syntax'
> +
> +. ./test-lib.sh
> +
> +
> +make_commit () {
> +	echo "$1" > "$1" &&
> +	git add "$1" &&
> +	git commit -m "$1"
> +}
> +
> +
> +test_expect_success 'setup' '
> +
> +	make_commit 1 &&
> +	git branch side &&
> +	make_commit 2 &&
> +	make_commit 3 &&
> +	git checkout side &&
> +	make_commit 4 &&
> +	git merge master &&
> +	git checkout master
> +
> +'
> +
> +# 1 -- 2 -- 3 master
> +#  \         \
> +#   \         \
> +#    --- 4 --- 5 side
> +#
> +# and 'side' should be the last branch
> +
> +git log --graph --all --pretty=oneline --decorate
> +

Maybe you want to squash this in, so that the output of "make test" is not cluttered by the graph?

-- snipsnap --
Show changes to t/t1505-rev-parse-last.sh +5 −1
diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
index 1e49dd2..72e8322 100755
--- a/t/t1505-rev-parse-last.sh
+++ b/t/t1505-rev-parse-last.sh
@@ -32,7 +32,11 @@ test_expect_success 'setup' '
 #
 # and 'side' should be the last branch
 
-git log --graph --all --pretty=oneline --decorate
+test_expect_success 'show a log (for debugging)' '
+
+	git log --graph --all --pretty=oneline --decorate
+
+'
 
 test_rev_equivalent () {
 
-- 
1.6.1.332.g9a59d
Junio C Hamano· Jan 17, 2009, 19:37 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v4 3/5] sha1_name: support @{-N} syntax in get_sha1()

Thomas Rast <trast@student.ethz.ch> writes:
Show 21 quoted lines
> Let get_sha1() parse the @{-N} syntax, with docs and tests.
>
> Note that while @{-1}^2, @{-2}~5 and such are supported, @{-1}@{1} is
> currently not allowed.
>
> Signed-off-by: Thomas Rast <trast@student.ethz.ch>
> ...
> @@ -324,6 +326,16 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
>  		return -1;
>  
>  	if (!len && reflog_len) {
> +		struct strbuf buf = STRBUF_INIT;
> +		int ret;
> +		/* try the @{-N} syntax for n-th checkout */
> +		ret = interpret_nth_last_branch(str+at, &buf);
> +		if (ret > 0) {
> +			/* substitute this branch name and restart */
> +			return get_sha1_1(buf.buf, buf.len, sha1);
> +		} else if (ret == 0) {
> +			return -1;
> +		}

What are the possible failure cases, and what do we want to tell the end-user?

 - You asked for 3rd but there weren't that many switches yet, and ask
   "git rev-parse --verify @{-3}".
   Are we Ok with "fatal: Needed a single revision" from rev-parse?  Do we
   want to show "fatal: @{-3}: not that many branch switches yet"?
   What happens to "git checkout @{-3}" in this case?  Having checkout say
   "fatal: invalid reference: @{-3}" would be fine in this case, I think.
 - You try "git checkout @{-3}", you were on "frotz" branch back then, but
   the branch does not exist anymore.
   I think you will get "fatal: invalid reference: frotz" from checkout,
   which should be fine.

There also is a case where nth_last_branch() may find something that is not a branch (e.g. "git checkout HEAD^"), but I am hoping we can label that as a bug in nth_last_branch() and fix it later.

Junio C Hamano· Jan 18, 2009, 00:54 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v4 2/5] sha1_name: tweak @{-N} lookup

Thomas Rast <trast@student.ethz.ch> writes:
Show 8 quoted lines
> Have the lookup only look at "interesting" checkouts, meaning those
> that tell you "Already on ..." don't count even though they also cause
> a reflog entry.
>
> Let interpret_nth_last_branch() return the number of characters
> parsed, so that git-checkout can verify that the branch spec was
> @{-N}, not @{-1}^2 or something like that.  (The latter will be added
> later.)

Thanks; you seem to have handled the issues I pointed out in response to my own weatherbaloon patch. I think it is probably better to squash the first two (and you take the authorship).

Show 25 quoted lines
> diff --git a/sha1_name.c b/sha1_name.c
> index 6377264..34e39db 100644
> --- a/sha1_name.c
> +++ b/sha1_name.c
> @@ -685,29 +685,28 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
>  				  const char *message, void *cb_data)
>  {
>  	struct grab_nth_branch_switch_cbdata *cb = cb_data;
> -	const char *match = NULL;
> -
> -	if (!prefixcmp(message, "checkout: moving to "))
> -		match = message + strlen("checkout: moving to ");
> -	else if (!prefixcmp(message, "checkout: moving from ")) {
> -		const char *cp = message + strlen("checkout: moving from ");
> -		if ((cp = strstr(cp, " to ")) != NULL) {
> -			match = cp + 4;
> -		}
> +	const char *match = NULL, *target = NULL;
> +	size_t len;
> +
> +	if (!prefixcmp(message, "checkout: moving from ")) {
> +		match = message + strlen("checkout: moving from ");
> +		if ((target = strstr(match, " to ")) != NULL)
> +			target += 4;
>  	}

This drops support for older reflog records, but I think it would be Ok. This "N-th" support is really meant to be for small number of N anyway.

Show 8 quoted lines
> -	if (--cb->nth <= 0) {
> -		size_t len = strlen(match);
> -		while (match[len-1] == '\n')
> -			len--;
> +	if (cb->nth-- <= 0) {
>  		strbuf_reset(cb->buf);
>  		strbuf_add(cb->buf, match, len);
>  		return 1;
Hmm, did I have an off-by-one I did not notice? ;-)
Show 18 quoted lines
>  int interpret_nth_last_branch(const char *name, struct strbuf *buf)
>  {
> -	int nth, i;
> +	int nth;
>  	struct grab_nth_branch_switch_cbdata cb;
> +	const char *brace;
> +	char *num_end;
>  
>  	if (name[0] != '@' || name[1] != '{' || name[2] != '-')
>  		return -1;
> -	for (i = 3, nth = 0; name[i] && name[i] != '}'; i++) {
> -		char ch = name[i];
> -		if ('0' <= ch && ch <= '9')
> -			nth = nth * 10 + ch - '0';
> -		else
> -			return -1;
> -	}
> -	if (nth < 0 || 10 <= nth)

The removal of "limit to reasonably small recent N" I somewhat have reservations on, but I think we can later re-add something based on configuration variable if we need to.

Johannes Schindelin· Jan 17, 2009, 16:49 UTC · re: Thomas Rast · lore

Re: [PATCH/RFC v4 0/5] N-th last checked out branch

Hi,
On Sat, 17 Jan 2009, Thomas Rast wrote:
> I [...] added a fixed up version of your patch on top, since it had some 
> context that is not in any version I have.
Thanks.

Yeah, I know, I have too many patches in my fork, but I'm working on it... :-)

Ciao, Dscho

Junio C Hamano· Jan 17, 2009, 19:13 UTC · re: Johannes Schindelin · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> Instead of traversing them twice, we just build a list of branch switches,
> pick the one we're interested in, and free the list again.
Isn't the code keeping them all in core, or am I reading the patch wrong?

If you know that you are interested in the nth-from-the-last switch, and if you are reading from the beginning, you would need to keep at most n last switches you have seen in core, wouldn't you?

Johannes Schindelin· Jan 17, 2009, 19:29 UTC · re: Junio C Hamano · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Hi,
On Sat, 17 Jan 2009, Junio C Hamano wrote:
Show 10 quoted lines
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > Instead of traversing them twice, we just build a list of branch switches,
> > pick the one we're interested in, and free the list again.
> 
> Isn't the code keeping them all in core, or am I reading the patch wrong?
> 
> If you know that you are interested in the nth-from-the-last switch, and
> if you are reading from the beginning, you would need to keep at most n 
> last switches you have seen in core, wouldn't you?  

That is correct. But this is such a highly uncritical code path that I'd like to keep this simple rather than fast.

Ciao, Dscho

Junio C Hamano· Jan 18, 2009, 00:43 UTC · re: Johannes Schindelin · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> That is correct.  But this is such a highly uncritical code path that I'd 
> like to keep this simple rather than fast.

I actually not worried about "fast" at all; it was more about unbounded memory consumption.

Johannes Schindelin· Jan 18, 2009, 01:12 UTC · re: Junio C Hamano · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Hi,
On Sat, 17 Jan 2009, Junio C Hamano wrote:
Show 7 quoted lines
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > That is correct.  But this is such a highly uncritical code path that 
> > I'd like to keep this simple rather than fast.
> 
> I actually not worried about "fast" at all; it was more about unbounded 
> memory consumption.

Let's just assume that I make a branch switch per minute, continuously, for 90 days (until the reflogs are expired), and let's say that all my branchnames have 20 characters. Conservatively, the memory requirement would be 100 * 2000 * 30 = 3 megabyte.

Note: these are the memory requirements after some really unrealistically 
high activity, and the memory is free()d during parameter parsing.

A much more realistical expectation would be to switch branches maybe 20 times a day, which would amount to something like 36 kilobyte. And again, they are free()d before the action really starts.

Ciao, Dscho

Junio C Hamano· Jan 18, 2009, 07:25 UTC · re: Johannes Schindelin · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
Show 6 quoted lines
> Note: these are the memory requirements after some really unrealistically 
> high activity, and the memory is free()d during parameter parsing.
>
> A much more realistical expectation would be to switch branches maybe 20 
> times a day, which would amount to something like 36 kilobyte.  And again, 
> they are free()d before the action really starts.

My HEAD reflog is 7MB long with 39000 entries, and among them, 13100 entries have "checkout: moving ".

I know I will never want to switch back to the 10000th from the last branch. I am quite sure that I would forget which branch I was on after switching branches three or four times (hence my original hardcoded limitation of 10 which "should be plenty"). When I know I only have to keep track of 10 entries, having to keep track of 13100 entries, even if it is 36kB (it would actually be 260kB in my case) feels there is something wrong in the design.

Johannes Schindelin· Jan 18, 2009, 20:59 UTC · re: Junio C Hamano · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Hi,
On Sat, 17 Jan 2009, Junio C Hamano wrote:
Show 19 quoted lines
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > Note: these are the memory requirements after some really unrealistically 
> > high activity, and the memory is free()d during parameter parsing.
> >
> > A much more realistical expectation would be to switch branches maybe 20 
> > times a day, which would amount to something like 36 kilobyte.  And again, 
> > they are free()d before the action really starts.
> 
> My HEAD reflog is 7MB long with 39000 entries, and among them, 13100
> entries have "checkout: moving ".
> 
> I know I will never want to switch back to the 10000th from the last 
> branch.  I am quite sure that I would forget which branch I was on after 
> switching branches three or four times (hence my original hardcoded 
> limitation of 10 which "should be plenty").  When I know I only have to 
> keep track of 10 entries, having to keep track of 13100 entries, even if 
> it is 36kB (it would actually be 260kB in my case) feels there is 
> something wrong in the design.
Hrm.  So let's leave it as a two-pass thing?

Ciao, Dscho

Junio C Hamano· Jan 19, 2009, 08:08 UTC · re: Johannes Schindelin · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
Show 14 quoted lines
> On Sat, 17 Jan 2009, Junio C Hamano wrote:
> ...
>> My HEAD reflog is 7MB long with 39000 entries, and among them, 13100
>> entries have "checkout: moving ".
>> 
>> I know I will never want to switch back to the 10000th from the last 
>> branch.  I am quite sure that I would forget which branch I was on after 
>> switching branches three or four times (hence my original hardcoded 
>> limitation of 10 which "should be plenty").  When I know I only have to 
>> keep track of 10 entries, having to keep track of 13100 entries, even if 
>> it is 36kB (it would actually be 260kB in my case) feels there is 
>> something wrong in the design.
>
> Hrm.  So let's leave it as a two-pass thing?
Well, I would rather be in favor of something like this.
-- >8 --
Subject: interpret_nth_last_branch(): avoid traversing the reflog twice

You can have quite a many reflog entries, but you typically won't recall which branch you were on after switching branches for more than several times.

Instead of reading the reflog twice, this reads the branch switching event and keeps the latest 16 (which is an arbitrary limitation that should be plenty) such entry, to switch back to the branch we were recently on.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 sha1_name.c              |   48 +++++++++++++++++++++------------------------
 t/t2012-checkout-last.sh |   44 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 66 insertions(+), 26 deletions(-)
Show changes to 2 files +66 −26

sha1_name.c, t/t2012-checkout-last.sh

diff --git a/sha1_name.c b/sha1_name.c
index 9e1538e..d6622f2 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -684,10 +684,11 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
 	return retval;
 }
 
+#define MAX_PREVIOUS_BRANCH 16
+
 struct grab_nth_branch_switch_cbdata {
-	int counting;
-	int nth;
-	struct strbuf *buf;
+	long cnt;
+	struct strbuf buf[MAX_PREVIOUS_BRANCH];
 };
 
 static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
@@ -697,6 +698,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 	struct grab_nth_branch_switch_cbdata *cb = cb_data;
 	const char *match = NULL, *target = NULL;
 	size_t len;
+	int nth;
 
 	if (!prefixcmp(message, "checkout: moving from ")) {
 		match = message + strlen("checkout: moving from ");
@@ -711,16 +713,9 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 	if (target[len] == '\n' && !strncmp(match, target, len))
 		return 0;
 
-	if (cb->counting) {
-		cb->nth++;
-		return 0;
-	}
-
-	if (cb->nth-- <= 0) {
-		strbuf_reset(cb->buf);
-		strbuf_add(cb->buf, match, len);
-		return 1;
-	}
+	nth = cb->cnt++ % MAX_PREVIOUS_BRANCH;
+	strbuf_reset(&cb->buf[nth]);
+	strbuf_add(&cb->buf[nth], match, len);
 	return 0;
 }
 
@@ -737,7 +732,8 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
  */
 int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 {
-	int nth;
+	long nth;
+	int i;
 	struct grab_nth_branch_switch_cbdata cb;
 	const char *brace;
 	char *num_end;
@@ -750,19 +746,19 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 	nth = strtol(name+3, &num_end, 10);
 	if (num_end != brace)
 		return -1;
-
-	cb.counting = 1;
-	cb.nth = 0;
-	cb.buf = buf;
-	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
-
-	if (cb.nth < nth)
-		return 0;
-
-	cb.counting = 0;
-	cb.nth -= nth;
-	cb.buf = buf;
+	if (nth <= 0 || MAX_PREVIOUS_BRANCH < nth)
+		return -1;
+	for (i = 0; i < MAX_PREVIOUS_BRANCH; i++)
+		strbuf_init(&cb.buf[i], 20);
+	cb.cnt = 0;
 	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+	if (cb.cnt < nth)
+		return -1;
+	i = (cb.cnt + MAX_PREVIOUS_BRANCH - nth) % MAX_PREVIOUS_BRANCH;
+	strbuf_reset(buf);
+	strbuf_add(buf, cb.buf[i].buf, cb.buf[i].len);
+	for (i = 0; i < MAX_PREVIOUS_BRANCH; i++)
+		strbuf_release(&cb.buf[i]);
 
 	return brace-name+1;
 }
diff --git a/t/t2012-checkout-last.sh b/t/t2012-checkout-last.sh
index 320f6eb..87b30a2 100755
--- a/t/t2012-checkout-last.sh
+++ b/t/t2012-checkout-last.sh
@@ -47,4 +47,48 @@ test_expect_success '"checkout -" detaches again' '
 	test_must_fail git symbolic-ref HEAD
 '
 
+test_expect_success 'more switches' '
+	for i in 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
+	do
+		git checkout -b branch$i
+	done
+'
+
+more_switches () {
+	for i in 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
+	do
+		git checkout branch$i
+	done
+}
+
+test_expect_success 'switch to the last' '
+	more_switches &&
+	git checkout @{-1} &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/branch2"
+'
+
+test_expect_success 'switch to second from the last' '
+	more_switches &&
+	git checkout @{-2} &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/branch3"
+'
+
+test_expect_success 'switch to third from the last' '
+	more_switches &&
+	git checkout @{-3} &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/branch4"
+'
+
+test_expect_success 'switch to fourth from the last' '
+	more_switches &&
+	git checkout @{-4} &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/branch5"
+'
+
+test_expect_success 'switch to twelfth from the last' '
+	more_switches &&
+	git checkout @{-12} &&
+	test "z$(git symbolic-ref HEAD)" = "zrefs/heads/branch13"
+'
+
 test_done
-- 
1.6.1.245.gdd9f9
Junio C Hamano· Jan 19, 2009, 08:19 UTC · re: Junio C Hamano · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Junio C Hamano <gitster@pobox.com> writes:
Show 33 quoted lines
> Well, I would rather be in favor of something like this.
>
> -- >8 --
> Subject: interpret_nth_last_branch(): avoid traversing the reflog twice
>
> You can have quite a many reflog entries, but you typically won't recall
> which branch you were on after switching branches for more than several
> times.
>
> Instead of reading the reflog twice, this reads the branch switching event
> and keeps the latest 16 (which is an arbitrary limitation that should be
> plenty) such entry, to switch back to the branch we were recently on.
>
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>  sha1_name.c              |   48 +++++++++++++++++++++------------------------
>  t/t2012-checkout-last.sh |   44 ++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 66 insertions(+), 26 deletions(-)
>
> diff --git a/sha1_name.c b/sha1_name.c
> index 9e1538e..d6622f2 100644
> --- a/sha1_name.c
> +++ b/sha1_name.c
> @@ -750,19 +746,19 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
>  	nth = strtol(name+3, &num_end, 10);
>  	if (num_end != brace)
>  		return -1;
> ...
> -	if (cb.nth < nth)
> -		return 0;
> ...
> +	if (cb.cnt < nth)
> +		return -1;
This should (obviously) be "return 0".
Johannes Schindelin· Jan 19, 2009, 12:33 UTC · re: Junio C Hamano · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Hi,
On Mon, 19 Jan 2009, Junio C Hamano wrote:
Show 37 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
> 
> > Well, I would rather be in favor of something like this.
> >
> > -- >8 --
> > Subject: interpret_nth_last_branch(): avoid traversing the reflog twice
> >
> > You can have quite a many reflog entries, but you typically won't recall
> > which branch you were on after switching branches for more than several
> > times.
> >
> > Instead of reading the reflog twice, this reads the branch switching event
> > and keeps the latest 16 (which is an arbitrary limitation that should be
> > plenty) such entry, to switch back to the branch we were recently on.
> >
> > Signed-off-by: Junio C Hamano <gitster@pobox.com>
> > ---
> >  sha1_name.c              |   48 +++++++++++++++++++++------------------------
> >  t/t2012-checkout-last.sh |   44 ++++++++++++++++++++++++++++++++++++++++++
> >  2 files changed, 66 insertions(+), 26 deletions(-)
> >
> > diff --git a/sha1_name.c b/sha1_name.c
> > index 9e1538e..d6622f2 100644
> > --- a/sha1_name.c
> > +++ b/sha1_name.c
> > @@ -750,19 +746,19 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
> >  	nth = strtol(name+3, &num_end, 10);
> >  	if (num_end != brace)
> >  		return -1;
> > ...
> > -	if (cb.nth < nth)
> > -		return 0;
> > ...
> > +	if (cb.cnt < nth)
> > +		return -1;
> 
> This should (obviously) be "return 0".

This, together with a removal of the hard-coded limit of 16 could be squashed with this patch:

-- snipsnap --
Show changes to sha1_name.c +11 −10
diff --git a/sha1_name.c b/sha1_name.c
index 2c5461e..9e5f444 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -691,11 +691,9 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
 	return retval;
 }
 
-#define MAX_PREVIOUS_BRANCH 16
-
 struct grab_nth_branch_switch_cbdata {
-	long cnt;
-	struct strbuf buf[MAX_PREVIOUS_BRANCH];
+	long cnt, alloc;
+	struct strbuf *buf;
 };
 
 static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
@@ -720,7 +718,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 	if (target[len] == '\n' && !strncmp(match, target, len))
 		return 0;
 
-	nth = cb->cnt++ % MAX_PREVIOUS_BRANCH;
+	nth = cb->cnt++ % cb->alloc;
 	strbuf_reset(&cb->buf[nth]);
 	strbuf_add(&cb->buf[nth], match, len);
 	return 0;
@@ -753,19 +751,22 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 	nth = strtol(name+3, &num_end, 10);
 	if (num_end != brace)
 		return -1;
-	if (nth <= 0 || MAX_PREVIOUS_BRANCH < nth)
+	if (nth <= 0)
 		return -1;
-	for (i = 0; i < MAX_PREVIOUS_BRANCH; i++)
+	cb.alloc = nth;
+	cb.buf = xmalloc(nth * sizeof(struct strbuf));
+	for (i = 0; i < nth; i++)
 		strbuf_init(&cb.buf[i], 20);
 	cb.cnt = 0;
 	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
 	if (cb.cnt < nth)
-		return -1;
-	i = (cb.cnt + MAX_PREVIOUS_BRANCH - nth) % MAX_PREVIOUS_BRANCH;
+		return 0;
+	i = cb.cnt % nth;
 	strbuf_reset(buf);
 	strbuf_add(buf, cb.buf[i].buf, cb.buf[i].len);
-	for (i = 0; i < MAX_PREVIOUS_BRANCH; i++)
+	for (i = 0; i < nth; i++)
 		strbuf_release(&cb.buf[i]);
+	free(cb.buf);
 
 	return brace-name+1;
 }
-- 
1.6.1.347.g7b62749
Thomas Rast· Jan 20, 2009, 00:11 UTC · re: Johannes Schindelin · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Johannes Schindelin wrote:
Show 7 quoted lines
> > Junio C Hamano <gitster@pobox.com> writes:
> > > You can have quite a many reflog entries, but you typically won't recall
> > > which branch you were on after switching branches for more than several
> > > times.
> 
> This, together with a removal of the hard-coded limit of 16 could be 
> squashed with this patch:

You know, I'm quite puzzled as to why we had working code that could read the reflog backwards earlier in this thread, but it got shot down solely based on impact and line counts, and you now have to jump through hoops to work around the lack of this exact functionality.

So how about I resurrect the part about for_each_reflog_ent() and _backward(), without touching read_ref_at(). This would actually avoid the worst (hard to check) part of the patch since the refactoring of for_each_reflog_ent()'s error checking is quite trivial and IMHO actually results in more readable code.

I'm just asking because I'm not particularly inclined to do it first and get rejected _again_.

-- 
Thomas Rast
trast@{inf,student}.ethz.ch
Johannes Schindelin· Jan 20, 2009, 00:23 UTC · re: Thomas Rast · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Hi,
On Tue, 20 Jan 2009, Thomas Rast wrote:
Show 13 quoted lines
> Johannes Schindelin wrote:
> > > Junio C Hamano <gitster@pobox.com> writes:
> > > > You can have quite a many reflog entries, but you typically won't recall
> > > > which branch you were on after switching branches for more than several
> > > > times.
> > 
> > This, together with a removal of the hard-coded limit of 16 could be 
> > squashed with this patch:
> 
> You know, I'm quite puzzled as to why we had working code that could
> read the reflog backwards earlier in this thread, but it got shot down
> solely based on impact and line counts, and you now have to jump
> through hoops to work around the lack of this exact functionality.

Okay, I should have told you what my two main concerns with the patch were.

1) it introduces a lot of code, with a lot of possibility for bugs to hide 
   (and I found it not simple enough to slap my head and say "of course, 
   this is obvious" as I did with Junio's code (except the modulo thing 
   which I had to thing about for half a minute)).
2) on Windows, mmap() is really implemented as xmalloc() && fread().  So 
   all the shortcomings of what Junio said about my array approach would 
   hold true for your approach, too.

Ciao, Dscho

Thomas Rast· Jan 20, 2009, 00:41 UTC · re: Johannes Schindelin · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Johannes Schindelin wrote:
> 1) it introduces a lot of code, with a lot of possibility for bugs to hide 

And I can understand in the case of read_ref_at() since there the translation is really nontrivial (though I'd readily put the blame on the optimized-to-death original code ;-). But in the case of for_each_reflog_ent(), it should be really straightforward.

> 2) on Windows, mmap() is really implemented as xmalloc() && fread().  So 
>    all the shortcomings of what Junio said about my array approach would 
>    hold true for your approach, too.

But the @{} syntax _already_ uses mmap via read_ref_at(). And both uses of for_each_reflog_ent() I'm aware of, the existing git log -g and the present @{-N} syntax, have to read to the end anyway because they're mostly interested in the newer stuff.

So while the mmap() might occasionally grab a few more MB of RAM than would actually be required with simple line-based input, Windows has to read the whole reflog no matter what.

(Well, unless we make a for_each_reflog_ent_backward() that can jump in somewhere near the end and start parsing lines backwards.)

-- 
Thomas Rast
trast@{inf,student}.ethz.ch
Junio C Hamano· Jan 20, 2009, 06:21 UTC · re: Johannes Schindelin · lore

[PATCH] interpret_nth_last_branch(): plug small memleak

The error return path leaked both cb.buf[] strbuf array itself, and the strings contained in its elements.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 sha1_name.c |   10 +++++++---
 1 files changed, 7 insertions(+), 3 deletions(-)
Show changes to sha1_name.c +7 −3
diff --git a/sha1_name.c b/sha1_name.c
index f54b6cb..4c0370b 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -754,7 +754,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 {
 	long nth;
-	int i;
+	int i, retval;
 	struct grab_nth_branch_switch_cbdata cb;
 	const char *brace;
 	char *num_end;
@@ -774,17 +774,21 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 	for (i = 0; i < nth; i++)
 		strbuf_init(&cb.buf[i], 20);
 	cb.cnt = 0;
+	retval = 0;
 	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
 	if (cb.cnt < nth)
-		return 0;
+		goto release_return;
 	i = cb.cnt % nth;
 	strbuf_reset(buf);
 	strbuf_add(buf, cb.buf[i].buf, cb.buf[i].len);
+	retval = brace-name+1;
+
+release_return:
 	for (i = 0; i < nth; i++)
 		strbuf_release(&cb.buf[i]);
 	free(cb.buf);
 
-	return brace-name+1;
+	return retval;
 }
 
 /*
-- 
1.6.1.267.g11c6e
Johannes Schindelin· Jan 20, 2009, 10:15 UTC · re: Junio C Hamano · lore

Re: [PATCH] interpret_nth_last_branch(): plug small memleak

Hi,
On Mon, 19 Jan 2009, Junio C Hamano wrote:
> The error return path leaked both cb.buf[] strbuf array itself, and the 
> strings contained in its elements.

Oops. Thanks, Dscho

Junio C Hamano· Jan 20, 2009, 06:22 UTC · re: Johannes Schindelin · lore

[PATCH] Introduce for_each_recent_reflog_ent().

This can be used to scan only the last few kilobytes of a reflog, as a cheap optimization when the data you are looking for is likely to be found near the end of it. The caller is expected to fall back to the full scan if that is not the case.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 refs.c      |   17 ++++++++++++++++-
 refs.h      |    1 +
 sha1_name.c |    8 +++++++-
 3 files changed, 24 insertions(+), 2 deletions(-)
Show changes to 3 files +24 −2

refs.c, refs.h, sha1_name.c

diff --git a/refs.c b/refs.c
index 33ced65..024211d 100644
--- a/refs.c
+++ b/refs.c
@@ -1453,7 +1453,7 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
 	return 1;
 }
 
-int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
+int for_each_recent_reflog_ent(const char *ref, each_reflog_ent_fn fn, long ofs, void *cb_data)
 {
 	const char *logfile;
 	FILE *logfp;
@@ -1464,6 +1464,16 @@ int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
 	logfp = fopen(logfile, "r");
 	if (!logfp)
 		return -1;
+
+	if (ofs) {
+		struct stat statbuf;
+		if (fstat(fileno(logfp), &statbuf) ||
+		    statbuf.st_size < ofs ||
+		    fseek(logfp, -ofs, SEEK_END) ||
+		    fgets(buf, sizeof(buf), logfp))
+			return -1;
+	}
+
 	while (fgets(buf, sizeof(buf), logfp)) {
 		unsigned char osha1[20], nsha1[20];
 		char *email_end, *message;
@@ -1497,6 +1507,11 @@ int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
 	return ret;
 }
 
+int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
+{
+	return for_each_recent_reflog_ent(ref, fn, 0, cb_data);
+}
+
 static int do_for_each_reflog(const char *base, each_ref_fn fn, void *cb_data)
 {
 	DIR *dir = opendir(git_path("logs/%s", base));
diff --git a/refs.h b/refs.h
index 06ad260..3bb529d 100644
--- a/refs.h
+++ b/refs.h
@@ -60,6 +60,7 @@ extern int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned
 /* iterate over reflog entries */
 typedef int each_reflog_ent_fn(unsigned char *osha1, unsigned char *nsha1, const char *, unsigned long, int, const char *, void *);
 int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data);
+int for_each_recent_reflog_ent(const char *ref, each_reflog_ent_fn fn, long, void *cb_data);
 
 /*
  * Calls the specified function for each reflog file until it returns nonzero,
diff --git a/sha1_name.c b/sha1_name.c
index 4c0370b..38c9f1b 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -775,7 +775,13 @@ int interpret_nth_last_branch(const char *name, struct strbuf *buf)
 		strbuf_init(&cb.buf[i], 20);
 	cb.cnt = 0;
 	retval = 0;
-	for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+	for_each_recent_reflog_ent("HEAD", grab_nth_branch_switch, 40960, &cb);
+	if (cb.cnt < nth) {
+		cb.cnt = 0;
+		for (i = 0; i < nth; i++)
+			strbuf_release(&cb.buf[i]);
+		for_each_reflog_ent("HEAD", grab_nth_branch_switch, &cb);
+	}
 	if (cb.cnt < nth)
 		goto release_return;
 	i = cb.cnt % nth;
-- 
1.6.1.267.g11c6e
Johannes Schindelin· Jan 20, 2009, 10:15 UTC · re: Junio C Hamano · lore

Re: [PATCH] Introduce for_each_recent_reflog_ent().

Hi,
On Mon, 19 Jan 2009, Junio C Hamano wrote:
> This can be used to scan only the last few kilobytes of a reflog, as a 
> cheap optimization when the data you are looking for is likely to be 
> found near the end of it.  The caller is expected to fall back to the 
> full scan if that is not the case.
FWIW I really like it, as it works around mmap() nicely.

Ciao, Dscho

Junio C Hamano· Jan 20, 2009, 08:35 UTC · re: Johannes Schindelin · lore

Re: [PATCH] interpret_nth_last_branch(): avoid traversing the reflogs twice

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> This, together with a removal of the hard-coded limit of 16 could be 
> squashed with this patch:

Yeah, I think that makes very much sense, not because 16 is too small, but because it does not make sense to keep track of all 16 when you asked for the last (or the second from the last) event.

Thanks for a free sanity ;-)
Johannes Schindelin· Jan 21, 2009, 00:16 UTC · re: Johannes Schindelin · lore

[VALGRIND PATCH for nth_last patch series] Fix invalid memory access

Please squash in. ---

	On Mon, 19 Jan 2009, Johannes Schindelin wrote:
	> @@ -720,7 +718,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
	>  	if (target[len] == '\n' && !strncmp(match, target, len))
	>  		return 0;
	This code is still not valid, as target[len] can be well after the 
	NUL marker.
	Found by valgrind.
 sha1_name.c |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
Show changes to sha1_name.c +1 −1
diff --git a/sha1_name.c b/sha1_name.c
index 4d10705..803f9d2 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -735,7 +735,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 	if ((target = strstr(match, " to ")) != NULL) {
 		len = target - match;
 		target += 4;
-		if (target[len] == '\n' && !strncmp(match, target, len))
+		if (len == strlen(target) && !strncmp(match, target, len))
 			return 0;
 	}
 	else
-- 
1.6.1.243.g6c8bb35
Junio C Hamano· Jan 21, 2009, 08:45 UTC · re: Johannes Schindelin · lore

Re: [VALGRIND PATCH for nth_last patch series] Fix invalid memory access

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
Show 16 quoted lines
>  sha1_name.c |    2 +-
>  1 files changed, 1 insertions(+), 1 deletions(-)
>
> diff --git a/sha1_name.c b/sha1_name.c
> index 4d10705..803f9d2 100644
> --- a/sha1_name.c
> +++ b/sha1_name.c
> @@ -735,7 +735,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
>  	if ((target = strstr(match, " to ")) != NULL) {
>  		len = target - match;
>  		target += 4;
> -		if (target[len] == '\n' && !strncmp(match, target, len))
> +		if (len == strlen(target) && !strncmp(match, target, len))
>  			return 0;
>  	}
>  	else

Actually, I think this patch to a884d0c (sha1_name: tweak @{-N} lookup, 2009-01-17) would make more sense.

-- >8 --
Subject: [PATCH] Simplify parsing branch switching events in reflog

We only accept "checkout: moving from A to B" newer style reflog entries, in order to pick up A. There is no point computing where B begins at after running strstr to locate " to ", nor adding 4 and then subtracting 4 from the same pointer.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 sha1_name.c |    8 ++------
 1 files changed, 2 insertions(+), 6 deletions(-)
Show changes to sha1_name.c +2 −6
diff --git a/sha1_name.c b/sha1_name.c
index 38c9f1b..7d95bbb 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -723,17 +723,13 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 
 	if (!prefixcmp(message, "checkout: moving from ")) {
 		match = message + strlen("checkout: moving from ");
-		if ((target = strstr(match, " to ")) != NULL)
-			target += 4;
+		target = strstr(match, " to ");
 	}
 
 	if (!match || !target)
 		return 0;
 
-	len = target - match - 4;
-	if (target[len] == '\n' && !strncmp(match, target, len))
-		return 0;
-
+	len = target - match;
 	nth = cb->cnt++ % cb->alloc;
 	strbuf_reset(&cb->buf[nth]);
 	strbuf_add(&cb->buf[nth], match, len);
-- 
1.6.1.281.g16db
Thomas Rast· Jan 21, 2009, 09:18 UTC · re: Junio C Hamano · lore

Re: [VALGRIND PATCH for nth_last patch series] Fix invalid memory access

Junio C Hamano wrote:
> We only accept "checkout: moving from A to B" newer style reflog entries,
> in order to pick up A.  There is no point computing where B begins at
> after running strstr to locate " to ", nor adding 4 and then subtracting 4
> from the same pointer.
[...]
Show 5 quoted lines
> -	len = target - match - 4;
> -	if (target[len] == '\n' && !strncmp(match, target, len))
> -		return 0;
> -
> +	len = target - match;

Actually the point of that exercise was to ignore branch (non)switches of the form

  checkout: moving from A to A

I originally thought that this would be desirable behaviour, but now that it causes so much trouble, I'm not that sure any more. I still think it would be more intuitive to not count them as switches (after all git-checkout says 'Already on "$branch"'), but OTOH 'cd .; cd -' also stays in the same directory.

Thanks for all the work you (both) are doing on this. I hope to find the time to read the current state of the series tonight.

-- 
Thomas Rast
trast@{inf,student}.ethz.ch
Junio C Hamano· Jan 21, 2009, 10:13 UTC · re: Thomas Rast · lore

Re: [VALGRIND PATCH for nth_last patch series] Fix invalid memory access

Thomas Rast <trast@student.ethz.ch> writes:
> Actually the point of that exercise was to ignore branch (non)switches
> of the form
>
>   checkout: moving from A to A
Ahhh, Ok, that is what I missed.
Show 5 quoted lines
> I originally thought that this would be desirable behaviour, but now
> that it causes so much trouble, I'm not that sure any more.  I still
> think it would be more intuitive to not count them as switches (after
> all git-checkout says 'Already on "$branch"'), but OTOH 'cd .; cd -'
> also stays in the same directory.

An entry of the form "from A to A" is made only when you explicitly ask to checkout the current branch by name (i.e. "git checkout" without any parameter won't add such an entry to the reflog), so I tend to agree with "cd" that the users may find it more natural if we counted them.

Having said all that, I think Dscho's one had an off-by-one (but it is getting late and it may be I who has one).

When parsing "checkout: moving from master to side\n", match points at "master to...", target points at "side\n", and len is 6 (length of "master"). We want to see if target is "master\n" and ignore such an entry, so we should be checking if target is one longer than len.

 sha1_name.c |    5 ++++-
 1 files changed, 4 insertions(+), 1 deletions(-)
Show changes to diff +4 −1
diff --git c/sha1_name.c w/sha1_name.c
index 38c9f1b..9aed8ae 100644
--- c/sha1_name.c
+++ w/sha1_name.c
@@ -731,7 +731,10 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 		return 0;
 
 	len = target - match - 4;
-	if (target[len] == '\n' && !strncmp(match, target, len))
+	if (len + 1 == strlen(target) &&
+	    target[len] == '\n' &&
+	    !memcmp(target, match, len))
+		/* switching same branch "from A to A\n" */
 		return 0;
 
 	nth = cb->cnt++ % cb->alloc;
Johannes Schindelin· Jan 21, 2009, 12:06 UTC · re: Junio C Hamano · lore

Re: [VALGRIND PATCH for nth_last patch series] Fix invalid memory access

Hi,
On Wed, 21 Jan 2009, Junio C Hamano wrote:
> Having said all that, I think Dscho's one had an off-by-one (but it is
> getting late and it may be I who has one).
Yep, I missed the "\n" at the end.

Ciao, Dscho

Thomas Rast· Jan 24, 2009, 22:21 UTC · re: Thomas Rast · lore

Re: [VALGRIND PATCH for nth_last patch series] Fix invalid memory access

Thomas Rast wrote:
> Thanks for all the work you (both) are doing on this.  I hope to find
> the time to read the current state of the series tonight.
I was off by half a week :-(

The debugging 'git log --graph ...' that I forgot about is still in, sorry. I'll follow up with a small patch. Other than that, I didn't notice anything, though admittedly it wouldn't have been very hard to sneak something past me.

By the way, you (Junio) remarked earlier in the thread that we could forbid any use of 'git checkout -' except in literally that command (i.e., no options or paths). I'm midly opposed to it because I can see myself saying 'git checkout -b sidebranch -' (and @{} is somewhat awkward to type on Swiss keyboards). Admittedly 'git checkout - -- file' looks rather confusing.

-- 
Thomas Rast
trast@{inf,student}.ethz.ch
Thomas Rast· Jan 24, 2009, 22:23 UTC · re: Thomas Rast · lore

[PATCH next] t1505: remove debugging cruft

Remove a call to git-log that I introduced for debugging and that accidentally made it into d18ba22 (sha1_name: support @{-N} syntax in get_sha1(), 2009-01-17).

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
---
 t/t1505-rev-parse-last.sh |    2 --
 1 files changed, 0 insertions(+), 2 deletions(-)
Show changes to t/t1505-rev-parse-last.sh +0 −2
diff --git a/t/t1505-rev-parse-last.sh b/t/t1505-rev-parse-last.sh
index c745ec4..d709ecf 100755
--- a/t/t1505-rev-parse-last.sh
+++ b/t/t1505-rev-parse-last.sh
@@ -32,8 +32,6 @@ test_expect_success 'setup' '
 #
 # and 'side' should be the last branch
 
-git log --graph --all --pretty=oneline --decorate
-
 test_rev_equivalent () {
 
 	git rev-parse "$1" > expect &&
-- 
1.6.1.468.g15c0
Johannes Schindelin· Jan 21, 2009, 11:56 UTC · re: Junio C Hamano · lore

Re: [VALGRIND PATCH for nth_last patch series] Fix invalid memory access

Hi,
On Wed, 21 Jan 2009, Junio C Hamano wrote:
Show 29 quoted lines
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> >  sha1_name.c |    2 +-
> >  1 files changed, 1 insertions(+), 1 deletions(-)
> >
> > diff --git a/sha1_name.c b/sha1_name.c
> > index 4d10705..803f9d2 100644
> > --- a/sha1_name.c
> > +++ b/sha1_name.c
> > @@ -735,7 +735,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
> >  	if ((target = strstr(match, " to ")) != NULL) {
> >  		len = target - match;
> >  		target += 4;
> > -		if (target[len] == '\n' && !strncmp(match, target, len))
> > +		if (len == strlen(target) && !strncmp(match, target, len))
> >  			return 0;
> >  	}
> >  	else
> 
> Actually, I think this patch to a884d0c (sha1_name: tweak @{-N} lookup,
> 2009-01-17) would make more sense.
> 
> -- >8 --
> Subject: [PATCH] Simplify parsing branch switching events in reflog
> 
> We only accept "checkout: moving from A to B" newer style reflog entries,
> in order to pick up A.  There is no point computing where B begins at
> after running strstr to locate " to ", nor adding 4 and then subtracting 4
> from the same pointer.
Yeah, you're right.

Ciao, Dscho

Johannes Schindelin· Jan 19, 2009, 12:41 UTC · re: Junio C Hamano · lore

[PATCH] @{-<n>}: avoid crash with corrupt reflog

The earlier code checked if a " to " was found after "checkout: Moving from ". However, it then went on to access the pointer to " to ", regardless if it was still NULL (if no " to " was found) or not.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
	You might want to squash this into "sha1_name: tweak @{-N} 
	lookup", just as a safety belt.
 sha1_name.c |   20 ++++++++++----------
 1 files changed, 10 insertions(+), 10 deletions(-)
Show changes to sha1_name.c +10 −10
diff --git a/sha1_name.c b/sha1_name.c
index 9e5f444..853bac6 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -705,18 +705,18 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 	size_t len;
 	int nth;
 
-	if (!prefixcmp(message, "checkout: moving from ")) {
-		match = message + strlen("checkout: moving from ");
-		if ((target = strstr(match, " to ")) != NULL)
-			target += 4;
-	}
-
-	if (!match)
+	if (prefixcmp(message, "checkout: moving from "))
 		return 0;
 
-	len = target - match - 4;
-	if (target[len] == '\n' && !strncmp(match, target, len))
-		return 0;
+	match = message + strlen("checkout: moving from ");
+	if ((target = strstr(match, " to ")) != NULL) {
+		len = target - match - 4;
+		target += 4;
+		if (target[len] == '\n' && !strncmp(match, target, len))
+			return 0;
+	}
+	else
+		len = strchrnul(match, ' ') - match;
 
 	nth = cb->cnt++ % cb->alloc;
 	strbuf_reset(&cb->buf[nth]);
-- 
1.6.1.347.g7b62749
Johannes Schindelin· Jan 19, 2009, 14:57 UTC · re: Johannes Schindelin · lore

Re: [PATCH] @{-<n>}: avoid crash with corrupt reflog

Hi,
On Mon, 19 Jan 2009, Johannes Schindelin wrote:
Show 24 quoted lines
> diff --git a/sha1_name.c b/sha1_name.c
> index 9e5f444..853bac6 100644
> --- a/sha1_name.c
> +++ b/sha1_name.c
> @@ -705,18 +705,18 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
>  	size_t len;
>  	int nth;
>  
> -	if (!prefixcmp(message, "checkout: moving from ")) {
> -		match = message + strlen("checkout: moving from ");
> -		if ((target = strstr(match, " to ")) != NULL)
> -			target += 4;
> -	}
> -
> -	if (!match)
> +	if (prefixcmp(message, "checkout: moving from "))
>  		return 0;
>  
> -	len = target - match - 4;
> -	if (target[len] == '\n' && !strncmp(match, target, len))
> -		return 0;
> +	match = message + strlen("checkout: moving from ");
> +	if ((target = strstr(match, " to ")) != NULL) {
> +		len = target - match - 4;
Aargh, the "- 4" is wrong, of course.
Show 9 quoted lines
> +		target += 4;
> +		if (target[len] == '\n' && !strncmp(match, target, len))
> +			return 0;
> +	}
> +	else
> +		len = strchrnul(match, ' ') - match;
>  
>  	nth = cb->cnt++ % cb->alloc;
>  	strbuf_reset(&cb->buf[nth]);

Sorry for the noise, Dscho

← back to recent threads