diff --git a/builtin-log.c b/builtin-log.c index b9035ab7997612a91cd032e3cb1bcd5296585768..073a2a16a3fafd66d13b1ed106a0016617ec63ad 100644 --- a/builtin-log.c +++ b/builtin-log.c @@ -58,6 +58,11 @@ static void cmd_log_init(int argc, const char **argv, const char *prefix, argc = setup_revisions(argc, argv, rev, "HEAD"); if (rev->diffopt.pickaxe || rev->diffopt.filter) rev->always_show_header = 0; + if (rev->diffopt.follow_renames) { + rev->always_show_header = 0; + if (rev->diffopt.nr_paths != 1) + usage("git logs can only follow renames on one pathname at a time"); + } for (i = 1; i < argc; i++) { const char *arg = argv[i]; if (!strcmp(arg, "--decorate")) { diff --git a/diff.c b/diff.c index 4aa9bbc0116ac9081dee7aa8aa85507c401b5e53..9938969fa50e8af2a4eabe96ae122111ae42fe75 100644 --- a/diff.c +++ b/diff.c @@ -2210,6 +2210,8 @@ int diff_opt_parse(struct diff_options *options, const char **av, int ac) } else if (!strcmp(arg, "--find-copies-harder")) options->find_copies_harder = 1; + else if (!strcmp(arg, "--follow")) + options->follow_renames = 1; else if (!strcmp(arg, "--abbrev")) options->abbrev = DEFAULT_ABBREV; else if (!prefixcmp(arg, "--abbrev=")) { diff --git a/diff.h b/diff.h index a7ee6d8c87887b111ef84b95266e1ff43496e7c3..9fd6d447d4c62e158c941a736e547c7e516cdd93 100644 --- a/diff.h +++ b/diff.h @@ -55,6 +55,7 @@ struct diff_options { full_index:1, silent_on_remove:1, find_copies_harder:1, + follow_renames:1, color_diff:1, color_diff_words:1, has_changes:1, diff --git a/revision.c b/revision.c index 1f4590b89649a9d1397af2f35af142cc6ab36847..7834bb108e27a819a4a619a85123443f254d421d 100644 --- a/revision.c +++ b/revision.c @@ -1230,7 +1230,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch if (revs->prune_data) { diff_tree_setup_paths(revs->prune_data, &revs->pruning); - revs->prune_fn = try_to_simplify_commit; + /* Can't prune commits with rename following: the paths change.. */ + if (!revs->diffopt.follow_renames) + revs->prune_fn = try_to_simplify_commit; if (!revs->full_diff) diff_tree_setup_paths(revs->prune_data, &revs->diffopt); } diff --git a/tree-diff.c b/tree-diff.c index 852498eb49fe0cba5e1cd21661288e8321a4b20e..26bdbdd2bfea5eab99b9f1c38936efe56f1ab45a 100644 --- a/tree-diff.c +++ b/tree-diff.c @@ -3,6 +3,7 @@ */ #include "cache.h" #include "diff.h" +#include "diffcore.h" #include "tree.h" static char *malloc_base(const char *base, int baselen, const char *path, int pathlen) @@ -290,6 +291,78 @@ int diff_tree(struct tree_desc *t1, struct tree_desc *t2, const char *base, stru return 0; } +/* + * Does it look like the resulting diff might be due to a rename? + * - single entry + * - not a valid previous file + */ +static inline int diff_might_be_rename(void) +{ + return diff_queued_diff.nr == 1 && + !DIFF_FILE_VALID(diff_queued_diff.queue[0]->one); +} + +static void try_to_follow_renames(struct tree_desc *t1, struct tree_desc *t2, const char *base, struct diff_options *opt) +{ + struct diff_options diff_opts; + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_filepair *choice; + const char *paths[1]; + int i; + + /* Remove the file creation entry from the diff queue, and remember it */ + choice = q->queue[0]; + q->nr = 0; + + diff_setup(&diff_opts); + diff_opts.recursive = 1; + diff_opts.detect_rename = DIFF_DETECT_RENAME; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_opts.single_follow = opt->paths[0]; + paths[0] = NULL; + diff_tree_setup_paths(paths, &diff_opts); + if (diff_setup_done(&diff_opts) < 0) + die("unable to set up diff options to follow renames"); + diff_tree(t1, t2, base, &diff_opts); + diffcore_std(&diff_opts); + + /* Go through the new set of filepairing, and see if we find a more interesting one */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + + /* + * Found a source? Not only do we use that for the new + * diff_queued_diff, we will also use that as the path in + * the future! + */ + if ((p->status == 'R' || p->status == 'C') && !strcmp(p->two->path, opt->paths[0])) { + /* Switch the file-pairs around */ + q->queue[i] = choice; + choice = p; + + /* Update the path we use from now on.. */ + opt->paths[0] = xstrdup(p->one->path); + diff_tree_setup_paths(opt->paths, opt); + break; + } + } + + /* + * Then, discard all the non-relevane file pairs... + */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + diff_free_filepair(p); + } + + /* + * .. and re-instate the one we want (which might be either the + * original one, or the rename/copy we found) + */ + q->queue[0] = choice; + q->nr = 1; +} + int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const char *base, struct diff_options *opt) { void *tree1, *tree2; @@ -306,6 +379,11 @@ int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const cha init_tree_desc(&t1, tree1, size1); init_tree_desc(&t2, tree2, size2); retval = diff_tree(&t1, &t2, base, opt); + if (opt->follow_renames && diff_might_be_rename()) { + init_tree_desc(&t1, tree1, size1); + init_tree_desc(&t2, tree2, size2); + try_to_follow_renames(&t1, &t2, base, opt); + } free(tree1); free(tree2); return retval;