Commit Diff


commit - e26bafba995edab19824ed3ed6d81535259b39f1
commit + 11cc08c1dfab6c56e9e4bd98ba204b5a7d56ea9e
blob - 3b8ae9a28141f69e42518bafe9e4ad73f3227cc8
blob + 18f9d880dc8bece0503a96fcd827fbaddd5c5125
--- lib/worktree.c
+++ lib/worktree.c
@@ -844,6 +844,73 @@ update_symlink(const char *ondisk_path, const char *ta
 }
 
 /*
+ * Overwrite a symlink (or a regular file in case there was a "bad" symlink)
+ * in the work tree with a file that contains conflict markers and the
+ * conflicting target paths of the original version and two derived versions
+ * of a symlink.
+ */
+static const struct got_error *
+install_symlink_conflict(const char *deriv_target,
+    struct got_object_id *deriv_base_commit_id, const char *orig_target,
+    const char *label_orig, const char *local_target, const char *ondisk_path)
+{
+	const struct got_error *err;
+	char *id_str = NULL, *label_deriv = NULL, *path = NULL;
+	FILE *f = NULL;
+
+	err = got_object_id_str(&id_str, deriv_base_commit_id);
+	if (err)
+		return got_error_from_errno("asprintf");
+
+	if (asprintf(&label_deriv, "%s: commit %s",
+	    GOT_MERGE_LABEL_MERGED, id_str) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	err = got_opentemp_named(&path, &f, "got-symlink-conflict");
+	if (err)
+		goto done;
+
+	if (fprintf(f, "%s: Could not install symbolic link because of merge "
+	    "conflict.\nln(1) may be used to fix the situation. If this is "
+	    "intended to be a\nregular file instead then its expected "
+	    "contents may be filled in.\nThe following conflicting symlink "
+	    "target paths were found:\n"
+	    "%s %s\n%s\n%s%s%s%s%s\n%s\n%s\n", getprogname(),
+	    GOT_DIFF_CONFLICT_MARKER_BEGIN, label_deriv, deriv_target,
+	    orig_target ? label_orig : "",
+	    orig_target ? "\n" : "",
+	    orig_target ? orig_target : "",
+	    orig_target ? "\n" : "",
+	    GOT_DIFF_CONFLICT_MARKER_SEP,
+	    local_target, GOT_DIFF_CONFLICT_MARKER_END) < 0) {
+		err = got_error_from_errno2("fprintf", path);
+		goto done;
+	}
+
+	if (unlink(ondisk_path) == -1) {
+		err = got_error_from_errno2("unlink", ondisk_path);
+		goto done;
+	}
+	if (rename(path, ondisk_path) == -1) {
+		err = got_error_from_errno3("rename", path, ondisk_path);
+		goto done;
+	}
+	if (chmod(ondisk_path, GOT_DEFAULT_FILE_MODE) == -1) {
+		err = got_error_from_errno2("chmod", ondisk_path);
+		goto done;
+	}
+done:
+	if (f != NULL && fclose(f) == EOF && err == NULL)
+		err = got_error_from_errno2("fclose", path);
+	free(path);
+	free(id_str);
+	free(label_deriv);
+	return err;
+}
+
+/*
  * Merge a symlink into the work tree, where blob_orig acts as the common
  * ancestor, blob_deriv acts as the first derived version, and the symlink
  * on disk acts as the second derived version.
@@ -860,8 +927,10 @@ merge_symlink(struct got_worktree *worktree,
 	const struct got_error *err = NULL;
 	char *ancestor_target = NULL, *deriv_target = NULL;
 	struct stat sb;
-	ssize_t ondisk_len;
+	ssize_t ondisk_len, deriv_len;
 	char ondisk_target[PATH_MAX];
+	int have_local_change = 0;
+	int have_incoming_change = 0;
 
 	if (lstat(ondisk_path, &sb) == -1)
 		return got_error_from_errno2("lstat", ondisk_path);
@@ -889,28 +958,54 @@ merge_symlink(struct got_worktree *worktree,
 	if (err)
 		goto done;
 
-	if (ancestor_target && (ondisk_len != strlen(ancestor_target) ||
-	    memcmp(ondisk_target, ancestor_target, ondisk_len) != 0)) {
-		/*
-		 * The symlink has changed on-disk (second derived version).
-		 * Keep that change and discard the incoming change (first
-		 * derived version).
-		 * TODO: Need tree-conflict resolution to handle this.
-		 */
-		err = (*progress_cb)(progress_arg, GOT_STATUS_OBSTRUCTED,
-		    path);
-	} else if (ondisk_len == strlen(deriv_target) &&
-	    memcmp(ondisk_target, deriv_target, ondisk_len) == 0) {
-		/* Both versions made the same change. */
-		err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, path);
-	} else {
+	if (ancestor_target == NULL ||
+	    (ondisk_len != strlen(ancestor_target) ||
+	    memcmp(ondisk_target, ancestor_target, ondisk_len) != 0))
+		have_local_change = 1;
+
+	deriv_len = strlen(deriv_target);
+	if (ancestor_target == NULL ||
+	    (deriv_len != strlen(ancestor_target) ||
+	    memcmp(deriv_target, ancestor_target, deriv_len) != 0))
+		have_incoming_change = 1;
+
+	if (!have_local_change && !have_incoming_change) {
+		if (ancestor_target) {
+			/* Both sides made the same change. */
+			err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE,
+			    path);
+		} else if (deriv_len == ondisk_len &&
+		    memcmp(ondisk_target, deriv_target, deriv_len) == 0) {
+			/* Both sides added the same symlink. */
+			err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE,
+			    path);
+		} else {
+			/* Both sides added symlinks which don't match. */
+			err = install_symlink_conflict(deriv_target,
+			    deriv_base_commit_id, ancestor_target,
+			    label_orig, ondisk_target, ondisk_path);
+			if (err)
+				goto done;
+			err = (*progress_cb)(progress_arg, GOT_STATUS_CONFLICT,
+			    path);
+		}
+	} else if (!have_local_change && have_incoming_change) {
 		/* Apply the incoming change. */
 		err = update_symlink(ondisk_path, deriv_target,
 		    strlen(deriv_target));
 		if (err)
 			goto done;
 		err = (*progress_cb)(progress_arg, GOT_STATUS_MERGE, path);
+	} else if (have_local_change && have_incoming_change) {
+		err = install_symlink_conflict(deriv_target,
+		    deriv_base_commit_id, ancestor_target, label_orig,
+		    ondisk_target, ondisk_path);
+		if (err)
+			goto done;
+		err = (*progress_cb)(progress_arg, GOT_STATUS_CONFLICT,
+		    path);
 	}
+
 done:
 	free(ancestor_target);
 	free(deriv_target);
blob - d6bd1a3b13dd12f24ee44b0e0b915d66d1912d1e
blob + 28666959922bc27bcad3ced40c4aed3ca140f2c0
--- regress/cmdline/cherrypick.sh
+++ regress/cmdline/cherrypick.sh
@@ -511,17 +511,16 @@ function test_cherrypick_symlink_conflicts {
 	(cd $testroot/wt && got cherrypick $commit_id2 > $testroot/stdout)
 
 	echo -n > $testroot/stdout.expected
-	echo "~  alpha.link" >> $testroot/stdout.expected
-	echo "~  epsilon/beta.link" >> $testroot/stdout.expected
+	echo "C  alpha.link" >> $testroot/stdout.expected
+	echo "C  epsilon/beta.link" >> $testroot/stdout.expected
 	echo "U  dotgotbar.link" >> $testroot/stdout.expected
-	echo "~  epsilon.link" >> $testroot/stdout.expected
+	echo "C  epsilon.link" >> $testroot/stdout.expected
 	echo "U  dotgotfoo.link" >> $testroot/stdout.expected
 	echo "D  nonexistent.link" >> $testroot/stdout.expected
 	echo "!  zeta.link" >> $testroot/stdout.expected
-	echo "G  new.link" >> $testroot/stdout.expected
+	echo "C  new.link" >> $testroot/stdout.expected
 	echo "Merged commit $commit_id2" >> $testroot/stdout.expected
-	echo "File paths obstructed by a non-regular file: 3" \
-		>> $testroot/stdout.expected
+	echo "Files with new merge conflicts: 4" >> $testroot/stdout.expected
 	cmp -s $testroot/stdout.expected $testroot/stdout
 	ret="$?"
 	if [ "$ret" != "0" ]; then
@@ -530,34 +529,62 @@ function test_cherrypick_symlink_conflicts {
 		return 1
 	fi
 
-	if ! [ -h $testroot/wt/alpha.link ]; then
-		echo "alpha.link is not a symlink"
+	if [ -h $testroot/wt/alpha.link ]; then
+		echo "alpha.link is a symlink"
 		test_done "$testroot" "1"
 		return 1
 	fi
 
-	readlink $testroot/wt/alpha.link > $testroot/stdout
-	echo "gamma/delta" > $testroot/stdout.expected
-	cmp -s $testroot/stdout.expected $testroot/stdout
+	cat > $testroot/symlink-conflict-header <<EOF
+got: Could not install symbolic link because of merge conflict.
+ln(1) may be used to fix the situation. If this is intended to be a
+regular file instead then its expected contents may be filled in.
+The following conflicting symlink target paths were found:
+EOF
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo "3-way merge base: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo "alpha" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "gamma/delta" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/alpha.link $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
 	ret="$?"
 	if [ "$ret" != "0" ]; then
-		diff -u $testroot/stdout.expected $testroot/stdout
+		diff -u $testroot/content.expected $testroot/content
 		test_done "$testroot" "$ret"
 		return 1
 	fi
 
-	if ! [ -h $testroot/wt/epsilon.link ]; then
-		echo "epsilon.link is not a symlink"
+	if [ -h $testroot/wt/epsilon.link ]; then
+		echo "epsilon.link is a symlink"
 		test_done "$testroot" "1"
 		return 1
 	fi
 
-	readlink $testroot/wt/epsilon.link > $testroot/stdout
-	echo "beta" > $testroot/stdout.expected
-	cmp -s $testroot/stdout.expected $testroot/stdout
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "gamma" >> $testroot/content.expected
+	echo "3-way merge base: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo "epsilon" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/epsilon.link $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
 	ret="$?"
 	if [ "$ret" != "0" ]; then
-		diff -u $testroot/stdout.expected $testroot/stdout
+		diff -u $testroot/content.expected $testroot/content
 		test_done "$testroot" "$ret"
 		return 1
 	fi
@@ -580,12 +607,29 @@ function test_cherrypick_symlink_conflicts {
 		return 1
 	fi
 
-	readlink $testroot/wt/epsilon/beta.link > $testroot/stdout
-	echo "../gamma" > $testroot/stdout.expected
-	cmp -s $testroot/stdout.expected $testroot/stdout
+	if [ -h $testroot/wt/epsilon/beta.link ]; then
+		echo "epsilon/beta.link is a symlink"
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "../gamma/delta" >> $testroot/content.expected
+	echo "3-way merge base: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo "../beta" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "../gamma" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/epsilon/beta.link $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
 	ret="$?"
 	if [ "$ret" != "0" ]; then
-		diff -u $testroot/stdout.expected $testroot/stdout
+		diff -u $testroot/content.expected $testroot/content
 		test_done "$testroot" "$ret"
 		return 1
 	fi
@@ -628,18 +672,26 @@ function test_cherrypick_symlink_conflicts {
 		return 1
 	fi
 
-	if ! [ -h $testroot/wt/new.link ]; then
-		echo "new.link is not a symlink"
+	if [ -h $testroot/wt/new.link ]; then
+		echo "new.link is a symlink"
 		test_done "$testroot" "1"
 		return 1
 	fi
 
-	readlink $testroot/wt/new.link > $testroot/stdout
-	echo "alpha" > $testroot/stdout.expected
-	cmp -s $testroot/stdout.expected $testroot/stdout
+	cp $testroot/symlink-conflict-header $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo "alpha" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	echo -n "" >> $testroot/content.expected
+
+	cp $testroot/wt/new.link $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
 	ret="$?"
 	if [ "$ret" != "0" ]; then
-		diff -u $testroot/stdout.expected $testroot/stdout
+		diff -u $testroot/content.expected $testroot/content
 		test_done "$testroot" "$ret"
 		return 1
 	fi