All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jonathan Tan <jonathantanmy@google.com>
To: git@vger.kernel.org
Cc: Jonathan Tan <jonathantanmy@google.com>, newren@gmail.com
Subject: [RFC PATCH] merge-recursive: symlink's descendants not in way
Date: Tue, 17 Sep 2019 14:50:40 -0700	[thread overview]
Message-ID: <20190917215040.132503-1-jonathantanmy@google.com> (raw)
In-Reply-To: <CABPp-BHpXWF+1hKUTfn8s-y4MJZXz+jUVS_K10eKyD6PGwo=gg@mail.gmail.com>

When the working tree has:
 - foo (symlink)
 - foo/bar (directory)

and the user merges a commit that deletes the foo symlink and instead
contains:
 - foo (directory)
 - foo/bar (file)

the merge should happen without requiring user intervention. However,
this does not happen.

In merge_trees(), process_entry() will be invoked first for "foo/bar",
then "foo" (in reverse lexicographical order). process_entry() correctly
reaches "Case B: Added in one", but dir_in_way() states that "bar" is
already present as a directory, causing a directory/file conflict at the
wrong point.

Instead, teach dir_in_way() that directories under symlinks are not "in
the way", so that symlinks are treated as regular files instead of
directories containing other directories and files. Thus, the "else"
branch will be followed instead: "foo/bar" will be added to the working
tree, make_room_for_path() being indirectly called to unlink the "foo"
symlink (just like if "foo" were a regular file instead). When
process_entry() is subsequently invoked for "foo", process_entry() will
reach "Case A: Deleted in one", and will handle it as "Add/delete" or
"Modify/delete" appropriately (including reinstatement of the previously
unlinked symlink with a new unique filename if necessary, again, just
like if "foo" were a regular file instead).

Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
---
Thanks to Elijah for his help. Some of the commit message is based on
his explanation [1].

I'm finding this relatively complicated, so I'm sending this as RFC. My
main concern is that whether all callers of dir_in_way() are OK with its
behavior change, and if yes, how to explain it. I suspect that this is
correct because dir_in_way() should behave consistently for all its
callers, but I might be wrong.

The other thing is whether the commit message is clear enough. In
particular, whether it needs more detail (I didn't mention the index,
for example), or whether it should be more concise (for example, I
thought of just stating that we treat symlinks as regular files and this
would be backed up by the fact that I've updated the only part of
merge-recursive.c that calls lstat() and then S_ISDIR).

[1] https://public-inbox.org/git/CABPp-BHpXWF+1hKUTfn8s-y4MJZXz+jUVS_K10eKyD6PGwo=gg@mail.gmail.com/
---
 merge-recursive.c          | 40 +++++++++++++++++++++++++++++++++-----
 t/t3030-merge-recursive.sh | 27 +++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 5 deletions(-)

diff --git a/merge-recursive.c b/merge-recursive.c
index 6b812d67e3..a2029a4e94 100644
--- a/merge-recursive.c
+++ b/merge-recursive.c
@@ -747,7 +747,7 @@ static int dir_in_way(struct index_state *istate, const char *path,
 {
 	int pos;
 	struct strbuf dirpath = STRBUF_INIT;
-	struct stat st;
+	int ret = 0;
 
 	strbuf_addstr(&dirpath, path);
 	strbuf_addch(&dirpath, '/');
@@ -758,13 +758,43 @@ static int dir_in_way(struct index_state *istate, const char *path,
 		pos = -1 - pos;
 	if (pos < istate->cache_nr &&
 	    !strncmp(dirpath.buf, istate->cache[pos]->name, dirpath.len)) {
-		strbuf_release(&dirpath);
-		return 1;
+		ret = 1;
+		goto cleanup;
 	}
 
+	if (check_working_copy) {
+		struct stat st;
+
+		strbuf_trim_trailing_dir_sep(&dirpath);
+		if (lstat(dirpath.buf, &st))
+			goto cleanup; /* doesn't exist, OK */
+		if (!S_ISDIR(st.st_mode))
+			goto cleanup; /* not directory, OK */
+		if (empty_ok && is_empty_dir(dirpath.buf))
+			goto cleanup;
+
+		/*
+		 * The given path is a directory, but this should not
+		 * be treated as "in the way" if one of this
+		 * directory's ancestors is a symlink. Check for this
+		 * case.
+		 */
+		while (1) {
+			char *slash = strrchr(dirpath.buf, '/');
+
+			if (!slash) {
+				ret = 1;
+				goto cleanup;
+			}
+			*slash = '\0';
+			if (!lstat(dirpath.buf, &st) && S_ISLNK(st.st_mode))
+				goto cleanup;
+		}
+	}
+
+cleanup:
 	strbuf_release(&dirpath);
-	return check_working_copy && !lstat(path, &st) && S_ISDIR(st.st_mode) &&
-		!(empty_ok && is_empty_dir(path));
+	return ret;
 }
 
 /*
diff --git a/t/t3030-merge-recursive.sh b/t/t3030-merge-recursive.sh
index ff641b348a..dfd617a845 100755
--- a/t/t3030-merge-recursive.sh
+++ b/t/t3030-merge-recursive.sh
@@ -452,6 +452,33 @@ test_expect_success 'merge-recursive d/f conflict result' '
 
 '
 
+test_expect_success 'dir in working tree with symlink ancestor does not produce d/f conflict' '
+	git init sym &&
+	(
+		cd sym &&
+		ln -s . foo &&
+		mkdir bar &&
+		touch bar/file &&
+		git add foo bar/file &&
+		git commit -m "foo symlink" &&
+
+		git checkout -b branch1 &&
+		git commit --allow-empty -m "empty commit" &&
+
+		git checkout master &&
+		git rm foo &&
+		mkdir foo &&
+		touch foo/bar &&
+		git add foo/bar &&
+		git commit -m "replace foo symlink with real foo dir and foo/bar file" &&
+
+		git checkout branch1 &&
+
+		# Exercise to make sure that this works without errors
+		git cherry-pick master
+	)
+'
+
 test_expect_success 'reset and 3-way merge' '
 
 	git reset --hard "$c2" &&
-- 
2.23.0.237.gc6a4ce50a0-goog


  reply	other threads:[~2019-09-17 21:50 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-09-16 21:47 merge-recursive thinks symlink's child dirs are "real" Jonathan Tan
2019-09-16 22:15 ` SZEDER Gábor
2019-09-17 15:54   ` Elijah Newren
2019-09-17  0:09 ` Jonathan Nieder
2019-09-17 16:02   ` Elijah Newren
2019-09-17 15:48 ` Elijah Newren
2019-09-17 21:50   ` Jonathan Tan [this message]
2019-09-17 22:23     ` [RFC PATCH] merge-recursive: symlink's descendants not in way Junio C Hamano
2019-09-17 22:32       ` Jonathan Tan
2019-09-17 22:37         ` Junio C Hamano
2019-09-17 22:49           ` Jonathan Tan
2019-09-17 23:02     ` SZEDER Gábor
2019-09-18  0:35     ` Elijah Newren

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20190917215040.132503-1-jonathantanmy@google.com \
    --to=jonathantanmy@google.com \
    --cc=git@vger.kernel.org \
    --cc=newren@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.