From bda324cf6158672b91a478036731d332c1a14cac Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 3 Jan 2007 01:09:34 -0800 Subject: [PATCH 1/9] git-status: show detached HEAD This makes git-status to state when you are not on any branch. Signed-off-by: Junio C Hamano --- wt-status.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/wt-status.c b/wt-status.c index c48127daca..2a002baabd 100644 --- a/wt-status.c +++ b/wt-status.c @@ -288,9 +288,18 @@ void wt_status_print(struct wt_status *s) unsigned char sha1[20]; s->is_initial = get_sha1(s->reference, sha1) ? 1 : 0; - if (s->branch) + if (s->branch) { + const char *on_what = "On branch "; + const char *branch_name = s->branch; + if (!strncmp(branch_name, "refs/heads/", 11)) + branch_name += 11; + else if (!strcmp(branch_name, "HEAD")) { + branch_name = ""; + on_what = "Not currently on any branch."; + } color_printf_ln(color(WT_STATUS_HEADER), - "# On branch %s", s->branch); + "# %s%s", on_what, branch_name); + } if (s->is_initial) { color_printf_ln(color(WT_STATUS_HEADER), "#"); From 0016a48251abefed11efc919703d980a21c95f2c Mon Sep 17 00:00:00 2001 From: Lars Hjemli Date: Wed, 3 Jan 2007 21:10:10 +0100 Subject: [PATCH 2/9] git-branch: show detached HEAD This makes git-branch show a detached HEAD as '* (no branch)'. Signed-off-by: Lars Hjemli Signed-off-by: Junio C Hamano --- builtin-branch.c | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/builtin-branch.c b/builtin-branch.c index d3df5a57f1..487965b540 100644 --- a/builtin-branch.c +++ b/builtin-branch.c @@ -275,7 +275,7 @@ static void print_ref_item(struct ref_item *item, int maxwidth, int verbose, } } -static void print_ref_list(int kinds, int verbose, int abbrev) +static void print_ref_list(int kinds, int detached, int verbose, int abbrev) { int i; struct ref_list ref_list; @@ -286,8 +286,20 @@ static void print_ref_list(int kinds, int verbose, int abbrev) qsort(ref_list.list, ref_list.index, sizeof(struct ref_item), ref_cmp); + detached = (detached && (kinds & REF_LOCAL_BRANCH)); + if (detached) { + struct ref_item item; + item.name = "(no branch)"; + item.kind = REF_LOCAL_BRANCH; + hashcpy(item.sha1, head_sha1); + if (strlen(item.name) > ref_list.maxwidth) + ref_list.maxwidth = strlen(item.name); + print_ref_item(&item, ref_list.maxwidth, verbose, abbrev, 1); + } + for (i = 0; i < ref_list.index; i++) { - int current = (ref_list.list[i].kind == REF_LOCAL_BRANCH) && + int current = !detached && + (ref_list.list[i].kind == REF_LOCAL_BRANCH) && !strcmp(ref_list.list[i].name, head); print_ref_item(&ref_list.list[i], ref_list.maxwidth, verbose, abbrev, current); @@ -367,7 +379,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) { int delete = 0, force_delete = 0, force_create = 0; int rename = 0, force_rename = 0; - int verbose = 0, abbrev = DEFAULT_ABBREV; + int verbose = 0, abbrev = DEFAULT_ABBREV, detached = 0; int reflog = 0; int kinds = REF_LOCAL_BRANCH; int i; @@ -444,14 +456,19 @@ int cmd_branch(int argc, const char **argv, const char *prefix) head = xstrdup(resolve_ref("HEAD", head_sha1, 0, NULL)); if (!head) die("Failed to resolve HEAD as a valid ref."); - if (strncmp(head, "refs/heads/", 11)) - die("HEAD not found below refs/heads!"); - head += 11; + if (!strcmp(head, "HEAD")) { + detached = 1; + } + else { + if (strncmp(head, "refs/heads/", 11)) + die("HEAD not found below refs/heads!"); + head += 11; + } if (delete) return delete_branches(argc - i, argv + i, force_delete, kinds); else if (i == argc) - print_ref_list(kinds, verbose, abbrev); + print_ref_list(kinds, detached, verbose, abbrev); else if (rename && (i == argc - 1)) rename_branch(head, argv[i], force_rename); else if (rename && (i == argc - 2)) From c847f537125ceab3425205721fdaaa834e6d8a83 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 1 Jan 2007 23:31:08 -0800 Subject: [PATCH 3/9] Detached HEAD (experimental) This allows "git checkout v1.4.3" to dissociate the HEAD of repository from any branch. After this point, "git branch" starts reporting that you are not on any branch. You can go back to an existing branch by saying "git checkout master", for example. This is still experimental. While I think it makes sense to allow commits on top of detached HEAD, it is rather dangerous unless you are careful in the current form. Next "git checkout master" will obviously lose what you have done, so we might want to require "git checkout -f" out of a detached HEAD if we find that the HEAD commit is not an ancestor of any other branches. There is no such safety valve implemented right now. On the other hand, the reason the user did not start the ad-hoc work on a new branch with "git checkout -b" was probably because the work was of a throw-away nature, so the convenience of not having that safety valve might be even better. The user, after accumulating some commits on top of a detached HEAD, can always create a new branch with "git checkout -b" not to lose useful work done while the HEAD was detached. We'll see. Signed-off-by: Junio C Hamano --- builtin-branch.c | 24 +++++++++++++++++------- cache.h | 2 +- git-checkout.sh | 16 +++++++++++----- path.c | 26 ++++++++++++++++++-------- setup.c | 5 +++-- 5 files changed, 50 insertions(+), 23 deletions(-) diff --git a/builtin-branch.c b/builtin-branch.c index 487965b540..c760e188ea 100644 --- a/builtin-branch.c +++ b/builtin-branch.c @@ -308,7 +308,8 @@ static void print_ref_list(int kinds, int detached, int verbose, int abbrev) free_ref_list(&ref_list); } -static void create_branch(const char *name, const char *start, +static void create_branch(const char *name, const char *start_name, + unsigned char *start_sha1, int force, int reflog) { struct ref_lock *lock; @@ -327,9 +328,14 @@ static void create_branch(const char *name, const char *start, die("Cannot force update the current branch."); } - if (get_sha1(start, sha1) || - (commit = lookup_commit_reference(sha1)) == NULL) - die("Not a valid branch point: '%s'.", start); + if (start_sha1) + /* detached HEAD */ + hashcpy(sha1, start_sha1); + else if (get_sha1(start_name, sha1)) + die("Not a valid object name: '%s'.", start_name); + + if ((commit = lookup_commit_reference(sha1)) == NULL) + die("Not a valid branch point: '%s'.", start_name); hashcpy(sha1, commit->object.sha1); lock = lock_any_ref_for_update(ref, NULL); @@ -338,7 +344,8 @@ static void create_branch(const char *name, const char *start, if (reflog) { log_all_ref_updates = 1; - snprintf(msg, sizeof msg, "branch: Created from %s", start); + snprintf(msg, sizeof msg, "branch: Created from %s", + start_name); } if (write_ref_sha1(lock, sha1, msg) < 0) @@ -350,6 +357,9 @@ static void rename_branch(const char *oldname, const char *newname, int force) char oldref[PATH_MAX], newref[PATH_MAX], logmsg[PATH_MAX*2 + 100]; unsigned char sha1[20]; + if (!oldname) + die("cannot rename the curren branch while not on any."); + if (snprintf(oldref, sizeof(oldref), "refs/heads/%s", oldname) > sizeof(oldref)) die("Old branchname too long"); @@ -474,9 +484,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix) else if (rename && (i == argc - 2)) rename_branch(argv[i], argv[i + 1], force_rename); else if (i == argc - 1) - create_branch(argv[i], head, force_create, reflog); + create_branch(argv[i], head, head_sha1, force_create, reflog); else if (i == argc - 2) - create_branch(argv[i], argv[i + 1], force_create, reflog); + create_branch(argv[i], argv[i+1], NULL, force_create, reflog); else usage(builtin_branch_usage); diff --git a/cache.h b/cache.h index 36be64e386..5218548ccf 100644 --- a/cache.h +++ b/cache.h @@ -299,7 +299,7 @@ extern char *sha1_to_hex(const unsigned char *sha1); /* static buffer result! */ 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 create_symref(const char *ref, const char *refs_heads_master); -extern int validate_symref(const char *ref); +extern int validate_headref(const char *ref); extern int base_name_compare(const char *name1, int len1, int mode1, const char *name2, int len2, int mode2); extern int cache_name_compare(const char *name1, int len1, const char *name2, int len2); diff --git a/git-checkout.sh b/git-checkout.sh index 92ec069a3a..8e11ca4bc8 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -144,13 +144,19 @@ fi # are switching to, then we'd better just be checking out # what we already had -[ -z "$branch$newbranch" ] && - [ "$new" != "$old" ] && - die "git checkout: provided reference cannot be checked out directly - - You need -b to associate a new branch with the wanted checkout. Example: +if test -z "$branch$newbranch" && test "$new" != "$old" +then + # NEEDSWORK: we would want to have this command here + # that allows us to detach the HEAD atomically. + # git update-ref --detach HEAD "$new" + rm -f "$GIT_DIR/HEAD" + echo "$new" >"$GIT_DIR/HEAD" + echo >&2 "WARNING: you are not on ANY branch anymore. +If you meant to create a new branch from the commit, you need -b to +associate a new branch with the wanted checkout. Example: git checkout -b $arg " +fi if [ "X$old" = X ] then diff --git a/path.c b/path.c index 066f621955..94ddd7eafc 100644 --- a/path.c +++ b/path.c @@ -90,10 +90,11 @@ int git_mkstemp(char *path, size_t len, const char *template) } -int validate_symref(const char *path) +int validate_headref(const char *path) { struct stat st; char *buf, buffer[256]; + unsigned char sha1[20]; int len, fd; if (lstat(path, &st) < 0) @@ -119,14 +120,23 @@ int validate_symref(const char *path) /* * Is it a symbolic ref? */ - if (len < 4 || memcmp("ref:", buffer, 4)) + if (len < 4) return -1; - buf = buffer + 4; - len -= 4; - while (len && isspace(*buf)) - buf++, len--; - if (len >= 5 && !memcmp("refs/", buf, 5)) + if (!memcmp("ref:", buffer, 4)) { + buf = buffer + 4; + len -= 4; + while (len && isspace(*buf)) + buf++, len--; + if (len >= 5 && !memcmp("refs/", buf, 5)) + return 0; + } + + /* + * Is this a detached HEAD? + */ + if (!get_sha1_hex(buffer, sha1)) return 0; + return -1; } @@ -241,7 +251,7 @@ char *enter_repo(char *path, int strict) return NULL; if (access("objects", X_OK) == 0 && access("refs", X_OK) == 0 && - validate_symref("HEAD") == 0) { + validate_headref("HEAD") == 0) { putenv("GIT_DIR=."); check_repository_format(); return path; diff --git a/setup.c b/setup.c index 2ae57f7c94..cc97f9f5c1 100644 --- a/setup.c +++ b/setup.c @@ -138,7 +138,8 @@ const char **get_pathspec(const char *prefix, const char **pathspec) * GIT_OBJECT_DIRECTORY environment variable * - a refs/ directory * - either a HEAD symlink or a HEAD file that is formatted as - * a proper "ref:". + * a proper "ref:", or a regular file HEAD that has a properly + * formatted sha1 object name. */ static int is_git_directory(const char *suspect) { @@ -161,7 +162,7 @@ static int is_git_directory(const char *suspect) return 0; strcpy(path + len, "/HEAD"); - if (validate_symref(path)) + if (validate_headref(path)) return 0; return 1; From 648861040f2c5fd001a5edcfbb05813505bbb8f1 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 8 Jan 2007 02:19:38 -0800 Subject: [PATCH 4/9] git-checkout: do not warn detaching HEAD when it is already detached. Signed-off-by: Junio C Hamano --- git-checkout.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/git-checkout.sh b/git-checkout.sh index 8e11ca4bc8..5a7388759c 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -6,6 +6,7 @@ SUBDIRECTORY_OK=Sometimes old_name=HEAD old=$(git-rev-parse --verify $old_name 2>/dev/null) +oldbranch=$(git-symbolic-ref $old_name 2>/dev/null) new= new_name= force= @@ -149,13 +150,17 @@ then # NEEDSWORK: we would want to have this command here # that allows us to detach the HEAD atomically. # git update-ref --detach HEAD "$new" - rm -f "$GIT_DIR/HEAD" - echo "$new" >"$GIT_DIR/HEAD" - echo >&2 "WARNING: you are not on ANY branch anymore. + echo "$new" >"$GIT_DIR/HEAD.new" && + mv "$GIT_DIR/HEAD.new" "$GIT_DIR/HEAD" || die "Cannot detach HEAD" + + if test -n "$oldbranch" + then + echo >&2 "WARNING: you are not on ANY branch anymore. If you meant to create a new branch from the commit, you need -b to associate a new branch with the wanted checkout. Example: git checkout -b $arg " + fi fi if [ "X$old" = X ] From 73c838e4c9437a0c41082c32a3a832aa385460a9 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 8 Jan 2007 02:08:47 -0800 Subject: [PATCH 5/9] git-checkout: rewording comments regarding detached HEAD. We used to say "you are not on a branch" before the initial commit. This is incorrect -- the user is on a branch yet to be born, but its name has been already determined. Signed-off-by: Junio C Hamano --- git-checkout.sh | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/git-checkout.sh b/git-checkout.sh index 5a7388759c..3250f64ccc 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -140,22 +140,25 @@ fi [ -z "$new" ] && new=$old && new_name="$old_name" -# If we don't have an old branch that we're switching to, +# If we don't have an existing branch that we're switching to, # and we don't have a new branch name for the target we -# are switching to, then we'd better just be checking out -# what we already had +# are switching to, then we are detaching our HEAD from any +# branch. However, if "git checkout HEAD" detaches the HEAD +# from the current branch, even though that may be logically +# correct, it feels somewhat funny. More importantly, we do not +# want "git checkout" nor "git checkout -f" to detach HEAD. if test -z "$branch$newbranch" && test "$new" != "$old" then - # NEEDSWORK: we would want to have this command here - # that allows us to detach the HEAD atomically. - # git update-ref --detach HEAD "$new" + # NEEDSWORK: we would want to have a command here + # that allows us to detach the HEAD atomically. Perhaps + # something like "git update-ref --detach HEAD $new" echo "$new" >"$GIT_DIR/HEAD.new" && mv "$GIT_DIR/HEAD.new" "$GIT_DIR/HEAD" || die "Cannot detach HEAD" if test -n "$oldbranch" then - echo >&2 "WARNING: you are not on ANY branch anymore. + echo >&2 "warning: you are not on ANY branch anymore. If you meant to create a new branch from the commit, you need -b to associate a new branch with the wanted checkout. Example: git checkout -b $arg @@ -165,8 +168,8 @@ fi if [ "X$old" = X ] then - echo "warning: You do not appear to currently be on a branch." >&2 - echo "warning: Forcing checkout of $new_name." >&2 + echo >&2 "warning: You appear to be on a branch yet to be born." + echo >&2 "warning: Forcing checkout of $new_name." force=1 fi From ead80606d47ed94aad958ee660f62cde00bb6018 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 8 Jan 2007 02:42:30 -0800 Subject: [PATCH 6/9] git-checkout: safety when coming back from the detached HEAD state. After making commits in the detached HEAD state, if you run "git checkout" to switch to an existing branch, you will lose your work. Make sure the switched-to branch is a fast-forward of the current HEAD, or require -f when switching. Signed-off-by: Junio C Hamano --- git-checkout.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/git-checkout.sh b/git-checkout.sh index 3250f64ccc..69d0c1c43a 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -14,6 +14,8 @@ branch= newbranch= newbranch_log= merge= +LF=' +' while [ "$#" != "0" ]; do arg="$1" shift @@ -164,6 +166,22 @@ associate a new branch with the wanted checkout. Example: git checkout -b $arg " fi +elif test -z "$oldbranch" && test -n "$branch" +then + # Coming back... + if test -z "$force" + then + mb=$(git merge-base --all $old $new) && + case "$LF$mb$LF" in + *"$LF$old$LF"*) : ;; + *) false ;; + esac || { + echo >&2 \ +"You are not on a branch and switching to $new_name branch may lose +your changes. Use 'git checkout -f $new_name' if you want to." + exit 1; + } + fi fi if [ "X$old" = X ] From 8d78b5af2376be533ed11b53b292bdf0f2e6173b Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 8 Jan 2007 02:44:55 -0800 Subject: [PATCH 7/9] git-checkout: fix branch name output from the command When switching branches with "git checkout", we internally did $arg^0 (aka $arg^{commit}) suffix but there was no need to. The improvement is easily visible in the change to an existing test t/3200-branch.sh in this commit; it was expecting rather ugly message. Signed-off-by: Junio C Hamano --- git-checkout.sh | 2 +- t/t3200-branch.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/git-checkout.sh b/git-checkout.sh index 69d0c1c43a..a309bf0a1a 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -53,7 +53,7 @@ while [ "$#" != "0" ]; do exit 1 fi new="$rev" - new_name="$arg^0" + new_name="$arg" if git-show-ref --verify --quiet -- "refs/heads/$arg" then branch="$arg" diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index a6ea0f6a19..bb80e4286a 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -48,7 +48,7 @@ test_expect_success \ test ! -f .git/logs/refs/heads/d/e/f' cat >expect < 1117150200 +0000 checkout: Created from master^0 +0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 checkout: Created from master EOF test_expect_success \ 'git checkout -b g/h/i -l should create a branch and a log' \ From 75b364dfe28cf1700ac4f519ff7d3e42505e17a6 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 9 Jan 2007 17:37:50 -0800 Subject: [PATCH 8/9] git-checkout: safety check for detached HEAD checks existing refs Checking for reachability from refs does not help much if the state we are currently on is somewhere in the middle. We will lose where we were. So this makes sureh that HEAD is something directly pointed at by one of the existing refs (most likely a tag for a user who has been "sightseeing"). Signed-off-by: Junio C Hamano --- git-checkout.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/git-checkout.sh b/git-checkout.sh index a309bf0a1a..dcf6ddbdf1 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -171,14 +171,14 @@ then # Coming back... if test -z "$force" then - mb=$(git merge-base --all $old $new) && - case "$LF$mb$LF" in - *"$LF$old$LF"*) : ;; - *) false ;; - esac || { + git show-ref -d -s | grep "$old" >/dev/null || { echo >&2 \ -"You are not on a branch and switching to $new_name branch may lose -your changes. Use 'git checkout -f $new_name' if you want to." +"You are not on any branch and switching to branch '$new_name' +may lose your changes. At this point, you can do one of two things: + (1) Decide it is Ok and say 'git checkout -f $new_name'; + (2) Start a new branch from the current commit, by saying + 'git checkout -b '. +Leaving your HEAD detached; not switching to branch '$new_name'." exit 1; } fi From bfbbb8f8cf8250718ffd028efe179557d29ae72d Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 9 Jan 2007 20:39:09 -0800 Subject: [PATCH 9/9] git-checkout: handle local changes sanely when detaching HEAD When switching branches, we usually first try read-tree to make sure that we do not lose the local changes and then updated the HEAD using update-ref. However, we detached and updated HEAD before these checks, which was quite bad in a repository with local changes. Signed-off-by: Junio C Hamano --- git-checkout.sh | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/git-checkout.sh b/git-checkout.sh index dcf6ddbdf1..8f4356d49a 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -150,21 +150,18 @@ fi # correct, it feels somewhat funny. More importantly, we do not # want "git checkout" nor "git checkout -f" to detach HEAD. +detached= +detach_warn= + if test -z "$branch$newbranch" && test "$new" != "$old" then - # NEEDSWORK: we would want to have a command here - # that allows us to detach the HEAD atomically. Perhaps - # something like "git update-ref --detach HEAD $new" - echo "$new" >"$GIT_DIR/HEAD.new" && - mv "$GIT_DIR/HEAD.new" "$GIT_DIR/HEAD" || die "Cannot detach HEAD" - + detached="$new" if test -n "$oldbranch" then - echo >&2 "warning: you are not on ANY branch anymore. + detach_warn="warning: you are not on ANY branch anymore. If you meant to create a new branch from the commit, you need -b to associate a new branch with the wanted checkout. Example: - git checkout -b $arg -" + git checkout -b $arg" fi elif test -z "$oldbranch" && test -n "$branch" then @@ -258,8 +255,25 @@ if [ "$?" -eq 0 ]; then git-update-ref -m "checkout: Created from $new_name" "refs/heads/$newbranch" $new || exit branch="$newbranch" fi - [ "$branch" ] && - GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD "refs/heads/$branch" + if test -n "$branch" + then + GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD "refs/heads/$branch" + elif test -n "$detached" + then + # NEEDSWORK: we would want a command to detach the HEAD + # atomically, instead of this handcrafted command sequence. + # Perhaps: + # git update-ref --detach HEAD $new + # or something like that... + # + echo "$detached" >"$GIT_DIR/HEAD.new" && + mv "$GIT_DIR/HEAD.new" "$GIT_DIR/HEAD" || + die "Cannot detach HEAD" + if test -n "$detach_warn" + then + echo >&2 "$detach_warn" + fi + fi rm -f "$GIT_DIR/MERGE_HEAD" else exit 1