From 8e2dc6ac06ae90a00965e5c00aa4918dfd447639 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 18 Feb 2011 16:04:47 -0800 Subject: [PATCH] commit: give final warning when reattaching HEAD to leave commits behind You can detach the HEAD at an arbitrary commit in order to browse the files in various points in the history or build older versions of the software, without recording any new commit, and come back to an existing branch. When used in this "sightseer" mode, detached HEAD is a perfectly safe mechanism. It also is a useful state to experiment with throw-away commits. When coming back to an existing branch with "git checkout master", however, the commits that were created on the detached HEAD will become unreachable from anywhere but the reflog of the HEAD. Check if the commit we are about to leave is connected to some ref, and give a final warning otherwise to remind the user for safety. Signed-off-by: Junio C Hamano --- builtin/checkout.c | 101 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 757f9a08d..e44364c55 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -578,6 +578,100 @@ static void update_refs_for_switch(struct checkout_opts *opts, report_tracking(new); } +struct rev_list_args { + int argc; + int alloc; + const char **argv; +}; + +static void add_one_rev_list_arg(struct rev_list_args *args, const char *s) +{ + ALLOC_GROW(args->argv, args->argc + 1, args->alloc); + args->argv[args->argc++] = s; +} + +static int add_one_ref_to_rev_list_arg(const char *refname, + const unsigned char *sha1, + int flags, + void *cb_data) +{ + add_one_rev_list_arg(cb_data, refname); + return 0; +} + + +static void describe_one_orphan(struct strbuf *sb, struct commit *commit) +{ + struct pretty_print_context ctx = { 0 }; + + parse_commit(commit); + strbuf_addstr(sb, " - "); + pretty_print_commit(CMIT_FMT_ONELINE, commit, sb, &ctx); + strbuf_addch(sb, '\n'); +} + +#define ORPHAN_CUTOFF 4 +static void suggest_reattach(struct commit *commit, struct rev_info *revs) +{ + struct commit *c, *last = NULL; + struct strbuf sb = STRBUF_INIT; + int lost = 0; + while ((c = get_revision(revs)) != NULL) { + if (lost < ORPHAN_CUTOFF) + describe_one_orphan(&sb, c); + last = c; + lost++; + } + if (ORPHAN_CUTOFF < lost) { + int more = lost - ORPHAN_CUTOFF; + if (more == 1) + describe_one_orphan(&sb, last); + else + strbuf_addf(&sb, " ... and %d more.\n", more); + } + + fprintf(stderr, + "Warning: you are leaving %d commit%s behind, " + "not connected to\n" + "any of your branches:\n\n" + "%s\n" + "If you want to keep them by creating a new branch, " + "this may be a good time\nto do so with:\n\n" + " git branch new_branch_name %s\n\n", + lost, ((1 < lost) ? "s" : ""), + sb.buf, + sha1_to_hex(commit->object.sha1)); + strbuf_release(&sb); +} + +/* + * We are about to leave commit that was at the tip of a detached + * HEAD. If it is not reachable from any ref, this is the last chance + * for the user to do so without resorting to reflog. + */ +static void orphaned_commit_warning(struct commit *commit) +{ + struct rev_list_args args = { 0, 0, NULL }; + struct rev_info revs; + + add_one_rev_list_arg(&args, "(internal)"); + add_one_rev_list_arg(&args, sha1_to_hex(commit->object.sha1)); + add_one_rev_list_arg(&args, "--not"); + for_each_ref(add_one_ref_to_rev_list_arg, &args); + add_one_rev_list_arg(&args, "--"); + add_one_rev_list_arg(&args, NULL); + + init_revisions(&revs, NULL); + if (setup_revisions(args.argc - 1, args.argv, &revs, NULL) != 1) + die("internal error: only -- alone should have been left"); + if (prepare_revision_walk(&revs)) + die("internal error in revision walk"); + if (!(commit->object.flags & UNINTERESTING)) + suggest_reattach(commit, &revs); + else + describe_detached_head("Previous HEAD position was", commit); +} + static int switch_branches(struct checkout_opts *opts, struct branch_info *new) { int ret = 0; @@ -605,13 +699,8 @@ static int switch_branches(struct checkout_opts *opts, struct branch_info *new) if (ret) return ret; - /* - * If we were on a detached HEAD, but have now moved to - * a new commit, we want to mention the old commit once more - * to remind the user that it might be lost. - */ if (!opts->quiet && !old.path && old.commit && new->commit != old.commit) - describe_detached_head("Previous HEAD position was", old.commit); + orphaned_commit_warning(old.commit); update_refs_for_switch(opts, &old, new); -- 2.30.2