commit af179be739cacd6576fdf9596ac7e61b714ee367 from: Stefan Sperling date: Fri Apr 14 12:15:45 2023 UTC when aborting rebase/histedit/merge, unlink files added by merged changes Otherwise we leave unversioned files behind in the work tree which may interfere with new attempts to rebase or merge the changes again. Problem found by + ok naddy@ commit - 8cdd231889a848b735f84ed6772eab46c2512db9 commit + af179be739cacd6576fdf9596ac7e61b714ee367 blob - b08ea7c98c8952d224400390c47972f2e057ee4e blob + df29e65f90431310315474962e5c738ec544f38d --- lib/worktree.c +++ lib/worktree.c @@ -4700,6 +4700,7 @@ struct revert_file_args { void *patch_arg; struct got_repository *repo; int unlink_added_files; + struct got_pathlist_head *added_files_to_unlink; }; static const struct got_error * @@ -5002,16 +5003,33 @@ revert_file(void *arg, unsigned char status, unsigned goto done; got_fileindex_entry_remove(a->fileindex, ie); if (a->unlink_added_files) { - if (asprintf(&ondisk_path, "%s/%s", - got_worktree_get_root_path(a->worktree), - relpath) == -1) { - err = got_error_from_errno("asprintf"); - goto done; + int do_unlink = a->added_files_to_unlink ? 0 : 1; + + if (a->added_files_to_unlink) { + struct got_pathlist_entry *pe; + + TAILQ_FOREACH(pe, a->added_files_to_unlink, + entry) { + if (got_path_cmp(pe->path, relpath, + pe->path_len, strlen(relpath))) + continue; + do_unlink = 1; + break; + } } - if (unlink(ondisk_path) == -1) { - err = got_error_from_errno2("unlink", - ondisk_path); - break; + + if (do_unlink) { + if (asprintf(&ondisk_path, "%s/%s", + got_worktree_get_root_path(a->worktree), + relpath) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + if (unlink(ondisk_path) == -1) { + err = got_error_from_errno2("unlink", + ondisk_path); + break; + } } } break; @@ -7327,7 +7345,134 @@ done: err = unlockerr; return err; } + +static const struct got_error * +get_paths_changed_between_commits(struct got_pathlist_head *paths, + struct got_object_id *id1, struct got_object_id *id2, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *commit1 = NULL, *commit2 = NULL; + struct got_tree_object *tree1 = NULL, *tree2 = NULL; + + if (id1) { + err = got_object_open_as_commit(&commit1, repo, id1); + if (err) + goto done; + + err = got_object_open_as_tree(&tree1, repo, + got_object_commit_get_tree_id(commit1)); + if (err) + goto done; + } + if (id2) { + err = got_object_open_as_commit(&commit2, repo, id2); + if (err) + goto done; + + err = got_object_open_as_tree(&tree2, repo, + got_object_commit_get_tree_id(commit2)); + if (err) + goto done; + } + + err = got_diff_tree(tree1, tree2, NULL, NULL, -1, -1, "", "", repo, + got_diff_tree_collect_changed_paths, paths, 0); + if (err) + goto done; +done: + if (commit1) + got_object_commit_close(commit1); + if (commit2) + got_object_commit_close(commit2); + if (tree1) + got_object_tree_close(tree1); + if (tree2) + got_object_tree_close(tree2); + return err; +} + +static const struct got_error * +get_paths_added_between_commits(struct got_pathlist_head *added_paths, + struct got_object_id *id1, struct got_object_id *id2, + const char *path_prefix, struct got_repository *repo) +{ + const struct got_error *err; + struct got_pathlist_head merged_paths; + struct got_pathlist_entry *pe; + char *abspath = NULL, *wt_path = NULL; + + TAILQ_INIT(&merged_paths); + + err = get_paths_changed_between_commits(&merged_paths, id1, id2, repo); + if (err) + goto done; + + TAILQ_FOREACH(pe, &merged_paths, entry) { + struct got_diff_changed_path *change = pe->data; + + if (change->status != GOT_STATUS_ADD) + continue; + + if (got_path_is_root_dir(path_prefix)) { + wt_path = strdup(pe->path); + if (wt_path == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + } else { + if (asprintf(&abspath, "/%s", pe->path) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + + err = got_path_skip_common_ancestor(&wt_path, + path_prefix, abspath); + if (err) + goto done; + free(abspath); + abspath = NULL; + } + + err = got_pathlist_append(added_paths, wt_path, NULL); + if (err) + goto done; + wt_path = NULL; + } + +done: + got_pathlist_free(&merged_paths, GOT_PATHLIST_FREE_ALL); + free(abspath); + free(wt_path); + return err; +} + +static const struct got_error * +get_paths_added_in_commit(struct got_pathlist_head *added_paths, + struct got_object_id *id, const char *path_prefix, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_commit_object *commit = NULL; + struct got_object_qid *pid; + + err = got_object_open_as_commit(&commit, repo, id); + if (err) + goto done; + + pid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit)); + + err = get_paths_added_between_commits(added_paths, + pid ? &pid->id : NULL, id, path_prefix, repo); + if (err) + goto done; +done: + if (commit) + got_object_commit_close(commit); + return err; +} + const struct got_error * got_worktree_rebase_abort(struct got_worktree *worktree, struct got_fileindex *fileindex, struct got_repository *repo, @@ -7337,15 +7482,44 @@ got_worktree_rebase_abort(struct got_worktree *worktre const struct got_error *err, *unlockerr, *sync_err; struct got_reference *resolved = NULL; struct got_object_id *commit_id = NULL; + struct got_object_id *merged_commit_id = NULL; struct got_commit_object *commit = NULL; char *fileindex_path = NULL; + char *commit_ref_name = NULL; + struct got_reference *commit_ref = NULL; struct revert_file_args rfa; struct got_object_id *tree_id = NULL; + struct got_pathlist_head added_paths; + TAILQ_INIT(&added_paths); + err = lock_worktree(worktree, LOCK_EX); if (err) return err; + + err = get_rebase_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + + err = got_ref_open(&commit_ref, repo, commit_ref_name, 0); + if (err) + goto done; + + err = got_ref_resolve(&merged_commit_id, repo, commit_ref); + if (err) + goto done; + /* + * Determine which files in added status can be safely removed + * from disk while reverting changes in the work tree. + * We want to avoid deleting unrelated files which were added by + * the user for conflict resolution purposes. + */ + err = get_paths_added_in_commit(&added_paths, merged_commit_id, + got_worktree_get_path_prefix(worktree), repo); + if (err) + goto done; + err = got_ref_open(&resolved, repo, got_ref_get_symref_target(new_base_branch), 0); if (err) @@ -7393,7 +7567,8 @@ got_worktree_rebase_abort(struct got_worktree *worktre rfa.patch_cb = NULL; rfa.patch_arg = NULL; rfa.repo = repo; - rfa.unlink_added_files = 0; + rfa.unlink_added_files = 1; + rfa.added_files_to_unlink = &added_paths; err = worktree_status(worktree, "", fileindex, repo, revert_file, &rfa, NULL, NULL, 1, 0); if (err) @@ -7406,14 +7581,19 @@ sync: if (sync_err && err == NULL) err = sync_err; done: + got_pathlist_free(&added_paths, GOT_PATHLIST_FREE_PATH); got_ref_close(resolved); free(tree_id); free(commit_id); + free(merged_commit_id); if (commit) got_object_commit_close(commit); if (fileindex) got_fileindex_free(fileindex); free(fileindex_path); + free(commit_ref_name); + if (commit_ref) + got_ref_close(commit_ref); unlockerr = lock_worktree(worktree, LOCK_SH); if (unlockerr && err == NULL) @@ -7709,14 +7889,49 @@ got_worktree_histedit_abort(struct got_worktree *workt const struct got_error *err, *unlockerr, *sync_err; struct got_reference *resolved = NULL; char *fileindex_path = NULL; + struct got_object_id *merged_commit_id = NULL; struct got_commit_object *commit = NULL; + char *commit_ref_name = NULL; + struct got_reference *commit_ref = NULL; struct got_object_id *tree_id = NULL; struct revert_file_args rfa; + struct got_pathlist_head added_paths; + TAILQ_INIT(&added_paths); + err = lock_worktree(worktree, LOCK_EX); if (err) return err; + err = get_histedit_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + + err = got_ref_open(&commit_ref, repo, commit_ref_name, 0); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + goto done; + /* Can happen on early abort due to invalid histedit script. */ + commit_ref = NULL; + } + + if (commit_ref) { + err = got_ref_resolve(&merged_commit_id, repo, commit_ref); + if (err) + goto done; + + /* + * Determine which files in added status can be safely removed + * from disk while reverting changes in the work tree. + * We want to avoid deleting unrelated files added by the + * user during conflict resolution or during histedit -e. + */ + err = get_paths_added_in_commit(&added_paths, merged_commit_id, + got_worktree_get_path_prefix(worktree), repo); + if (err) + goto done; + } + err = got_ref_open(&resolved, repo, got_ref_get_symref_target(branch), 0); if (err) @@ -7755,7 +7970,8 @@ got_worktree_histedit_abort(struct got_worktree *workt rfa.patch_cb = NULL; rfa.patch_arg = NULL; rfa.repo = repo; - rfa.unlink_added_files = 0; + rfa.unlink_added_files = 1; + rfa.added_files_to_unlink = &added_paths; err = worktree_status(worktree, "", fileindex, repo, revert_file, &rfa, NULL, NULL, 1, 0); if (err) @@ -7768,9 +7984,14 @@ sync: if (sync_err && err == NULL) err = sync_err; done: - got_ref_close(resolved); + if (resolved) + got_ref_close(resolved); + if (commit_ref) + got_ref_close(commit_ref); + free(merged_commit_id); free(tree_id); free(fileindex_path); + free(commit_ref_name); unlockerr = lock_worktree(worktree, LOCK_SH); if (unlockerr && err == NULL) @@ -8448,12 +8669,42 @@ got_worktree_merge_abort(struct got_worktree *worktree got_worktree_checkout_cb progress_cb, void *progress_arg) { const struct got_error *err, *unlockerr, *sync_err; - struct got_object_id *commit_id = NULL; struct got_commit_object *commit = NULL; char *fileindex_path = NULL; struct revert_file_args rfa; + char *commit_ref_name = NULL; + struct got_reference *commit_ref = NULL; + struct got_object_id *merged_commit_id = NULL; struct got_object_id *tree_id = NULL; + struct got_pathlist_head added_paths; + + TAILQ_INIT(&added_paths); + + err = get_merge_commit_ref_name(&commit_ref_name, worktree); + if (err) + goto done; + + err = got_ref_open(&commit_ref, repo, commit_ref_name, 0); + if (err) + goto done; + + err = got_ref_resolve(&merged_commit_id, repo, commit_ref); + if (err) + goto done; + + /* + * Determine which files in added status can be safely removed + * from disk while reverting changes in the work tree. + * We want to avoid deleting unrelated files which were added by + * the user for conflict resolution purposes. + */ + err = get_paths_added_between_commits(&added_paths, + got_worktree_get_base_commit_id(worktree), merged_commit_id, + got_worktree_get_path_prefix(worktree), repo); + if (err) + goto done; + err = got_object_open_as_commit(&commit, repo, worktree->base_commit_id); if (err) @@ -8480,6 +8731,7 @@ got_worktree_merge_abort(struct got_worktree *worktree rfa.patch_arg = NULL; rfa.repo = repo; rfa.unlink_added_files = 1; + rfa.added_files_to_unlink = &added_paths; err = worktree_status(worktree, "", fileindex, repo, revert_file, &rfa, NULL, NULL, 1, 0); if (err) @@ -8493,12 +8745,15 @@ sync: err = sync_err; done: free(tree_id); - free(commit_id); + free(merged_commit_id); if (commit) got_object_commit_close(commit); if (fileindex) got_fileindex_free(fileindex); free(fileindex_path); + if (commit_ref) + got_ref_close(commit_ref); + free(commit_ref_name); unlockerr = lock_worktree(worktree, LOCK_SH); if (unlockerr && err == NULL) blob - efdeafbe917d9f2c945e958629fc06b7bd9af2ba blob + 6080151a8a3e5c278b52bd1288edfdf4aa4383fb --- regress/cmdline/histedit.sh +++ regress/cmdline/histedit.sh @@ -902,20 +902,15 @@ test_histedit_abort() { fi done - echo "new file on master" > $testroot/content.expected - cat $testroot/wt/epsilon/new > $testroot/content - cmp -s $testroot/content.expected $testroot/content - ret=$? - if [ $ret -ne 0 ]; then - diff -u $testroot/content.expected $testroot/content - test_done "$testroot" "$ret" + if [ -e $testroot/wt/epsilon/new ]; then + echo "removed file new still exists on disk" >&2 + test_done "$testroot" "1" return 1 fi (cd $testroot/wt && got status > $testroot/stdout) - echo "? epsilon/new" > $testroot/stdout.expected - echo "? unversioned-file" >> $testroot/stdout.expected + echo "? unversioned-file" > $testroot/stdout.expected cmp -s $testroot/stdout.expected $testroot/stdout ret=$? if [ $ret -ne 0 ]; then blob - 876eced1d6b84caf0d92277204c67399f2876875 blob + 9093dd5b9fb524b14d2d17d4fe754da0ec8ef9f4 --- regress/cmdline/merge.sh +++ regress/cmdline/merge.sh @@ -672,10 +672,15 @@ test_merge_abort() { test_done "$testroot" "$ret" return 1 fi + + # unrelated added file added during conflict resolution + touch $testroot/wt/added-file + (cd $testroot/wt && got add added-file > /dev/null) (cd $testroot/wt && got status > $testroot/stdout) - echo "C alpha" > $testroot/stdout.expected + echo "A added-file" > $testroot/stdout.expected + echo "C alpha" >> $testroot/stdout.expected echo "D beta" >> $testroot/stdout.expected echo "A epsilon/new" >> $testroot/stdout.expected echo "M gamma/delta" >> $testroot/stdout.expected @@ -697,11 +702,13 @@ test_merge_abort() { return 1 fi - echo "R alpha" > $testroot/stdout.expected + echo "R added-file" > $testroot/stdout.expected + echo "R alpha" >> $testroot/stdout.expected echo "R beta" >> $testroot/stdout.expected echo "R epsilon/new" >> $testroot/stdout.expected echo "R gamma/delta" >> $testroot/stdout.expected echo "R symlink" >> $testroot/stdout.expected + echo "G added-file" >> $testroot/stdout.expected echo "Merge of refs/heads/newbranch aborted" \ >> $testroot/stdout.expected @@ -757,7 +764,8 @@ test_merge_abort() { (cd $testroot/wt && got status > $testroot/stdout) - echo "? unversioned-file" > $testroot/stdout.expected + echo "? added-file" > $testroot/stdout.expected + echo "? unversioned-file" >> $testroot/stdout.expected cmp -s $testroot/stdout.expected $testroot/stdout ret=$? if [ $ret -ne 0 ]; then blob - bb762cd05b066923e9538c4bdf31982707dba134 blob + 1d339f654c90ee75c99eac9cc5a7f39e8b6275ce --- regress/cmdline/rebase.sh +++ regress/cmdline/rebase.sh @@ -419,7 +419,10 @@ test_rebase_abort() { local short_orig_commit1=`trim_obj_id 28 $orig_commit1` echo "modified alpha on branch" > $testroot/repo/alpha - git_commit $testroot/repo -m "committing to alpha on newbranch" + echo "new file on branch" > $testroot/repo/epsilon/new + (cd $testroot/repo && git add epsilon/new) + git_commit $testroot/repo \ + -m "changing alpha and adding new on newbranch" local orig_commit2=`git_show_head $testroot/repo` local short_orig_commit2=`trim_obj_id 28 $orig_commit2` @@ -450,10 +453,12 @@ test_rebase_abort() { >> $testroot/stdout.expected echo ": committing to beta on newbranch" >> $testroot/stdout.expected echo "C alpha" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected echo "Files with new merge conflicts: 1" >> $testroot/stdout.expected echo -n "$short_orig_commit2 -> merge conflict" \ >> $testroot/stdout.expected - echo ": committing to alpha on newbranch" >> $testroot/stdout.expected + echo ": changing alpha and adding new on newbranch" \ + >> $testroot/stdout.expected cmp -s $testroot/stdout.expected $testroot/stdout ret=$? if [ $ret -ne 0 ]; then @@ -490,9 +495,15 @@ test_rebase_abort() { return 1 fi + # unrelated file in work tree added during conflict resolution + touch $testroot/wt/added-file + (cd $testroot/wt && got add added-file > /dev/null) + (cd $testroot/wt && got status > $testroot/stdout) - echo "C alpha" > $testroot/stdout.expected + echo "A added-file" > $testroot/stdout.expected + echo "C alpha" >> $testroot/stdout.expected + echo "A epsilon/new" >> $testroot/stdout.expected echo "? unversioned-file" >> $testroot/stdout.expected cmp -s $testroot/stdout.expected $testroot/stdout ret=$? @@ -508,7 +519,10 @@ test_rebase_abort() { echo "Switching work tree to refs/heads/master" \ > $testroot/stdout.expected + echo 'R added-file' >> $testroot/stdout.expected echo 'R alpha' >> $testroot/stdout.expected + echo 'R epsilon/new' >> $testroot/stdout.expected + echo 'G added-file' >> $testroot/stdout.expected echo 'U beta' >> $testroot/stdout.expected echo "Rebase of refs/heads/newbranch aborted" \ >> $testroot/stdout.expected @@ -556,7 +570,8 @@ test_rebase_abort() { fi (cd $testroot/wt && got status > $testroot/stdout) - echo "? unversioned-file" > $testroot/stdout.expected + echo "? added-file" > $testroot/stdout.expected + echo "? unversioned-file" >> $testroot/stdout.expected cmp -s $testroot/stdout.expected $testroot/stdout ret=$? if [ $ret -ne 0 ]; then