"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Omar Polo <op@omarpolo.com>
Subject:
Re: check file status before applying patches (second try)
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Sun, 13 Mar 2022 11:33:49 +0100

Download raw body.

Thread
Stefan Sperling <stsp@stsp.name> wrote:
> On Sat, Mar 12, 2022 at 11:22:37PM +0100, Omar Polo wrote:
> > Hello,
> > 
> > currently 'got patch' is happy to do all sorts of things to all sorts of
> > files.  Well, it's not really ideal I think.  The following patch adds
> > some checks to the targeted files before patching 'em.
> > 
> > I think it's fine to allow patching modified files, but 'got patch'
> > should disallow patching non-existant or unknown files. Or add files
> > that are already there, or delete files there are not known.
> > 
> > There's a bit of churn because I just had to re-introduce in apply_patch
> > the pathlists i segregated to schedule_* in previous commit.  Also, at
> > this points those two helpers are useless.
> > 
> > This actually made the rename test to fail because in the last part
> > `eta' (the target of the rename) exists (untracked) and so it won't
> > proceed with the rename.
> > 
> > note that it needs the latest commit by stsp "fix 'got status' with an
> > obstructed file".
> > 
> > thoughts?
> 
> This is a great start, yes.
> 
> Some comments below.
> 
> > +can_rm(void *arg, unsigned char status, unsigned char staged_status,
> 
> > +can_add(void *arg, unsigned char status, unsigned char staged_status,
> 
> > +can_edit(void *arg, unsigned char status, unsigned char staged_status,
> 
> Please document the status checks done by these functions in
> the man page. These are essentially pre-conditions which are
> specific to 'got patch' and should be documented.
> Otherwise errors raised here will be difficult to interpret.

yeah absolutely.  i forgot about it, sorry.  Updated diff belows has
also some manpage improvements too.  (but mind that i suck at writing,
so any suggestion is well accepted :-)

> If a patch file contains a series of patches which will be applied
> in sequence, then patches which pass pre-conditions will affect
> the work tree, and any later patch which does not pass pre-conditions
> will make 'got patch' error out, leaving any modifications made by
> previous patches behind.
> I think this behaviour is correct. The application of patch N to
> the work tree can affect pre-condition checks for patch N+1, so
> there is no way to check pre-conditions for patch N+1 before
> applying patch N to the work tree.
> Still, this might be worth documenting because users might otherwise
> expect that 'got patch' will not modify the work tree in any way
> unless *all* patches in the input pass pre-condition checks.

Yep, i tried to point this out in the diff below.

In theory it should be possible to apply a patch in a way that if
everything succeeds then the changes are persisted but otherwise the
worktree is unmodified, but it seems a bit difficult to do correctly.

If we strengthen the preconditions so that only unmodified files are
allowed to be changed, than we could run an implicit 'got revert' but
i don't really like the idea.  Producing .rej files (which atm we still
don't do) or conflict markers on failures but still trying to continue
seems the most useful behavior to me, at least by default.

(I have a follow-up diff to add a nop mode to test a diff without
affecting the worktree and another one after to recover from some
classes of errors.)

> > -	err = got_worktree_resolve_path(&oldpath, worktree,
> > -	    p->old != NULL ? p->old : p->new);
> > +	TAILQ_INIT(&oldpaths);
> > +	TAILQ_INIT(&newpaths);
> > +
> > +	err = build_pathlist(p->old != NULL ? p->old : p->new, &oldpath,
> > +	    &oldpaths, worktree);
> >  	if (err)
> >  		goto done;
> 
> Maybe worktree.c should provide a build_pathlist style funcion?
> This function returns a pathlist that corresponds to a single path
> in the work tree, and could be useful elsewhere, e.g. in got.c?
> In any case, we can refactor this later.

I'm not really sure which other parts of got works with only one file at
time...

Furthermore, I'm expecting to handle some other paths operation there in
the future.  Atm we fail to work on `got checkout -p ...' worktrees and
with patches that have "non-worktree-absolute" paths.  I still haven't
solved these problems, but I'm guessing that I'll be able to solve them
in `build_pathlist'.

> >  	if (p->old != NULL && p->new == NULL) {
> >  		/*
> >  		 * special case: delete a file.  don't try to match
> >  		 * the lines but just schedule the removal.
> >  		 */
> 
> In the long term, this code should try to match the lines.
> If lines don't match then we are deleting "with fuzz", which
> might not be what the user wants. It should at least result in a
> warning or fuzz factor or similar appearing in progress output.

absolutely.  This "special case" is really just a workaround for the
missing fuzzying in locate_hunk and test_hunk.

> > +test_patch_illegal_status() {
> > +	local testroot=`test_init patch_illegal_status`
> > +
> > +	got checkout $testroot/repo $testroot/wt > /dev/null
> > +	ret=$?
> > +	if [ $ret -ne 0 ]; then
> > +		test_done $testroot $ret
> > +		return 1
> > +	fi
> > +
> > +	# edit an non-existant and unknown file
> 
> typo: should say "non-existent"

fixed

> > +	cat <<EOF > $testroot/wt/patch
> > +--- iota
> > ++++ iota
> > +@@ -1 +1 @@
> > +- iota
> > ++ IOTA
> > +EOF
> > +
> > +	(cd $testroot/wt && got patch patch) > /dev/null \
> > +		2> $testroot/stderr
> > +	ret=$?
> > +	if [ $ret -eq 0 ]; then
> > +		echo "edited a missing file" >&2
> > +		test_done $testroot $ret
> > +		return 1
> > +	fi
> > +
> > +	echo "got: iota: file has unexpected status" \
> > +		> $testroot/stderr.expected
> 
> Maybe refine this error to say "iota: No such file or directory"?
> You could use got_error_set_errno(ENOENT, path) somewhere in this
> specific case.

agreed, it's more intuitive this way.  I've used got_error_set_errno in
can_rm and can_edit if the file is NONEXISTENT.

I've also added two other checks to test_patch_illegal_status.


-----------------------------------------------
commit 5044ab17d83bcf78d9cca7d38815f9e9e6427e2d
from: Omar Polo <op@omarpolo.com>
date: Sun Mar 13 10:06:20 2022 UTC
 
 check file status before applying the patch
 
 Don't allow `got patch' to delete file that are not known, or add files
 that are already known (but as an exception allow to re-add missing
 files) and only change file that are known and without conflicts or
 obstructions.
 
diff c0c2b3d1e2c80fe9ee36670cff033f948cca9fd6 5b12f37b51cbef2c1d56cba3d6811d8172c60a5d
blob - 09c52c1e9bd95ceb4f5fa1f135b3fb570b7285b0
blob + 4bcea52af617043cda2fa50d87f21effce198199
--- got/got.c
+++ got/got.c
@@ -7245,7 +7245,7 @@ cmd_patch(int argc, char *argv[])
 #endif
 
 	error = got_patch(patchfd, worktree, repo, &print_remove_status,
-	    NULL, &add_progress, NULL);
+	    NULL, &add_progress, NULL, check_cancelled, NULL);
 
 done:
 	if (repo) {
blob - 04a23fc7f7da83078d23e8ec24820ce98aa43702
blob + 448bb17e65d75d8d5ba94bd577f5cde4fef566bf
--- include/got_patch.h
+++ include/got_patch.h
@@ -22,4 +22,5 @@
  */
 const struct got_error *
 got_patch(int, struct got_worktree *, struct got_repository *,
-    got_worktree_delete_cb, void *, got_worktree_checkout_cb, void *);
+    got_worktree_delete_cb, void *, got_worktree_checkout_cb, void *,
+    got_cancel_cb, void *);
blob - 8dc632ef6f52371e65674a21a73f8c9d2496afd5
blob + a8c35b3dbc60b36d31157aa8c3a0e6fb6d789bb3
--- lib/patch.c
+++ lib/patch.c
@@ -28,6 +28,7 @@
 #include <sys/socket.h>
 #include <sys/uio.h>
 
+#include <errno.h>
 #include <limits.h>
 #include <sha1.h>
 #include <stdint.h>
@@ -381,44 +382,6 @@ apply_hunk(FILE *tmp, struct got_patch_hunk *h, long *
 }
 
 static const struct got_error *
-schedule_add(const char *path, struct got_worktree *worktree,
-    struct got_repository *repo, got_worktree_checkout_cb add_cb,
-    void *add_arg)
-{
-	static const struct got_error *err = NULL;
-	struct got_pathlist_head paths;
-	struct got_pathlist_entry *pe;
-
-	TAILQ_INIT(&paths);
-
-	err = got_pathlist_insert(&pe, &paths, path, NULL);
-	if (err == NULL)
-		err = got_worktree_schedule_add(worktree, &paths,
-		    add_cb, add_arg, repo, 1);
-	got_pathlist_free(&paths);
-	return err;
-}
-
-static const struct got_error *
-schedule_del(const char *path, struct got_worktree *worktree,
-    struct got_repository *repo, got_worktree_delete_cb delete_cb,
-    void *delete_arg)
-{
-	static const struct got_error *err = NULL;
-	struct got_pathlist_head paths;
-	struct got_pathlist_entry *pe;
-
-	TAILQ_INIT(&paths);
-
-	err = got_pathlist_insert(&pe, &paths, path, NULL);
-	if (err == NULL)
-		err = got_worktree_schedule_delete(worktree, &paths,
-		    0, NULL, delete_cb, delete_arg, repo, 0, 0);
-	got_pathlist_free(&paths);
-	return err;
-}
-
-static const struct got_error *
 patch_file(struct got_patch *p, const char *path, FILE *tmp)
 {
 	const struct got_error *err = NULL;
@@ -503,33 +466,132 @@ done:
 }
 
 static const struct got_error *
+build_pathlist(const char *p, char **path, struct got_pathlist_head *head,
+    struct got_worktree *worktree)
+{
+	const struct got_error *err;
+	struct got_pathlist_entry *pe;
+
+	err = got_worktree_resolve_path(path, worktree, p);
+	if (err == NULL)
+		err = got_pathlist_insert(&pe, head, *path, NULL);
+	return err;
+}
+
+static const struct got_error *
+can_rm(void *arg, unsigned char status, unsigned char staged_status,
+    const char *path, struct got_object_id *blob_id,
+    struct got_object_id *staged_blob_id, struct got_object_id *commit_id,
+    int dirfd, const char *de_name)
+{
+	if (status == GOT_STATUS_NONEXISTENT)
+		return got_error_set_errno(ENOENT, path);
+	if (status != GOT_STATUS_NO_CHANGE &&
+	    status != GOT_STATUS_ADD &&
+	    status != GOT_STATUS_MODIFY &&
+	    status != GOT_STATUS_MODE_CHANGE)
+		return got_error_path(path, GOT_ERR_FILE_STATUS);
+	if (staged_status == GOT_STATUS_DELETE)
+		return got_error_path(path, GOT_ERR_FILE_STATUS);
+	return NULL;
+}
+
+static const struct got_error *
+can_add(void *arg, unsigned char status, unsigned char staged_status,
+    const char *path, struct got_object_id *blob_id,
+    struct got_object_id *staged_blob_id, struct got_object_id *commit_id,
+    int dirfd, const char *de_name)
+{
+	if (status != GOT_STATUS_NONEXISTENT &&
+	    status != GOT_STATUS_MISSING)
+		return got_error_path(path, GOT_ERR_FILE_STATUS);
+	return NULL;
+}
+
+static const struct got_error *
+can_edit(void *arg, unsigned char status, unsigned char staged_status,
+    const char *path, struct got_object_id *blob_id,
+    struct got_object_id *staged_blob_id, struct got_object_id *commit_id,
+    int dirfd, const char *de_name)
+{
+	if (status == GOT_STATUS_NONEXISTENT)
+		return got_error_set_errno(ENOENT, path);
+	if (status != GOT_STATUS_NO_CHANGE &&
+	    status != GOT_STATUS_ADD &&
+	    status != GOT_STATUS_MODIFY)
+		return got_error_path(path, GOT_ERR_FILE_STATUS);
+	if (staged_status == GOT_STATUS_DELETE)
+		return got_error_path(path, GOT_ERR_FILE_STATUS);
+	return NULL;
+}
+
+static const struct got_error *
+check_file_status(struct got_patch *p, int file_renamed,
+    struct got_worktree *worktree, struct got_repository *repo,
+    struct got_pathlist_head *old, struct got_pathlist_head *new,
+    got_cancel_cb cancel_cb, void *cancel_arg)
+{
+	static const struct got_error *err;
+
+	if (p->old != NULL && p->new == NULL)
+		return got_worktree_status(worktree, old, repo, 0,
+		    can_rm, NULL, cancel_cb, cancel_arg);
+	else if (file_renamed) {
+		err = got_worktree_status(worktree, old, repo, 0,
+		    can_rm, NULL, cancel_cb, cancel_arg);
+		if (err)
+			return err;
+		return got_worktree_status(worktree, new, repo, 0,
+		    can_add, NULL, cancel_cb, cancel_arg);
+	} else if (p->old == NULL)
+		return got_worktree_status(worktree, new, repo, 0,
+		    can_add, NULL, cancel_cb, cancel_arg);
+	else
+		return got_worktree_status(worktree, new, repo, 0,
+		    can_edit, NULL, cancel_cb, cancel_arg);
+}
+
+static const struct got_error *
 apply_patch(struct got_worktree *worktree, struct got_repository *repo,
     struct got_patch *p, got_worktree_delete_cb delete_cb, void *delete_arg,
-    got_worktree_checkout_cb add_cb, void *add_arg)
+    got_worktree_checkout_cb add_cb, void *add_arg, got_cancel_cb cancel_cb,
+    void *cancel_arg)
 {
 	const struct got_error *err = NULL;
+	struct got_pathlist_head oldpaths, newpaths;
 	int file_renamed = 0;
 	char *oldpath = NULL, *newpath = NULL;
 	char *tmppath = NULL, *template = NULL;
 	FILE *tmp = NULL;
 
-	err = got_worktree_resolve_path(&oldpath, worktree,
-	    p->old != NULL ? p->old : p->new);
+	TAILQ_INIT(&oldpaths);
+	TAILQ_INIT(&newpaths);
+
+	err = build_pathlist(p->old != NULL ? p->old : p->new, &oldpath,
+	    &oldpaths, worktree);
 	if (err)
 		goto done;
 
-	err = got_worktree_resolve_path(&newpath, worktree,
-	    p->new != NULL ? p->new : p->old);
+	err = build_pathlist(p->new != NULL ? p->new : p->old, &newpath,
+	    &newpaths, worktree);
 	if (err)
 		goto done;
 
+	if (p->old != NULL && p->new != NULL && strcmp(p->old, p->new))
+		file_renamed = 1;
+
+	err = check_file_status(p, file_renamed, worktree, repo, &oldpaths,
+	    &newpaths, cancel_cb, cancel_arg);
+	if (err)
+		goto done;
+
 	if (p->old != NULL && p->new == NULL) {
 		/*
 		 * special case: delete a file.  don't try to match
 		 * the lines but just schedule the removal.
 		 */
-		err = schedule_del(p->old, worktree, repo, delete_cb,
-		    delete_arg);
+		err = got_worktree_schedule_delete(worktree, &oldpaths,
+		    0, NULL, delete_cb, delete_arg, repo, 0, 0);
 		goto done;
 	}
 
@@ -551,26 +613,27 @@ apply_patch(struct got_worktree *worktree, struct got_
 		goto done;
 	}
 
-	file_renamed = p->old != NULL && strcmp(p->old, p->new);
 	if (file_renamed) {
-		err = schedule_del(oldpath, worktree, repo, delete_cb,
-		    delete_arg);
+		err = got_worktree_schedule_delete(worktree, &oldpaths,
+		    0, NULL, delete_cb, delete_arg, repo, 0, 0);
 		if (err == NULL)
-			err = schedule_add(newpath, worktree, repo,
-			    add_cb, add_arg);
+			err = got_worktree_schedule_add(worktree, &newpaths,
+			    add_cb, add_arg, repo, 1);
 	} else if (p->old == NULL)
-		err = schedule_add(newpath, worktree, repo, add_cb,
-		    add_arg);
+		err = got_worktree_schedule_add(worktree, &newpaths,
+		    add_cb, add_arg, repo, 1);
 	else
 		printf("M  %s\n", oldpath); /* XXX */
 
 done:
-	if (err != NULL && (file_renamed || p->old == NULL))
+	if (err != NULL && newpath != NULL && (file_renamed || p->old == NULL))
 		unlink(newpath);
 	free(template);
 	if (tmppath != NULL)
 		unlink(tmppath);
 	free(tmppath);
+	got_pathlist_free(&oldpaths);
+	got_pathlist_free(&newpaths);
 	free(oldpath);
 	free(newpath);
 	return err;
@@ -579,7 +642,8 @@ done:
 const struct got_error *
 got_patch(int fd, struct got_worktree *worktree, struct got_repository *repo,
     got_worktree_delete_cb delete_cb, void *delete_arg,
-    got_worktree_checkout_cb add_cb, void *add_arg)
+    got_worktree_checkout_cb add_cb, void *add_arg, got_cancel_cb cancel_cb,
+    void *cancel_arg)
 {
 	const struct got_error *err = NULL;
 	struct imsgbuf *ibuf;
@@ -628,7 +692,7 @@ got_patch(int fd, struct got_worktree *worktree, struc
 			break;
 
 		err = apply_patch(worktree, repo, &p, delete_cb, delete_arg,
-		    add_cb, add_arg);
+		    add_cb, add_arg, cancel_cb, cancel_arg);
 		patch_free(&p);
 		if (err)
 			break;
blob - 5916921f260f5a0eaefdbf6036b813fbf3570dd5
blob + 36c9ef55d45f97cd15c3f787c35d5a7623def72a
--- regress/cmdline/patch.sh
+++ regress/cmdline/patch.sh
@@ -685,6 +685,7 @@ EOF
 		test_done $testroot $ret
 		return 1
 	fi
+	rm $testroot/wt/eta
 
 	cat <<EOF > $testroot/wt/patch
 --- alpha
@@ -730,6 +731,151 @@ EOF
 	test_done $testroot $ret
 }
 
+test_patch_illegal_status() {
+	local testroot=`test_init patch_illegal_status`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# edit an non-existent and unknown file
+	cat <<EOF > $testroot/wt/patch
+--- iota
++++ iota
+@@ -1 +1 @@
+- iota
++ IOTA
+EOF
+
+	(cd $testroot/wt && got patch patch) > /dev/null \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "edited a missing file" >&2
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo "got: iota: No such file or directory" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# create iota and re-try
+	echo iota > $testroot/wt/iota
+
+	(cd $testroot/wt && got patch patch) > /dev/null \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "patched an unknown file" >&2
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo "got: iota: file has unexpected status" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+	
+	rm $testroot/wt/iota
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# edit obstructed file
+	rm $testroot/wt/alpha
+	mkdir $testroot/wt/alpha
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ alpha
+@@ -1 +1,2 @@
+ alpha
++was edited
+EOF
+
+	(cd $testroot/wt && got patch patch) > /dev/null \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "edited a missing file" >&2
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo "got: alpha: file has unexpected status" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# delete an unknown file
+	cat <<EOF > $testroot/wt/patch
+--- iota
++++ /dev/null
+@@ -1 +0,0 @@
+-iota
+EOF
+
+	(cd $testroot/wt && got patch patch) > /dev/null \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "deleted a missing file?" >&2
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo "got: iota: No such file or directory" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# try again with iota in place but still not registered
+	echo iota > $testroot/wt/iota
+	(cd $testroot/wt && got patch patch) > /dev/null \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "deleted an unversioned file?" >&2
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo "got: iota: file has unexpected status" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
+	test_done $testroot $ret
+}
+
 test_parseargs "$@"
 run_test test_patch_simple_add_file
 run_test test_patch_simple_rm_file
@@ -743,3 +889,4 @@ run_test test_patch_malformed
 run_test test_patch_no_patch
 run_test test_patch_equals_for_context
 run_test test_patch_rename
+run_test test_patch_illegal_status

-----------------------------------------------
commit 82f60a9a23894c1fa5c3098d8d9d9559c2ad2329 (patchcfs)
from: Omar Polo <op@omarpolo.com>
date: Sun Mar 13 10:06:20 2022 UTC
 
 improve `got patch' section of the manpage
 
 Simplify some pharsings, explain what preconditions `got patch' has and
 what happens to the worktree after an error occurs.
 
diff 5b12f37b51cbef2c1d56cba3d6811d8172c60a5d 9eec00bfb3e7e3a68db41095f894e1f33a0d4727
blob - 61596d1cfc534598b2516ffa737754cfb667abb1
blob + d7bdf56abfff3111d815aab14e11d14fb3c31c58
--- got/got.1
+++ got/got.1
@@ -889,7 +889,6 @@ arguments are provided, only show differences for the 
 Cannot be used together with the
 .Fl P
 option.
-.Pp
 .It Fl C Ar number
 Set the number of context lines shown in the diff.
 By default, 3 lines of context are shown.
@@ -1291,18 +1290,19 @@ option)
 Apply changes from
 .Ar patchfile
 .Pq or standard input
-and record the state of the affected files afterwards.
-The content of
-.Ar patchfile
-must be an unified diff.
+that must be an unified diff.
 If
 .Ar patchfile
 contains more than one patch,
 .Nm
 .Cm patch
-will try to apply them all.
+will try to apply each of them in sequence.
+Files added or removed from the repository as consequence are automatically
+scheduled as such.
 .Pp
-Show the status of each affected file, using the following status codes:
+While applying the
+.Ar patchfile ,
+show the status of each affected file, using the following status codes:
 .Bl -column XYZ description
 .It M Ta modified file
 .It D Ta deleted file
@@ -1314,6 +1314,23 @@ If a change does not match at its exact line number,
 .Cm patch
 applies it somewhere else in the file if it can find a good spot before
 giving up.
+.Pp
+.Nm
+.Cm patch
+will refuse to apply a patch if certain preconditions are not met.
+Files to be deleted must be present in the repository and not have been
+scheduled for deletion already.
+Files to be added must be unknown and not present in the worktree, but
+are also allowed to be missing.
+Files to be modified must be already present in the repository and in
+the worktree, and not have conflicts or being obstructed.
+.Pp
+If a patch fails to apply for any reason
+.Nm
+stops and the affected file is unchanged.
+Modifications done to the worktree due to previous patches in the same
+.Ar patchfile
+are still visible.
 .Tg rv
 .It Cm revert Oo Fl p Oc Oo Fl F Ar response-script Oc Oo Fl R Oc Ar path ...
 .Dl Pq alias: Cm rv