commit 11cc08c1dfab6c56e9e4bd98ba204b5a7d56ea9e from: Stefan Sperling date: Thu Jul 23 14:21:31 2020 UTC handle symlink conflicts by installing a file that contains conflict markers 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 <> $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