Merge branch 'tr/previous-branch'
* tr/previous-branch: t1505: remove debugging cruft Simplify parsing branch switching events in reflog Introduce for_each_recent_reflog_ent(). interpret_nth_last_branch(): plug small memleak Fix reflog parsing for a malformed branch switching entry Fix parsing of @{-1}@{1} interpret_nth_last_branch(): avoid traversing the reflog twice checkout: implement "-" abbreviation, add docs and tests sha1_name: support @{-N} syntax in get_sha1() sha1_name: tweak @{-N} lookup checkout: implement "@{-N}" shortcut name for N-th last branch Conflicts: sha1_name.cmaint
commit
8712b3cdb0
|
@ -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
|
||||
|
|
|
@ -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}'
|
||||
|
|
|
@ -351,8 +351,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);
|
||||
}
|
||||
|
||||
|
@ -661,6 +669,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);
|
||||
|
|
1
cache.h
1
cache.h
|
@ -667,6 +667,7 @@ extern int read_ref(const char *filename, unsigned char *sha1);
|
|||
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[];
|
||||
|
|
17
refs.c
17
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));
|
||||
|
|
1
refs.h
1
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,
|
||||
|
|
127
sha1_name.c
127
sha1_name.c
|
@ -238,8 +238,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;
|
||||
|
||||
|
@ -259,11 +279,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;
|
||||
|
||||
|
@ -294,9 +316,12 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
|
|||
if (!warn_ambiguous_refs)
|
||||
break;
|
||||
}
|
||||
free(last_branch);
|
||||
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,10 +332,10 @@ 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 (len && 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;
|
||||
|
@ -324,6 +349,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 +414,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 +707,92 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1)
|
|||
return retval;
|
||||
}
|
||||
|
||||
struct grab_nth_branch_switch_cbdata {
|
||||
long cnt, alloc;
|
||||
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;
|
||||
int nth;
|
||||
|
||||
if (!prefixcmp(message, "checkout: moving from ")) {
|
||||
match = message + strlen("checkout: moving from ");
|
||||
target = strstr(match, " to ");
|
||||
}
|
||||
|
||||
if (!match || !target)
|
||||
return 0;
|
||||
|
||||
len = target - match;
|
||||
nth = cb->cnt++ % cb->alloc;
|
||||
strbuf_reset(&cb->buf[nth]);
|
||||
strbuf_add(&cb->buf[nth], match, len);
|
||||
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)
|
||||
{
|
||||
long nth;
|
||||
int i, retval;
|
||||
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;
|
||||
if (nth <= 0)
|
||||
return -1;
|
||||
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;
|
||||
retval = 0;
|
||||
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;
|
||||
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 retval;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is like "get_sha1_basic()", except it allows "sha1 expressions",
|
||||
* notably "xyz^" for "parent of xyz"
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
#!/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
|
||||
|
||||
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_success '@{-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
|
||||
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
#!/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_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
|
Loading…
Reference in New Issue