commit c4cdcb68ce13515b0852719b9690a470731c3d5d from: Stefan Sperling date: Wed Apr 03 08:17:53 2019 UTC add support for partial updates, which affect specific paths only commit - c02c541e86ab0a00bfa00cfe311ad98d536355c3 commit + c4cdcb68ce13515b0852719b9690a470731c3d5d blob - 785da132116932e9208b30119726b3a7885b8e0a blob + 83aee6576831b519a3f1d35c2beb93c6197794c5 --- got/got.1 +++ got/got.1 @@ -86,7 +86,7 @@ Only files beneath the specified .Ar path-prefix will be checked out. .El -.It Cm update [ Fl c Ar commit ] [ work-tree-path ] +.It Cm update [ Fl c Ar commit ] [ Ar path ] Update an existing work tree to another commit on the current branch. By default, the latest commit on the current branch is assumed. Show the status of each affected file, using the following status codes: @@ -100,10 +100,14 @@ Show the status of each affected file, using the follo .It ! Ta a missing versioned file was restored .El .Pp -If the -.Ar work tree path -is omitted, use the current working directory. -.Pp +If a +.Ar path +is specified, restrict the update operation to files at or within this path. +Files outside this path will remain at their currently recorded base commit. +Some operations may refuse to run while the work tree contains files from +multiple base commits. +Inconsistent base commits in a work tree can be repaired by running another +update operation across the entire work tree. .Pp The options for .Cm got update blob - cef2aab59352903c01572876d40672efeca7ceaf blob + 3ae9806466513fb65046cb695b43db95ee9175ec --- got/got.c +++ got/got.c @@ -423,7 +423,7 @@ cmd_checkout(int argc, char *argv[]) goto done; } - error = got_worktree_checkout_files(worktree, repo, + error = got_worktree_checkout_files(worktree, "", repo, checkout_progress, worktree_path, check_cancelled, NULL); if (error != NULL) goto done; @@ -440,7 +440,7 @@ done: __dead static void usage_update(void) { - fprintf(stderr, "usage: %s update [-c commit] [worktree-path]\n", + fprintf(stderr, "usage: %s update [-c commit] [path]\n", getprogname()); exit(1); } @@ -465,7 +465,7 @@ cmd_update(int argc, char *argv[]) const struct got_error *error = NULL; struct got_repository *repo = NULL; struct got_worktree *worktree = NULL; - char *worktree_path = NULL; + char *worktree_path = NULL, *path = NULL; struct got_object_id *commit_id = NULL; char *commit_id_str = NULL; int ch, did_something = 0; @@ -491,25 +491,28 @@ cmd_update(int argc, char *argv[]) "unveil", NULL) == -1) err(1, "pledge"); #endif + worktree_path = getcwd(NULL, 0); + if (worktree_path == NULL) { + error = got_error_from_errno(); + goto done; + } + error = got_worktree_open(&worktree, worktree_path); + if (error) + goto done; + if (argc == 0) { - worktree_path = getcwd(NULL, 0); - if (worktree_path == NULL) { + path = strdup(""); + if (path == NULL) { error = got_error_from_errno(); goto done; } } else if (argc == 1) { - worktree_path = realpath(argv[0], NULL); - if (worktree_path == NULL) { - error = got_error_from_errno(); + error = got_worktree_resolve_path(&path, worktree, argv[0]); + if (error) goto done; - } } else usage_update(); - error = got_worktree_open(&worktree, worktree_path); - if (error != NULL) - goto done; - error = got_repo_open(&repo, got_worktree_get_repo_path(worktree)); if (error != NULL) goto done; @@ -549,7 +552,7 @@ cmd_update(int argc, char *argv[]) goto done; } - error = got_worktree_checkout_files(worktree, repo, + error = got_worktree_checkout_files(worktree, path, repo, update_progress, &did_something, check_cancelled, NULL); if (error != NULL) goto done; @@ -560,6 +563,7 @@ cmd_update(int argc, char *argv[]) printf("Already up-to-date\n"); done: free(worktree_path); + free(path); free(commit_id); free(commit_id_str); return error; blob - d49c5f89501111450660c1051f14f61b0ce3dd09 blob + b3cc035a8b5e38c3d0857b2483ef8813565d0922 --- include/got_worktree.h +++ include/got_worktree.h @@ -108,9 +108,21 @@ typedef const struct got_error *(*got_worktree_cancel_ * inside the tree corresponding to the work tree's base commit. * The checkout progress callback will be invoked with the provided * void * argument, and the path of each checked out file. + * + * It is possible to restrict the checkout operation to a specific path in + * the work tree, in which case all files outside this path will remain at + * their currently recorded base commit. Inconsistent base commits can be + * repaired later by running another update operation across the entire work + * tree. Inconsistent base-commits may also occur if this function runs into + * an error or if the checkout operation is cancelled by the cancel callback. + * The specified path is relative to the work tree's root. Pass "" to check + * out files across the entire work tree. + * + * Some operations may refuse to run while the work tree contains files from + * multiple base commits. */ const struct got_error *got_worktree_checkout_files(struct got_worktree *, - struct got_repository *, got_worktree_checkout_cb, void *, + const char *, struct got_repository *, got_worktree_checkout_cb, void *, got_worktree_cancel_cb, void *); /* A callback function which is invoked to report a path's status. */ blob - 4a33abd6b06c91758f991512a2d7475a06651970 blob + 544113f9721705798aabdcf094882df9d3539d6a --- lib/fileindex.c +++ lib/fileindex.c @@ -618,13 +618,13 @@ walk_fileindex(struct got_fileindex *fileindex, struct static const struct got_error * diff_fileindex_tree(struct got_fileindex *, struct got_fileindex_entry **, - struct got_tree_object *, const char *, struct got_repository *, - struct got_fileindex_diff_tree_cb *, void *); + struct got_tree_object *, const char *, const char *, + struct got_repository *, struct got_fileindex_diff_tree_cb *, void *); static const struct got_error * walk_tree(struct got_tree_entry **next, struct got_fileindex *fileindex, struct got_fileindex_entry **ie, struct got_tree_entry *te, - const char *path, struct got_repository *repo, + const char *path, const char *entry_name, struct got_repository *repo, struct got_fileindex_diff_tree_cb *cb, void *cb_arg) { const struct got_error *err = NULL; @@ -644,7 +644,7 @@ walk_tree(struct got_tree_entry **next, struct got_fil } err = diff_fileindex_tree(fileindex, ie, subtree, - subpath, repo, cb, cb_arg); + subpath, entry_name, repo, cb, cb_arg); free(subpath); got_object_tree_close(subtree); if (err) @@ -658,7 +658,7 @@ walk_tree(struct got_tree_entry **next, struct got_fil static const struct got_error * diff_fileindex_tree(struct got_fileindex *fileindex, struct got_fileindex_entry **ie, struct got_tree_object *tree, - const char *path, struct got_repository *repo, + const char *path, const char *entry_name, struct got_repository *repo, struct got_fileindex_diff_tree_cb *cb, void *cb_arg) { const struct got_error *err = NULL; @@ -680,42 +680,60 @@ diff_fileindex_tree(struct got_fileindex *fileindex, cmp = got_path_cmp((*ie)->path, te_path); free(te_path); if (cmp == 0) { - err = cb->diff_old_new(cb_arg, *ie, te, - path); - if (err) - break; + if (got_path_is_child((*ie)->path, path, + path_len) && (entry_name == NULL || + strcmp(te->name, entry_name) == 0)) { + err = cb->diff_old_new(cb_arg, *ie, te, + path); + if (err || entry_name) + break; + } *ie = walk_fileindex(fileindex, *ie); err = walk_tree(&te, fileindex, ie, te, - path, repo, cb, cb_arg); - } else if (cmp < 0 ) { + path, entry_name, repo, cb, cb_arg); + } else if (cmp < 0) { next = walk_fileindex(fileindex, *ie); - err = cb->diff_old(cb_arg, *ie, path); - if (err) - break; + if (got_path_is_child((*ie)->path, path, + path_len) && (entry_name == NULL || + strcmp(te->name, entry_name) == 0)) { + err = cb->diff_old(cb_arg, *ie, path); + if (err || entry_name) + break; + } *ie = next; } else { - err = cb->diff_new(cb_arg, te, path); - if (err) - break; + if ((entry_name == NULL || + strcmp(te->name, entry_name) == 0)) { + err = cb->diff_new(cb_arg, te, path); + if (err || entry_name) + break; + } err = walk_tree(&te, fileindex, ie, te, - path, repo, cb, cb_arg); + path, entry_name, repo, cb, cb_arg); } if (err) break; } else if (*ie) { next = walk_fileindex(fileindex, *ie); - err = cb->diff_old(cb_arg, *ie, path); - if (err) - break; + if (got_path_is_child((*ie)->path, path, path_len) && + (entry_name == NULL || + strcmp(te->name, entry_name) == 0)) { + err = cb->diff_old(cb_arg, *ie, path); + if (err || entry_name) + break; + } *ie = next; } else if (te) { - err = cb->diff_new(cb_arg, te, path); + if (entry_name == NULL || + strcmp(te->name, entry_name) == 0) { + err = cb->diff_new(cb_arg, te, path); + if (err || entry_name) + break; + } + err = walk_tree(&te, fileindex, ie, te, path, + entry_name, repo, cb, cb_arg); if (err) break; - err = walk_tree(&te, fileindex, ie, te, path, repo, cb, - cb_arg); - if (err) - break; } } @@ -724,12 +742,14 @@ diff_fileindex_tree(struct got_fileindex *fileindex, const struct got_error * got_fileindex_diff_tree(struct got_fileindex *fileindex, - struct got_tree_object *tree, struct got_repository *repo, + struct got_tree_object *tree, const char *path, const char *entry_name, + struct got_repository *repo, struct got_fileindex_diff_tree_cb *cb, void *cb_arg) { struct got_fileindex_entry *min; min = RB_MIN(got_fileindex_tree, &fileindex->entries); - return diff_fileindex_tree(fileindex, &min, tree, "", repo, cb, cb_arg); + return diff_fileindex_tree(fileindex, &min, tree, path, entry_name, + repo, cb, cb_arg); } static const struct got_error * blob - 982945591293b88f08ae619ce9593f1eeb8bc22a blob + c3528c754414863d9d51edfae7b866d0ae7f5045 --- lib/got_lib_fileindex.h +++ lib/got_lib_fileindex.h @@ -121,8 +121,8 @@ struct got_fileindex_diff_tree_cb { got_fileindex_diff_tree_new_cb diff_new; }; const struct got_error *got_fileindex_diff_tree(struct got_fileindex *, - struct got_tree_object *, struct got_repository *, - struct got_fileindex_diff_tree_cb *, void *); + struct got_tree_object *, const char *, const char *, + struct got_repository *, struct got_fileindex_diff_tree_cb *, void *); typedef const struct got_error *(*got_fileindex_diff_dir_old_new_cb)(void *, struct got_fileindex_entry *, struct dirent *, const char *); blob - 106aa6ffdc6f810cb3b0515b4e20cb1a875123ed blob + 1c52dfccc40e6d19f1b6253bbb99f967a6947b67 --- lib/worktree.c +++ lib/worktree.c @@ -1363,7 +1363,7 @@ done: const struct got_error * -got_worktree_checkout_files(struct got_worktree *worktree, +got_worktree_checkout_files(struct got_worktree *worktree, const char *path, struct got_repository *repo, got_worktree_checkout_cb progress_cb, void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg) { @@ -1376,6 +1376,7 @@ got_worktree_checkout_files(struct got_worktree *workt FILE *index = NULL, *new_index = NULL; struct got_fileindex_diff_tree_cb diff_cb; struct diff_cb_arg arg; + char *relpath = NULL, *entry_name = NULL; err = lock_worktree(worktree, LOCK_EX); if (err) @@ -1426,14 +1427,86 @@ got_worktree_checkout_files(struct got_worktree *workt if (err) goto done; - err = got_object_id_by_path(&tree_id, repo, - worktree->base_commit_id, worktree->path_prefix); - if (err) - goto done; + if (path[0]) { + char *tree_path; + int obj_type; + relpath = strdup(path); + if (relpath == NULL) { + err = got_error_from_errno(); + goto done; + } + if (asprintf(&tree_path, "%s%s%s", worktree->path_prefix, + got_path_is_root_dir(worktree->path_prefix) ? "" : "/", + path) == -1) { + err = got_error_from_errno(); + goto done; + } + err = got_object_id_by_path(&tree_id, repo, + worktree->base_commit_id, tree_path); + free(tree_path); + if (err) + goto done; + err = got_object_get_type(&obj_type, repo, tree_id); + if (err) + goto done; + if (obj_type == GOT_OBJ_TYPE_BLOB) { + /* Split provided path into parent dir + entry name. */ + if (strchr(path, '/') == NULL) { + relpath = strdup(""); + if (relpath == NULL) { + err = got_error_from_errno(); + goto done; + } + tree_path = strdup(worktree->path_prefix); + if (tree_path == NULL) { + err = got_error_from_errno(); + goto done; + } + } else { + err = got_path_dirname(&relpath, path); + if (err) + goto done; + if (asprintf(&tree_path, "%s%s%s", + worktree->path_prefix, + got_path_is_root_dir( + worktree->path_prefix) ? "" : "/", + relpath) == -1) { + err = got_error_from_errno(); + goto done; + } + } + err = got_object_id_by_path(&tree_id, repo, + worktree->base_commit_id, tree_path); + free(tree_path); + if (err) + goto done; + entry_name = basename(path); + if (entry_name == NULL) { + err = got_error_from_errno(); + goto done; + } + } + } else { + relpath = strdup(""); + if (relpath == NULL) { + err = got_error_from_errno(); + goto done; + } + err = got_object_id_by_path(&tree_id, repo, + worktree->base_commit_id, worktree->path_prefix); + if (err) + goto done; + } err = got_object_open_as_tree(&tree, repo, tree_id); if (err) + goto done; + + if (entry_name && + got_object_tree_find_entry(tree, entry_name) == NULL) { + err = got_error(GOT_ERR_NO_TREE_ENTRY); goto done; + } diff_cb.diff_old_new = diff_old_new; diff_cb.diff_old = diff_old; @@ -1445,8 +1518,8 @@ got_worktree_checkout_files(struct got_worktree *workt arg.progress_arg = progress_arg; arg.cancel_cb = cancel_cb; arg.cancel_arg = cancel_arg; - checkout_err = got_fileindex_diff_tree(fileindex, tree, repo, - &diff_cb, &arg); + checkout_err = got_fileindex_diff_tree(fileindex, tree, relpath, + entry_name, repo, &diff_cb, &arg); /* Try to sync the fileindex back to disk in any case. */ err = got_fileindex_write(fileindex, new_index); @@ -1463,6 +1536,7 @@ got_worktree_checkout_files(struct got_worktree *workt new_fileindex_path = NULL; done: + free(relpath); if (tree) got_object_tree_close(tree); if (commit) @@ -1694,7 +1768,8 @@ got_worktree_resolve_path(char **wt_path, struct got_w goto done; } - path = strdup(resolved + strlen(got_worktree_get_root_path(worktree))); + path = strdup(resolved + + strlen(got_worktree_get_root_path(worktree)) + 1 /* skip '/' */); if (path == NULL) { err = got_error_from_errno(); goto done; blob - 76a9eeef29c6fa9d3a5d6e80c1c0e61692ecaab5 blob + 5f2aab89f919b879ec631497eacba660266869c4 --- regress/cmdline/update.sh +++ regress/cmdline/update.sh @@ -1070,7 +1070,178 @@ function test_update_conflict_wt_rm_vs_repo_rm { test_done "$testroot" "0" } + +function test_update_partial { + local testroot=`test_init update_partial` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + echo "modified alpha" > $testroot/repo/alpha + echo "modified beta" > $testroot/repo/beta + echo "modified epsilon/zeta" > $testroot/repo/epsilon/zeta + git_commit $testroot/repo -m "modified two files" + + for f in alpha beta epsilon/zeta; do + echo "U $f" > $testroot/stdout.expected + echo -n "Updated to commit " >> $testroot/stdout.expected + git_show_head $testroot/repo >> $testroot/stdout.expected + echo >> $testroot/stdout.expected + + (cd $testroot/wt && got update $f > $testroot/stdout) + + cmp $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "modified $f" > $testroot/content.expected + cat $testroot/wt/$f > $testroot/content + + cmp $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + done + test_done "$testroot" "$ret" +} + +function test_update_partial_add { + local testroot=`test_init update_partial_add` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "new" > $testroot/repo/new + echo "epsilon/new2" > $testroot/repo/epsilon/new2 + (cd $testroot/repo && git add .) + git_commit $testroot/repo -m "added two files" + + for f in new epsilon/new2; do + echo "A $f" > $testroot/stdout.expected + echo -n "Updated to commit " >> $testroot/stdout.expected + git_show_head $testroot/repo >> $testroot/stdout.expected + echo >> $testroot/stdout.expected + + (cd $testroot/wt && got update $f > $testroot/stdout) + + cmp $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "$f" > $testroot/content.expected + cat $testroot/wt/$f > $testroot/content + + cmp $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + done + test_done "$testroot" "$ret" +} + +function test_update_partial_rm { + local testroot=`test_init update_partial_rm` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + (cd $testroot/repo && git rm -q alpha) + (cd $testroot/repo && git rm -q epsilon/zeta) + git_commit $testroot/repo -m "removed two files" + + for f in alpha epsilon/zeta; do + echo "got: no such entry found in tree" \ + > $testroot/stderr.expected + + (cd $testroot/wt && got update $f 2> $testroot/stderr) + ret="$?" + if [ "$ret" == "0" ]; then + echo "update succeeded unexpectedly" >&2 + test_done "$testroot" "1" + return 1 + fi + + cmp $testroot/stderr.expected $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + done + test_done "$testroot" "$ret" +} + +function test_update_partial_dir { + local testroot=`test_init update_partial_dir` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "modified alpha" > $testroot/repo/alpha + echo "modified beta" > $testroot/repo/beta + echo "modified epsilon/zeta" > $testroot/repo/epsilon/zeta + git_commit $testroot/repo -m "modified two files" + + echo "U epsilon/zeta" > $testroot/stdout.expected + echo -n "Updated to commit " >> $testroot/stdout.expected + git_show_head $testroot/repo >> $testroot/stdout.expected + echo >> $testroot/stdout.expected + + (cd $testroot/wt && got update epsilon > $testroot/stdout) + + cmp $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "modified epsilon/zeta" > $testroot/content.expected + cat $testroot/wt/epsilon/zeta > $testroot/content + + cmp $testroot/content.expected $testroot/content + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/content.expected $testroot/content + test_done "$testroot" "$ret" + return 1 + fi + test_done "$testroot" "$ret" + +} + run_test test_update_basic run_test test_update_adds_file run_test test_update_deletes_file @@ -1092,3 +1263,7 @@ run_test test_update_conflict_wt_add_vs_repo_add run_test test_update_conflict_wt_edit_vs_repo_rm run_test test_update_conflict_wt_rm_vs_repo_edit run_test test_update_conflict_wt_rm_vs_repo_rm +run_test test_update_partial +run_test test_update_partial_add +run_test test_update_partial_rm +run_test test_update_partial_dir blob - bd0774ba18fbb378f1bc840c7d73a2171497b05f blob + 1755d39ad8b6509251ed8431b7b1064a563e2203 --- regress/worktree/worktree_test.c +++ regress/worktree/worktree_test.c @@ -379,7 +379,7 @@ worktree_checkout(const char *repo_path) if (err != NULL) goto done; - err = got_worktree_checkout_files(worktree, repo, progress_cb, NULL, + err = got_worktree_checkout_files(worktree, "", repo, progress_cb, NULL, NULL, NULL); if (err != NULL) goto done;