Merge branch 'nh/empty-rebase'

"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
This commit is contained in:
Junio C Hamano 2012-04-30 14:58:00 -07:00
commit 1692579dd3
9 changed files with 193 additions and 19 deletions

View File

@ -103,6 +103,25 @@ effect to your index in a row.
cherry-pick'ed commit, then a fast forward to this commit will
be performed.
--allow-empty::
By default, cherry-picking an empty commit will fail,
indicating that an explicit invocation of `git commit
--allow-empty` is required. This option overrides that
behavior, allowing empty commits to be preserved automatically
in a cherry-pick. Note that when "--ff" is in effect, empty
commits that meet the "fast-forward" requirement will be kept
even without this option. Note also, that use of this option only
keeps commits that were initially empty (i.e. the commit recorded the
same tree as its parent). Commits which are made empty due to a
previous commit are dropped. To force the inclusion of those commits
use `--keep-redundant-commits`.
--keep-redundant-commits::
If a commit being cherry picked duplicates a commit already in the
current history, it will become empty. By default these
redundant commits are ignored. This option overrides that behavior and
creates an empty commit object. Implies `--allow-empty`.
--strategy=<strategy>::
Use the given merge strategy. Should only be used once.
See the MERGE STRATEGIES section in linkgit:git-merge[1]

View File

@ -238,6 +238,10 @@ leave out at most one of A and B, in which case it defaults to HEAD.
will be reset to where it was when the rebase operation was
started.
--keep-empty::
Keep the commits that do not change anything from its
parents in the result.
--skip::
Restart the rebasing process by skipping the current patch.

View File

@ -115,12 +115,16 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
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))
@ -138,6 +142,10 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
"--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;

View File

@ -20,11 +20,20 @@ esac
test -n "$rebase_root" && root_flag=--root
git format-patch -k --stdout --full-index --ignore-if-in-upstream \
--src-prefix=a/ --dst-prefix=b/ \
--no-renames $root_flag "$revisions" |
git am $git_am_opt --rebasing --resolvemsg="$resolvemsg" &&
move_to_original_branch
if test -n "$keep_empty"
then
# we have to do this the hard way. git format-patch completely squashes
# empty commits and even if it didn't the format doesn't really lend
# itself well to recording empty patches. fortunately, cherry-pick
# makes this easy
git cherry-pick --allow-empty "$revisions"
else
git format-patch -k --stdout --full-index --ignore-if-in-upstream \
--src-prefix=a/ --dst-prefix=b/ \
--no-renames $root_flag "$revisions" |
git am $git_am_opt --rebasing --resolvemsg="$resolvemsg"
fi && move_to_original_branch
ret=$?
test 0 != $ret -a -d "$state_dir" && write_basic_state
exit $ret

View File

@ -167,6 +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 @@ 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 () {
@ -780,9 +795,17 @@ git rev-list $merges_option --pretty=oneline --abbrev-commit \
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"
@ -801,7 +824,7 @@ do
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
@ -853,6 +876,12 @@ cat >> "$todo" << EOF
#
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"

View File

@ -43,6 +43,7 @@ s,strategy=! use the given merge strategy
no-ff! cherry-pick all commits, even if unchanged
m,merge! use merging strategies to rebase
i,interactive! let the user edit the list of commits to rebase
k,keep-empty preserve empty commits during rebase
f,force-rebase! force rebase even if branch is up to date
X,strategy-option=! pass the argument through to the merge strategy
stat! display a diffstat of what changed upstream
@ -97,6 +98,7 @@ state_dir=
action=
preserve_merges=
autosquash=
keep_empty=
test "$(git config --bool rebase.autosquash)" = "true" && autosquash=t
read_basic_state () {
@ -220,6 +222,9 @@ do
-i)
interactive_rebase=explicit
;;
-k)
keep_empty=yes
;;
-p)
preserve_merges=t
test -z "$interactive_rebase" && interactive_rebase=implied

View File

@ -13,6 +13,7 @@
#include "rerere.h"
#include "merge-recursive.h"
#include "refs.h"
#include "argv-array.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@ -251,6 +252,30 @@ static int do_recursive_merge(struct commit *base, struct commit *next,
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
@ -260,21 +285,46 @@ static int do_recursive_merge(struct commit *base, struct commit *next,
*/
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);
}
args[i] = NULL;
return run_command_v_opt(args, RUN_GIT_CMD);
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 */
}
return !hashcmp(ptree_sha1, commit->tree->object.sha1);
}
static int do_pick_commit(struct commit *commit, struct replay_opts *opts)
@ -286,6 +336,8 @@ 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) {
/*
@ -411,6 +463,10 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts)
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.
@ -431,6 +487,25 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts)
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);
}

View File

@ -29,6 +29,8 @@ struct replay_opts {
int signoff;
int allow_ff;
int allow_rerere_auto;
int allow_empty;
int keep_redundant_commits;
int mainline;

View File

@ -18,7 +18,12 @@ test_expect_success setup '
echo third >> file1 &&
git add file1 &&
test_tick &&
git commit --allow-empty-message -m ""
git commit --allow-empty-message -m "" &&
git checkout master &&
git checkout -b empty-branch2 &&
test_tick &&
git commit --allow-empty -m "empty"
'
@ -48,4 +53,22 @@ test_expect_success 'index lockfile was removed' '
'
test_expect_success 'cherry pick an empty non-ff commit without --allow-empty' '
git checkout master &&
echo fourth >>file2 &&
git add file2 &&
git commit -m "fourth" &&
test_must_fail git cherry-pick empty-branch2
'
test_expect_success 'cherry pick an empty non-ff commit with --allow-empty' '
git checkout master &&
git cherry-pick --allow-empty empty-branch2
'
test_expect_success 'cherry pick with --keep-redundant-commits' '
git checkout master &&
git cherry-pick --keep-redundant-commits HEAD^
'
test_done