Merge branch 'en/combined-all-paths'
authorJunio C Hamano <gitster@pobox.com>
Thu, 7 Mar 2019 00:59:54 +0000 (09:59 +0900)
committerJunio C Hamano <gitster@pobox.com>
Thu, 7 Mar 2019 00:59:54 +0000 (09:59 +0900)
Output from "diff --cc" did not show the original paths when the
merge involved renames. A new option adds the paths in the
original trees to the output.

* en/combined-all-paths:
log,diff-tree: add --combined-all-paths option

1  2 
Documentation/git-diff-tree.txt
Documentation/rev-list-options.txt
builtin/diff-tree.c
combine-diff.c
diff.h
revision.c
revision.h
index 43daa7c046f472ac856d89292ede96f5e5dcb64e,28b93ecd54fd638ffb7b8ccb39f89b38d4e6f362..24f32e8c544a455829b25193ac6b646504ab5f34
@@@ -10,8 -10,8 +10,8 @@@ SYNOPSI
  --------
  [verse]
  'git diff-tree' [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty]
-             [-t] [-r] [-c | --cc] [--root] [<common diff options>]
-             <tree-ish> [<tree-ish>] [<path>...]
+             [-t] [-r] [-c | --cc] [--combined-all-paths] [--root]
+             [<common diff options>] <tree-ish> [<tree-ish>] [<path>...]
  
  DESCRIPTION
  -----------
@@@ -31,7 -31,10 +31,7 @@@ include::diff-options.txt[
  
  <path>...::
        If provided, the results are limited to a subset of files
 -      matching one of these prefix strings.
 -      i.e., file matches `/^<pattern1>|<pattern2>|.../`
 -      Note that this parameter does not provide any wildcard or regexp
 -      features.
 +      matching one of the provided pathspecs.
  
  -r::
          recurse into sub-trees
@@@ -105,12 -108,65 +105,19 @@@ include::pretty-options.txt[
        itself and the commit log message is not shown, just like in any other
        "empty diff" case.
  
+ --combined-all-paths::
+       This flag causes combined diffs (used for merge commits) to
+       list the name of the file from all parents.  It thus only has
+       effect when -c or --cc are specified, and is likely only
+       useful if filename changes are detected (i.e. when either
+       rename or copy detection have been requested).
  --always::
        Show the commit itself and the commit log message even
        if the diff itself is empty.
  
  
  include::pretty-formats.txt[]
 -
 -
 -LIMITING OUTPUT
 ----------------
 -If you're only interested in differences in a subset of files, for
 -example some architecture-specific files, you might do:
 -
 -      git diff-tree -r <tree-ish> <tree-ish> arch/ia64 include/asm-ia64
 -
 -and it will only show you what changed in those two directories.
 -
 -Or if you are searching for what changed in just `kernel/sched.c`, just do
 -
 -      git diff-tree -r <tree-ish> <tree-ish> kernel/sched.c
 -
 -and it will ignore all differences to other files.
 -
 -The pattern is always the prefix, and is matched exactly.  There are no
 -wildcards.  Even stricter, it has to match a complete path component.
 -I.e. "foo" does not pick up `foobar.h`.  "foo" does match `foo/bar.h`
 -so it can be used to name subdirectories.
 -
 -An example of normal usage is:
 -
 -  torvalds@ppc970:~/git> git diff-tree --abbrev 5319e4
 -  :100664 100664 ac348b... a01513...  git-fsck-objects.c
 -
 -which tells you that the last commit changed just one file (it's from
 -this one:
 -
 ------------------------------------------------------------------------------
 -commit 3c6f7ca19ad4043e9e72fa94106f352897e651a8
 -tree 5319e4d609cdd282069cc4dce33c1db559539b03
 -parent b4e628ea30d5ab3606119d2ea5caeab141d38df7
 -author Linus Torvalds <torvalds@ppc970.osdl.org> Sat Apr 9 12:02:30 2005
 -committer Linus Torvalds <torvalds@ppc970.osdl.org> Sat Apr 9 12:02:30 2005
 -
 -Make "git-fsck-objects" print out all the root commits it finds.
 -
 -Once I do the reference tracking, I'll also make it print out all the
 -HEAD commits it finds, which is even more interesting.
 ------------------------------------------------------------------------------
 -
 -in case you care).
 -
 -
  include::diff-format.txt[]
  
  GIT
index cad711ce0ac060d9356e49599ffc13d130fc593c,642ce943c1796211568e18f09c105785909b192b..ca959a72862048c205d67576fcd6c538f0c91b26
@@@ -270,13 -270,13 +270,13 @@@ depending on a few rules
  +
  --
  1. If the starting point is specified as `ref@{Nth}`, show the index
 -format.
 +   format.
  +
  2. If the starting point was specified as `ref@{now}`, show the
 -timestamp format.
 +   timestamp format.
  +
  3. If neither was used, but `--date` was given on the command line, show
 -the timestamp in the format requested by `--date`.
 +   the timestamp in the format requested by `--date`.
  +
  4. Otherwise, show the index format.
  --
@@@ -730,13 -730,8 +730,13 @@@ specification contained in <path>
  +
  The form '--filter=tree:<depth>' omits all blobs and trees whose depth
  from the root tree is >= <depth> (minimum depth if an object is located
 -at multiple depths in the commits traversed). Currently, only <depth>=0
 -is supported, which omits all blobs and trees.
 +at multiple depths in the commits traversed). <depth>=0 will not include
 +any trees or blobs unless included explicitly in the command-line (or
 +standard input when --stdin is used). <depth>=1 will include only the
 +tree and blobs which are referenced directly by a commit reachable from
 +<commit> or an explicitly-given object. <depth>=2 is like <depth>=1
 +while also including trees and blobs one more level removed from an
 +explicitly-given commit or tree.
  
  --no-filter::
        Turn off any previous `--filter=` argument.
@@@ -836,13 -831,6 +836,13 @@@ Note that the `-local` option does not 
  value (which is always measured in UTC), but does switch the accompanying
  timezone value.
  +
 +`--date=human` shows the timezone if the timezone does not match the
 +current time-zone, and doesn't print the whole date if that matches
 +(ie skip printing year for dates that are "this year", but also skip
 +the whole date itself if it's in the last few days and we can just say
 +what weekday it was).  For older dates the hour and minute is also
 +omitted.
 ++
  `--date=unix` shows the date as a Unix epoch timestamp (seconds since
  1970).  As with `--raw`, this is always in UTC and therefore `-local`
  has no effect.
@@@ -960,6 -948,13 +960,13 @@@ options may be given. See linkgit:git-d
        the parents have only two variants and the merge result picks
        one of them without modification.
  
+ --combined-all-paths::
+       This flag causes combined diffs (used for merge commits) to
+       list the name of the file from all parents.  It thus only has
+       effect when -c or --cc are specified, and is likely only
+       useful if filename changes are detected (i.e. when either
+       rename or copy detection have been requested).
  -m::
        This flag makes the merge commits show the full diff like
        regular commits; for each merge parent, a separate log entry
diff --combined builtin/diff-tree.c
index a90681bcbafcba04af3b669f0758c78ccecdd11f,ebe48f60c159bf61ae1dcf66448db861f0060519..cb9ea793675c850fb4bf9ab87a8e1b62f1695b02
@@@ -1,4 -1,3 +1,4 @@@
 +#define USE_THE_INDEX_COMPATIBILITY_MACROS
  #include "cache.h"
  #include "config.h"
  #include "diff.h"
@@@ -83,9 -82,13 +83,13 @@@ static int diff_tree_stdin(char *line
  }
  
  static const char diff_tree_usage[] =
- "git diff-tree [--stdin] [-m] [-c] [--cc] [-s] [-v] [--pretty] [-t] [-r] [--root] "
+ "git diff-tree [--stdin] [-m] [-c | --cc] [-s] [-v] [--pretty] [-t] [-r] [--root] "
  "[<common-diff-options>] <tree-ish> [<tree-ish>] [<path>...]\n"
  "  -r            diff recursively\n"
+ "  -c            show combined diff for merge commits\n"
+ "  --cc          show combined diff for merge commits removing uninteresting hunks\n"
+ "  --combined-all-paths\n"
+ "                show name of file in all parents for combined diffs\n"
  "  --root        include the initial commit as diff against /dev/null\n"
  COMMON_DIFF_OPTIONS_HELP;
  
@@@ -166,7 -169,7 +170,7 @@@ int cmd_diff_tree(int argc, const char 
  
                if (opt->diffopt.detect_rename) {
                        if (!the_index.cache)
 -                              read_index(&the_index);
 +                              repo_read_index(the_repository);
                        opt->diffopt.setup |= DIFF_SETUP_USE_SIZE_CACHE;
                }
                while (fgets(line, sizeof(line), stdin)) {
diff --combined combine-diff.c
index 23d8fabe75d9d80b2811acfc20f62d210ec6bba8,54cb892ae5c3ef559d87e61497ae20879a7aa0c0..3e49f3bda84b929f5ac6846dccc51df8de902b89
@@@ -23,11 -23,20 +23,20 @@@ static int compare_paths(const struct c
                                 two->path, strlen(two->path), two->mode);
  }
  
- static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent)
+ static int filename_changed(char status)
+ {
+       return status == 'R' || status == 'C';
+ }
+ static struct combine_diff_path *intersect_paths(
+       struct combine_diff_path *curr,
+       int n,
+       int num_parent,
+       int combined_all_paths)
  {
        struct diff_queue_struct *q = &diff_queued_diff;
        struct combine_diff_path *p, **tail = &curr;
-       int i, cmp;
+       int i, j, cmp;
  
        if (!n) {
                for (i = 0; i < q->nr; i++) {
                        oidcpy(&p->parent[n].oid, &q->queue[i]->one->oid);
                        p->parent[n].mode = q->queue[i]->one->mode;
                        p->parent[n].status = q->queue[i]->status;
+                       if (combined_all_paths &&
+                           filename_changed(p->parent[n].status)) {
+                               strbuf_init(&p->parent[n].path, 0);
+                               strbuf_addstr(&p->parent[n].path,
+                                             q->queue[i]->one->path);
+                       }
                        *tail = p;
                        tail = &p->next;
                }
                if (cmp < 0) {
                        /* p->path not in q->queue[]; drop it */
                        *tail = p->next;
+                       for (j = 0; j < num_parent; j++)
+                               if (combined_all_paths &&
+                                   filename_changed(p->parent[j].status))
+                                       strbuf_release(&p->parent[j].path);
                        free(p);
                        continue;
                }
                oidcpy(&p->parent[n].oid, &q->queue[i]->one->oid);
                p->parent[n].mode = q->queue[i]->one->mode;
                p->parent[n].status = q->queue[i]->status;
+               if (combined_all_paths &&
+                   filename_changed(p->parent[n].status))
+                       strbuf_addstr(&p->parent[n].path,
+                                     q->queue[i]->one->path);
  
                tail = &p->next;
                i++;
@@@ -960,12 -984,25 +984,25 @@@ static void show_combined_header(struc
        if (!show_file_header)
                return;
  
-       if (added)
-               dump_quoted_path("--- ", "", "/dev/null",
-                                line_prefix, c_meta, c_reset);
-       else
-               dump_quoted_path("--- ", a_prefix, elem->path,
-                                line_prefix, c_meta, c_reset);
+       if (rev->combined_all_paths) {
+               for (i = 0; i < num_parent; i++) {
+                       char *path = filename_changed(elem->parent[i].status)
+                               ? elem->parent[i].path.buf : elem->path;
+                       if (elem->parent[i].status == DIFF_STATUS_ADDED)
+                               dump_quoted_path("--- ", "", "/dev/null",
+                                                line_prefix, c_meta, c_reset);
+                       else
+                               dump_quoted_path("--- ", a_prefix, path,
+                                                line_prefix, c_meta, c_reset);
+               }
+       } else {
+               if (added)
+                       dump_quoted_path("--- ", "", "/dev/null",
+                                        line_prefix, c_meta, c_reset);
+               else
+                       dump_quoted_path("--- ", a_prefix, elem->path,
+                                        line_prefix, c_meta, c_reset);
+       }
        if (deleted)
                dump_quoted_path("+++ ", "", "/dev/null",
                                 line_prefix, c_meta, c_reset);
@@@ -1227,6 -1264,15 +1264,15 @@@ static void show_raw_diff(struct combin
                putchar(inter_name_termination);
        }
  
+       for (i = 0; i < num_parent; i++)
+               if (rev->combined_all_paths) {
+                       if (filename_changed(p->parent[i].status))
+                               write_name_quoted(p->parent[i].path.buf, stdout,
+                                                 inter_name_termination);
+                       else
+                               write_name_quoted(p->path, stdout,
+                                                 inter_name_termination);
+               }
        write_name_quoted(p->path, stdout, line_termination);
  }
  
@@@ -1321,18 -1367,12 +1367,20 @@@ static const char *path_path(void *obj
        return path->path;
  }
  
 +/*
 + * Diff stat formats which we always compute solely against the first parent.
 + */
 +#define STAT_FORMAT_MASK (DIFF_FORMAT_NUMSTAT \
 +                        | DIFF_FORMAT_SHORTSTAT \
 +                        | DIFF_FORMAT_SUMMARY \
 +                        | DIFF_FORMAT_DIRSTAT \
 +                        | DIFF_FORMAT_DIFFSTAT)
  
  /* find set of paths that every parent touches */
  static struct combine_diff_path *find_paths_generic(const struct object_id *oid,
-       const struct oid_array *parents, struct diff_options *opt)
+       const struct oid_array *parents,
+       struct diff_options *opt,
+       int combined_all_paths)
  {
        struct combine_diff_path *paths = NULL;
        int i, num_parent = parents->nr;
                 * show stat against the first parent even when doing
                 * combined diff.
                 */
 -              int stat_opt = (output_format &
 -                              (DIFF_FORMAT_NUMSTAT|DIFF_FORMAT_DIFFSTAT));
 +              int stat_opt = output_format & STAT_FORMAT_MASK;
                if (i == 0 && stat_opt)
                        opt->output_format = stat_opt;
                else
                        opt->output_format = DIFF_FORMAT_NO_OUTPUT;
                diff_tree_oid(&parents->oid[i], oid, "", opt);
                diffcore_std(opt);
-               paths = intersect_paths(paths, i, num_parent);
+               paths = intersect_paths(paths, i, num_parent,
+                                       combined_all_paths);
  
                /* if showing diff, show it in requested order */
                if (opt->output_format != DIFF_FORMAT_NO_OUTPUT &&
@@@ -1467,7 -1509,8 +1516,8 @@@ void diff_tree_combined(const struct ob
                 * diff(sha1,parent_i) for all i to do the job, specifically
                 * for parent0.
                 */
-               paths = find_paths_generic(oid, parents, &diffopts);
+               paths = find_paths_generic(oid, parents, &diffopts,
+                                          rev->combined_all_paths);
        }
        else {
                int stat_opt;
                 * show stat against the first parent even
                 * when doing combined diff.
                 */
 -              stat_opt = (opt->output_format &
 -                              (DIFF_FORMAT_NUMSTAT|DIFF_FORMAT_DIFFSTAT));
 +              stat_opt = opt->output_format & STAT_FORMAT_MASK;
                if (stat_opt) {
                        diffopts.output_format = stat_opt;
  
                                show_raw_diff(p, num_parent, rev);
                        needsep = 1;
                }
 -              else if (opt->output_format &
 -                       (DIFF_FORMAT_NUMSTAT|DIFF_FORMAT_DIFFSTAT))
 +              else if (opt->output_format & STAT_FORMAT_MASK)
                        needsep = 1;
                else if (opt->output_format & DIFF_FORMAT_CALLBACK)
                        handle_combined_callback(opt, paths, num_parent, num_paths);
        while (paths) {
                struct combine_diff_path *tmp = paths;
                paths = paths->next;
+               for (i = 0; i < num_parent; i++)
+                       if (rev->combined_all_paths &&
+                           filename_changed(tmp->parent[i].status))
+                               strbuf_release(&tmp->parent[i].path);
                free(tmp);
        }
  
diff --combined diff.h
index d9ad73f0e171e325240ff67f3351245547ca1858,90ea0256a584bb3c4feccb4ee907b5b7ca2a6636..3199ba31f8d697d0a73f64cde12433d8f1244181
--- 1/diff.h
--- 2/diff.h
+++ b/diff.h
@@@ -9,17 -9,16 +9,17 @@@
  #include "object.h"
  #include "oidset.h"
  
 -struct rev_info;
 +struct combine_diff_path;
 +struct commit;
 +struct diff_filespec;
  struct diff_options;
  struct diff_queue_struct;
 -struct strbuf;
 -struct diff_filespec;
 -struct userdiff_driver;
  struct oid_array;
 -struct commit;
 -struct combine_diff_path;
 +struct option;
  struct repository;
 +struct rev_info;
 +struct strbuf;
 +struct userdiff_driver;
  
  typedef int (*pathchange_fn_t)(struct diff_options *options,
                 struct combine_diff_path *path);
@@@ -65,39 -64,39 +65,39 @@@ typedef struct strbuf *(*diff_prefix_fn
  
  #define DIFF_FLAGS_INIT { 0 }
  struct diff_flags {
 -      unsigned recursive:1;
 -      unsigned tree_in_recursive:1;
 -      unsigned binary:1;
 -      unsigned text:1;
 -      unsigned full_index:1;
 -      unsigned silent_on_remove:1;
 -      unsigned find_copies_harder:1;
 -      unsigned follow_renames:1;
 -      unsigned rename_empty:1;
 -      unsigned has_changes:1;
 -      unsigned quick:1;
 -      unsigned no_index:1;
 -      unsigned allow_external:1;
 -      unsigned exit_with_status:1;
 -      unsigned reverse_diff:1;
 -      unsigned check_failed:1;
 -      unsigned relative_name:1;
 -      unsigned ignore_submodules:1;
 -      unsigned dirstat_cumulative:1;
 -      unsigned dirstat_by_file:1;
 -      unsigned allow_textconv:1;
 -      unsigned textconv_set_via_cmdline:1;
 -      unsigned diff_from_contents:1;
 -      unsigned dirty_submodules:1;
 -      unsigned ignore_untracked_in_submodules:1;
 -      unsigned ignore_dirty_submodules:1;
 -      unsigned override_submodule_config:1;
 -      unsigned dirstat_by_line:1;
 -      unsigned funccontext:1;
 -      unsigned default_follow_renames:1;
 -      unsigned stat_with_summary:1;
 -      unsigned suppress_diff_headers:1;
 -      unsigned dual_color_diffed_diffs:1;
 +      unsigned recursive;
 +      unsigned tree_in_recursive;
 +      unsigned binary;
 +      unsigned text;
 +      unsigned full_index;
 +      unsigned silent_on_remove;
 +      unsigned find_copies_harder;
 +      unsigned follow_renames;
 +      unsigned rename_empty;
 +      unsigned has_changes;
 +      unsigned quick;
 +      unsigned no_index;
 +      unsigned allow_external;
 +      unsigned exit_with_status;
 +      unsigned reverse_diff;
 +      unsigned check_failed;
 +      unsigned relative_name;
 +      unsigned ignore_submodules;
 +      unsigned dirstat_cumulative;
 +      unsigned dirstat_by_file;
 +      unsigned allow_textconv;
 +      unsigned textconv_set_via_cmdline;
 +      unsigned diff_from_contents;
 +      unsigned dirty_submodules;
 +      unsigned ignore_untracked_in_submodules;
 +      unsigned ignore_dirty_submodules;
 +      unsigned override_submodule_config;
 +      unsigned dirstat_by_line;
 +      unsigned funccontext;
 +      unsigned default_follow_renames;
 +      unsigned stat_with_summary;
 +      unsigned suppress_diff_headers;
 +      unsigned dual_color_diffed_diffs;
  };
  
  static inline void diff_flags_or(struct diff_flags *a,
@@@ -230,7 -229,6 +230,7 @@@ struct diff_options 
        unsigned color_moved_ws_handling;
  
        struct repository *repo;
 +      struct option *parseopts;
  };
  
  void diff_emit_submodule_del(struct diff_options *o, const char *line);
@@@ -296,6 -294,7 +296,7 @@@ struct combine_diff_path 
                char status;
                unsigned int mode;
                struct object_id oid;
+               struct strbuf path;
        } parent[FLEX_ARRAY];
  };
  #define combine_diff_path_size(n, l) \
diff --combined revision.c
index 162d511d4686bc43f2032a57fa95e3601eb3d6b2,384fa69aed1d1b8d69f3906d1b9d6404e653b1d5..eb8e51bc6302b65dfc5a921d59b2bcacd6cc19d9
@@@ -27,7 -27,6 +27,7 @@@
  #include "commit-reach.h"
  #include "commit-graph.h"
  #include "prio-queue.h"
 +#include "hashmap.h"
  
  volatile show_early_output_fn_t show_early_output;
  
@@@ -100,148 -99,6 +100,148 @@@ void mark_tree_uninteresting(struct rep
        mark_tree_contents_uninteresting(r, tree);
  }
  
 +struct path_and_oids_entry {
 +      struct hashmap_entry ent;
 +      char *path;
 +      struct oidset trees;
 +};
 +
 +static int path_and_oids_cmp(const void *hashmap_cmp_fn_data,
 +                           const struct path_and_oids_entry *e1,
 +                           const struct path_and_oids_entry *e2,
 +                           const void *keydata)
 +{
 +      return strcmp(e1->path, e2->path);
 +}
 +
 +static void paths_and_oids_init(struct hashmap *map)
 +{
 +      hashmap_init(map, (hashmap_cmp_fn) path_and_oids_cmp, NULL, 0);
 +}
 +
 +static void paths_and_oids_clear(struct hashmap *map)
 +{
 +      struct hashmap_iter iter;
 +      struct path_and_oids_entry *entry;
 +      hashmap_iter_init(map, &iter);
 +
 +      while ((entry = (struct path_and_oids_entry *)hashmap_iter_next(&iter))) {
 +              oidset_clear(&entry->trees);
 +              free(entry->path);
 +      }
 +
 +      hashmap_free(map, 1);
 +}
 +
 +static void paths_and_oids_insert(struct hashmap *map,
 +                                const char *path,
 +                                const struct object_id *oid)
 +{
 +      int hash = strhash(path);
 +      struct path_and_oids_entry key;
 +      struct path_and_oids_entry *entry;
 +
 +      hashmap_entry_init(&key, hash);
 +
 +      /* use a shallow copy for the lookup */
 +      key.path = (char *)path;
 +      oidset_init(&key.trees, 0);
 +
 +      if (!(entry = (struct path_and_oids_entry *)hashmap_get(map, &key, NULL))) {
 +              entry = xcalloc(1, sizeof(struct path_and_oids_entry));
 +              hashmap_entry_init(entry, hash);
 +              entry->path = xstrdup(key.path);
 +              oidset_init(&entry->trees, 16);
 +              hashmap_put(map, entry);
 +      }
 +
 +      oidset_insert(&entry->trees, oid);
 +}
 +
 +static void add_children_by_path(struct repository *r,
 +                               struct tree *tree,
 +                               struct hashmap *map)
 +{
 +      struct tree_desc desc;
 +      struct name_entry entry;
 +
 +      if (!tree)
 +              return;
 +
 +      if (parse_tree_gently(tree, 1) < 0)
 +              return;
 +
 +      init_tree_desc(&desc, tree->buffer, tree->size);
 +      while (tree_entry(&desc, &entry)) {
 +              switch (object_type(entry.mode)) {
 +              case OBJ_TREE:
 +                      paths_and_oids_insert(map, entry.path, &entry.oid);
 +
 +                      if (tree->object.flags & UNINTERESTING) {
 +                              struct tree *child = lookup_tree(r, &entry.oid);
 +                              if (child)
 +                                      child->object.flags |= UNINTERESTING;
 +                      }
 +                      break;
 +              case OBJ_BLOB:
 +                      if (tree->object.flags & UNINTERESTING) {
 +                              struct blob *child = lookup_blob(r, &entry.oid);
 +                              if (child)
 +                                      child->object.flags |= UNINTERESTING;
 +                      }
 +                      break;
 +              default:
 +                      /* Subproject commit - not in this repository */
 +                      break;
 +              }
 +      }
 +
 +      free_tree_buffer(tree);
 +}
 +
 +void mark_trees_uninteresting_sparse(struct repository *r,
 +                                   struct oidset *trees)
 +{
 +      unsigned has_interesting = 0, has_uninteresting = 0;
 +      struct hashmap map;
 +      struct hashmap_iter map_iter;
 +      struct path_and_oids_entry *entry;
 +      struct object_id *oid;
 +      struct oidset_iter iter;
 +
 +      oidset_iter_init(trees, &iter);
 +      while ((!has_interesting || !has_uninteresting) &&
 +             (oid = oidset_iter_next(&iter))) {
 +              struct tree *tree = lookup_tree(r, oid);
 +
 +              if (!tree)
 +                      continue;
 +
 +              if (tree->object.flags & UNINTERESTING)
 +                      has_uninteresting = 1;
 +              else
 +                      has_interesting = 1;
 +      }
 +
 +      /* Do not walk unless we have both types of trees. */
 +      if (!has_uninteresting || !has_interesting)
 +              return;
 +
 +      paths_and_oids_init(&map);
 +
 +      oidset_iter_init(trees, &iter);
 +      while ((oid = oidset_iter_next(&iter))) {
 +              struct tree *tree = lookup_tree(r, oid);
 +              add_children_by_path(r, tree, &map);
 +      }
 +
 +      hashmap_iter_init(&map, &map_iter);
 +      while ((entry = hashmap_iter_next(&map_iter)))
 +              mark_trees_uninteresting_sparse(r, &entry->trees);
 +
 +      paths_and_oids_clear(&map);
 +}
 +
  struct commit_stack {
        struct commit **items;
        size_t nr, alloc;
@@@ -356,20 -213,7 +356,20 @@@ static struct object *get_reference(str
  {
        struct object *object;
  
 -      object = parse_object(revs->repo, oid);
 +      /*
 +       * If the repository has commit graphs, repo_parse_commit() avoids
 +       * reading the object buffer, so use it whenever possible.
 +       */
 +      if (oid_object_info(revs->repo, oid, NULL) == OBJ_COMMIT) {
 +              struct commit *c = lookup_commit(revs->repo, oid);
 +              if (!repo_parse_commit(revs->repo, c))
 +                      object = (struct object *) c;
 +              else
 +                      object = NULL;
 +      } else {
 +              object = parse_object(revs->repo, oid);
 +      }
 +
        if (!object) {
                if (revs->ignore_missing)
                        return object;
@@@ -1540,7 -1384,7 +1540,7 @@@ void add_index_objects_to_pending(struc
  {
        struct worktree **worktrees, **p;
  
 -      read_index(revs->repo->index);
 +      repo_read_index(revs->repo);
        do_add_index_objects_to_pending(revs, revs->repo->index, flags);
  
        if (revs->single_worktree)
@@@ -1687,7 -1531,7 +1687,7 @@@ static void prepare_show_merge(struct r
        head->object.flags |= SYMMETRIC_LEFT;
  
        if (!istate->cache_nr)
 -              read_index(istate);
 +              repo_read_index(revs->repo);
        for (i = 0; i < istate->cache_nr; i++) {
                const struct cache_entry *ce = istate->cache[i];
                if (!ce_stage(ce))
@@@ -1746,8 -1590,8 +1746,8 @@@ static int handle_dotdot_1(const char *
        if (!*b_name)
                b_name = "HEAD";
  
 -      if (get_oid_with_context(a_name, oc_flags, &a_oid, a_oc) ||
 -          get_oid_with_context(b_name, oc_flags, &b_oid, b_oc))
 +      if (get_oid_with_context(revs->repo, a_name, oc_flags, &a_oid, a_oc) ||
 +          get_oid_with_context(revs->repo, b_name, oc_flags, &b_oid, b_oc))
                return -1;
  
        if (!cant_be_filename) {
@@@ -1881,7 -1725,7 +1881,7 @@@ int handle_revision_arg(const char *arg
        if (revarg_opt & REVARG_COMMITTISH)
                get_sha1_flags |= GET_OID_COMMITTISH;
  
 -      if (get_oid_with_context(arg, get_sha1_flags, &oid, &oc))
 +      if (get_oid_with_context(revs->repo, arg, get_sha1_flags, &oid, &oc))
                return revs->ignore_missing ? 0 : -1;
        if (!cant_be_filename)
                verify_non_filename(revs->prefix, arg);
@@@ -2151,6 -1995,9 +2151,9 @@@ static int handle_revision_opt(struct r
                revs->diff = 1;
                revs->dense_combined_merges = 0;
                revs->combine_merges = 1;
+       } else if (!strcmp(arg, "--combined-all-paths")) {
+               revs->diff = 1;
+               revs->combined_all_paths = 1;
        } else if (!strcmp(arg, "--cc")) {
                revs->diff = 1;
                revs->dense_combined_merges = 1;
@@@ -2614,7 -2461,7 +2617,7 @@@ int setup_revisions(int argc, const cha
                struct object_id oid;
                struct object *object;
                struct object_context oc;
 -              if (get_oid_with_context(revs->def, 0, &oid, &oc))
 +              if (get_oid_with_context(revs->repo, revs->def, 0, &oid, &oc))
                        diagnose_missing_default(revs->def);
                object = get_reference(revs, revs->def, &oid, 0);
                add_pending_object_with_mode(revs, object, revs->def, oc.mode);
        }
        if (revs->combine_merges)
                revs->ignore_merges = 0;
+       if (revs->combined_all_paths && !revs->combine_merges)
+               die("--combined-all-paths makes no sense without -c or --cc");
        revs->diffopt.abbrev = revs->abbrev;
  
        if (revs->line_level_traverse) {
diff --combined revision.h
index d32d62abc6a7e65ba04a9f25c5024d6f5bbb5f49,253c12c29a5a1dba63ab0240636eb453ee212476..4134dc6029c40f39659b39927ba239aed845673b
@@@ -67,7 -67,6 +67,7 @@@ struct rev_cmdline_info 
  #define REVISION_WALK_NO_WALK_SORTED 1
  #define REVISION_WALK_NO_WALK_UNSORTED 2
  
 +struct oidset;
  struct topo_walk_info;
  
  struct rev_info {
                        verbose_header:1,
                        ignore_merges:1,
                        combine_merges:1,
+                       combined_all_paths:1,
                        dense_combined_merges:1,
                        always_show_header:1;
  
@@@ -328,7 -328,6 +329,7 @@@ void put_revision_mark(const struct rev
  
  void mark_parents_uninteresting(struct commit *commit);
  void mark_tree_uninteresting(struct repository *r, struct tree *tree);
 +void mark_trees_uninteresting_sparse(struct repository *r, struct oidset *trees);
  
  void show_object_with_name(FILE *, struct object *, const char *);