{"thread":{"id":"3143","subject":"[PATCH 1/2] diff-tree -c: show a merge commit a bit more sensibly.","startedAt":"2006-01-25T02:08:53Z","lastAt":"2006-01-25T02:08:53Z","messageCount":1,"participants":["Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"15099","messageId":"7vu0bt2h56.fsf@assigned-by-dhcp.cox.net","threadId":"3143","inReplyTo":null,"subject":"[PATCH 1/2] diff-tree -c: show a merge commit a bit more sensibly.","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2006-01-25T02:08:53Z","receivedAt":"2006-01-25T02:08:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"A new option '-c' to diff-tree changes the way a merge commit is\ndisplayed when generating a patch output.  It shows a \"combined\ndiff\" (hence the option letter 'c'), which looks like this:\n\n    $ git-diff-tree --pretty -c -p fec9ebf1 | head -n 18\n    diff-tree fec9ebf... (from parents)\n    Merge: 0620db3... 8a263ae...\n    Author: Junio C Hamano <junkio@cox.net>\n    Date:   Sun Jan 15 22:25:35 2006 -0800\n\n\tMerge fixes up to GIT 1.1.3\n\n    diff --combined describe.c\n    @@@ +98,7 @@@\n\t    return (a_date > b_date) ? -1 : (a_date == b_date) ? 0 : 1;\n       }\n\n    -  static void describe(char *arg)\n     - static void describe(struct commit *cmit, int last_one)\n    ++ static void describe(char *arg, int last_one)\n       {\n     +      unsigned char sha1[20];\n     +      struct commit *cmit;\n\nThere are a few things to note about this feature:\n\n - The '-c' option implies '-p'.  It also implies '-m' halfway\n   in the sense that \"interesting\" merges are shown, but not all\n   merges.\n\n - When a blob matches one of the parents, we do not show a diff\n   for that path at all.  For a merge commit, this option shows\n   paths with real file-level merge (aka \"interesting things\").\n\n - As a concequence of the above, an \"uninteresting\" merge is\n   not shown at all.  You can use '-m' in addition to '-c' to\n   show the commit log for such a merge, but there will be no\n   combined diff output.\n\n - Unlike \"gitk\", the output is monochrome.\n\nA '-' character in the nth column means the line is from the nth\nparent and does not appear in the merge result (i.e. removed\nfrom that parent's version).\n\nA '+' character in the nth column means the line appears in the\nmerge result, and the nth parent does not have that line\n(i.e. added by the merge itself or inherited from another\nparent).\n\nThe above example output shows that the function signature was\nchanged from either parents (hence two \"-\" lines and a \"++\"\nline), and \"unsigned char sha1[20]\", prefixed by a \" +\", was\ninherited from the first parent.\n\nThe code as sent to the list was buggy in few corner cases,\nwhich I have fixed since then.\n\nIt does not bother to keep track of and show the line numbers\nfrom parent commits, which it probably should.\n\nSigned-off-by: Junio C Hamano <junkio@cox.net>\n\n---\n\n * This is a replacement for the previous one, and is in the\n   \"pu\" branch.\n\n Makefile       |    2 \n combine-diff.c |  469 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n diff-tree.c    |   36 ++++\n diff.h         |    2 \n 4 files changed, 502 insertions(+), 7 deletions(-)\n create mode 100644 combine-diff.c\n\n8085b27b67d0c4ccec206699f4292e6aed94f36b\ndiff --git a/Makefile b/Makefile\nindex 3046056..552b20f 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -182,7 +182,7 @@ LIB_H = \\\n \n DIFF_OBJS = \\\n \tdiff.o diffcore-break.o diffcore-order.o diffcore-pathspec.o \\\n-\tdiffcore-pickaxe.o diffcore-rename.o tree-diff.o\n+\tdiffcore-pickaxe.o diffcore-rename.o tree-diff.o combine-diff.o\n \n LIB_OBJS = \\\n \tblob.o commit.o connect.o count-delta.o csum-file.o \\\ndiff --git a/combine-diff.c b/combine-diff.c\nnew file mode 100644\nindex 0000000..6690023\n--- /dev/null\n+++ b/combine-diff.c\n@@ -0,0 +1,469 @@\n+#include \"cache.h\"\n+#include \"commit.h\"\n+#include \"diff.h\"\n+#include \"diffcore.h\"\n+#include \"quote.h\"\n+\n+struct path_list {\n+\tstruct path_list *next;\n+\tint len;\n+\tchar *path;\n+\tunsigned char sha1[20];\n+\tunsigned char parent_sha1[FLEX_ARRAY][20];\n+};\n+\n+static int uninteresting(struct diff_filepair *p)\n+{\n+\tif (diff_unmodified_pair(p))\n+\t\treturn 1;\n+\tif (!S_ISREG(p->one->mode) || !S_ISREG(p->two->mode))\n+\t\treturn 1;\n+\treturn 0;\n+}\n+\n+static struct path_list *intersect_paths(struct path_list *curr,\n+\t\t\t\t\t int n, int num_parent)\n+{\n+\tstruct diff_queue_struct *q = &diff_queued_diff;\n+\tstruct path_list *p;\n+\tint i;\n+\n+\tif (!n) {\n+\t\tstruct path_list *list = NULL, *tail = NULL;\n+\t\tfor (i = 0; i < q->nr; i++) {\n+\t\t\tint len;\n+\t\t\tconst char *path;\n+\t\t\tif (uninteresting(q->queue[i]))\n+\t\t\t\tcontinue;\n+\t\t\tpath = q->queue[i]->two->path;\n+\t\t\tlen = strlen(path);\n+\n+\t\t\tp = xmalloc(sizeof(*p) + len + 1 + num_parent * 20);\n+\t\t\tp->path = (char*) &(p->parent_sha1[num_parent][0]);\n+\t\t\tmemcpy(p->path, path, len);\n+\t\t\tp->path[len] = 0;\n+\t\t\tp->len = len;\n+\t\t\tp->next = NULL;\n+\t\t\tmemcpy(p->sha1, q->queue[i]->two->sha1, 20);\n+\t\t\tmemcpy(p->parent_sha1[n], q->queue[i]->one->sha1, 20);\n+\t\t\tif (!tail)\n+\t\t\t\tlist = tail = p;\n+\t\t\telse {\n+\t\t\t\ttail->next = p;\n+\t\t\t\tp = tail;\n+\t\t\t}\n+\t\t}\n+\t\treturn list;\n+\t}\n+\n+\tfor (p = curr; p; p = p->next) {\n+\t\tint found = 0;\n+\t\tif (!p->len)\n+\t\t\tcontinue;\n+\t\tfor (i = 0; i < q->nr; i++) {\n+\t\t\tconst char *path;\n+\t\t\tint len;\n+\n+\t\t\tif (uninteresting(q->queue[i]))\n+\t\t\t\tcontinue;\n+\t\t\tpath = q->queue[i]->two->path;\n+\t\t\tlen = strlen(path);\n+\t\t\tif (len == p->len && !memcmp(path, p->path, len)) {\n+\t\t\t\tfound = 1;\n+\t\t\t\tmemcpy(p->parent_sha1[n],\n+\t\t\t\t       q->queue[i]->one->sha1, 20);\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t\tif (!found)\n+\t\t\tp->len = 0;\n+\t}\n+\treturn curr;\n+}\n+\n+struct lline {\n+\tstruct lline *next;\n+\tint len;\n+\tunsigned long parent_map;\n+\tchar line[FLEX_ARRAY];\n+};\n+\n+struct sline {\n+\tstruct lline *lost_head, **lost_tail;\n+\tchar *bol;\n+\tint len;\n+\tunsigned long flag;\n+};\n+\n+static char *grab_blob(const unsigned char *sha1, unsigned long *size)\n+{\n+\tchar *blob;\n+\tchar type[20];\n+\tif (!memcmp(sha1, null_sha1, 20)) {\n+\t\t/* deleted blob */\n+\t\t*size = 0;\n+\t\treturn xcalloc(1, 1);\n+\t}\n+\tblob = read_sha1_file(sha1, type, size);\n+\tif (strcmp(type, \"blob\"))\n+\t\tdie(\"object '%s' is not a blob!\", sha1_to_hex(sha1));\n+\treturn blob;\n+}\n+\n+#define TMPPATHLEN 50\n+#define MAXLINELEN 10240\n+\n+static void write_to_temp_file(char *tmpfile, void *blob, unsigned long size)\n+{\n+\tint fd = git_mkstemp(tmpfile, TMPPATHLEN, \".diff_XXXXXX\");\n+\tif (fd < 0)\n+\t\tdie(\"unable to create temp-file\");\n+\tif (write(fd, blob, size) != size)\n+\t\tdie(\"unable to write temp-file\");\n+\tclose(fd);\n+}\n+\n+static void write_temp_blob(char *tmpfile, const unsigned char *sha1)\n+{\n+\tunsigned long size;\n+\tvoid *blob;\n+\tblob = grab_blob(sha1, &size);\n+\twrite_to_temp_file(tmpfile, blob, size);\n+\tfree(blob);\n+}\n+\n+static int parse_num(char **cp_p, unsigned int *num_p)\n+{\n+\tchar *cp = *cp_p;\n+\tunsigned int num = 0;\n+\tint read_some;\n+\n+\twhile ('0' <= *cp && *cp <= '9')\n+\t\tnum = num * 10 + *cp++ - '0';\n+\tif (!(read_some = cp - *cp_p))\n+\t\treturn -1;\n+\t*cp_p = cp;\n+\t*num_p = num;\n+\treturn 0;\n+}\n+\n+static int parse_hunk_header(char *line, int len,\n+\t\t\t     unsigned int *ob, unsigned int *on,\n+\t\t\t     unsigned int *nb, unsigned int *nn)\n+{\n+\tchar *cp;\n+\tcp = line + 4;\n+\tif (parse_num(&cp, ob)) {\n+\tbad_line:\n+\t\treturn error(\"malformed diff output: %s\", line);\n+\t}\n+\tif (*cp == ',') {\n+\t\tcp++;\n+\t\tif (parse_num(&cp, on))\n+\t\t\tgoto bad_line;\n+\t}\n+\telse\n+\t\t*on = 1;\n+\tif (*cp++ != ' ' || *cp++ != '+')\n+\t\tgoto bad_line;\n+\tif (parse_num(&cp, nb))\n+\t\tgoto bad_line;\n+\tif (*cp == ',') {\n+\t\tcp++;\n+\t\tif (parse_num(&cp, nn))\n+\t\t\tgoto bad_line;\n+\t}\n+\telse\n+\t\t*nn = 1;\n+\treturn -!!memcmp(cp, \" @@\", 3);\n+}\n+\n+static void append_lost(struct sline *sline, int n, const char *line)\n+{\n+\tstruct lline *lline;\n+\tint len = strlen(line);\n+\tunsigned long this_mask = (1UL<<n);\n+\tif (line[len-1] == '\\n')\n+\t\tlen--;\n+\n+\t/* Check to see if we can squash things */\n+\tif (sline->lost_head) {\n+\t\tstruct lline *last_one = NULL;\n+\t\t/* We cannot squash it with earlier one */\n+\t\tfor (lline = sline->lost_head;\n+\t\t     lline;\n+\t\t     lline = lline->next)\n+\t\t\tif (lline->parent_map & this_mask)\n+\t\t\t\tlast_one = lline;\n+\t\tlline = last_one ? last_one->next : sline->lost_head;\n+\t\twhile (lline) {\n+\t\t\tif (lline->len == len &&\n+\t\t\t    !memcmp(lline->line, line, len)) {\n+\t\t\t\tlline->parent_map |= this_mask;\n+\t\t\t\treturn;\n+\t\t\t}\n+\t\t\tlline = lline->next;\n+\t\t}\n+\t}\n+\n+\tlline = xmalloc(sizeof(*lline) + len + 1);\n+\tlline->len = len;\n+\tlline->next = NULL;\n+\tlline->parent_map = this_mask;\n+\tmemcpy(lline->line, line, len);\n+\tlline->line[len] = 0;\n+\tif (sline->lost_head)\n+\t\t*(sline->lost_tail) = lline;\n+\telse\n+\t\tsline->lost_head = lline;\n+\tsline->lost_tail = &lline->next;\n+}\n+\n+static void combine_diff(const unsigned char *parent, const char *ourtmp,\n+\t\t\t struct sline *sline, int cnt, int n)\n+{\n+\tFILE *in;\n+\tchar parent_tmp[TMPPATHLEN];\n+\tchar cmd[TMPPATHLEN * 2 + 1024];\n+\tchar line[MAXLINELEN];\n+\tunsigned int lno, ob, on, nb, nn;\n+\tunsigned long pmask = ~(1UL << n);\n+\tstruct sline *lost_bucket = NULL;\n+\n+\twrite_temp_blob(parent_tmp, parent);\n+\tsprintf(cmd, \"diff --unified=0 -La/x -Lb/x '%s' '%s'\",\n+\t\tparent_tmp, ourtmp);\n+\tin = popen(cmd, \"r\");\n+\tif (!in)\n+\t\treturn;\n+\n+\tlno = 1;\n+\twhile (fgets(line, sizeof(line), in) != NULL) {\n+\t\tint len = strlen(line);\n+\t\tif (5 < len && !memcmp(\"@@ -\", line, 4)) {\n+\t\t\tif (parse_hunk_header(line, len,\n+\t\t\t\t\t      &ob, &on, &nb, &nn))\n+\t\t\t\tbreak;\n+\t\t\tlno = nb;\n+\t\t\tif (!nb) {\n+\t\t\t\t/* @@ -1,2 +0,0 @@ to remove the\n+\t\t\t\t * first two lines...\n+\t\t\t\t */\n+\t\t\t\tnb = 1;\n+\t\t\t}\n+\t\t\tlost_bucket = &sline[nb-1]; /* sline is 0 based */\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!lost_bucket)\n+\t\t\tcontinue;\n+\t\tswitch (line[0]) {\n+\t\tcase '-':\n+\t\t\tappend_lost(lost_bucket, n, line+1);\n+\t\t\tbreak;\n+\t\tcase '+':\n+\t\t\tsline[lno-1].flag &= pmask;\n+\t\t\tlno++;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tfclose(in);\n+\tunlink(parent_tmp);\n+}\n+\n+static unsigned long context = 3;\n+static char combine_marker = '@';\n+\n+static int interesting(struct sline *sline, unsigned long all_mask)\n+{\n+\treturn ((sline->flag & all_mask) != all_mask || sline->lost_head);\n+}\n+\n+static void make_hunks(struct sline *sline, unsigned long cnt, int num_parent)\n+{\n+\tunsigned long all_mask = (1UL<<num_parent) - 1;\n+\tunsigned long mark = (1UL<<num_parent);\n+\tunsigned long i;\n+\n+\ti = 0;\n+\twhile (i < cnt) {\n+\t\tif (interesting(&sline[i], all_mask)) {\n+\t\t\tunsigned long j = (context < i) ? i - context : 0;\n+\t\t\twhile (j <= i)\n+\t\t\t\tsline[j++].flag |= mark;\n+\t\t\twhile (++i < cnt) {\n+\t\t\t\tif (!interesting(&sline[i], all_mask))\n+\t\t\t\t\tbreak;\n+\t\t\t\tsline[i].flag |= mark;\n+\t\t\t}\n+\t\t\tj = (i + context < cnt) ? i + context : cnt;\n+\t\t\twhile (i < j)\n+\t\t\t\tsline[i++].flag |= mark;\n+\t\t\tcontinue;\n+\t\t}\n+\t\ti++;\n+\t}\n+}\n+\n+static void dump_sline(struct sline *sline, int cnt, int num_parent)\n+{\n+\tunsigned long mark = (1UL<<num_parent);\n+\tint i;\n+\tint lno = 0;\n+\n+\twhile (1) {\n+\t\tstruct sline *sl = &sline[lno];\n+\t\tint hunk_end;\n+\t\twhile (lno < cnt && !(sline[lno].flag & mark))\n+\t\t\tlno++;\n+\t\tif (cnt <= lno)\n+\t\t\tbreak;\n+\t\tfor (hunk_end = lno + 1; hunk_end < cnt; hunk_end++)\n+\t\t\tif (!(sline[hunk_end].flag & mark))\n+\t\t\t\tbreak;\n+\t\tfor (i = 0; i <= num_parent; i++) putchar(combine_marker);\n+\t\tprintf(\" +%d,%d \", lno+1, hunk_end-lno);\n+\t\tfor (i = 0; i <= num_parent; i++) putchar(combine_marker);\n+\t\tputchar('\\n');\n+\t\twhile (lno < hunk_end) {\n+\t\t\tstruct lline *ll;\n+\t\t\tint j;\n+\t\t\tsl = &sline[lno++];\n+\t\t\tll = sl->lost_head;\n+\t\t\twhile (ll) {\n+\t\t\t\tfor (j = 0; j < num_parent; j++) {\n+\t\t\t\t\tif (ll->parent_map & (1UL<<j))\n+\t\t\t\t\t\tputchar('-');\n+\t\t\t\t\telse\n+\t\t\t\t\t\tputchar(' ');\n+\t\t\t\t}\n+\t\t\t\tputchar(' ');\n+\t\t\t\tputs(ll->line);\n+\t\t\t\tll = ll->next;\n+\t\t\t}\n+\t\t\tfor (j = 0; j < num_parent; j++) {\n+\t\t\t\tif ((1UL<<j) & sl->flag)\n+\t\t\t\t\tputchar(' ');\n+\t\t\t\telse\n+\t\t\t\t\tputchar('+');\n+\t\t\t}\n+\t\t\tprintf(\" %.*s\\n\", sl->len, sl->bol);\n+\t\t}\n+\t}\n+}\n+\n+static void show_combined_diff(struct path_list *elem, int num_parent)\n+{\n+\tunsigned long size, cnt, lno;\n+\tchar *result, *cp, *ep;\n+\tstruct sline *sline; /* survived lines */\n+\tint i;\n+\tchar ourtmp[TMPPATHLEN];\n+\n+\t/* Read the result of merge first */\n+\tresult = grab_blob(elem->sha1, &size);\n+\twrite_to_temp_file(ourtmp, result, size);\n+\n+\tfor (cnt = 0, cp = result; cp - result < size; cp++) {\n+\t\tif (*cp == '\\n')\n+\t\t\tcnt++;\n+\t}\n+\tif (result[size-1] != '\\n')\n+\t\tcnt++; /* incomplete line */\n+\n+\tsline = xcalloc(cnt, sizeof(*sline));\n+\tep = result;\n+\tsline[0].bol = result;\n+\tfor (lno = 0, cp = result; cp - result < size; cp++) {\n+\t\tif (*cp == '\\n') {\n+\t\t\tsline[lno].len = cp - sline[lno].bol;\n+\t\t\tsline[lno].flag = (1UL<<num_parent) - 1;\n+\t\t\tlno++;\n+\t\t\tif (lno < cnt)\n+\t\t\t\tsline[lno].bol = cp + 1;\n+\t\t}\n+\t}\n+\tif (result[size-1] != '\\n') {\n+\t\tsline[cnt-1].len = size - (sline[cnt-1].bol - result);\n+\t\tsline[cnt-1].flag = (1UL<<num_parent) - 1;\n+\t}\n+\n+\tfor (i = 0; i < num_parent; i++)\n+\t\tcombine_diff(elem->parent_sha1[i], ourtmp, sline, cnt, i);\n+\n+\tmake_hunks(sline, cnt, num_parent);\n+\n+\tdump_sline(sline, cnt, num_parent);\n+\tunlink(ourtmp);\n+\tfree(result);\n+\n+\tfor (i = 0; i < cnt; i++) {\n+\t\tif (sline[i].lost_head) {\n+\t\t\tstruct lline *ll = sline[i].lost_head;\n+\t\t\twhile (ll) {\n+\t\t\t\tstruct lline *tmp = ll;\n+\t\t\t\tll = ll->next;\n+\t\t\t\tfree(tmp);\n+\t\t\t}\n+\t\t}\n+\t}\n+\tfree(sline);\n+}\n+\n+int diff_tree_combined_merge(const unsigned char *sha1,\n+\t\t\t     const char *header, int show_empty_merge)\n+{\n+\tstruct commit *commit = lookup_commit(sha1);\n+\tstruct diff_options diffopts;\n+\tstruct commit_list *parents;\n+\tstruct path_list *p, *paths = NULL;\n+\tint num_parent, i, num_paths;\n+\n+\tdiff_setup(&diffopts);\n+\tdiffopts.output_format = DIFF_FORMAT_NO_OUTPUT;\n+\tdiffopts.recursive = 1;\n+\n+\t/* count parents */\n+\tfor (parents = commit->parents, num_parent = 0;\n+\t     parents;\n+\t     parents = parents->next, num_parent++)\n+\t\t; /* nothing */\n+\n+\t/* find set of paths that everybody touches */\n+\tfor (parents = commit->parents, i = 0;\n+\t     parents;\n+\t     parents = parents->next, i++) {\n+\t\tstruct commit *parent = parents->item;\n+\t\tdiff_tree_sha1(parent->object.sha1, commit->object.sha1, \"\",\n+\t\t\t       &diffopts);\n+\t\tpaths = intersect_paths(paths, i, num_parent);\n+\t\tdiff_flush(&diffopts);\n+\t}\n+\n+\t/* find out surviving paths */\n+\tfor (num_paths = 0, p = paths; p; p = p->next) {\n+\t\tif (p->len)\n+\t\t\tnum_paths++;\n+\t}\n+\tif (num_paths || show_empty_merge) {\n+\t\tputs(header);\n+\t\tfor (p = paths; p; p = p->next) {\n+\t\t\tif (!p->len)\n+\t\t\t\tcontinue;\n+\t\t\tprintf(\"diff --combined \");\n+\t\t\tif (quote_c_style(p->path, NULL, NULL, 0))\n+\t\t\t\tquote_c_style(p->path, NULL, stdout, 0);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s\", p->path);\n+\t\t\tputchar('\\n');\n+\t\t\tshow_combined_diff(p, num_parent);\n+\t\t}\n+\t}\n+\n+\t/* Clean things up */\n+\twhile (paths) {\n+\t\tstruct path_list *tmp = paths;\n+\t\tpaths = paths->next;\n+\t\tfree(tmp);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/diff-tree.c b/diff-tree.c\nindex efa2b94..0c68936 100644\n--- a/diff-tree.c\n+++ b/diff-tree.c\n@@ -6,6 +6,8 @@ static int show_root_diff = 0;\n static int no_commit_id = 0;\n static int verbose_header = 0;\n static int ignore_merges = 1;\n+static int show_empty_combined = 0;\n+static int combine_merges = 0;\n static int read_stdin = 0;\n \n static const char *header = NULL;\n@@ -79,9 +81,13 @@ static const char *generate_header(const\n \toffset = sprintf(this_header, \"%s%s \",\n \t\t\t header_prefix,\n \t\t\t diff_unique_abbrev(commit_sha1, abbrev));\n-\toffset += sprintf(this_header + offset, \"(from %s)\\n\",\n-\t\t\t parent_sha1 ?\n-\t\t\t diff_unique_abbrev(parent_sha1, abbrev) : \"root\");\n+\tif (commit_sha1 != parent_sha1)\n+\t\toffset += sprintf(this_header + offset, \"(from %s)\\n\",\n+\t\t\t\t  parent_sha1\n+\t\t\t\t  ? diff_unique_abbrev(parent_sha1, abbrev)\n+\t\t\t\t  : \"root\");\n+\telse\n+\t\toffset += sprintf(this_header + offset, \"(from parents)\\n\");\n \toffset += pretty_print_commit(commit_format, msg, len,\n \t\t\t\t      this_header + offset,\n \t\t\t\t      sizeof(this_header) - offset);\n@@ -108,8 +114,16 @@ static int diff_tree_commit(const unsign\n \t}\n \n \t/* More than one parent? */\n-\tif (ignore_merges && commit->parents && commit->parents->next)\n-\t\treturn 0;\n+\tif (commit->parents && commit->parents->next) {\n+\t\tif (ignore_merges)\n+\t\t\treturn 0;\n+\t\telse if (combine_merges) {\n+\t\t\theader = generate_header(sha1, sha1,\n+\t\t\t\t\t\t commit->buffer);\n+\t\t\treturn diff_tree_combined_merge(sha1, header,\n+\t\t\t\t\t\t\tshow_empty_combined);\n+\t\t}\n+\t}\n \n \tfor (parents = commit->parents; parents; parents = parents->next) {\n \t\tstruct commit *parent = parents->item;\n@@ -154,7 +168,7 @@ static int diff_tree_stdin(char *line)\n }\n \n static const char diff_tree_usage[] =\n-\"git-diff-tree [--stdin] [-m] [-s] [-v] [--pretty] [-t] [-r] [--root] \"\n+\"git-diff-tree [--stdin] [-m] [-c] [-s] [-v] [--pretty] [-t] [-r] [--root] \"\n \"[<common diff options>] <tree-ish> [<tree-ish>] [<path>...]\\n\"\n \"  -r            diff recursively\\n\"\n \"  --root        include the initial commit as diff against /dev/null\\n\"\n@@ -217,6 +231,10 @@ int main(int argc, const char **argv)\n \t\t\tignore_merges = 0;\n \t\t\tcontinue;\n \t\t}\n+\t\tif (!strcmp(arg, \"-c\")) {\n+\t\t\tcombine_merges = 1;\n+\t\t\tcontinue;\n+\t\t}\n \t\tif (!strcmp(arg, \"-v\")) {\n \t\t\tverbose_header = 1;\n \t\t\theader_prefix = \"diff-tree \";\n@@ -245,6 +263,12 @@ int main(int argc, const char **argv)\n \tif (diff_options.output_format == DIFF_FORMAT_PATCH)\n \t\tdiff_options.recursive = 1;\n \n+\tif (combine_merges) {\n+\t\tdiff_options.output_format = DIFF_FORMAT_PATCH;\n+\t\tshow_empty_combined = !ignore_merges;\n+\t\tignore_merges = 0;\n+\t}\n+\n \tdiff_tree_setup_paths(get_pathspec(prefix, argv));\n \tdiff_setup_done(&diff_options);\n \ndiff --git a/diff.h b/diff.h\nindex 5696f2a..081234c 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -56,6 +56,8 @@ extern int diff_tree(struct tree_desc *t\n extern int diff_tree_sha1(const unsigned char *old, const unsigned char *new,\n \t\t\t  const char *base, struct diff_options *opt);\n \n+extern int diff_tree_combined_merge(const unsigned char *sha1, const char *, int);\n+\n extern void diff_addremove(struct diff_options *,\n \t\t\t   int addremove,\n \t\t\t   unsigned mode,\n-- \n1.1.4.g869a\n"}]}