aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/rev-list-options.txt6
-rw-r--r--revision.c172
-rw-r--r--revision.h1
-rwxr-xr-xt/t6012-rev-list-simplify.sh93
4 files changed, 251 insertions, 21 deletions
diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt
index 0ce3f7fbd..059ae69d8 100644
--- a/Documentation/rev-list-options.txt
+++ b/Documentation/rev-list-options.txt
@@ -193,6 +193,12 @@ endif::git-rev-list[]
Stop when a given path disappears from the tree.
+--simplify-merges::
+
+ Simplify away commits that did not change the given paths, similar
+ to `--full-history`, and further remove merges none of whose
+ parent history changes the given paths.
+
--no-merges::
Do not print commits with more than one parent.
diff --git a/revision.c b/revision.c
index e75079a6e..0aaa4c10b 100644
--- a/revision.c
+++ b/revision.c
@@ -489,7 +489,7 @@ static int add_parents_to_list(struct rev_info *revs, struct commit *commit,
p->object.flags |= SEEN;
insert_by_date_cached(p, list, cached_base, cache_ptr);
}
- if(revs->first_parent_only)
+ if (revs->first_parent_only)
break;
}
return 0;
@@ -1045,6 +1045,11 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
} else if (!strcmp(arg, "--topo-order")) {
revs->lifo = 1;
revs->topo_order = 1;
+ } else if (!strcmp(arg, "--simplify-merges")) {
+ revs->simplify_merges = 1;
+ revs->rewrite_parents = 1;
+ revs->simplify_history = 0;
+ revs->limited = 1;
} else if (!strcmp(arg, "--date-order")) {
revs->lifo = 0;
revs->topo_order = 1;
@@ -1378,6 +1383,149 @@ static void add_child(struct rev_info *revs, struct commit *parent, struct commi
l->next = add_decoration(&revs->children, &parent->object, l);
}
+static int remove_duplicate_parents(struct commit *commit)
+{
+ struct commit_list **pp, *p;
+ int surviving_parents;
+
+ /* Examine existing parents while marking ones we have seen... */
+ pp = &commit->parents;
+ while ((p = *pp) != NULL) {
+ struct commit *parent = p->item;
+ if (parent->object.flags & TMP_MARK) {
+ *pp = p->next;
+ continue;
+ }
+ parent->object.flags |= TMP_MARK;
+ pp = &p->next;
+ }
+ /* count them while clearing the temporary mark */
+ surviving_parents = 0;
+ for (p = commit->parents; p; p = p->next) {
+ p->item->object.flags &= ~TMP_MARK;
+ surviving_parents++;
+ }
+ return surviving_parents;
+}
+
+static struct commit_list **simplify_one(struct commit *commit, struct commit_list **tail)
+{
+ struct commit_list *p;
+ int cnt;
+
+ /*
+ * We store which commit each one simplifies to in its util field.
+ * Have we handled this one?
+ */
+ if (commit->util)
+ return tail;
+
+ /*
+ * An UNINTERESTING commit simplifies to itself, so does a
+ * root commit. We do not rewrite parents of such commit
+ * anyway.
+ */
+ if ((commit->object.flags & UNINTERESTING) || !commit->parents) {
+ commit->util = commit;
+ return tail;
+ }
+
+ /*
+ * Do we know what commit all of our parents should be rewritten to?
+ * Otherwise we are not ready to rewrite this one yet.
+ */
+ for (cnt = 0, p = commit->parents; p; p = p->next) {
+ if (!p->item->util) {
+ tail = &commit_list_insert(p->item, tail)->next;
+ cnt++;
+ }
+ }
+ if (cnt)
+ return tail;
+
+ /*
+ * Rewrite our list of parents.
+ */
+ for (p = commit->parents; p; p = p->next)
+ p->item = p->item->util;
+ cnt = remove_duplicate_parents(commit);
+
+ /*
+ * It is possible that we are a merge and one side branch
+ * does not have any commit that touches the given paths;
+ * in such a case, the immediate parents will be rewritten
+ * to different commits.
+ *
+ * o----X X: the commit we are looking at;
+ * / / o: a commit that touches the paths;
+ * ---o----'
+ *
+ * Further reduce the parents by removing redundant parents.
+ */
+ if (1 < cnt) {
+ struct commit_list *h = reduce_heads(commit->parents);
+ cnt = commit_list_count(h);
+ free_commit_list(commit->parents);
+ commit->parents = h;
+ }
+
+ /*
+ * A commit simplifies to itself if it is a root, if it is
+ * UNINTERESTING, if it touches the given paths, or if it is a
+ * merge and its parents simplifies to more than one commits
+ * (the first two cases are already handled at the beginning of
+ * this function).
+ *
+ * Otherwise, it simplifies to what its sole parent simplifies to.
+ */
+ if (!cnt ||
+ (commit->object.flags & UNINTERESTING) ||
+ !(commit->object.flags & TREESAME) ||
+ (1 < cnt))
+ commit->util = commit;
+ else
+ commit->util = commit->parents->item->util;
+ return tail;
+}
+
+static void simplify_merges(struct rev_info *revs)
+{
+ struct commit_list *list;
+ struct commit_list *yet_to_do, **tail;
+
+ sort_in_topological_order(&revs->commits, revs->lifo);
+
+ /* feed the list reversed */
+ yet_to_do = NULL;
+ for (list = revs->commits; list; list = list->next)
+ commit_list_insert(list->item, &yet_to_do);
+ while (yet_to_do) {
+ list = yet_to_do;
+ yet_to_do = NULL;
+ tail = &yet_to_do;
+ while (list) {
+ struct commit *commit = list->item;
+ struct commit_list *next = list->next;
+ free(list);
+ list = next;
+ tail = simplify_one(commit, tail);
+ }
+ }
+
+ /* clean up the result, removing the simplified ones */
+ list = revs->commits;
+ revs->commits = NULL;
+ tail = &revs->commits;
+ while (list) {
+ struct commit *commit = list->item;
+ struct commit_list *next = list->next;
+ free(list);
+ list = next;
+ if (commit->util == commit)
+ tail = &commit_list_insert(commit, tail)->next;
+ }
+}
+
static void set_children(struct rev_info *revs)
{
struct commit_list *l;
@@ -1418,6 +1566,8 @@ int prepare_revision_walk(struct rev_info *revs)
return -1;
if (revs->topo_order)
sort_in_topological_order(&revs->commits, revs->lifo);
+ if (revs->simplify_merges)
+ simplify_merges(revs);
if (revs->children.name)
set_children(revs);
return 0;
@@ -1450,26 +1600,6 @@ static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp
}
}
-static void remove_duplicate_parents(struct commit *commit)
-{
- struct commit_list **pp, *p;
-
- /* Examine existing parents while marking ones we have seen... */
- pp = &commit->parents;
- while ((p = *pp) != NULL) {
- struct commit *parent = p->item;
- if (parent->object.flags & TMP_MARK) {
- *pp = p->next;
- continue;
- }
- parent->object.flags |= TMP_MARK;
- pp = &p->next;
- }
- /* ... and clear the temporary mark */
- for (p = commit->parents; p; p = p->next)
- p->item->object.flags &= ~TMP_MARK;
-}
-
static int rewrite_parents(struct rev_info *revs, struct commit *commit)
{
struct commit_list **pp = &commit->parents;
diff --git a/revision.h b/revision.h
index f64e8ce7f..dfa06b521 100644
--- a/revision.h
+++ b/revision.h
@@ -41,6 +41,7 @@ struct rev_info {
simplify_history:1,
lifo:1,
topo_order:1,
+ simplify_merges:1,
tag_objects:1,
tree_objects:1,
blob_objects:1,
diff --git a/t/t6012-rev-list-simplify.sh b/t/t6012-rev-list-simplify.sh
new file mode 100755
index 000000000..510bb9679
--- /dev/null
+++ b/t/t6012-rev-list-simplify.sh
@@ -0,0 +1,93 @@
+#!/bin/sh
+
+test_description='merge simplification'
+
+. ./test-lib.sh
+
+note () {
+ git tag "$1"
+}
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+
+unnote () {
+ git name-rev --tags --stdin | sed -e "s|$_x40 (tags/\([^)]*\)) |\1 |g"
+}
+
+test_expect_success setup '
+ echo "Hi there" >file &&
+ git add file &&
+ test_tick && git commit -m "Initial file" &&
+ note A &&
+
+ git branch other-branch &&
+
+ echo "Hello" >file &&
+ git add file &&
+ test_tick && git commit -m "Modified file" &&
+ note B &&
+
+ git checkout other-branch &&
+
+ echo "Hello" >file &&
+ git add file &&
+ test_tick && git commit -m "Modified the file identically" &&
+ note C &&
+
+ echo "This is a stupid example" >another-file &&
+ git add another-file &&
+ test_tick && git commit -m "Add another file" &&
+ note D &&
+
+ test_tick && git merge -m "merge" master &&
+ note E &&
+
+ echo "Yet another" >elif &&
+ git add elif &&
+ test_tick && git commit -m "Irrelevant change" &&
+ note F &&
+
+ git checkout master &&
+ echo "Yet another" >elif &&
+ git add elif &&
+ test_tick && git commit -m "Another irrelevant change" &&
+ note G &&
+
+ test_tick && git merge -m "merge" other-branch &&
+ note H &&
+
+ echo "Final change" >file &&
+ test_tick && git commit -a -m "Final change" &&
+ note I
+'
+
+FMT='tformat:%P %H | %s'
+
+check_result () {
+ for c in $1
+ do
+ echo "$c"
+ done >expect &&
+ shift &&
+ param="$*" &&
+ test_expect_success "log $param" '
+ git log --pretty="$FMT" --parents $param |
+ unnote >actual &&
+ sed -e "s/^.* \([^ ]*\) .*/\1/" >check <actual &&
+ test_cmp expect check || {
+ cat actual
+ false
+ }
+ '
+}
+
+check_result 'I H G F E D C B A' --full-history
+check_result 'I H E C B A' --full-history -- file
+check_result 'I H E C B A' --full-history --topo-order -- file
+check_result 'I H E C B A' --full-history --date-order -- file
+check_result 'I E C B A' --simplify-merges -- file
+check_result 'I B A' -- file
+check_result 'I B A' --topo-order -- file
+
+test_done