提交 765ac8ec 编写于 作者: L Linus Torvalds 提交者: Junio C Hamano

Rip out merge-order and make "git log <paths>..." work again.

Well, assuming breaking --merge-order is fine, here's a patch (on top of
the other ones) that makes

	git log <filename>

actually work, as far as I can tell.

I didn't add the logic for --before/--after flags, but that should be
pretty trivial, and is independent of this anyway.
Signed-off-by: NJunio C Hamano <junkio@cox.net>
上级 70b006b9
...@@ -16,7 +16,7 @@ SYNOPSIS ...@@ -16,7 +16,7 @@ SYNOPSIS
[ \--no-merges ] [ \--no-merges ]
[ \--remove-empty ] [ \--remove-empty ]
[ \--all ] [ \--all ]
[ [ \--merge-order [ \--show-breaks ] ] | [ \--topo-order ] ] [ \--topo-order ]
[ \--parents ] [ \--parents ]
[ \--objects [ \--unpacked ] ] [ \--objects [ \--unpacked ] ]
[ \--pretty | \--header ] [ \--pretty | \--header ]
...@@ -94,57 +94,10 @@ OPTIONS ...@@ -94,57 +94,10 @@ OPTIONS
topological order (i.e. descendant commits are shown topological order (i.e. descendant commits are shown
before their parents). before their parents).
--merge-order::
When specified the commit history is decomposed into a unique
sequence of minimal, non-linear epochs and maximal, linear epochs.
Non-linear epochs are then linearised by sorting them into merge
order, which is described below.
+
Maximal, linear epochs correspond to periods of sequential development.
Minimal, non-linear epochs correspond to periods of divergent development
followed by a converging merge. The theory of epochs is described in more
detail at
link:http://blackcubes.dyndns.org/epoch/[http://blackcubes.dyndns.org/epoch/].
+
The merge order for a non-linear epoch is defined as a linearisation for which
the following invariants are true:
+
1. if a commit P is reachable from commit N, commit P sorts after commit N
in the linearised list.
2. if Pi and Pj are any two parents of a merge M (with i < j), then any
commit N, such that N is reachable from Pj but not reachable from Pi,
sorts before all commits reachable from Pi.
+
Invariant 1 states that later commits appear before earlier commits they are
derived from.
+
Invariant 2 states that commits unique to "later" parents in a merge, appear
before all commits from "earlier" parents of a merge.
--show-breaks::
Each item of the list is output with a 2-character prefix consisting
of one of: (|), (^), (=) followed by a space.
+
Commits marked with (=) represent the boundaries of minimal, non-linear epochs
and correspond either to the start of a period of divergent development or to
the end of such a period.
+
Commits marked with (|) are direct parents of commits immediately preceding
the marked commit in the list.
+
Commits marked with (^) are not parents of the immediately preceding commit.
These "breaks" represent necessary discontinuities implied by trying to
represent an arbitrary DAG in a linear form.
+
`--show-breaks` is only valid if `--merge-order` is also specified.
Author Author
------ ------
Written by Linus Torvalds <torvalds@osdl.org> Written by Linus Torvalds <torvalds@osdl.org>
Original *--merge-order* logic by Jon Seymour <jon.seymour@gmail.com>
Documentation Documentation
-------------- --------------
Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>.
......
...@@ -40,9 +40,7 @@ Issues of note: ...@@ -40,9 +40,7 @@ Issues of note:
If you don't have openssl, you can use one of the SHA1 libraries If you don't have openssl, you can use one of the SHA1 libraries
that come with git (git includes the one from Mozilla, and has that come with git (git includes the one from Mozilla, and has
its own PowerPC-optimized one too - see the Makefile), and you its own PowerPC and ARM optimized ones too - see the Makefile).
can avoid the bignum support by excising git-rev-list support
for "--merge-order" (by hand).
- "libcurl" and "curl" executable. git-http-fetch and - "libcurl" and "curl" executable. git-http-fetch and
git-fetch use them. If you do not use http git-fetch use them. If you do not use http
......
...@@ -6,8 +6,8 @@ all: ...@@ -6,8 +6,8 @@ all:
# on non-x86 architectures (e.g. PowerPC), while the OpenSSL version (default # on non-x86 architectures (e.g. PowerPC), while the OpenSSL version (default
# choice) has very fast version optimized for i586. # choice) has very fast version optimized for i586.
# #
# Define NO_OPENSSL environment variable if you do not have OpenSSL. You will # Define NO_OPENSSL environment variable if you do not have OpenSSL.
# miss out git-rev-list --merge-order. This also implies MOZILLA_SHA1. # This also implies MOZILLA_SHA1.
# #
# Define NO_CURL if you do not have curl installed. git-http-pull and # Define NO_CURL if you do not have curl installed. git-http-pull and
# git-http-push are not built, and you cannot use http:// and https:// # git-http-push are not built, and you cannot use http:// and https://
...@@ -191,7 +191,7 @@ LIB_FILE=libgit.a ...@@ -191,7 +191,7 @@ LIB_FILE=libgit.a
LIB_H = \ LIB_H = \
blob.h cache.h commit.h count-delta.h csum-file.h delta.h \ blob.h cache.h commit.h count-delta.h csum-file.h delta.h \
diff.h epoch.h object.h pack.h pkt-line.h quote.h refs.h \ diff.h object.h pack.h pkt-line.h quote.h refs.h \
run-command.h strbuf.h tag.h tree.h git-compat-util.h revision.h run-command.h strbuf.h tag.h tree.h git-compat-util.h revision.h
DIFF_OBJS = \ DIFF_OBJS = \
...@@ -324,7 +324,6 @@ ifndef NO_CURL ...@@ -324,7 +324,6 @@ ifndef NO_CURL
endif endif
ifndef NO_OPENSSL ifndef NO_OPENSSL
LIB_OBJS += epoch.o
OPENSSL_LIBSSL = -lssl OPENSSL_LIBSSL = -lssl
ifdef OPENSSLDIR ifdef OPENSSLDIR
# Again this may be problematic -- gcc does not always want -R. # Again this may be problematic -- gcc does not always want -R.
......
/*
* Copyright (c) 2005, Jon Seymour
*
* For more information about epoch theory on which this module is based,
* refer to http://blackcubes.dyndns.org/epoch/. That web page defines
* terms such as "epoch" and "minimal, non-linear epoch" and provides rationales
* for some of the algorithms used here.
*
*/
#include <stdlib.h>
/* Provides arbitrary precision integers required to accurately represent
* fractional mass: */
#include <openssl/bn.h>
#include "cache.h"
#include "commit.h"
#include "revision.h"
#include "epoch.h"
struct fraction {
BIGNUM numerator;
BIGNUM denominator;
};
#define HAS_EXACTLY_ONE_PARENT(n) ((n)->parents && !(n)->parents->next)
static BN_CTX *context = NULL;
static struct fraction *one = NULL;
static struct fraction *zero = NULL;
static BN_CTX *get_BN_CTX(void)
{
if (!context) {
context = BN_CTX_new();
}
return context;
}
static struct fraction *new_zero(void)
{
struct fraction *result = xmalloc(sizeof(*result));
BN_init(&result->numerator);
BN_init(&result->denominator);
BN_zero(&result->numerator);
BN_one(&result->denominator);
return result;
}
static void clear_fraction(struct fraction *fraction)
{
BN_clear(&fraction->numerator);
BN_clear(&fraction->denominator);
}
static struct fraction *divide(struct fraction *result, struct fraction *fraction, int divisor)
{
BIGNUM bn_divisor;
BN_init(&bn_divisor);
BN_set_word(&bn_divisor, divisor);
BN_copy(&result->numerator, &fraction->numerator);
BN_mul(&result->denominator, &fraction->denominator, &bn_divisor, get_BN_CTX());
BN_clear(&bn_divisor);
return result;
}
static struct fraction *init_fraction(struct fraction *fraction)
{
BN_init(&fraction->numerator);
BN_init(&fraction->denominator);
BN_zero(&fraction->numerator);
BN_one(&fraction->denominator);
return fraction;
}
static struct fraction *get_one(void)
{
if (!one) {
one = new_zero();
BN_one(&one->numerator);
}
return one;
}
static struct fraction *get_zero(void)
{
if (!zero) {
zero = new_zero();
}
return zero;
}
static struct fraction *copy(struct fraction *to, struct fraction *from)
{
BN_copy(&to->numerator, &from->numerator);
BN_copy(&to->denominator, &from->denominator);
return to;
}
static struct fraction *add(struct fraction *result, struct fraction *left, struct fraction *right)
{
BIGNUM a, b, gcd;
BN_init(&a);
BN_init(&b);
BN_init(&gcd);
BN_mul(&a, &left->numerator, &right->denominator, get_BN_CTX());
BN_mul(&b, &left->denominator, &right->numerator, get_BN_CTX());
BN_mul(&result->denominator, &left->denominator, &right->denominator, get_BN_CTX());
BN_add(&result->numerator, &a, &b);
BN_gcd(&gcd, &result->denominator, &result->numerator, get_BN_CTX());
BN_div(&result->denominator, NULL, &result->denominator, &gcd, get_BN_CTX());
BN_div(&result->numerator, NULL, &result->numerator, &gcd, get_BN_CTX());
BN_clear(&a);
BN_clear(&b);
BN_clear(&gcd);
return result;
}
static int compare(struct fraction *left, struct fraction *right)
{
BIGNUM a, b;
int result;
BN_init(&a);
BN_init(&b);
BN_mul(&a, &left->numerator, &right->denominator, get_BN_CTX());
BN_mul(&b, &left->denominator, &right->numerator, get_BN_CTX());
result = BN_cmp(&a, &b);
BN_clear(&a);
BN_clear(&b);
return result;
}
struct mass_counter {
struct fraction seen;
struct fraction pending;
};
static struct mass_counter *new_mass_counter(struct commit *commit, struct fraction *pending)
{
struct mass_counter *mass_counter = xmalloc(sizeof(*mass_counter));
memset(mass_counter, 0, sizeof(*mass_counter));
init_fraction(&mass_counter->seen);
init_fraction(&mass_counter->pending);
copy(&mass_counter->pending, pending);
copy(&mass_counter->seen, get_zero());
if (commit->object.util) {
die("multiple attempts to initialize mass counter for %s",
sha1_to_hex(commit->object.sha1));
}
commit->object.util = mass_counter;
return mass_counter;
}
static void free_mass_counter(struct mass_counter *counter)
{
clear_fraction(&counter->seen);
clear_fraction(&counter->pending);
free(counter);
}
/*
* Finds the base commit of a list of commits.
*
* One property of the commit being searched for is that every commit reachable
* from the base commit is reachable from the commits in the starting list only
* via paths that include the base commit.
*
* This algorithm uses a conservation of mass approach to find the base commit.
*
* We start by injecting one unit of mass into the graph at each
* of the commits in the starting list. Injecting mass into a commit
* is achieved by adding to its pending mass counter and, if it is not already
* enqueued, enqueuing the commit in a list of pending commits, in latest
* commit date first order.
*
* The algorithm then proceeds to visit each commit in the pending queue.
* Upon each visit, the pending mass is added to the mass already seen for that
* commit and then divided into N equal portions, where N is the number of
* parents of the commit being visited. The divided portions are then injected
* into each of the parents.
*
* The algorithm continues until we discover a commit which has seen all the
* mass originally injected or until we run out of things to do.
*
* If we find a commit that has seen all the original mass, we have found
* the common base of all the commits in the starting list.
*
* The algorithm does _not_ depend on accurate timestamps for correct operation.
* However, reasonably sane (e.g. non-random) timestamps are required in order
* to prevent an exponential performance characteristic. The occasional
* timestamp inaccuracy will not dramatically affect performance but may
* result in more nodes being processed than strictly necessary.
*
* This procedure sets *boundary to the address of the base commit. It returns
* non-zero if, and only if, there was a problem parsing one of the
* commits discovered during the traversal.
*/
static int find_base_for_list(struct commit_list *list, struct commit **boundary)
{
int ret = 0;
struct commit_list *cleaner = NULL;
struct commit_list *pending = NULL;
struct fraction injected;
init_fraction(&injected);
*boundary = NULL;
for (; list; list = list->next) {
struct commit *item = list->item;
if (!item->object.util) {
new_mass_counter(list->item, get_one());
add(&injected, &injected, get_one());
commit_list_insert(list->item, &cleaner);
commit_list_insert(list->item, &pending);
}
}
while (!*boundary && pending && !ret) {
struct commit *latest = pop_commit(&pending);
struct mass_counter *latest_node = (struct mass_counter *) latest->object.util;
int num_parents;
if ((ret = parse_commit(latest)))
continue;
add(&latest_node->seen, &latest_node->seen, &latest_node->pending);
num_parents = count_parents(latest);
if (num_parents) {
struct fraction distribution;
struct commit_list *parents;
divide(init_fraction(&distribution), &latest_node->pending, num_parents);
for (parents = latest->parents; parents; parents = parents->next) {
struct commit *parent = parents->item;
struct mass_counter *parent_node = (struct mass_counter *) parent->object.util;
if (!parent_node) {
parent_node = new_mass_counter(parent, &distribution);
insert_by_date(parent, &pending);
commit_list_insert(parent, &cleaner);
} else {
if (!compare(&parent_node->pending, get_zero()))
insert_by_date(parent, &pending);
add(&parent_node->pending, &parent_node->pending, &distribution);
}
}
clear_fraction(&distribution);
}
if (!compare(&latest_node->seen, &injected))
*boundary = latest;
copy(&latest_node->pending, get_zero());
}
while (cleaner) {
struct commit *next = pop_commit(&cleaner);
free_mass_counter((struct mass_counter *) next->object.util);
next->object.util = NULL;
}
if (pending)
free_commit_list(pending);
clear_fraction(&injected);
return ret;
}
/*
* Finds the base of an minimal, non-linear epoch, headed at head, by
* applying the find_base_for_list to a list consisting of the parents
*/
static int find_base(struct commit *head, struct commit **boundary)
{
int ret = 0;
struct commit_list *pending = NULL;
struct commit_list *next;
for (next = head->parents; next; next = next->next) {
commit_list_insert(next->item, &pending);
}
ret = find_base_for_list(pending, boundary);
free_commit_list(pending);
return ret;
}
/*
* This procedure traverses to the boundary of the first epoch in the epoch
* sequence of the epoch headed at head_of_epoch. This is either the end of
* the maximal linear epoch or the base of a minimal non-linear epoch.
*
* The queue of pending nodes is sorted in reverse date order and each node
* is currently in the queue at most once.
*/
static int find_next_epoch_boundary(struct commit *head_of_epoch, struct commit **boundary)
{
int ret;
struct commit *item = head_of_epoch;
ret = parse_commit(item);
if (ret)
return ret;
if (HAS_EXACTLY_ONE_PARENT(item)) {
/*
* We are at the start of a maximimal linear epoch.
* Traverse to the end.
*/
while (HAS_EXACTLY_ONE_PARENT(item) && !ret) {
item = item->parents->item;
ret = parse_commit(item);
}
*boundary = item;
} else {
/*
* Otherwise, we are at the start of a minimal, non-linear
* epoch - find the common base of all parents.
*/
ret = find_base(item, boundary);
}
return ret;
}
/*
* Returns non-zero if parent is known to be a parent of child.
*/
static int is_parent_of(struct commit *parent, struct commit *child)
{
struct commit_list *parents;
for (parents = child->parents; parents; parents = parents->next) {
if (!memcmp(parent->object.sha1, parents->item->object.sha1,
sizeof(parents->item->object.sha1)))
return 1;
}
return 0;
}
/*
* Pushes an item onto the merge order stack. If the top of the stack is
* marked as being a possible "break", we check to see whether it actually
* is a break.
*/
static void push_onto_merge_order_stack(struct commit_list **stack, struct commit *item)
{
struct commit_list *top = *stack;
if (top && (top->item->object.flags & DISCONTINUITY)) {
if (is_parent_of(top->item, item)) {
top->item->object.flags &= ~DISCONTINUITY;
}
}
commit_list_insert(item, stack);
}
/*
* Marks all interesting, visited commits reachable from this commit
* as uninteresting. We stop recursing when we reach the epoch boundary,
* an unvisited node or a node that has already been marking uninteresting.
*
* This doesn't actually mark all ancestors between the start node and the
* epoch boundary uninteresting, but does ensure that they will eventually
* be marked uninteresting when the main sort_first_epoch() traversal
* eventually reaches them.
*/
static void mark_ancestors_uninteresting(struct commit *commit)
{
unsigned int flags = commit->object.flags;
int visited = flags & VISITED;
int boundary = flags & BOUNDARY;
int uninteresting = flags & UNINTERESTING;
struct commit_list *next;
commit->object.flags |= UNINTERESTING;
/*
* We only need to recurse if
* we are not on the boundary and
* we have not already been marked uninteresting and
* we have already been visited.
*
* The main sort_first_epoch traverse will mark unreachable
* all uninteresting, unvisited parents as they are visited
* so there is no need to duplicate that traversal here.
*
* Similarly, if we are already marked uninteresting
* then either all ancestors have already been marked
* uninteresting or will be once the sort_first_epoch
* traverse reaches them.
*/
if (uninteresting || boundary || !visited)
return;
for (next = commit->parents; next; next = next->next)
mark_ancestors_uninteresting(next->item);
}
/*
* Sorts the nodes of the first epoch of the epoch sequence of the epoch headed at head
* into merge order.
*/
static void sort_first_epoch(struct commit *head, struct commit_list **stack)
{
struct commit_list *parents;
head->object.flags |= VISITED;
/*
* TODO: By sorting the parents in a different order, we can alter the
* merge order to show contemporaneous changes in parallel branches
* occurring after "local" changes. This is useful for a developer
* when a developer wants to see all changes that were incorporated
* into the same merge as her own changes occur after her own
* changes.
*/
for (parents = head->parents; parents; parents = parents->next) {
struct commit *parent = parents->item;
if (head->object.flags & UNINTERESTING) {
/*
* Propagates the uninteresting bit to all parents.
* if we have already visited this parent, then
* the uninteresting bit will be propagated to each
* reachable commit that is still not marked
* uninteresting and won't otherwise be reached.
*/
mark_ancestors_uninteresting(parent);
}
if (!(parent->object.flags & VISITED)) {
if (parent->object.flags & BOUNDARY) {
if (*stack) {
die("something else is on the stack - %s",
sha1_to_hex((*stack)->item->object.sha1));
}
push_onto_merge_order_stack(stack, parent);
parent->object.flags |= VISITED;
} else {
sort_first_epoch(parent, stack);
if (parents) {
/*
* This indicates a possible
* discontinuity it may not be be
* actual discontinuity if the head
* of parent N happens to be the tail
* of parent N+1.
*
* The next push onto the stack will
* resolve the question.
*/
(*stack)->item->object.flags |= DISCONTINUITY;
}
}
}
}
push_onto_merge_order_stack(stack, head);
}
/*
* Emit the contents of the stack.
*
* The stack is freed and replaced by NULL.
*
* Sets the return value to STOP if no further output should be generated.
*/
static int emit_stack(struct commit_list **stack, emitter_func emitter, int include_last)
{
unsigned int seen = 0;
int action = CONTINUE;
while (*stack && (action != STOP)) {
struct commit *next = pop_commit(stack);
seen |= next->object.flags;
if (*stack || include_last) {
if (!*stack)
next->object.flags |= BOUNDARY;
action = emitter(next);
}
}
if (*stack) {
free_commit_list(*stack);
*stack = NULL;
}
return (action == STOP || (seen & UNINTERESTING)) ? STOP : CONTINUE;
}
/*
* Sorts an arbitrary epoch into merge order by sorting each epoch
* of its epoch sequence into order.
*
* Note: this algorithm currently leaves traces of its execution in the
* object flags of nodes it discovers. This should probably be fixed.
*/
static int sort_in_merge_order(struct commit *head_of_epoch, emitter_func emitter)
{
struct commit *next = head_of_epoch;
int ret = 0;
int action = CONTINUE;
ret = parse_commit(head_of_epoch);
next->object.flags |= BOUNDARY;
while (next && next->parents && !ret && (action != STOP)) {
struct commit *base = NULL;
ret = find_next_epoch_boundary(next, &base);
if (ret)
return ret;
next->object.flags |= BOUNDARY;
if (base)
base->object.flags |= BOUNDARY;
if (HAS_EXACTLY_ONE_PARENT(next)) {
while (HAS_EXACTLY_ONE_PARENT(next)
&& (action != STOP)
&& !ret) {
if (next->object.flags & UNINTERESTING) {
action = STOP;
} else {
action = emitter(next);
}
if (action != STOP) {
next = next->parents->item;
ret = parse_commit(next);
}
}
} else {
struct commit_list *stack = NULL;
sort_first_epoch(next, &stack);
action = emit_stack(&stack, emitter, (base == NULL));
next = base;
}
}
if (next && (action != STOP) && !ret) {
emitter(next);
}
return ret;
}
/*
* Sorts the nodes reachable from a starting list in merge order, we
* first find the base for the starting list and then sort all nodes
* in this subgraph using the sort_first_epoch algorithm. Once we have
* reached the base we can continue sorting using sort_in_merge_order.
*/
int sort_list_in_merge_order(struct commit_list *list, emitter_func emitter)
{
struct commit_list *stack = NULL;
struct commit *base;
int ret = 0;
int action = CONTINUE;
struct commit_list *reversed = NULL;
for (; list; list = list->next)
commit_list_insert(list->item, &reversed);
if (!reversed)
return ret;
else if (!reversed->next) {
/*
* If there is only one element in the list, we can sort it
* using sort_in_merge_order.
*/
base = reversed->item;
} else {
/*
* Otherwise, we search for the base of the list.
*/
ret = find_base_for_list(reversed, &base);
if (ret)
return ret;
if (base)
base->object.flags |= BOUNDARY;
while (reversed) {
struct commit * next = pop_commit(&reversed);
if (!(next->object.flags & VISITED) && next!=base) {
sort_first_epoch(next, &stack);
if (reversed) {
/*
* If we have more commits
* to push, then the first
* push for the next parent may
* (or may * not) represent a
* discontinuity with respect
* to the parent currently on
* the top of the stack.
*
* Mark it for checking here,
* and check it with the next
* push. See sort_first_epoch()
* for more details.
*/
stack->item->object.flags |= DISCONTINUITY;
}
}
}
action = emit_stack(&stack, emitter, (base==NULL));
}
if (base && (action != STOP)) {
ret = sort_in_merge_order(base, emitter);
}
return ret;
}
#ifndef EPOCH_H
#define EPOCH_H
// return codes for emitter_func
#define STOP 0
#define CONTINUE 1
#define DO 2
typedef int (*emitter_func) (struct commit *);
int sort_list_in_merge_order(struct commit_list *list, emitter_func emitter);
/* Low bits are used by rev-list */
#define BOUNDARY (1u<<11)
#define VISITED (1u<<12)
#define DISCONTINUITY (1u<<13)
#define LAST_EPOCH_FLAG (1u<<14)
#endif /* EPOCH_H */
...@@ -928,7 +928,7 @@ sub find_parents { ...@@ -928,7 +928,7 @@ sub find_parents {
# now walk up to the mergepoint collecting what patches we have # now walk up to the mergepoint collecting what patches we have
my $branchtip = git_rev_parse($ps->{branch}); my $branchtip = git_rev_parse($ps->{branch});
my @ancestors = `git-rev-list --merge-order $branchtip ^$mergebase`; my @ancestors = `git-rev-list --topo-order $branchtip ^$mergebase`;
my %have; # collected merges this branch has my %have; # collected merges this branch has
foreach my $merge (@{$ps->{merges}}) { foreach my $merge (@{$ps->{merges}}) {
$have{$merge} = 1; $have{$merge} = 1;
...@@ -951,7 +951,7 @@ sub find_parents { ...@@ -951,7 +951,7 @@ sub find_parents {
# see what the remote branch has - these are the merges we # see what the remote branch has - these are the merges we
# will want to have in a consecutive series from the mergebase # will want to have in a consecutive series from the mergebase
my $otherbranchtip = git_rev_parse($branch); my $otherbranchtip = git_rev_parse($branch);
my @needraw = `git-rev-list --merge-order $otherbranchtip ^$mergebase`; my @needraw = `git-rev-list --topo-order $otherbranchtip ^$mergebase`;
my @need; my @need;
foreach my $needps (@needraw) { # get the psets foreach my $needps (@needraw) { # get the psets
$needps = commitid2pset($needps); $needps = commitid2pset($needps);
......
...@@ -4,14 +4,12 @@ ...@@ -4,14 +4,12 @@
#include "commit.h" #include "commit.h"
#include "tree.h" #include "tree.h"
#include "blob.h" #include "blob.h"
#include "epoch.h"
#include "diff.h" #include "diff.h"
#include "revision.h" #include "revision.h"
/* bits #0-2 in revision.h */ /* bits #0-3 in revision.h */
#define COUNTED (1u << 3) #define COUNTED (1u << 4)
#define SHOWN (1u << 4)
#define TMP_MARK (1u << 5) /* for isolated cases; clean after use */ #define TMP_MARK (1u << 5) /* for isolated cases; clean after use */
static const char rev_list_usage[] = static const char rev_list_usage[] =
...@@ -25,7 +23,6 @@ static const char rev_list_usage[] = ...@@ -25,7 +23,6 @@ static const char rev_list_usage[] =
" --remove-empty\n" " --remove-empty\n"
" --all\n" " --all\n"
" ordering output:\n" " ordering output:\n"
" --merge-order [ --show-breaks ]\n"
" --topo-order\n" " --topo-order\n"
" --date-order\n" " --date-order\n"
" formatting output:\n" " formatting output:\n"
...@@ -47,22 +44,9 @@ static int show_parents = 0; ...@@ -47,22 +44,9 @@ static int show_parents = 0;
static int hdr_termination = 0; static int hdr_termination = 0;
static const char *commit_prefix = ""; static const char *commit_prefix = "";
static enum cmit_fmt commit_format = CMIT_FMT_RAW; static enum cmit_fmt commit_format = CMIT_FMT_RAW;
static int merge_order = 0;
static int show_breaks = 0;
static int stop_traversal = 0;
static int no_merges = 0;
static void show_commit(struct commit *commit) static void show_commit(struct commit *commit)
{ {
commit->object.flags |= SHOWN;
if (show_breaks) {
commit_prefix = "| ";
if (commit->object.flags & DISCONTINUITY) {
commit_prefix = "^ ";
} else if (commit->object.flags & BOUNDARY) {
commit_prefix = "= ";
}
}
printf("%s%s", commit_prefix, sha1_to_hex(commit->object.sha1)); printf("%s%s", commit_prefix, sha1_to_hex(commit->object.sha1));
if (show_parents) { if (show_parents) {
struct commit_list *parents = commit->parents; struct commit_list *parents = commit->parents;
...@@ -96,73 +80,6 @@ static void show_commit(struct commit *commit) ...@@ -96,73 +80,6 @@ static void show_commit(struct commit *commit)
fflush(stdout); fflush(stdout);
} }
static int rewrite_one(struct commit **pp)
{
for (;;) {
struct commit *p = *pp;
if (p->object.flags & (TREECHANGE | UNINTERESTING))
return 0;
if (!p->parents)
return -1;
*pp = p->parents->item;
}
}
static void rewrite_parents(struct commit *commit)
{
struct commit_list **pp = &commit->parents;
while (*pp) {
struct commit_list *parent = *pp;
if (rewrite_one(&parent->item) < 0) {
*pp = parent->next;
continue;
}
pp = &parent->next;
}
}
static int filter_commit(struct commit * commit)
{
if (stop_traversal && (commit->object.flags & BOUNDARY))
return STOP;
if (commit->object.flags & (UNINTERESTING|SHOWN))
return CONTINUE;
if (revs.min_age != -1 && (commit->date > revs.min_age))
return CONTINUE;
if (revs.max_age != -1 && (commit->date < revs.max_age)) {
stop_traversal=1;
return CONTINUE;
}
if (no_merges && (commit->parents && commit->parents->next))
return CONTINUE;
if (revs.paths && revs.dense) {
if (!(commit->object.flags & TREECHANGE))
return CONTINUE;
rewrite_parents(commit);
}
return DO;
}
static int process_commit(struct commit * commit)
{
int action=filter_commit(commit);
if (action == STOP) {
return STOP;
}
if (action == CONTINUE) {
return CONTINUE;
}
if (revs.max_count != -1 && !revs.max_count--)
return STOP;
show_commit(commit);
return CONTINUE;
}
static struct object_list **process_blob(struct blob *blob, static struct object_list **process_blob(struct blob *blob,
struct object_list **p, struct object_list **p,
struct name_path *path, struct name_path *path,
...@@ -219,8 +136,7 @@ static void show_commit_list(struct rev_info *revs) ...@@ -219,8 +136,7 @@ static void show_commit_list(struct rev_info *revs)
while ((commit = get_revision(revs)) != NULL) { while ((commit = get_revision(revs)) != NULL) {
p = process_tree(commit->tree, p, NULL, ""); p = process_tree(commit->tree, p, NULL, "");
if (process_commit(commit) == STOP) show_commit(commit);
break;
} }
for (pending = revs->pending_objects; pending; pending = pending->next) { for (pending = revs->pending_objects; pending; pending = pending->next) {
struct object *obj = pending->item; struct object *obj = pending->item;
...@@ -416,10 +332,6 @@ int main(int argc, const char **argv) ...@@ -416,10 +332,6 @@ int main(int argc, const char **argv)
commit_prefix = "commit "; commit_prefix = "commit ";
continue; continue;
} }
if (!strncmp(arg, "--no-merges", 11)) {
no_merges = 1;
continue;
}
if (!strcmp(arg, "--parents")) { if (!strcmp(arg, "--parents")) {
show_parents = 1; show_parents = 1;
continue; continue;
...@@ -428,14 +340,6 @@ int main(int argc, const char **argv) ...@@ -428,14 +340,6 @@ int main(int argc, const char **argv)
bisect_list = 1; bisect_list = 1;
continue; continue;
} }
if (!strcmp(arg, "--merge-order")) {
merge_order = 1;
continue;
}
if (!strcmp(arg, "--show-breaks")) {
show_breaks = 1;
continue;
}
usage(rev_list_usage); usage(rev_list_usage);
} }
...@@ -456,17 +360,7 @@ int main(int argc, const char **argv) ...@@ -456,17 +360,7 @@ int main(int argc, const char **argv)
save_commit_buffer = verbose_header; save_commit_buffer = verbose_header;
track_object_refs = 0; track_object_refs = 0;
if (!merge_order) { show_commit_list(&revs);
show_commit_list(&revs);
} else {
#ifndef NO_OPENSSL
if (sort_list_in_merge_order(list, &process_commit)) {
die("merge order sort failed\n");
}
#else
die("merge order sort unsupported, OpenSSL not linked");
#endif
}
return 0; return 0;
} }
...@@ -39,14 +39,12 @@ static int is_rev_argument(const char *arg) ...@@ -39,14 +39,12 @@ static int is_rev_argument(const char *arg)
"--header", "--header",
"--max-age=", "--max-age=",
"--max-count=", "--max-count=",
"--merge-order",
"--min-age=", "--min-age=",
"--no-merges", "--no-merges",
"--objects", "--objects",
"--objects-edge", "--objects-edge",
"--parents", "--parents",
"--pretty", "--pretty",
"--show-breaks",
"--sparse", "--sparse",
"--topo-order", "--topo-order",
"--date-order", "--date-order",
......
...@@ -381,6 +381,9 @@ static void limit_list(struct rev_info *revs) ...@@ -381,6 +381,9 @@ static void limit_list(struct rev_info *revs)
struct commit_list *newlist = NULL; struct commit_list *newlist = NULL;
struct commit_list **p = &newlist; struct commit_list **p = &newlist;
if (revs->paths)
diff_tree_setup_paths(revs->paths);
while (list) { while (list) {
struct commit_list *entry = list; struct commit_list *entry = list;
struct commit *commit = list->item; struct commit *commit = list->item;
...@@ -436,12 +439,13 @@ static void handle_all(struct rev_info *revs, unsigned flags) ...@@ -436,12 +439,13 @@ static void handle_all(struct rev_info *revs, unsigned flags)
* Parse revision information, filling in the "rev_info" structure, * Parse revision information, filling in the "rev_info" structure,
* and removing the used arguments from the argument list. * and removing the used arguments from the argument list.
* *
* Returns the number of arguments left ("new argc"). * Returns the number of arguments left that weren't recognized
* (which are also moved to the head of the argument list)
*/ */
int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def) int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def)
{ {
int i, flags, seen_dashdash; int i, flags, seen_dashdash;
const char **unrecognized = argv+1; const char **unrecognized = argv + 1;
int left = 1; int left = 1;
memset(revs, 0, sizeof(*revs)); memset(revs, 0, sizeof(*revs));
...@@ -525,6 +529,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch ...@@ -525,6 +529,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch
revs->remove_empty_trees = 1; revs->remove_empty_trees = 1;
continue; continue;
} }
if (!strncmp(arg, "--no-merges", 11)) {
revs->no_merges = 1;
continue;
}
if (!strcmp(arg, "--objects")) { if (!strcmp(arg, "--objects")) {
revs->tag_objects = 1; revs->tag_objects = 1;
revs->tree_objects = 1; revs->tree_objects = 1;
...@@ -601,14 +609,11 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch ...@@ -601,14 +609,11 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch
} }
if (revs->paths) if (revs->paths)
revs->limited = 1; revs->limited = 1;
*unrecognized = NULL;
return left; return left;
} }
void prepare_revision_walk(struct rev_info *revs) void prepare_revision_walk(struct rev_info *revs)
{ {
if (revs->paths)
diff_tree_setup_paths(revs->paths);
sort_by_date(&revs->commits); sort_by_date(&revs->commits);
if (revs->limited) if (revs->limited)
limit_list(revs); limit_list(revs);
...@@ -616,11 +621,67 @@ void prepare_revision_walk(struct rev_info *revs) ...@@ -616,11 +621,67 @@ void prepare_revision_walk(struct rev_info *revs)
sort_in_topological_order(&revs->commits, revs->lifo); sort_in_topological_order(&revs->commits, revs->lifo);
} }
static int rewrite_one(struct commit **pp)
{
for (;;) {
struct commit *p = *pp;
if (p->object.flags & (TREECHANGE | UNINTERESTING))
return 0;
if (!p->parents)
return -1;
*pp = p->parents->item;
}
}
static void rewrite_parents(struct commit *commit)
{
struct commit_list **pp = &commit->parents;
while (*pp) {
struct commit_list *parent = *pp;
if (rewrite_one(&parent->item) < 0) {
*pp = parent->next;
continue;
}
pp = &parent->next;
}
}
struct commit *get_revision(struct rev_info *revs) struct commit *get_revision(struct rev_info *revs)
{ {
if (!revs->commits) struct commit_list *list = revs->commits;
struct commit *commit;
if (!list)
return NULL; return NULL;
return pop_most_recent_commit(&revs->commits, SEEN);
}
/* Check the max_count ... */
commit = list->item;
switch (revs->max_count) {
case -1:
break;
case 0:
return NULL;
default:
revs->max_count--;
}
do {
commit = pop_most_recent_commit(&revs->commits, SEEN);
if (commit->object.flags & (UNINTERESTING|SHOWN))
continue;
if (revs->min_age != -1 && (commit->date > revs->min_age))
continue;
if (revs->max_age != -1 && (commit->date < revs->max_age))
return NULL;
if (revs->no_merges && commit->parents && commit->parents->next)
continue;
if (revs->paths && revs->dense) {
if (!(commit->object.flags & TREECHANGE))
continue;
rewrite_parents(commit);
}
commit->object.flags |= SHOWN;
return commit;
} while (revs->commits);
return NULL;
}
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#define SEEN (1u<<0) #define SEEN (1u<<0)
#define UNINTERESTING (1u<<1) #define UNINTERESTING (1u<<1)
#define TREECHANGE (1u<<2) #define TREECHANGE (1u<<2)
#define SHOWN (1u<<3)
struct rev_info { struct rev_info {
/* Starting list */ /* Starting list */
...@@ -16,6 +17,7 @@ struct rev_info { ...@@ -16,6 +17,7 @@ struct rev_info {
/* Traversal flags */ /* Traversal flags */
unsigned int dense:1, unsigned int dense:1,
no_merges:1,
remove_empty_trees:1, remove_empty_trees:1,
lifo:1, lifo:1,
topo_order:1, topo_order:1,
......
#!/bin/sh
#
# Copyright (c) 2005 Jon Seymour
#
test_description='Tests git-rev-list --merge-order functionality'
. ./test-lib.sh
. ../t6000lib.sh # t6xxx specific functions
# test-case specific test function
check_adjacency()
{
read previous
echo "= $previous"
while read next
do
if ! (git-cat-file commit $previous | grep "^parent $next" >/dev/null)
then
echo "^ $next"
else
echo "| $next"
fi
previous=$next
done
}
list_duplicates()
{
"$@" | sort | uniq -d
}
grep_stderr()
{
args=$1
shift 1
"$@" 2>&1 | grep "$args"
}
date >path0
git-update-index --add path0
save_tag tree git-write-tree
on_committer_date "1971-08-16 00:00:00" hide_error save_tag root unique_commit root tree
on_committer_date "1971-08-16 00:00:01" save_tag l0 unique_commit l0 tree -p root
on_committer_date "1971-08-16 00:00:02" save_tag l1 unique_commit l1 tree -p l0
on_committer_date "1971-08-16 00:00:03" save_tag l2 unique_commit l2 tree -p l1
on_committer_date "1971-08-16 00:00:04" save_tag a0 unique_commit a0 tree -p l2
on_committer_date "1971-08-16 00:00:05" save_tag a1 unique_commit a1 tree -p a0
on_committer_date "1971-08-16 00:00:06" save_tag b1 unique_commit b1 tree -p a0
on_committer_date "1971-08-16 00:00:07" save_tag c1 unique_commit c1 tree -p b1
on_committer_date "1971-08-16 00:00:08" as_author foobar@example.com save_tag b2 unique_commit b2 tree -p b1
on_committer_date "1971-08-16 00:00:09" save_tag b3 unique_commit b2 tree -p b2
on_committer_date "1971-08-16 00:00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2
on_committer_date "1971-08-16 00:00:11" save_tag c3 unique_commit c3 tree -p c2
on_committer_date "1971-08-16 00:00:12" save_tag a2 unique_commit a2 tree -p a1
on_committer_date "1971-08-16 00:00:13" save_tag a3 unique_commit a3 tree -p a2
on_committer_date "1971-08-16 00:00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3
on_committer_date "1971-08-16 00:00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3
on_committer_date "1971-08-16 00:00:16" save_tag l3 unique_commit l3 tree -p a4
on_committer_date "1971-08-16 00:00:17" save_tag l4 unique_commit l4 tree -p l3
on_committer_date "1971-08-16 00:00:18" save_tag l5 unique_commit l5 tree -p l4
on_committer_date "1971-08-16 00:00:19" save_tag m1 unique_commit m1 tree -p a4 -p c3
on_committer_date "1971-08-16 00:00:20" save_tag m2 unique_commit m2 tree -p c3 -p a4
on_committer_date "1971-08-16 00:00:21" hide_error save_tag alt_root unique_commit alt_root tree
on_committer_date "1971-08-16 00:00:22" save_tag r0 unique_commit r0 tree -p alt_root
on_committer_date "1971-08-16 00:00:23" save_tag r1 unique_commit r1 tree -p r0
on_committer_date "1971-08-16 00:00:24" save_tag l5r1 unique_commit l5r1 tree -p l5 -p r1
on_committer_date "1971-08-16 00:00:25" save_tag r1l5 unique_commit r1l5 tree -p r1 -p l5
#
# note: as of 20/6, it isn't possible to create duplicate parents, so this
# can't be tested.
#
#on_committer_date "1971-08-16 00:00:20" save_tag m3 unique_commit m3 tree -p c3 -p a4 -p c3
hide_error save_tag e1 as_author e@example.com unique_commit e1 tree
save_tag e2 as_author e@example.com unique_commit e2 tree -p e1
save_tag f1 as_author f@example.com unique_commit f1 tree -p e1
save_tag e3 as_author e@example.com unique_commit e3 tree -p e2
save_tag f2 as_author f@example.com unique_commit f2 tree -p f1
save_tag e4 as_author e@example.com unique_commit e4 tree -p e3 -p f2
save_tag e5 as_author e@example.com unique_commit e5 tree -p e4
save_tag f3 as_author f@example.com unique_commit f3 tree -p f2
save_tag f4 as_author f@example.com unique_commit f4 tree -p f3
save_tag e6 as_author e@example.com unique_commit e6 tree -p e5 -p f4
save_tag f5 as_author f@example.com unique_commit f5 tree -p f4
save_tag f6 as_author f@example.com unique_commit f6 tree -p f5 -p e6
save_tag e7 as_author e@example.com unique_commit e7 tree -p e6
save_tag e8 as_author e@example.com unique_commit e8 tree -p e7
save_tag e9 as_author e@example.com unique_commit e9 tree -p e8
save_tag f7 as_author f@example.com unique_commit f7 tree -p f6
save_tag f8 as_author f@example.com unique_commit f8 tree -p f7
save_tag f9 as_author f@example.com unique_commit f9 tree -p f8
save_tag e10 as_author e@example.com unique_commit e1 tree -p e9 -p f8
hide_error save_tag g0 unique_commit g0 tree
save_tag g1 unique_commit g1 tree -p g0
save_tag h1 unique_commit g2 tree -p g0
save_tag g2 unique_commit g3 tree -p g1 -p h1
save_tag h2 unique_commit g4 tree -p g2
save_tag g3 unique_commit g5 tree -p g2
save_tag g4 unique_commit g6 tree -p g3 -p h2
git-update-ref HEAD $(tag l5)
test_output_expect_success 'rev-list has correct number of entries' 'git-rev-list HEAD | wc -l | tr -d \" \"' <<EOF
19
EOF
if git-rev-list --merge-order HEAD 2>&1 | grep 'OpenSSL not linked' >/dev/null
then
test_expect_success 'skipping merge-order test' :
test_done
exit
fi
normal_adjacency_count=$(git-rev-list HEAD | check_adjacency | grep -c "\^" | tr -d ' ')
merge_order_adjacency_count=$(git-rev-list --merge-order HEAD | check_adjacency | grep -c "\^" | tr -d ' ')
test_expect_success '--merge-order produces as many or fewer discontinuities' '[ $merge_order_adjacency_count -le $normal_adjacency_count ]'
test_output_expect_success 'simple merge order' 'git-rev-list --merge-order --show-breaks HEAD' <<EOF
= l5
| l4
| l3
= a4
| c3
| c2
| c1
^ b4
| b3
| b2
| b1
^ a3
| a2
| a1
= a0
| l2
| l1
| l0
= root
EOF
test_output_expect_success 'two diamonds merge order (g6)' 'git-rev-list --merge-order --show-breaks g4' <<EOF
= g4
| h2
^ g3
= g2
| h1
^ g1
= g0
EOF
test_output_expect_success 'multiple heads' 'git-rev-list --merge-order a3 b3 c3' <<EOF
c3
c2
c1
b3
b2
b1
a3
a2
a1
a0
l2
l1
l0
root
EOF
test_output_expect_success 'multiple heads, prune at a1' 'git-rev-list --merge-order a3 b3 c3 ^a1' <<EOF
c3
c2
c1
b3
b2
b1
a3
a2
EOF
test_output_expect_success 'multiple heads, prune at l1' 'git-rev-list --merge-order a3 b3 c3 ^l1' <<EOF
c3
c2
c1
b3
b2
b1
a3
a2
a1
a0
l2
EOF
test_output_expect_success 'cross-epoch, head at l5, prune at l1' 'git-rev-list --merge-order l5 ^l1' <<EOF
l5
l4
l3
a4
c3
c2
c1
b4
b3
b2
b1
a3
a2
a1
a0
l2
EOF
test_output_expect_success 'duplicated head arguments' 'git-rev-list --merge-order l5 l5 ^l1' <<EOF
l5
l4
l3
a4
c3
c2
c1
b4
b3
b2
b1
a3
a2
a1
a0
l2
EOF
test_output_expect_success 'prune near merge' 'git-rev-list --merge-order a4 ^c3' <<EOF
a4
b4
b3
a3
a2
a1
EOF
test_output_expect_success "head has no parent" 'git-rev-list --merge-order --show-breaks root' <<EOF
= root
EOF
test_output_expect_success "two nodes - one head, one base" 'git-rev-list --merge-order --show-breaks l0' <<EOF
= l0
= root
EOF
test_output_expect_success "three nodes one head, one internal, one base" 'git-rev-list --merge-order --show-breaks l1' <<EOF
= l1
| l0
= root
EOF
test_output_expect_success "linear prune l2 ^root" 'git-rev-list --merge-order --show-breaks l2 ^root' <<EOF
^ l2
| l1
| l0
EOF
test_output_expect_success "linear prune l2 ^l0" 'git-rev-list --merge-order --show-breaks l2 ^l0' <<EOF
^ l2
| l1
EOF
test_output_expect_success "linear prune l2 ^l1" 'git-rev-list --merge-order --show-breaks l2 ^l1' <<EOF
^ l2
EOF
test_output_expect_success "linear prune l5 ^a4" 'git-rev-list --merge-order --show-breaks l5 ^a4' <<EOF
^ l5
| l4
| l3
EOF
test_output_expect_success "linear prune l5 ^l3" 'git-rev-list --merge-order --show-breaks l5 ^l3' <<EOF
^ l5
| l4
EOF
test_output_expect_success "linear prune l5 ^l4" 'git-rev-list --merge-order --show-breaks l5 ^l4' <<EOF
^ l5
EOF
test_output_expect_success "max-count 10 - merge order" 'git-rev-list --merge-order --show-breaks --max-count=10 l5' <<EOF
= l5
| l4
| l3
= a4
| c3
| c2
| c1
^ b4
| b3
| b2
EOF
test_output_expect_success "max-count 10 - non merge order" 'git-rev-list --max-count=10 l5' <<EOF
l5
l4
l3
a4
b4
a3
a2
c3
c2
b3
EOF
test_output_expect_success '--max-age=c3, no --merge-order' "git-rev-list --max-age=$(commit_date c3) l5" <<EOF
l5
l4
l3
a4
b4
a3
a2
c3
EOF
test_output_expect_success '--max-age=c3, --merge-order' "git-rev-list --merge-order --max-age=$(commit_date c3) l5" <<EOF
l5
l4
l3
a4
c3
b4
a3
a2
EOF
test_output_expect_success 'one specified head reachable from another a4, c3, --merge-order' "list_duplicates git-rev-list --merge-order a4 c3" <<EOF
EOF
test_output_expect_success 'one specified head reachable from another c3, a4, --merge-order' "list_duplicates git-rev-list --merge-order c3 a4" <<EOF
EOF
test_output_expect_success 'one specified head reachable from another a4, c3, no --merge-order' "list_duplicates git-rev-list a4 c3" <<EOF
EOF
test_output_expect_success 'one specified head reachable from another c3, a4, no --merge-order' "list_duplicates git-rev-list c3 a4" <<EOF
EOF
test_output_expect_success 'graph with c3 and a4 parents of head' "list_duplicates git-rev-list m1" <<EOF
EOF
test_output_expect_success 'graph with a4 and c3 parents of head' "list_duplicates git-rev-list m2" <<EOF
EOF
test_expect_success "head ^head --merge-order" 'git-rev-list --merge-order --show-breaks a3 ^a3' <<EOF
EOF
#
# can't test this now - duplicate parents can't be created
#
#test_output_expect_success 'duplicate parents' 'git-rev-list --parents --merge-order --show-breaks m3' <<EOF
#= m3 c3 a4 c3
#| a4 c3 b4 a3
#| b4 a3 b3
#| b3 b2
#^ a3 a2
#| a2 a1
#| a1 a0
#^ c3 c2
#| c2 b2 c1
#| b2 b1
#^ c1 b1
#| b1 a0
#= a0 l2
#| l2 l1
#| l1 l0
#| l0 root
#= root
#EOF
test_expect_success "head ^head no --merge-order" 'git-rev-list a3 ^a3' <<EOF
EOF
test_output_expect_success 'simple merge order (l5r1)' 'git-rev-list --merge-order --show-breaks l5r1' <<EOF
= l5r1
| r1
| r0
| alt_root
^ l5
| l4
| l3
| a4
| c3
| c2
| c1
^ b4
| b3
| b2
| b1
^ a3
| a2
| a1
| a0
| l2
| l1
| l0
= root
EOF
test_output_expect_success 'simple merge order (r1l5)' 'git-rev-list --merge-order --show-breaks r1l5' <<EOF
= r1l5
| l5
| l4
| l3
| a4
| c3
| c2
| c1
^ b4
| b3
| b2
| b1
^ a3
| a2
| a1
| a0
| l2
| l1
| l0
| root
^ r1
| r0
= alt_root
EOF
test_output_expect_success "don't print things unreachable from one branch" "git-rev-list a3 ^b3 --merge-order" <<EOF
a3
a2
a1
EOF
test_output_expect_success "--merge-order a4 l3" "git-rev-list --merge-order a4 l3" <<EOF
l3
a4
c3
c2
c1
b4
b3
b2
b1
a3
a2
a1
a0
l2
l1
l0
root
EOF
#
#
test_done
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册