git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v10 18/36] merge-recursive: add get_directory_renames()

From
Elijah Newren <newren@gmail.com>
Date
Oct 11, 2019, 20:02 UTC
Message-ID
<CABPp-BFrFrm1-MdttBDRHMmun++HxwV1tySoDb75f58ObwMwfg@mail.gmail.com>
In-Reply-To
<nycvar.QRO.7.76.6.1910092044590.46@tvgsbejvaqbjf.bet>

// Dropping a few folks from the cc list as the thread is so old that I think it should just be the normal git mailing list.

Hi Dscho,

On Wed, Oct 9, 2019 at 1:39 PM Johannes Schindelin <Johannes.Schindelin@gmx.de> wrote:

Show 5 quoted lines
>
> Hi Elijah,
>
> sorry about the blast from the past, but I just stumbled over something
> I could not even find any discussion about:

I'm curious what brought you to this part of the codebase, but either way, thanks for sending an email with your findings.

More comments below...
[...]
Show 47 quoted lines
> > @@ -1357,6 +1395,169 @@ static struct diff_queue_struct *get_diffpairs(struct merge_options *o,
> >       return ret;
> >  }
> >
> > +static void get_renamed_dir_portion(const char *old_path, const char *new_path,
> > +                                 char **old_dir, char **new_dir)
> > +{
> > +     char *end_of_old, *end_of_new;
> > +     int old_len, new_len;
> > +
> > +     *old_dir = NULL;
> > +     *new_dir = NULL;
> > +
> > +     /*
> > +      * For
> > +      *    "a/b/c/d/e/foo.c" -> "a/b/some/thing/else/e/foo.c"
> > +      * the "e/foo.c" part is the same, we just want to know that
> > +      *    "a/b/c/d" was renamed to "a/b/some/thing/else"
> > +      * so, for this example, this function returns "a/b/c/d" in
> > +      * *old_dir and "a/b/some/thing/else" in *new_dir.
> > +      *
> > +      * Also, if the basename of the file changed, we don't care.  We
> > +      * want to know which portion of the directory, if any, changed.
> > +      */
> > +     end_of_old = strrchr(old_path, '/');
> > +     end_of_new = strrchr(new_path, '/');
> > +
> > +     if (end_of_old == NULL || end_of_new == NULL)
> > +             return;
> > +     while (*--end_of_new == *--end_of_old &&
> > +            end_of_old != old_path &&
> > +            end_of_new != new_path)
> > +             ; /* Do nothing; all in the while loop */
> > +     /*
> > +      * We've found the first non-matching character in the directory
> > +      * paths.  That means the current directory we were comparing
> > +      * represents the rename.  Move end_of_old and end_of_new back
> > +      * to the full directory name.
> > +      */
> > +     if (*end_of_old == '/')
> > +             end_of_old++;
> > +     if (*end_of_old != '/')
> > +             end_of_new++;
>
> Is this intentional? Even after thinking about it for fifteen minutes, I
> think it was probable meant to test for `*end_of_new == '/'` instead of
> `*end_of_old != '/'`. And...

Yeah, looks like a mess-up, and yes your suspicion is correct about what was intended.

Hilariously, though, no bug results from this. Since these are paths, as canonicalized by git (i.e. not as specified by the user where they might accidentally type multiple consecutive slashes), there will never be two slashes in a row (because we can't have directories with an empty name). Thus, it is guaranteed at this point that *end_of_old != '/', and end_of_new is thus unconditionally advanced. Further, since we wanted to find the _next_ '/' character after end_of_new, then there were two cases: (1) end_of_new already pointed at a slash character in which case we needed it to be advanced, or (2) end_of_new didn't point to a slash character so it wouldn't hurt at all to advance it.

Show 15 quoted lines
> > +     end_of_old = strchr(end_of_old, '/');
> > +     end_of_new = strchr(end_of_new, '/');
>
> ... while I satisfied myself that these calls cannot return `NULL` at
> this point, it took quite a few minutes of reasoning.
>
> So I think we might want to rewrite these past 6 lines, to make
> everything quite a bit more obvious, like this:
>
>         if (end_of_old != old_path)
>                 while (*(++end_of_old) != '/')
>                         ; /* keep looking */
>         if (end_of_new != new_path)
>                 while (*(++end_of_new) != '/')
>                         ; /* keep looking */

I think your if-checks here are not correct. Let's say that old_path was "tar/foo.c" and new_path was "star/foo.c". The initial strrchr will bring both end_of_* variables back to the slash. The moving left while equal will move end_of_old back to old_path (i.e. pointing to the "t") and end_of_new back to pointing at "t" as well. Here's where your six alternate lines would kick in, and would leave end_of_old at old_path, while moving end_of_new to the '/', making it look like we had a rename of "" (the empty string or root directory) to "star" instead of a rename of "tar" to "star". If you dropped your if-checks (just having the while loops), then I think it does the right thing.

Show 7 quoted lines
> There is _still_ one thing that makes this harder than trivial to reason
> about: the case where one of `*end_of_old` and `*end_of_new` is a slash.
> At this point, we assume that `*end_of_old != *end_of_new` (more about
> that assumption in the next paragraph), therefore only one of them can
> be a slash, and we want to advance beyond it. But even if the pointer
> does not point at a slash, we want to look for one, so we want to
> advance beyond it.

I should probably add a comment that we want to advance BOTH to the next slash. I would have just used strchr() but it wouldn't advance the string if it already points to what I'm looking for. Actually, I guess I could simplify the code by unconditionally advancing by one character, then calling strchr(). In other words, simplifying these six lines to just

       end_of_old = strchr(++end_of_old, '/');
       end_of_new = strchr(++end_of_new, '/');
Show 8 quoted lines
> I also think that we need an extra guard: we do not handle the case
> `a/b/c` -> `a/b/d` well. As stated a few lines above, "if the basename
> of the file changed, we don't care". So we start looking at the last
> slash, then go backwards, and since everything matches, end up with
> `end_of_old == old_path` and `end_of_new == new_path`. The current code
> will advance `end_of_new` (which I think is wrong) and then looks for
> the next slash in both `end_of_new` and `end_of_old` (which is also
> wrong).

The current code is slightly convoluted, but I would say it's not wrong for this case. If we renamed a/b/c -> a/b/d, then there isn't a directory rename; the leading directory (a/b/) is the same for both. You are right that the advancing of end_of_old and end_of_new to the next slash would result in what looks like a rename of "a" to "a", but the checks at the end checked for this case and only returned something for *old_dir and *new_dir if these didn't match; in fact, it's the part of the code at the end of your email that you didn't comment on, here:

Show 14 quoted lines
> > +
> > +     /*
> > +      * It may have been the case that old_path and new_path were the same
> > +      * directory all along.  Don't claim a rename if they're the same.
> > +      */
> > +     old_len = end_of_old - old_path;
> > +     new_len = end_of_new - new_path;
> > +
> > +     if (old_len != new_len || strncmp(old_path, new_path, old_len)) {
> > +             *old_dir = xstrndup(old_path, old_len);
> > +             *new_dir = xstrndup(new_path, new_len);
> > +     }
> > +}
> > [...]

However, we could drop this late check by just doing a simpler earlier check to see if end_of_old == old_path and end_of_new == new_path after the "find first non-equal character" step and before advancing to the next '/', and if that condition is found, then return early with no match.

However, since you highlighted this code, there are two other special cases that might be interesting:

1) What if we are renaming e.g. foo/bar/baz.c ->
leading/dir/foo/bar/baz.c?  Then after trying to find the first
non-matching char we'll have end_of_old == old_path and *end_of_new ==
'f', and the advancing makes it look like "foo" being renamed to
"leading/dir/foo".  Since the root directory cannot be renamed (it
always exists on both sides of history), this probably makes sense as
the right thing to return.
2) What if the renaming went the other way, from
leading/dir/foo/bar/baz.c -> foo/bar/baz.c?  The whole advancing thing
makes this look like "leading/dir/foo" being renamed to "foo", instead
of "leading/dir" being renamed to "" (the root directory).  If we
don't detect it as "leading/dir" being renamed (merged into) the root
directory, then new files added directly within leading/dir/ on the
other side of history won't be moved by directory rename detection
into the root directory.
> Is my reading correct?

I'm not sure if I've answered your questions; let me know if not. I have generated a couple patches to (1) make the code easier to follow, and (2) support the rename/merge of a subdirectory into the root directory. They're waiting for the gitgitgadget CI checks right now, then I'll send them to the list.

Previous: Johannes SchindelinNext: Johannes Schindelin
Message 44 of 78 in “Add directory rename detection to git”
  1. 00/36 Add directory rename detection to gitElijah Newren, Apr 19, 2018
  2. 36/36 merge-recursive: fix check for skipability of working tree updatesElijah Newren, Apr 19, 2018
  3. 29/36 merge-recursive: improve add_cacheinfo error handlingElijah Newren, Apr 19, 2018
  4. 19/36 merge-recursive: check for directory level conflictsElijah Newren, Apr 19, 2018
  5. 07/36 directory rename detection: more involved edge/corner testcasesElijah Newren, Apr 19, 2018
  6. 26/36 merge-recursive: fix remaining directory rename + dirty overwrite casesElijah Newren, Apr 19, 2018
  7. 14/36 merge-recursive: fix leaks of allocated renames and diff_filepairsElijah Newren, Apr 19, 2018
  8. 33/36 merge-recursive: fix was_tracked() to quit lying with some renamed pathsElijah Newren, Apr 19, 2018
  9. Martin ÅgrenApr 19, 2018
  10. Elijah NewrenApr 19, 2018
  11. SZEDER GáborApr 20, 2018
  12. Elijah NewrenApr 20, 2018
  13. 5/36 unpack_trees: fix memory corruption with split_index when src != dstElijah Newren, Apr 21, 2018
  14. Elijah NewrenApr 21, 2018
  15. Duy NguyenApr 22, 2018
  16. Elijah NewrenApr 23, 2018
  17. Duy NguyenApr 23, 2018
  18. Elijah NewrenApr 23, 2018
  19. unpack_trees: fix breakage when o->src_index != o->dst_indexElijah Newren, Apr 24, 2018
  20. Junio C HamanoApr 24, 2018
  21. Junio C HamanoApr 24, 2018
  22. unpack_trees: fix breakage when o->src_index != o->dst_indexElijah Newren, Apr 24, 2018
  23. Duy NguyenApr 29, 2018
  24. Johannes SchindelinApr 29, 2018
  25. Duy NguyenApr 30, 2018
  26. Duy NguyenApr 30, 2018
  27. Elijah NewrenApr 30, 2018
  28. Duy NguyenApr 30, 2018
  29. 24/36 merge-recursive: avoid clobbering untracked files with directory renamesElijah Newren, Apr 19, 2018
  30. 34/36 merge-recursive: fix remainder of was_dirty() to use original indexElijah Newren, Apr 19, 2018
  31. 31/36 merge-recursive: avoid triggering add_cacheinfo error with dirty modElijah Newren, Apr 19, 2018
  32. 20/36 merge-recursive: add computation of collisions due to dir rename & mergingElijah Newren, Apr 19, 2018
  33. 32/36 t6046: testcases checking whether updates can be skipped in a mergeElijah Newren, Apr 19, 2018
  34. SZEDER GáborApr 19, 2018
  35. Elijah NewrenApr 19, 2018
  36. 35/36 merge-recursive: make "Auto-merging" comment show for other mergesElijah Newren, Apr 19, 2018
  37. 23/36 merge-recursive: apply necessary modifications for directory renamesElijah Newren, Apr 19, 2018
  38. 30/36 merge-recursive: move more is_dirty handling to merge_contentElijah Newren, Apr 19, 2018
  39. 28/36 merge-recursive: avoid spurious rename/rename conflict from dir renamesElijah Newren, Apr 19, 2018
  40. 18/36 merge-recursive: add get_directory_renames()Elijah Newren, Apr 19, 2018
  41. SZEDER GáborMay 6, 2018
  42. fixup! merge-recursive: add get_directory_renames()Elijah Newren, May 7, 2018
  43. Johannes SchindelinOct 9, 2019
  44. Elijah NewrenOct 11, 2019
  45. Johannes SchindelinOct 12, 2019
  46. 25/36 merge-recursive: fix overwriting dirty files involved in renamesElijah Newren, Apr 19, 2018
  47. Martin ÅgrenApr 19, 2018
  48. Martin ÅgrenApr 19, 2018
  49. Elijah NewrenApr 19, 2018
  50. 22/36 merge-recursive: when comparing files, don't include treesElijah Newren, Apr 19, 2018
  51. 11/36 directory rename detection: tests for handling overwriting dirty filesElijah Newren, Apr 19, 2018
  52. 06/36 directory rename detection: testcases checking which side did the renameElijah Newren, Apr 19, 2018
  53. 13/36 merge-recursive: introduce new functions to handle rename logicElijah Newren, Apr 19, 2018
  54. 10/36 directory rename detection: tests for handling overwriting untracked filesElijah Newren, Apr 19, 2018
  55. 21/36 merge-recursive: check for file level conflicts then get new nameElijah Newren, Apr 19, 2018
  56. 17/36 merge-recursive: make a helper function for cleanup for handle_renamesElijah Newren, Apr 19, 2018
  57. 16/36 merge-recursive: split out code for determining diff_filepairsElijah Newren, Apr 19, 2018
  58. 15/36 merge-recursive: make !o->detect_rename codepath more obviousElijah Newren, Apr 19, 2018
  59. 27/36 directory rename detection: new testcases showcasing a pair of bugsElijah Newren, Apr 19, 2018
  60. 01/36 directory rename detection: basic testcasesElijah Newren, Apr 19, 2018
  61. 08/36 directory rename detection: testcases exploring possibly suboptimal mergesElijah Newren, Apr 19, 2018
  62. 04/36 directory rename detection: partially renamed directory testcase/discussionElijah Newren, Apr 19, 2018
  63. 09/36 directory rename detection: miscellaneous testcases to complete coverageElijah Newren, Apr 19, 2018
  64. 02/36 directory rename detection: directory splitting testcasesElijah Newren, Apr 19, 2018
  65. 12/36 merge-recursive: move the get_renames() functionElijah Newren, Apr 19, 2018
  66. 03/36 directory rename detection: testcases to avoid taking detection too farElijah Newren, Apr 19, 2018
  67. 05/36 directory rename detection: files/directories in the way of some renamesElijah Newren, Apr 19, 2018
  68. Elijah NewrenApr 19, 2018
  69. Stefan BellerApr 19, 2018
  70. Derrick StoleeApr 19, 2018
  71. Elijah NewrenApr 19, 2018
  72. Junio C HamanoApr 20, 2018
  73. Elijah NewrenApr 23, 2018
  74. 1/2 fixup! merge-recursive: fix was_tracked() to quit lying with some renamed pathsElijah Newren, Apr 24, 2018
  75. 2/2 fixup! t6046: testcases checking whether updates can be skipped in a mergeElijah Newren, Apr 24, 2018
  76. Elijah NewrenApr 23, 2018
  77. Junio C HamanoApr 23, 2018
  78. Elijah NewrenApr 24, 2018

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.