Merge branch 'nh/empty-rebase'
authorJunio C Hamano <gitster@pobox.com>
Mon, 30 Apr 2012 21:58:00 +0000 (14:58 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 30 Apr 2012 21:58:01 +0000 (14:58 -0700)
"git rebase" learned to optionally keep commits that do not introduce
any change in the original history.

By Neil Horman
* nh/empty-rebase:
git-rebase: add keep_empty flag
git-cherry-pick: Add test to validate new options
git-cherry-pick: Add keep-redundant-commits option
git-cherry-pick: add allow-empty option

1  2 
builtin/revert.c
git-rebase--interactive.sh
sequencer.c
diff --combined builtin/revert.c
index 5462e676e2151452adae8fe5b0cb42b1a5edff31,b0b9b1a4e6550806b4ab71f85a628aa87df62ab4..82d1bf844b996d5d121169f244358964fd36fd19
@@@ -86,7 -86,6 +86,7 @@@ static void verify_opt_mutually_compati
                                break;
                }
        }
 +      va_end(ap);
  
        if (opt1 && opt2)
                die(_("%s: %s cannot be used with %s"), me, opt1, opt2);
@@@ -115,12 -114,16 +115,16 @@@ static void parse_args(int argc, const 
                OPT_END(),
                OPT_END(),
                OPT_END(),
+               OPT_END(),
+               OPT_END(),
        };
  
        if (opts->action == REPLAY_PICK) {
                struct option cp_extra[] = {
                        OPT_BOOLEAN('x', NULL, &opts->record_origin, "append commit name"),
                        OPT_BOOLEAN(0, "ff", &opts->allow_ff, "allow fast-forward"),
+                       OPT_BOOLEAN(0, "allow-empty", &opts->allow_empty, "preserve initially empty commits"),
+                       OPT_BOOLEAN(0, "keep-redundant-commits", &opts->keep_redundant_commits, "keep redundant, empty commits"),
                        OPT_END(),
                };
                if (parse_options_concat(options, ARRAY_SIZE(options), cp_extra))
                                "--abort", rollback,
                                NULL);
  
+       /* implies allow_empty */
+       if (opts->keep_redundant_commits)
+               opts->allow_empty = 1;
        /* Set the subcommand */
        if (remove_state)
                opts->subcommand = REPLAY_REMOVE_STATE;
        if (opts->subcommand != REPLAY_NONE) {
                opts->revs = NULL;
        } else {
 +              struct setup_revision_opt s_r_opt;
                opts->revs = xmalloc(sizeof(*opts->revs));
                init_revisions(opts->revs, NULL);
                opts->revs->no_walk = 1;
                if (argc < 2)
                        usage_with_options(usage_str, options);
 -              argc = setup_revisions(argc, argv, opts->revs, NULL);
 +              memset(&s_r_opt, 0, sizeof(s_r_opt));
 +              s_r_opt.assume_dashdash = 1;
 +              argc = setup_revisions(argc, argv, opts->revs, &s_r_opt);
        }
  
        if (argc > 1)
index 2e1325824c5d1457a3a29fbf2b80661c05f035e6,70538bb20f39519a7f9aef26605f3f8583df3ad9..0c19b7c7539192851d5bd43df686baa97dc0df73
@@@ -167,6 -167,14 +167,14 @@@ has_action () 
        sane_grep '^[^#]' "$1" >/dev/null
  }
  
+ is_empty_commit() {
+       tree=$(git rev-parse -q --verify "$1"^{tree} 2>/dev/null ||
+               die "$1: not a commit that can be picked")
+       ptree=$(git rev-parse -q --verify "$1"^^{tree} 2>/dev/null ||
+               ptree=4b825dc642cb6eb9a060e54bf8d69288fbee4904)
+       test "$tree" = "$ptree"
+ }
  # Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
  # GIT_AUTHOR_DATE exported from the current environment.
  do_with_author () {
@@@ -191,12 -199,19 +199,19 @@@ git_sequence_editor () 
  
  pick_one () {
        ff=--ff
        case "$1" in -n) sha1=$2; ff= ;; *) sha1=$1 ;; esac
        case "$force_rebase" in '') ;; ?*) ff= ;; esac
        output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
+       if is_empty_commit "$sha1"
+       then
+               empty_args="--allow-empty"
+       fi
        test -d "$rewritten" &&
                pick_one_preserving_merges "$@" && return
-       output git cherry-pick $ff "$@"
+       output git cherry-pick $empty_args $ff "$@"
  }
  
  pick_one_preserving_merges () {
@@@ -672,7 -687,7 +687,7 @@@ rearrange_squash () 
  case "$action" in
  continue)
        # do we have anything to commit?
 -      if git diff-index --cached --quiet --ignore-submodules HEAD --
 +      if git diff-index --cached --quiet HEAD --
        then
                : Nothing to commit -- skip this
        else
@@@ -780,9 -795,17 +795,17 @@@ git rev-list $merges_option --pretty=on
        sed -n "s/^>//p" |
  while read -r shortsha1 rest
  do
+       if test -z "$keep_empty" && is_empty_commit $shortsha1
+       then
+               comment_out="# "
+       else
+               comment_out=
+       fi
        if test t != "$preserve_merges"
        then
-               printf '%s\n' "pick $shortsha1 $rest" >> "$todo"
+               printf '%s\n' "${comment_out}pick $shortsha1 $rest" >>"$todo"
        else
                sha1=$(git rev-parse $shortsha1)
                if test -z "$rebase_root"
                if test f = "$preserve"
                then
                        touch "$rewritten"/$sha1
-                       printf '%s\n' "pick $shortsha1 $rest" >> "$todo"
+                       printf '%s\n' "${comment_out}pick $shortsha1 $rest" >>"$todo"
                fi
        fi
  done
@@@ -846,13 -869,17 +869,19 @@@ cat >> "$todo" << EO
  #  f, fixup = like "squash", but discard this commit's log message
  #  x, exec = run command (the rest of the line) using shell
  #
 +# These lines can be re-ordered; they are executed from top to bottom.
 +#
  # If you remove a line here THAT COMMIT WILL BE LOST.
  # However, if you remove everything, the rebase will be aborted.
  #
  EOF
  
+ if test -z "$keep_empty"
+ then
+       echo "# Note that empty commits are commented out" >>"$todo"
+ fi
  has_action "$todo" ||
        die_abort "Nothing to do"
  
diff --combined sequencer.c
index 81d8ace35fa51f029cf78a4710a5668277a95af6,4e3af82442b949a387bb630c77c2f3f4add0bb03..f83cdfd63764b6c20432b2226648f4fd9eda62fe
@@@ -13,6 -13,7 +13,7 @@@
  #include "rerere.h"
  #include "merge-recursive.h"
  #include "refs.h"
+ #include "argv-array.h"
  
  #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
  
@@@ -164,7 -165,7 +165,7 @@@ static void write_message(struct strbu
  
  static struct tree *empty_tree(void)
  {
 -      return lookup_tree((const unsigned char *)EMPTY_TREE_SHA1_BIN);
 +      return lookup_tree(EMPTY_TREE_SHA1_BIN);
  }
  
  static int error_dirty_index(struct replay_opts *opts)
@@@ -234,7 -235,7 +235,7 @@@ static int do_recursive_merge(struct co
  
        if (!clean) {
                int i;
 -              strbuf_addstr(msgbuf, "\nConflicts:\n\n");
 +              strbuf_addstr(msgbuf, "\nConflicts:\n");
                for (i = 0; i < active_nr;) {
                        struct cache_entry *ce = active_cache[i++];
                        if (ce_stage(ce)) {
        return !clean;
  }
  
+ static int is_index_unchanged(void)
+ {
+       unsigned char head_sha1[20];
+       struct commit *head_commit;
+       if (!resolve_ref_unsafe("HEAD", head_sha1, 1, NULL))
+               return error(_("Could not resolve HEAD commit\n"));
+       head_commit = lookup_commit(head_sha1);
+       if (!head_commit || parse_commit(head_commit))
+               return error(_("could not parse commit %s\n"),
+                            sha1_to_hex(head_commit->object.sha1));
+       if (!active_cache_tree)
+               active_cache_tree = cache_tree();
+       if (!cache_tree_fully_valid(active_cache_tree))
+               if (cache_tree_update(active_cache_tree, active_cache,
+                                 active_nr, 0))
+                       return error(_("Unable to update cache tree\n"));
+       return !hashcmp(active_cache_tree->sha1, head_commit->tree->object.sha1);
+ }
  /*
   * If we are cherry-pick, and if the merge did not result in
   * hand-editing, we will hit this commit and inherit the original
   */
  static int run_git_commit(const char *defmsg, struct replay_opts *opts)
  {
-       /* 6 is max possible length of our args array including NULL */
-       const char *args[6];
-       int i = 0;
+       struct argv_array array;
+       int rc;
+       argv_array_init(&array);
+       argv_array_push(&array, "commit");
+       argv_array_push(&array, "-n");
  
-       args[i++] = "commit";
-       args[i++] = "-n";
        if (opts->signoff)
-               args[i++] = "-s";
+               argv_array_push(&array, "-s");
        if (!opts->edit) {
-               args[i++] = "-F";
-               args[i++] = defmsg;
+               argv_array_push(&array, "-F");
+               argv_array_push(&array, defmsg);
+       }
+       if (opts->allow_empty)
+               argv_array_push(&array, "--allow-empty");
+       rc = run_command_v_opt(array.argv, RUN_GIT_CMD);
+       argv_array_clear(&array);
+       return rc;
+ }
+ static int is_original_commit_empty(struct commit *commit)
+ {
+       const unsigned char *ptree_sha1;
+       if (parse_commit(commit))
+               return error(_("Could not parse commit %s\n"),
+                            sha1_to_hex(commit->object.sha1));
+       if (commit->parents) {
+               struct commit *parent = commit->parents->item;
+               if (parse_commit(parent))
+                       return error(_("Could not parse parent commit %s\n"),
+                               sha1_to_hex(parent->object.sha1));
+               ptree_sha1 = parent->tree->object.sha1;
+       } else {
+               ptree_sha1 = EMPTY_TREE_SHA1_BIN; /* commit is root */
        }
-       args[i] = NULL;
  
-       return run_command_v_opt(args, RUN_GIT_CMD);
+       return !hashcmp(ptree_sha1, commit->tree->object.sha1);
  }
  
  static int do_pick_commit(struct commit *commit, struct replay_opts *opts)
        char *defmsg = NULL;
        struct strbuf msgbuf = STRBUF_INIT;
        int res;
+       int empty_commit;
+       int index_unchanged;
  
        if (opts->no_commit) {
                /*
                free_commit_list(remotes);
        }
  
+       empty_commit = is_original_commit_empty(commit);
+       if (empty_commit < 0)
+               return empty_commit;
        /*
         * If the merge was clean or if it failed due to conflict, we write
         * CHERRY_PICK_HEAD for the subsequent invocation of commit to use.
                print_advice(res == 1, opts);
                rerere(opts->allow_rerere_auto);
        } else {
+               index_unchanged = is_index_unchanged();
+               /*
+                * If index_unchanged is less than 0, that indicates we either
+                * couldn't parse HEAD or the index, so error out here.
+                */
+               if (index_unchanged < 0)
+                       return index_unchanged;
+               if (!empty_commit && !opts->keep_redundant_commits && index_unchanged)
+                       /*
+                        * The head tree and the index match
+                        * meaning the commit is empty.  Since it wasn't created
+                        * empty (based on the previous test), we can conclude
+                        * the commit has been made redundant.  Since we don't
+                        * want to keep redundant commits, we can just return
+                        * here, skipping this commit
+                        */
+                       return 0;
                if (!opts->no_commit)
                        res = run_git_commit(defmsg, opts);
        }
@@@ -468,6 -543,33 +543,6 @@@ static void read_and_refresh_cache(stru
        rollback_lock_file(&index_lock);
  }
  
 -/*
 - * Append a commit to the end of the commit_list.
 - *
 - * next starts by pointing to the variable that holds the head of an
 - * empty commit_list, and is updated to point to the "next" field of
 - * the last item on the list as new commits are appended.
 - *
 - * Usage example:
 - *
 - *     struct commit_list *list;
 - *     struct commit_list **next = &list;
 - *
 - *     next = commit_list_append(c1, next);
 - *     next = commit_list_append(c2, next);
 - *     assert(commit_list_count(list) == 2);
 - *     return list;
 - */
 -static struct commit_list **commit_list_append(struct commit *commit,
 -                                             struct commit_list **next)
 -{
 -      struct commit_list *new = xmalloc(sizeof(struct commit_list));
 -      new->item = commit;
 -      *next = new;
 -      new->next = NULL;
 -      return &new->next;
 -}
 -
  static int format_todo(struct strbuf *buf, struct commit_list *todo_list,
                struct replay_opts *opts)
  {