mirror of
https://github.com/git-for-windows/git.git
synced 2026-06-18 10:23:06 -05:00
Merge branch 'mv/log-follow-mergy' into seen
"git log --follow" has been updated to handle non-linear history, in which the path being tracked gets renamed differently in multiple history lines, better. * mv/log-follow-mergy: log: improve --follow following renames for non-linear history
This commit is contained in:
@@ -53,8 +53,7 @@ This is the same as the `--decorate` option of the `git log`.
|
||||
`log.follow`::
|
||||
If `true`, `git log` will act as if the `--follow` option was used when
|
||||
a single <path> is given. This has the same limitations as `--follow`,
|
||||
i.e. it cannot be used to follow multiple files and does not work well
|
||||
on non-linear history.
|
||||
i.e. it cannot be used to follow multiple files.
|
||||
|
||||
`log.graphColors`::
|
||||
A list of colors, separated by commas, that can be used to draw
|
||||
|
||||
116
log-tree.c
116
log-tree.c
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "git-compat-util.h"
|
||||
#include "commit-reach.h"
|
||||
#include "commit-slab.h"
|
||||
#include "config.h"
|
||||
#include "diff.h"
|
||||
#include "diffcore.h"
|
||||
@@ -1089,6 +1090,96 @@ static int do_remerge_diff(struct rev_info *opt,
|
||||
return !opt->loginfo;
|
||||
}
|
||||
|
||||
/* Per-commit path storage for --follow across merges */
|
||||
define_commit_slab(follow_pathspec_slab, char *);
|
||||
|
||||
static const char *pathspec_single_path(const struct pathspec *ps)
|
||||
{
|
||||
if (ps->nr != 1)
|
||||
return NULL;
|
||||
return ps->items[0].match;
|
||||
}
|
||||
|
||||
static void set_pathspec_to_single_path(struct pathspec *ps, const char *path)
|
||||
{
|
||||
const char *paths[2] = { path, NULL };
|
||||
|
||||
clear_pathspec(ps);
|
||||
parse_pathspec(ps,
|
||||
PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,
|
||||
PATHSPEC_LITERAL_PATH, "", paths);
|
||||
}
|
||||
|
||||
static void remember_follow_pathspec(struct rev_info *opt,
|
||||
struct commit *c, const char *path)
|
||||
{
|
||||
char **slot;
|
||||
|
||||
if (!path)
|
||||
return;
|
||||
if (!opt->follow_pathspec_slab) {
|
||||
opt->follow_pathspec_slab = xmalloc(sizeof(*opt->follow_pathspec_slab));
|
||||
init_follow_pathspec_slab(opt->follow_pathspec_slab);
|
||||
}
|
||||
slot = follow_pathspec_slab_at(opt->follow_pathspec_slab, c);
|
||||
if (*slot && !strcmp(*slot, path))
|
||||
return;
|
||||
free(*slot);
|
||||
*slot = xstrdup(path);
|
||||
}
|
||||
|
||||
static const char *recall_follow_pathspec(struct rev_info *opt,
|
||||
struct commit *c)
|
||||
{
|
||||
char **slot;
|
||||
|
||||
if (!opt->follow_pathspec_slab)
|
||||
return NULL;
|
||||
slot = follow_pathspec_slab_peek(opt->follow_pathspec_slab, c);
|
||||
return slot ? *slot : NULL;
|
||||
}
|
||||
|
||||
static void free_follow_pathspec_slot(char **slot)
|
||||
{
|
||||
FREE_AND_NULL(*slot);
|
||||
}
|
||||
|
||||
void release_follow_pathspec_slab(struct rev_info *opt)
|
||||
{
|
||||
if (!opt->follow_pathspec_slab)
|
||||
return;
|
||||
deep_clear_follow_pathspec_slab(opt->follow_pathspec_slab,
|
||||
free_follow_pathspec_slot);
|
||||
FREE_AND_NULL(opt->follow_pathspec_slab);
|
||||
}
|
||||
|
||||
/* Compute a path to follow in parent, if there is one */
|
||||
static void propagate_follow_pathspec_to_parent(struct rev_info *opt,
|
||||
struct commit *commit,
|
||||
struct commit *parent)
|
||||
{
|
||||
struct diff_options diff_opts;
|
||||
const char *path;
|
||||
|
||||
parse_commit_or_die(parent);
|
||||
repo_diff_setup(opt->diffopt.repo, &diff_opts);
|
||||
copy_pathspec(&diff_opts.pathspec, &opt->diffopt.pathspec);
|
||||
diff_opts.flags.recursive = 1;
|
||||
diff_opts.flags.follow_renames = 1;
|
||||
diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
|
||||
diff_setup_done(&diff_opts);
|
||||
diff_tree_oid(get_commit_tree_oid(parent),
|
||||
get_commit_tree_oid(commit),
|
||||
"", &diff_opts);
|
||||
|
||||
path = pathspec_single_path(&diff_opts.pathspec);
|
||||
if (path)
|
||||
remember_follow_pathspec(opt, parent, path);
|
||||
|
||||
diff_queue_clear(&diff_queued_diff);
|
||||
diff_free(&diff_opts);
|
||||
}
|
||||
|
||||
/*
|
||||
* Show the diff of a commit.
|
||||
*
|
||||
@@ -1185,6 +1276,16 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
|
||||
opt->loginfo = &log;
|
||||
opt->diffopt.no_free = 1;
|
||||
|
||||
/* Any recorded path for this commit? If so, restore it */
|
||||
if (opt->diffopt.flags.follow_renames) {
|
||||
const char *stored = recall_follow_pathspec(opt, commit);
|
||||
if (stored) {
|
||||
const char *current = pathspec_single_path(&opt->diffopt.pathspec);
|
||||
if (!current || strcmp(current, stored))
|
||||
set_pathspec_to_single_path(&opt->diffopt.pathspec, stored);
|
||||
}
|
||||
}
|
||||
|
||||
if (opt->track_linear && !opt->linear && !opt->reverse_output_stage)
|
||||
fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar);
|
||||
shown = log_tree_diff(opt, commit, &log);
|
||||
@@ -1197,6 +1298,21 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
|
||||
fprintf(opt->diffopt.file, "\n%s\n", opt->break_bar);
|
||||
if (shown)
|
||||
show_diff_of_diff(opt);
|
||||
|
||||
/* Record what path each parent of this commit should use */
|
||||
if (opt->diffopt.flags.follow_renames) {
|
||||
struct commit_list *parents = get_saved_parents(opt, commit);
|
||||
if (parents && parents->next) {
|
||||
struct commit_list *p;
|
||||
for (p = parents; p; p = p->next)
|
||||
propagate_follow_pathspec_to_parent(opt, commit,
|
||||
p->item);
|
||||
} else if (parents) {
|
||||
remember_follow_pathspec(opt, parents->item,
|
||||
pathspec_single_path(&opt->diffopt.pathspec));
|
||||
}
|
||||
}
|
||||
|
||||
opt->loginfo = NULL;
|
||||
maybe_flush_or_die(opt->diffopt.file, "stdout");
|
||||
opt->diffopt.no_free = no_free;
|
||||
|
||||
@@ -26,6 +26,7 @@ struct decoration_options {
|
||||
int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);
|
||||
int log_tree_diff_flush(struct rev_info *);
|
||||
int log_tree_commit(struct rev_info *, struct commit *);
|
||||
void release_follow_pathspec_slab(struct rev_info *);
|
||||
void show_log(struct rev_info *opt);
|
||||
void format_decorations(struct strbuf *sb, const struct commit *commit,
|
||||
enum git_colorbool use_color, const struct decoration_options *opts);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "decorate.h"
|
||||
#include "string-list.h"
|
||||
#include "line-log.h"
|
||||
#include "log-tree.h"
|
||||
#include "mailmap.h"
|
||||
#include "commit-slab.h"
|
||||
#include "cache-tree.h"
|
||||
@@ -3302,6 +3303,7 @@ void release_revisions(struct rev_info *revs)
|
||||
line_log_free(revs);
|
||||
oidset_clear(&revs->missing_commits);
|
||||
release_revisions_bloom_keyvecs(revs);
|
||||
release_follow_pathspec_slab(revs);
|
||||
}
|
||||
|
||||
static void add_child(struct rev_info *revs, struct commit *parent, struct commit *child)
|
||||
|
||||
@@ -66,6 +66,7 @@ struct repository;
|
||||
struct rev_info;
|
||||
struct string_list;
|
||||
struct saved_parents;
|
||||
struct follow_pathspec_slab;
|
||||
struct bloom_keyvec;
|
||||
struct bloom_filter_settings;
|
||||
struct option;
|
||||
@@ -363,6 +364,9 @@ struct rev_info {
|
||||
/* copies of the parent lists, for --full-diff display */
|
||||
struct saved_parents *saved_parents_slab;
|
||||
|
||||
/* per-commit pathspec for --follow across merges */
|
||||
struct follow_pathspec_slab *follow_pathspec_slab;
|
||||
|
||||
struct commit_list *previous_parents;
|
||||
struct commit_list *ancestry_path_bottoms;
|
||||
const char *break_bar;
|
||||
|
||||
@@ -580,6 +580,7 @@ integration_tests = [
|
||||
't4216-log-bloom.sh',
|
||||
't4217-log-limit.sh',
|
||||
't4218-log-graph-indentation.sh',
|
||||
't4219-log-follow-merge.sh',
|
||||
't4252-am-options.sh',
|
||||
't4253-am-keep-cr-dos.sh',
|
||||
't4254-am-corrupt.sh',
|
||||
|
||||
129
t/t4219-log-follow-merge.sh
Executable file
129
t/t4219-log-follow-merge.sh
Executable file
@@ -0,0 +1,129 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='Test --follow follows renames across merges'
|
||||
|
||||
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master
|
||||
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
test_expect_success 'setup subtree-merged repository' '
|
||||
git init inner &&
|
||||
echo inner >inner/inner.txt &&
|
||||
git -C inner add inner.txt &&
|
||||
git -C inner commit -m "inner init" &&
|
||||
|
||||
git init outer &&
|
||||
echo outer >outer/outer.txt &&
|
||||
git -C outer add outer.txt &&
|
||||
git -C outer commit -m "outer init" &&
|
||||
|
||||
git -C outer fetch ../inner master &&
|
||||
git -C outer merge -s ours --no-commit --allow-unrelated-histories \
|
||||
FETCH_HEAD &&
|
||||
git -C outer read-tree --prefix=inner/ -u FETCH_HEAD &&
|
||||
git -C outer commit -m "Merge inner repo into inner/ subdirectory"
|
||||
'
|
||||
|
||||
test_expect_success '--follow finds the pre-merge commit through a subtree merge' '
|
||||
git -C outer log --follow --pretty=tformat:%s inner/inner.txt >actual &&
|
||||
echo "inner init" >expect &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success 'setup merge of two branches that both renamed a file to README' '
|
||||
git init foo &&
|
||||
mkdir foo/foo &&
|
||||
echo "foo readme" >foo/foo/README &&
|
||||
git -C foo add foo/README &&
|
||||
git -C foo commit -m "add foo README" &&
|
||||
|
||||
git -C foo mv foo/README README &&
|
||||
git -C foo commit -m "promote foo README to toplevel" &&
|
||||
|
||||
echo "foo c" >foo/foo.c &&
|
||||
git -C foo add foo.c &&
|
||||
git -C foo commit -m "add foo C impl" &&
|
||||
|
||||
git init bar &&
|
||||
mkdir bar/bar &&
|
||||
echo "bar readme" >bar/bar/README &&
|
||||
git -C bar add bar/README &&
|
||||
git -C bar commit -m "add bar README" &&
|
||||
|
||||
git -C bar mv bar/README README &&
|
||||
git -C bar commit -m "promote bar README to toplevel" &&
|
||||
|
||||
echo "bar c" >bar/bar.c &&
|
||||
git -C bar add bar.c &&
|
||||
git -C bar commit -m "add bar C impl" &&
|
||||
|
||||
git -C foo fetch ../bar master &&
|
||||
git -C foo merge -s ours --no-commit --allow-unrelated-histories \
|
||||
FETCH_HEAD &&
|
||||
git -C foo checkout FETCH_HEAD -- bar.c &&
|
||||
git -C foo commit -m "merge bar into foo"
|
||||
'
|
||||
|
||||
test_expect_success '--follow follows renames across both sides of a merge' '
|
||||
git -C foo log --follow --pretty=tformat:%s README >actual &&
|
||||
sort actual >actual.sorted &&
|
||||
cat >expect <<-\EOF &&
|
||||
add bar README
|
||||
add foo README
|
||||
promote bar README to toplevel
|
||||
promote foo README to toplevel
|
||||
EOF
|
||||
test_cmp expect actual.sorted
|
||||
'
|
||||
|
||||
test_expect_success 'setup diamond with renames on both sides of a fork' '
|
||||
git init diamond &&
|
||||
test_lines="line 1\nline 2\nline 3\nline 4\nline 5\n" &&
|
||||
|
||||
printf "$test_lines" >diamond/path0 &&
|
||||
git -C diamond add path0 &&
|
||||
git -C diamond commit -m "A: add path0" &&
|
||||
|
||||
git -C diamond checkout -b upper &&
|
||||
printf "line 1\nline 2\nline 3 modified by B\nline 4\nline 5\n" \
|
||||
>diamond/path0 &&
|
||||
git -C diamond commit -am "B: modify path0 on upper" &&
|
||||
git -C diamond mv path0 path1 &&
|
||||
git -C diamond commit -m "X: rename path0 to path1" &&
|
||||
|
||||
git -C diamond checkout -b lower master &&
|
||||
printf "line 1\nline 2\nline 3 modified by C\nline 4\nline 5\n" \
|
||||
>diamond/path0 &&
|
||||
git -C diamond commit -am "C: modify path0 on lower" &&
|
||||
git -C diamond mv path0 path2 &&
|
||||
git -C diamond commit -m "Y: rename path0 to path2" &&
|
||||
|
||||
git -C diamond checkout upper &&
|
||||
git -C diamond merge -s ours --no-commit lower &&
|
||||
git -C diamond rm path1 &&
|
||||
printf "line 1\nline 2\nline 3 merged\nline 4\nline 5\n" \
|
||||
>diamond/path &&
|
||||
git -C diamond add path &&
|
||||
git -C diamond commit -m "M: merge with rename to path" &&
|
||||
|
||||
printf "line 1\nline 2\nline 3 merged again\nline 4\nline 5\n" \
|
||||
>diamond/path &&
|
||||
git -C diamond commit -am "Z: modify path"
|
||||
'
|
||||
|
||||
test_expect_success '--follow follows renames through a fork in a single history' '
|
||||
git -C diamond log --follow --pretty=tformat:%s path >actual &&
|
||||
sort actual >actual.sorted &&
|
||||
cat >expect <<-\EOF &&
|
||||
A: add path0
|
||||
B: modify path0 on upper
|
||||
C: modify path0 on lower
|
||||
X: rename path0 to path1
|
||||
Y: rename path0 to path2
|
||||
Z: modify path
|
||||
EOF
|
||||
test_cmp expect actual.sorted
|
||||
'
|
||||
|
||||
test_done
|
||||
Reference in New Issue
Block a user