{"thread":{"id":"6840","subject":"[PATCH] Add `git diff2`, a GNU diff workalike","startedAt":"2007-02-16T04:01:19Z","lastAt":"2007-02-18T11:44:43Z","messageCount":5,"participants":["Johannes Schindelin","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"34763","messageId":"Pine.LNX.4.63.0702160500280.22628@wbgn013.biozentrum.uni-wuerzburg.de","threadId":"6840","inReplyTo":null,"subject":"[PATCH] Add `git diff2`, a GNU diff workalike","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2007-02-16T04:01:19Z","receivedAt":"2007-02-16T04:01:19Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"\nGit does have a wonderful diff engine. For example, colored diffs\nreally shine, and there are other useful options like --check,\n--patch-with-stat, etc. I always dreamt of using this diff engine\nalso outside of a git repository.\n\nWith this commit, you can say\n\n\tgit diff2 file1 file2\n\nto compare the (possibly untracked) files \"file1\" and \"file2\", and\n\n\tgit diff2 dir1 dir2\n\nto compare the directories \"dir1\" and \"dir2\".\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n .gitignore      |    1 +\n Makefile        |    1 +\n builtin-diff2.c |  163 +++++++++++++++++++++++++++++++++++++++++++++++++++++++\n builtin.h       |    1 +\n diff.c          |    3 +-\n git.c           |    1 +\n 6 files changed, 169 insertions(+), 1 deletions(-)\n create mode 100644 builtin-diff2.c\n\ndiff --git a/.gitignore b/.gitignore\nindex 9b5502b..9809a7d 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -32,6 +32,7 @@ git-cvsserver\n git-daemon\n git-describe\n git-diff\n+git-diff2\n git-diff-files\n git-diff-index\n git-diff-tree\ndiff --git a/Makefile b/Makefile\nindex ea2b4f5..94b1c75 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -284,6 +284,7 @@ BUILTIN_OBJS = \\\n \tbuiltin-count-objects.o \\\n \tbuiltin-describe.o \\\n \tbuiltin-diff.o \\\n+\tbuiltin-diff2.o \\\n \tbuiltin-diff-files.o \\\n \tbuiltin-diff-index.o \\\n \tbuiltin-diff-tree.o \\\ndiff --git a/builtin-diff2.c b/builtin-diff2.c\nnew file mode 100644\nindex 0000000..1de82c1\n--- /dev/null\n+++ b/builtin-diff2.c\n@@ -0,0 +1,163 @@\n+#include \"cache.h\"\n+#include \"diff.h\"\n+#include \"diffcore.h\"\n+#include \"path-list.h\"\n+\n+static const char diff2_usage[] = \"git diff2 [diff-opts] file1 file2\";\n+\n+static int read_directory(const char *path, struct path_list *list)\n+{\n+\tDIR *dir;\n+\tstruct dirent *e;\n+\n+\tif (!(dir = opendir(path)))\n+\t\treturn error(\"Could not open directory %s\", path);\n+\n+\twhile ((e = readdir(dir)))\n+\t\tif (strcmp(\".\", e->d_name) && strcmp(\"..\", e->d_name))\n+\t\t\tpath_list_insert(xstrdup(e->d_name), list);\n+\n+\tclosedir(dir);\n+\treturn 0;\n+}\n+\n+static int queue_diff(struct diff_options *o,\n+\t\tconst char *name1, const char *name2)\n+{\n+\tstruct stat st;\n+\tint mode1 = 0, mode2 = 0;\n+\n+\tif (name1) {\n+\t\tif (stat(name1, &st))\n+\t\t\treturn error(\"Could not access '%s'\", name1);\n+\t\tmode1 = st.st_mode;\n+\t}\n+\tif (name2) {\n+\t\tif (stat(name2, &st))\n+\t\t\treturn error(\"Could not access '%s'\", name1);\n+\t\tmode2 = st.st_mode;\n+\t}\n+\n+\tif (mode1 && mode2 && S_ISDIR(mode1) != S_ISDIR(mode2))\n+\t\treturn error(\"file/directory conflict: %s, %s\", name1, name2);\n+\n+\tif (S_ISDIR(mode1) || S_ISDIR(mode2)) {\n+\t\tchar buffer1[PATH_MAX], buffer2[PATH_MAX];\n+\t\tstruct path_list p1 = {NULL, 0, 0, 1}, p2 = {NULL, 0, 0, 1};\n+\t\tint len1 = 0, len2 = 0, i1, i2, ret = 0;\n+\n+\t\tif (name1 && read_directory(name1, &p1))\n+\t\t\treturn -1;\n+\t\tif (name2 && read_directory(name2, &p2)) {\n+\t\t\tpath_list_clear(&p1, 0);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (name1) {\n+\t\t\tlen1 = strlen(name1);\n+\t\t\tif (len1 > 0 && name1[len1 - 1] == '/')\n+\t\t\t\tlen1--;\n+\t\t\tmemcpy(buffer1, name1, len1);\n+\t\t\tbuffer1[len1++] = '/';\n+\t\t}\n+\n+\t\tif (name2) {\n+\t\t\tlen2 = strlen(name2);\n+\t\t\tif (len2 > 0 && name2[len2 - 1] == '/')\n+\t\t\t\tlen2--;\n+\t\t\tmemcpy(buffer2, name2, len2);\n+\t\t\tbuffer2[len2++] = '/';\n+\t\t}\n+\n+\t\tfor (i1 = i2 = 0; !ret && (i1 < p1.nr || i2 < p2.nr); ) {\n+\t\t\tconst char *n1, *n2;\n+\t\t\tint comp;\n+\n+\t\t\tif (i1 == p1.nr)\n+\t\t\t\tcomp = 1;\n+\t\t\telse if (i2 == p2.nr)\n+\t\t\t\tcomp = -1;\n+\t\t\telse\n+\t\t\t\tcomp = strcmp(p1.items[i1].path,\n+\t\t\t\t\tp2.items[i2].path);\n+\n+\t\t\tif (comp > 0)\n+\t\t\t\tn1 = NULL;\n+\t\t\telse {\n+\t\t\t\tn1 = buffer1;\n+\t\t\t\tstrncpy(buffer1 + len1, p1.items[i1++].path,\n+\t\t\t\t\t\tPATH_MAX - len1);\n+\t\t\t}\n+\n+\t\t\tif (comp < 0)\n+\t\t\t\tn2 = NULL;\n+\t\t\telse {\n+\t\t\t\tn2 = buffer2;\n+\t\t\t\tstrncpy(buffer2 + len2, p2.items[i2++].path,\n+\t\t\t\t\t\tPATH_MAX - len2);\n+\t\t\t}\n+\n+\t\t\tret = queue_diff(o, n1, n2);\n+\t\t}\n+\t\tpath_list_clear(&p1, 0);\n+\t\tpath_list_clear(&p2, 0);\n+\n+\t\treturn ret;\n+\t} else {\n+\t\tstruct diff_filespec *d1, *d2;\n+\n+\t\tif (o->reverse_diff) {\n+\t\t\tunsigned tmp;\n+\t\t\tconst char *tmp_c;\n+\t\t\ttmp = mode1; mode1 = mode2; mode2 = tmp;\n+\t\t\ttmp_c = name1; name1 = name2; name2 = tmp_c;\n+\t\t}\n+\n+\t\tif (!name1)\n+\t\t\tname1 = \"/dev/null\";\n+\t\tif (!name2)\n+\t\t\tname2 = \"/dev/null\";\n+\t\td1 = alloc_filespec(name1);\n+\t\td2 = alloc_filespec(name2);\n+\t\tfill_filespec(d1, null_sha1, mode1);\n+\t\tfill_filespec(d2, null_sha1, mode2);\n+\n+\t\tdiff_queue(&diff_queued_diff, d1, d2);\n+\t\treturn 0;\n+\t}\n+}\n+\n+int cmd_diff2(int argc, char **argv, char **envp)\n+{\n+\tstruct diff_options options;\n+\tint i, i2;\n+        int nongit = 0;\n+\n+        setup_git_directory_gently(&nongit);\n+\tgit_config(git_diff_ui_config);\n+\n+\tdiff_setup(&options);\n+\tfor (i = 1; i < argc; ) {\n+\t\tif (!strcmp(\"--\", argv[i])) {\n+\t\t\ti++;\n+\t\t\tbreak;\n+\t\t}\n+\t\ti2 = diff_opt_parse(&options,\n+\t\t\t\t(const char **)argv + i, argc - i);\n+\t\tif (!i2)\n+\t\t\tbreak;\n+\t\ti += i2;\n+\t}\n+\tif (diff_setup_done(&options) < 0)\n+\t\tdie(\"diff_setup_done failed\");\n+\tif (!options.output_format)\n+\t\toptions.output_format = DIFF_FORMAT_PATCH;\n+\n+\tif (argc - i != 2)\n+\t\tusage(diff2_usage);\n+\n+\tqueue_diff(&options, argv[i], argv[i + 1]);\n+\tdiffcore_std(&options);\n+\tdiff_flush(&options);\n+\treturn 0;\n+}\ndiff --git a/builtin.h b/builtin.h\nindex f8e1425..72f94ec 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -29,6 +29,7 @@ extern int cmd_describe(int argc, const char **argv, const char *prefix);\n extern int cmd_diff_files(int argc, const char **argv, const char *prefix);\n extern int cmd_diff_index(int argc, const char **argv, const char *prefix);\n extern int cmd_diff(int argc, const char **argv, const char *prefix);\n+extern int cmd_diff2(int argc, const char **argv, const char *prefix);\n extern int cmd_diff_tree(int argc, const char **argv, const char *prefix);\n extern int cmd_fetch__tool(int argc, const char **argv, const char *prefix);\n extern int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix);\ndiff --git a/diff.c b/diff.c\nindex c2d9abe..be73621 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -2443,7 +2443,8 @@ static void diff_resolve_rename_copy(void)\n \t\t\t\tp->status = DIFF_STATUS_RENAMED;\n \t\t}\n \t\telse if (hashcmp(p->one->sha1, p->two->sha1) ||\n-\t\t\t p->one->mode != p->two->mode)\n+\t\t\t p->one->mode != p->two->mode ||\n+\t\t\t is_null_sha1(p->one->sha1))\n \t\t\tp->status = DIFF_STATUS_MODIFIED;\n \t\telse {\n \t\t\t/* This is a \"no-change\" entry and should not\ndiff --git a/git.c b/git.c\nindex be04fb5..5e6b355 100644\n--- a/git.c\n+++ b/git.c\n@@ -249,6 +249,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp)\n \t\t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n \t\t{ \"describe\", cmd_describe, RUN_SETUP },\n \t\t{ \"diff\", cmd_diff, RUN_SETUP | USE_PAGER },\n+\t\t{ \"diff2\", cmd_diff2, USE_PAGER },\n \t\t{ \"diff-files\", cmd_diff_files, RUN_SETUP },\n \t\t{ \"diff-index\", cmd_diff_index, RUN_SETUP },\n \t\t{ \"diff-tree\", cmd_diff_tree, RUN_SETUP },\n-- \n1.5.0.2137.g20bab\n"},{"id":"34782","messageId":"7v1wkq8r26.fsf@assigned-by-dhcp.cox.net","threadId":"6840","inReplyTo":"Pine.LNX.4.63.0702160500280.22628@wbgn013.biozentrum.uni-wuerzburg.de","subject":"Re: [PATCH] Add `git diff2`, a GNU diff workalike","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2007-02-16T08:06:25Z","receivedAt":"2007-02-16T08:06:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> Git does have a wonderful diff engine. For example, colored diffs\n> really shine, and there are other useful options like --check,\n> --patch-with-stat, etc. I always dreamt of using this diff engine\n> also outside of a git repository.\n>\n> With this commit, you can say\n>\n> \tgit diff2 file1 file2\n\nWhy diff2?  I would have expected this to be a new low-level\nsibling of diff-files and diff-index, which in turn hook into\nthe overall \"diff\" driver.\n\n> +static int queue_diff(struct diff_options *o,\n> +\t\tconst char *name1, const char *name2)\n> +{\n> +\tstruct stat st;\n> +\tint mode1 = 0, mode2 = 0;\n> +\n> +\tif (name1) {\n> +\t\tif (stat(name1, &st))\n> +\t\t\treturn error(\"Could not access '%s'\", name1);\n> +\t\tmode1 = st.st_mode;\n> +\t}\n> +\tif (name2) {\n> +\t\tif (stat(name2, &st))\n> +\t\t\treturn error(\"Could not access '%s'\", name1);\n> +\t\tmode2 = st.st_mode;\n> +\t}\n\nI am still debating myself if these should be lstat(2) instead\nof stat(2).  The former is more consistent with what git does.\n\n> +\tif (mode1 && mode2 && S_ISDIR(mode1) != S_ISDIR(mode2))\n> +\t\treturn error(\"file/directory conflict: %s, %s\", name1, name2);\n\nIf a/frotz is a file and b/frotz/nitfol is there, I do not think\nwe show an error; we say \"a/frotz\" was removed (see notes below,\nthough).\n\n> +\tif (S_ISDIR(mode1) || S_ISDIR(mode2)) {\n> +\t\tchar buffer1[PATH_MAX], buffer2[PATH_MAX];\n> +\t\tstruct path_list p1 = {NULL, 0, 0, 1}, p2 = {NULL, 0, 0, 1};\n> +\t\tint len1 = 0, len2 = 0, i1, i2, ret = 0;\n> +\n> +\t\tif (name1 && read_directory(name1, &p1))\n> +\t\t\treturn -1;\n> +\t\tif (name2 && read_directory(name2, &p2)) {\n> +\t\t\tpath_list_clear(&p1, 0);\n> +\t\t\treturn -1;\n> +\t\t}\n\nI suspect your favorite path-list might not be optimal for this\nkind of codeflow; wouldn't reading everything in an expanding\narray and sorting them with a single qsort() after reading one\ndirectory more efficient?\n\n> +\t\tfor (i1 = i2 = 0; !ret && (i1 < p1.nr || i2 < p2.nr); ) {\n> +\t\t\tconst char *n1, *n2;\n> +\t\t\tint comp;\n> +\n> +\t\t\tif (i1 == p1.nr)\n> +\t\t\t\tcomp = 1;\n> +\t\t\telse if (i2 == p2.nr)\n> +\t\t\t\tcomp = -1;\n> +\t\t\telse\n> +\t\t\t\tcomp = strcmp(p1.items[i1].path,\n> +\t\t\t\t\tp2.items[i2].path);\n\nI think you would want to be consistent with how git sorts paths\nhere.  In particular, you can always pretend that a path that is\na directory has '/' at the end.  Which (as a side effect) means\nthat you do not have to worry about a/frotz and b/frotz/nitfol,\nbecause element from p1 will be \"frotz\" and the one from p2 will\nbe \"frotz/\" in this case.  You will never feed queue_diff() with\nthe same name with d/f conflicts that way.\n\n> +int cmd_diff2(int argc, char **argv, char **envp)\n> +{\n\nThe rest looks quite straightforward use of diffcore API, done\nvery cleanly.\n\n> diff --git a/diff.c b/diff.c\n> index c2d9abe..be73621 100644\n> --- a/diff.c\n> +++ b/diff.c\n> @@ -2443,7 +2443,8 @@ static void diff_resolve_rename_copy(void)\n>  \t\t\t\tp->status = DIFF_STATUS_RENAMED;\n>  \t\t}\n>  \t\telse if (hashcmp(p->one->sha1, p->two->sha1) ||\n> -\t\t\t p->one->mode != p->two->mode)\n> +\t\t\t p->one->mode != p->two->mode ||\n> +\t\t\t is_null_sha1(p->one->sha1))\n>  \t\t\tp->status = DIFF_STATUS_MODIFIED;\n\nI didn't look, but you might also need to teach diffcore-rename\nthat two objects both with null object names are not equal.\n"},{"id":"34795","messageId":"Pine.LNX.4.63.0702161447080.22628@wbgn013.biozentrum.uni-wuerzburg.de","threadId":"6840","inReplyTo":"7v1wkq8r26.fsf@assigned-by-dhcp.cox.net","subject":"Re: [PATCH] Add `git diff2`, a GNU diff workalike","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2007-02-16T14:01:54Z","receivedAt":"2007-02-16T14:01:54Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi,\n\nOn Fri, 16 Feb 2007, Junio C Hamano wrote:\n\n> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n> \n> > Git does have a wonderful diff engine. For example, colored diffs\n> > really shine, and there are other useful options like --check,\n> > --patch-with-stat, etc. I always dreamt of using this diff engine\n> > also outside of a git repository.\n> >\n> > With this commit, you can say\n> >\n> > \tgit diff2 file1 file2\n> \n> Why diff2?  I would have expected this to be a new low-level sibling of \n> diff-files and diff-index, which in turn hook into the overall \"diff\" \n> driver.\n\nI planned this. However, I could not think of any sane way to handle the \ncase in an intuitive manner where you want to compare two _tracked_ files. \nHow would you do that in a consistent manner?\n\nBTW I also realized that `git diff bla` will _not_ complain when the file \nbla exists, but is untracked. Neither will it show a diff. I did not come \naround to fix that yet.\n\n> > +static int queue_diff(struct diff_options *o,\n> > +\t\tconst char *name1, const char *name2)\n> > +{\n> > +\tstruct stat st;\n> > +\tint mode1 = 0, mode2 = 0;\n> > +\n> > +\tif (name1) {\n> > +\t\tif (stat(name1, &st))\n> > +\t\t\treturn error(\"Could not access '%s'\", name1);\n> > +\t\tmode1 = st.st_mode;\n> > +\t}\n> > +\tif (name2) {\n> > +\t\tif (stat(name2, &st))\n> > +\t\t\treturn error(\"Could not access '%s'\", name1);\n> > +\t\tmode2 = st.st_mode;\n> > +\t}\n> \n> I am still debating myself if these should be lstat(2) instead of \n> stat(2).  The former is more consistent with what git does.\n\nWell, it should probably be lstat(), with an addremove when one or both \nfiles are links. But they could be identical also.\n\n> > +\tif (mode1 && mode2 && S_ISDIR(mode1) != S_ISDIR(mode2))\n> > +\t\treturn error(\"file/directory conflict: %s, %s\", name1, name2);\n> \n> If a/frotz is a file and b/frotz/nitfol is there, I do not think we show \n> an error; we say \"a/frotz\" was removed (see notes below, though).\n\nGood point. Will fix.\n\n> > +\tif (S_ISDIR(mode1) || S_ISDIR(mode2)) {\n> > +\t\tchar buffer1[PATH_MAX], buffer2[PATH_MAX];\n> > +\t\tstruct path_list p1 = {NULL, 0, 0, 1}, p2 = {NULL, 0, 0, 1};\n> > +\t\tint len1 = 0, len2 = 0, i1, i2, ret = 0;\n> > +\n> > +\t\tif (name1 && read_directory(name1, &p1))\n> > +\t\t\treturn -1;\n> > +\t\tif (name2 && read_directory(name2, &p2)) {\n> > +\t\t\tpath_list_clear(&p1, 0);\n> > +\t\t\treturn -1;\n> > +\t\t}\n> \n> I suspect your favorite path-list might not be optimal for this kind of \n> codeflow; wouldn't reading everything in an expanding array and sorting \n> them with a single qsort() after reading one directory more efficient?\n\nYou are right. But for the moment, I'll leave it, since it is one of the \nleast performance critical parts of diff2, _and_ the use of path-list is \nreally easy.\n\n> > +\t\tfor (i1 = i2 = 0; !ret && (i1 < p1.nr || i2 < p2.nr); ) {\n> > +\t\t\tconst char *n1, *n2;\n> > +\t\t\tint comp;\n> > +\n> > +\t\t\tif (i1 == p1.nr)\n> > +\t\t\t\tcomp = 1;\n> > +\t\t\telse if (i2 == p2.nr)\n> > +\t\t\t\tcomp = -1;\n> > +\t\t\telse\n> > +\t\t\t\tcomp = strcmp(p1.items[i1].path,\n> > +\t\t\t\t\tp2.items[i2].path);\n> \n> I think you would want to be consistent with how git sorts paths\n> here.  In particular, you can always pretend that a path that is\n> a directory has '/' at the end.\n\nAt that point, the entries in p1 and p2 are from readdir(), so I do not \nknow the type (e->d_type is non-portable according to my man-page here).\n\nBTW I had a memory leak in path_list_insert(xstrdup(...\n\n> The rest looks quite straightforward use of diffcore API, done\n> very cleanly.\n\nThanks!\n\n> > diff --git a/diff.c b/diff.c\n> > index c2d9abe..be73621 100644\n> > --- a/diff.c\n> > +++ b/diff.c\n> > @@ -2443,7 +2443,8 @@ static void diff_resolve_rename_copy(void)\n> >  \t\t\t\tp->status = DIFF_STATUS_RENAMED;\n> >  \t\t}\n> >  \t\telse if (hashcmp(p->one->sha1, p->two->sha1) ||\n> > -\t\t\t p->one->mode != p->two->mode)\n> > +\t\t\t p->one->mode != p->two->mode ||\n> > +\t\t\t is_null_sha1(p->one->sha1))\n> >  \t\t\tp->status = DIFF_STATUS_MODIFIED;\n> \n> I didn't look, but you might also need to teach diffcore-rename\n> that two objects both with null object names are not equal.\n\nBut would not every diff be marked as a rename then?\n\nCiao,\nDscho\n"},{"id":"34796","messageId":"Pine.LNX.4.63.0702161506100.22628@wbgn013.biozentrum.uni-wuerzburg.de","threadId":"6840","inReplyTo":"Pine.LNX.4.63.0702161447080.22628@wbgn013.biozentrum.uni-wuerzburg.de","subject":"Re: [PATCH] Add `git diff2`, a GNU diff workalike","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2007-02-16T14:20:40Z","receivedAt":"2007-02-16T14:20:40Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi,\n\nOn Fri, 16 Feb 2007, Johannes Schindelin wrote:\n\n> On Fri, 16 Feb 2007, Junio C Hamano wrote:\n> \n> > Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n> > \n> > > +\tif (mode1 && mode2 && S_ISDIR(mode1) != S_ISDIR(mode2))\n> > > +\t\treturn error(\"file/directory conflict: %s, %s\", name1, name2);\n> > \n> > If a/frotz is a file and b/frotz/nitfol is there, I do not think we show \n> > an error; we say \"a/frotz\" was removed (see notes below, though).\n> \n> Good point. Will fix.\n\nThinking about this again, I do not know of any patch implementation which \nremoves a directory which just became empty, so dir->file is a real \nproblem. Also, if dir is empty, another problem looms.\n\nSo at least the dir->non-dir case should be an error.\n\nBTW I just realized that diff2 as-is will output a diff header for _every_ \nfile pair, even if they do compare equally. Actually, I like it, so I \ndon't want to imitate GNU diff behaviour here.\n\nCiao,\nDscho\n\nP.S.: Here is a quick-fix patch on top of my original (tested with \ndir->file (error!), file->dir, link->link, link->null, file->link):\n\n builtin-diff2.c |   16 +++++++++++-----\n 1 files changed, 11 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin-diff2.c b/builtin-diff2.c\nindex 1de82c1..234dabb 100644\n--- a/builtin-diff2.c\n+++ b/builtin-diff2.c\n@@ -15,7 +15,7 @@ static int read_directory(const char *path, struct path_list *list)\n \n \twhile ((e = readdir(dir)))\n \t\tif (strcmp(\".\", e->d_name) && strcmp(\"..\", e->d_name))\n-\t\t\tpath_list_insert(xstrdup(e->d_name), list);\n+\t\t\tpath_list_insert(e->d_name, list);\n \n \tclosedir(dir);\n \treturn 0;\n@@ -28,18 +28,24 @@ static int queue_diff(struct diff_options *o,\n \tint mode1 = 0, mode2 = 0;\n \n \tif (name1) {\n-\t\tif (stat(name1, &st))\n+\t\tif (lstat(name1, &st))\n \t\t\treturn error(\"Could not access '%s'\", name1);\n \t\tmode1 = st.st_mode;\n \t}\n \tif (name2) {\n-\t\tif (stat(name2, &st))\n+\t\tif (lstat(name2, &st))\n \t\t\treturn error(\"Could not access '%s'\", name1);\n \t\tmode2 = st.st_mode;\n \t}\n \n-\tif (mode1 && mode2 && S_ISDIR(mode1) != S_ISDIR(mode2))\n-\t\treturn error(\"file/directory conflict: %s, %s\", name1, name2);\n+\tif (mode1 && mode2) {\n+\t\tif (S_ISDIR(mode1) && !S_ISDIR(mode2))\n+\t\t\treturn error(\"Cannot handle dir->file: %s -> %s\",\n+\t\t\t\tname1, name2);\n+\t\tif (!S_ISDIR(mode1) && S_ISDIR(mode2))\n+\t\t\treturn queue_diff(o, name1, NULL) ||\n+\t\t\t\tqueue_diff(o, NULL, name2);\n+\t}\n \n \tif (S_ISDIR(mode1) || S_ISDIR(mode2)) {\n \t\tchar buffer1[PATH_MAX], buffer2[PATH_MAX];\n"},{"id":"34954","messageId":"Pine.LNX.4.63.0702181243520.22628@wbgn013.biozentrum.uni-wuerzburg.de","threadId":"6840","inReplyTo":"7vtzxknrzp.fsf@assigned-by-dhcp.cox.net","subject":"[PATCH] Add `git diff2`, a GNU diff workalike","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2007-02-18T11:44:43Z","receivedAt":"2007-02-18T11:44:43Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"\nGit really has a wonderful diff engine. For example, colored diffs\nreally shine, and there are other useful options like --check,\n--check, etc. I always dreamt of using this diff engine also outside\nof a git repository.\n\nWith this commit, you can say\n\n\tgit diff2 file1 file2\n\nto compare the (possibly untracked) files \"file1\" and \"file2\", and\n\n\tgit diff2 dir1 dir2\n\nto compare the directories \"dir1\" and \"dir2\".\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n\n\tJunio tricked me into writing documentation, so be gentle.\n\n\tBack to bed.\n\n .gitignore                  |    1 +\n Documentation/cmd-list.perl |    1 +\n Documentation/git-diff2.txt |   63 +++++++++++++++++\n Makefile                    |    1 +\n builtin-diff2.c             |  163 +++++++++++++++++++++++++++++++++++++++++++\n builtin.h                   |    1 +\n diff.c                      |    3 +-\n git.c                       |    1 +\n 8 files changed, 233 insertions(+), 1 deletions(-)\n create mode 100644 Documentation/git-diff2.txt\n create mode 100644 builtin-diff2.c\n\ndiff --git a/.gitignore b/.gitignore\nindex f15155d..7c653fb 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -31,6 +31,7 @@ git-cvsimport\n git-cvsserver\n git-daemon\n git-diff\n+git-diff2\n git-diff-files\n git-diff-index\n git-diff-tree\ndiff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl\nindex a2d6268..e2b0bd3 100755\n--- a/Documentation/cmd-list.perl\n+++ b/Documentation/cmd-list.perl\n@@ -90,6 +90,7 @@ git-describe                            mainporcelain\n git-diff-files                          plumbinginterrogators\n git-diff-index                          plumbinginterrogators\n git-diff                                mainporcelain\n+git-diff2                               mainporcelain\n git-diff-tree                           plumbinginterrogators\n git-fast-import\t\t\t\tancillarymanipulators\n git-fetch                               mainporcelain\ndiff --git a/Documentation/git-diff2.txt b/Documentation/git-diff2.txt\nnew file mode 100644\nindex 0000000..8909119\n--- /dev/null\n+++ b/Documentation/git-diff2.txt\n@@ -0,0 +1,63 @@\n+git-diff2(1)\n+===========\n+\n+NAME\n+----\n+git-diff2 - Show changes between files, directories and symlinks\n+\n+\n+SYNOPSIS\n+--------\n+'git-diff2' [ --diff-options ] <path1> <path2>\n+\n+DESCRIPTION\n+-----------\n+Show changes between two files, directories or symlinks.\n+\n+'git-diff2' [--options] [--] <path1> <path2>::\n+\n+\tThis command operates independently of a git repository.\n+\tIt is meant to bring the full power of git's diff engine\n+\tto your files, without having to check them into a git\n+\trepository first.\n+\n+\n+OPTIONS\n+-------\n+include::diff-options.txt[]\n+\n+<path1> <path2>::\n+\tThe <paths> parameters are used to determine which files\n+\tshould be compared against each other.\n+\n+\n+EXAMPLES\n+--------\n+\n+See the differences README.new has relative to README::\n++\n+------------\n+$ git diff2 README README.new\n+------------\n++\n+\n+Check if there is white space breakage in the changes leading from\n+old/ to new/::\n++\n+------------\n+$ git diff2 --check old/ new/\n+------------\n++\n+\n+Author\n+------\n+Written by Johannes Schindelin <johannes.schindelin@gmx.de>\n+\n+Documentation\n+--------------\n+Documentation by Johannes Schindelin and the git-list <git@vger.kernel.org>.\n+\n+GIT\n+---\n+Part of the gitlink:git[7] suite\n+\ndiff --git a/Makefile b/Makefile\nindex ebecbbd..39a1a09 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -278,6 +278,7 @@ BUILTIN_OBJS = \\\n \tbuiltin-count-objects.o \\\n \tbuiltin-describe.o \\\n \tbuiltin-diff.o \\\n+\tbuiltin-diff2.o \\\n \tbuiltin-diff-files.o \\\n \tbuiltin-diff-index.o \\\n \tbuiltin-diff-tree.o \\\ndiff --git a/builtin-diff2.c b/builtin-diff2.c\nnew file mode 100644\nindex 0000000..1de82c1\n--- /dev/null\n+++ b/builtin-diff2.c\n@@ -0,0 +1,163 @@\n+#include \"cache.h\"\n+#include \"diff.h\"\n+#include \"diffcore.h\"\n+#include \"path-list.h\"\n+\n+static const char diff2_usage[] = \"git diff2 [diff-opts] file1 file2\";\n+\n+static int read_directory(const char *path, struct path_list *list)\n+{\n+\tDIR *dir;\n+\tstruct dirent *e;\n+\n+\tif (!(dir = opendir(path)))\n+\t\treturn error(\"Could not open directory %s\", path);\n+\n+\twhile ((e = readdir(dir)))\n+\t\tif (strcmp(\".\", e->d_name) && strcmp(\"..\", e->d_name))\n+\t\t\tpath_list_insert(xstrdup(e->d_name), list);\n+\n+\tclosedir(dir);\n+\treturn 0;\n+}\n+\n+static int queue_diff(struct diff_options *o,\n+\t\tconst char *name1, const char *name2)\n+{\n+\tstruct stat st;\n+\tint mode1 = 0, mode2 = 0;\n+\n+\tif (name1) {\n+\t\tif (stat(name1, &st))\n+\t\t\treturn error(\"Could not access '%s'\", name1);\n+\t\tmode1 = st.st_mode;\n+\t}\n+\tif (name2) {\n+\t\tif (stat(name2, &st))\n+\t\t\treturn error(\"Could not access '%s'\", name1);\n+\t\tmode2 = st.st_mode;\n+\t}\n+\n+\tif (mode1 && mode2 && S_ISDIR(mode1) != S_ISDIR(mode2))\n+\t\treturn error(\"file/directory conflict: %s, %s\", name1, name2);\n+\n+\tif (S_ISDIR(mode1) || S_ISDIR(mode2)) {\n+\t\tchar buffer1[PATH_MAX], buffer2[PATH_MAX];\n+\t\tstruct path_list p1 = {NULL, 0, 0, 1}, p2 = {NULL, 0, 0, 1};\n+\t\tint len1 = 0, len2 = 0, i1, i2, ret = 0;\n+\n+\t\tif (name1 && read_directory(name1, &p1))\n+\t\t\treturn -1;\n+\t\tif (name2 && read_directory(name2, &p2)) {\n+\t\t\tpath_list_clear(&p1, 0);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (name1) {\n+\t\t\tlen1 = strlen(name1);\n+\t\t\tif (len1 > 0 && name1[len1 - 1] == '/')\n+\t\t\t\tlen1--;\n+\t\t\tmemcpy(buffer1, name1, len1);\n+\t\t\tbuffer1[len1++] = '/';\n+\t\t}\n+\n+\t\tif (name2) {\n+\t\t\tlen2 = strlen(name2);\n+\t\t\tif (len2 > 0 && name2[len2 - 1] == '/')\n+\t\t\t\tlen2--;\n+\t\t\tmemcpy(buffer2, name2, len2);\n+\t\t\tbuffer2[len2++] = '/';\n+\t\t}\n+\n+\t\tfor (i1 = i2 = 0; !ret && (i1 < p1.nr || i2 < p2.nr); ) {\n+\t\t\tconst char *n1, *n2;\n+\t\t\tint comp;\n+\n+\t\t\tif (i1 == p1.nr)\n+\t\t\t\tcomp = 1;\n+\t\t\telse if (i2 == p2.nr)\n+\t\t\t\tcomp = -1;\n+\t\t\telse\n+\t\t\t\tcomp = strcmp(p1.items[i1].path,\n+\t\t\t\t\tp2.items[i2].path);\n+\n+\t\t\tif (comp > 0)\n+\t\t\t\tn1 = NULL;\n+\t\t\telse {\n+\t\t\t\tn1 = buffer1;\n+\t\t\t\tstrncpy(buffer1 + len1, p1.items[i1++].path,\n+\t\t\t\t\t\tPATH_MAX - len1);\n+\t\t\t}\n+\n+\t\t\tif (comp < 0)\n+\t\t\t\tn2 = NULL;\n+\t\t\telse {\n+\t\t\t\tn2 = buffer2;\n+\t\t\t\tstrncpy(buffer2 + len2, p2.items[i2++].path,\n+\t\t\t\t\t\tPATH_MAX - len2);\n+\t\t\t}\n+\n+\t\t\tret = queue_diff(o, n1, n2);\n+\t\t}\n+\t\tpath_list_clear(&p1, 0);\n+\t\tpath_list_clear(&p2, 0);\n+\n+\t\treturn ret;\n+\t} else {\n+\t\tstruct diff_filespec *d1, *d2;\n+\n+\t\tif (o->reverse_diff) {\n+\t\t\tunsigned tmp;\n+\t\t\tconst char *tmp_c;\n+\t\t\ttmp = mode1; mode1 = mode2; mode2 = tmp;\n+\t\t\ttmp_c = name1; name1 = name2; name2 = tmp_c;\n+\t\t}\n+\n+\t\tif (!name1)\n+\t\t\tname1 = \"/dev/null\";\n+\t\tif (!name2)\n+\t\t\tname2 = \"/dev/null\";\n+\t\td1 = alloc_filespec(name1);\n+\t\td2 = alloc_filespec(name2);\n+\t\tfill_filespec(d1, null_sha1, mode1);\n+\t\tfill_filespec(d2, null_sha1, mode2);\n+\n+\t\tdiff_queue(&diff_queued_diff, d1, d2);\n+\t\treturn 0;\n+\t}\n+}\n+\n+int cmd_diff2(int argc, char **argv, char **envp)\n+{\n+\tstruct diff_options options;\n+\tint i, i2;\n+        int nongit = 0;\n+\n+        setup_git_directory_gently(&nongit);\n+\tgit_config(git_diff_ui_config);\n+\n+\tdiff_setup(&options);\n+\tfor (i = 1; i < argc; ) {\n+\t\tif (!strcmp(\"--\", argv[i])) {\n+\t\t\ti++;\n+\t\t\tbreak;\n+\t\t}\n+\t\ti2 = diff_opt_parse(&options,\n+\t\t\t\t(const char **)argv + i, argc - i);\n+\t\tif (!i2)\n+\t\t\tbreak;\n+\t\ti += i2;\n+\t}\n+\tif (diff_setup_done(&options) < 0)\n+\t\tdie(\"diff_setup_done failed\");\n+\tif (!options.output_format)\n+\t\toptions.output_format = DIFF_FORMAT_PATCH;\n+\n+\tif (argc - i != 2)\n+\t\tusage(diff2_usage);\n+\n+\tqueue_diff(&options, argv[i], argv[i + 1]);\n+\tdiffcore_std(&options);\n+\tdiff_flush(&options);\n+\treturn 0;\n+}\ndiff --git a/builtin.h b/builtin.h\nindex 57e8741..b872ecc 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -29,6 +29,7 @@ extern int cmd_describe(int argc, const char **argv, const char *prefix);\n extern int cmd_diff_files(int argc, const char **argv, const char *prefix);\n extern int cmd_diff_index(int argc, const char **argv, const char *prefix);\n extern int cmd_diff(int argc, const char **argv, const char *prefix);\n+extern int cmd_diff2(int argc, const char **argv, const char *prefix);\n extern int cmd_diff_tree(int argc, const char **argv, const char *prefix);\n extern int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix);\n extern int cmd_for_each_ref(int argc, const char **argv, const char *prefix);\ndiff --git a/diff.c b/diff.c\nindex 12c8b2b..701880a 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -2406,7 +2406,8 @@ static void diff_resolve_rename_copy(void)\n \t\t\t\tp->status = DIFF_STATUS_RENAMED;\n \t\t}\n \t\telse if (hashcmp(p->one->sha1, p->two->sha1) ||\n-\t\t\t p->one->mode != p->two->mode)\n+\t\t\t p->one->mode != p->two->mode ||\n+\t\t\t is_null_sha1(p->one->sha1))\n \t\t\tp->status = DIFF_STATUS_MODIFIED;\n \t\telse {\n \t\t\t/* This is a \"no-change\" entry and should not\ndiff --git a/git.c b/git.c\nindex 4dd1967..dfeddf2 100644\n--- a/git.c\n+++ b/git.c\n@@ -238,6 +238,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp)\n \t\t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n \t\t{ \"describe\", cmd_describe, RUN_SETUP },\n \t\t{ \"diff\", cmd_diff, RUN_SETUP | USE_PAGER },\n+\t\t{ \"diff2\", cmd_diff2, USE_PAGER },\n \t\t{ \"diff-files\", cmd_diff_files, RUN_SETUP },\n \t\t{ \"diff-index\", cmd_diff_index, RUN_SETUP },\n \t\t{ \"diff-tree\", cmd_diff_tree, RUN_SETUP },\n-- \n1.5.0.rc4.2449.ga6f47\n"}]}