Download raw body.
add got patch subcommand
Hello, some time ago stsp suggested that got should be able to apply diffs by itself. Using the VCS to apply them allows in fact some nice comodities: for instance added and removed files are automatically tracked. The rather long diff attached is just that. It's a simple and (i think) straightforward implementation of patch(1) only integrated inside got. There's a new got-read-patch libexec helpers (which borrows some lines of code from Larry' patch, but it's mostly rewritten from scratch) that is used to parse the content of the diff, similarly to the others got-read-* helpers and under the same restrictions, and lib/patch.c that wraps it. There are quite a few missing parts yet, for example: * it doesn't handle yet the "\ No newline at end of file". * it doesn't create missing directories yet. * it doesn't try too hard to find a place where the patch is applicable (could be considered a feature.) * it doesn't support fuzzing (i.e. ignoring some line from the context) or ignoring whitespaces. and other additions that I'd like to add, like a "-p" strip option, a "-n" nop/check, or eventually handle multiple patch file. The diff is already long enough though, so these points can be sorted out in tree I think. stsp also kindly pointed me to the svn patch regression suite, which I still have to go through completely. More tests will follow. Possibly bug fixes too ;) In the meantime, I'm happy that note that `got patch' can handle itself: % got checkout ~/git/got.git ... % cd got % # it doesn't create missing directories (yet) % mkdir libexec/got-read-patch % got patch < /tmp/got-patch.diff M got/Makefile M got/got.1 M got/got.c M include/got_error.h A include/got_patch.h M lib/got_lib_privsep.h A lib/patch.c M lib/privsep.c M libexec/Makefile A libexec/got-read-patch/Makefile A libexec/got-read-patch/got-read-patch.c M regress/cmdline/Makefile A regress/cmdline/patch.sh Cheers, Omar Polo diff refs/heads/main refs/heads/patch-second-try blob - 8b431936ed43a5398c391d18637ae6dbc0f5348f blob + 8dfd844f3da472b6ed040a62acaf85403cbc07ea --- got/Makefile +++ got/Makefile @@ -13,7 +13,7 @@ SRCS= got.c blame.c commit_graph.c delta.c diff.c \ diff_myers.c diff_output.c diff_output_plain.c \ diff_output_unidiff.c diff_output_edscript.c \ diff_patience.c send.c deltify.c pack_create.c dial.c \ - bloom.c murmurhash2.c ratelimit.c + bloom.c murmurhash2.c ratelimit.c patch.c MAN = ${PROG}.1 got-worktree.5 git-repository.5 got.conf.5 blob - eaafde8411ebca559ac74b4970f3a9ee755e26b9 blob + 61596d1cfc534598b2516ffa737754cfb667abb1 --- got/got.1 +++ got/got.1 @@ -1285,6 +1285,35 @@ option) .It ! Ta versioned file expected on disk but missing .El .El +.Tg pa +.It Cm patch Op Ar patchfile +.Dl Pq alias: Cm pa +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. +If +.Ar patchfile +contains more than one patch, +.Nm +.Cm patch +will try to apply them all. +.Pp +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 +.It A Ta added file +.El +.Pp +If a change does not match at its exact line number, +.Nm +.Cm patch +applies it somewhere else in the file if it can find a good spot before +giving up. .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 blob - 2c02ca33857bec5cf55e156d4e9cb8783514c705 blob + 97248718d40eaf3a712653112d62e64d0beca870 --- got/got.c +++ got/got.c @@ -56,6 +56,7 @@ #include "got_opentemp.h" #include "got_gotconfig.h" #include "got_dial.h" +#include "got_patch.h" #ifndef nitems #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) @@ -101,6 +102,7 @@ __dead static void usage_branch(void); __dead static void usage_tag(void); __dead static void usage_add(void); __dead static void usage_remove(void); +__dead static void usage_patch(void); __dead static void usage_revert(void); __dead static void usage_commit(void); __dead static void usage_send(void); @@ -131,6 +133,7 @@ static const struct got_error* cmd_branch(int, char * static const struct got_error* cmd_tag(int, char *[]); static const struct got_error* cmd_add(int, char *[]); static const struct got_error* cmd_remove(int, char *[]); +static const struct got_error* cmd_patch(int, char *[]); static const struct got_error* cmd_revert(int, char *[]); static const struct got_error* cmd_commit(int, char *[]); static const struct got_error* cmd_send(int, char *[]); @@ -162,6 +165,7 @@ static const struct got_cmd got_commands[] = { { "tag", cmd_tag, usage_tag, "" }, { "add", cmd_add, usage_add, "" }, { "remove", cmd_remove, usage_remove, "rm" }, + { "patch", cmd_patch, usage_patch, "pa" }, { "revert", cmd_revert, usage_revert, "rv" }, { "commit", cmd_commit, usage_commit, "ci" }, { "send", cmd_send, usage_send, "se" }, @@ -7107,6 +7111,133 @@ done: } __dead static void +usage_patch(void) +{ + fprintf(stderr, "usage: %s patch patchfile\n", + getprogname()); + exit(1); +} + +static const struct got_error * +patch_from_stdin(int *patchfd) +{ + const struct got_error *err = NULL; + ssize_t r; + char *path, buf[BUFSIZ]; + sig_t sighup, sigint, sigquit; + + err = got_opentemp_named_fd(&path, patchfd, + GOT_TMPDIR_STR "/got-patch"); + if (err) + return err; + unlink(path); + free(path); + + sighup = signal(SIGHUP, SIG_DFL); + sigint = signal(SIGINT, SIG_DFL); + sigquit = signal(SIGQUIT, SIG_DFL); + + for (;;) { + r = read(0, buf, sizeof(buf)); + if (r == -1) { + err = got_error_from_errno("read"); + break; + } + if (r == 0) + break; + if (write(*patchfd, buf, r) == -1) { + err = got_error_from_errno("write"); + break; + } + } + + signal(SIGHUP, sighup); + signal(SIGINT, sigint); + signal(SIGQUIT, sigquit); + + if (err != NULL) + close(*patchfd); + return NULL; +} + +static const struct got_error * +cmd_patch(int argc, char *argv[]) +{ + const struct got_error *error = NULL, *close_error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + char *cwd = NULL; + int ch; + int patchfd; + + while ((ch = getopt(argc, argv, "")) != -1) { + switch (ch) { + default: + usage_patch(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + + if (argc == 0) { + error = patch_from_stdin(&patchfd); + if (error) + return error; + } else if (argc == 1) { + patchfd = open(argv[0], O_RDONLY); + if (patchfd == -1) { + error = got_error_from_errno2("open", argv[0]); + return error; + } + } else + usage_patch(); + + if ((cwd = getcwd(NULL, 0)) == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_worktree_open(&worktree, cwd); + if (error != NULL) + goto done; + + const char *repo_path = got_worktree_get_repo_path(worktree); + error = got_repo_open(&repo, repo_path, NULL); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error != NULL) + goto done; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath proc exec sendfd flock", + NULL) == -1) + err(1, "pledge"); +#endif + + error = got_patch(patchfd, worktree, repo, &print_remove_status, + &add_progress); + +done: + if (repo) { + close_error = got_repo_close(repo); + if (error == NULL) + error = close_error; + } + if (worktree != NULL) { + close_error = got_worktree_close(worktree); + if (error == NULL) + error = close_error; + } + free(cwd); + return error; +} + +__dead static void usage_revert(void) { fprintf(stderr, "usage: %s revert [-p] [-F response-script] [-R] " @@ -7238,7 +7369,6 @@ choose_patch(int *choice, void *arg, unsigned char sta return NULL; } - static const struct got_error * cmd_revert(int argc, char *argv[]) { blob - bfdc8fac28522667c8ec28af0e4485c8e46a75a3 blob + 64f2cb93558b933d2ffdcc0da7dedecf78d8ee52 --- include/got_error.h +++ include/got_error.h @@ -162,6 +162,11 @@ #define GOT_ERR_MERGE_BUSY 144 #define GOT_ERR_MERGE_PATH 145 #define GOT_ERR_FILE_BINARY 146 +#define GOT_ERR_PATCH_MALFORMED 147 +#define GOT_ERR_PATCH_TRUNCATED 148 +#define GOT_ERR_PATCH_DONT_APPLY 149 +#define GOT_ERR_PATCH_PATHS_DIFFER 150 +#define GOT_ERR_NO_PATCH 151 static const struct got_error { int code; @@ -338,6 +343,12 @@ static const struct got_error { { GOT_ERR_MERGE_PATH, "cannot merge branch which contains " "changes outside of this work tree's path prefix" }, { GOT_ERR_FILE_BINARY, "found a binary file instead of text" }, + { GOT_ERR_PATCH_MALFORMED, "malformed patch" }, + { GOT_ERR_PATCH_TRUNCATED, "patch truncated" }, + { GOT_ERR_PATCH_DONT_APPLY, "patch doesn't apply" }, + { GOT_ERR_PATCH_PATHS_DIFFER, "the paths mentioned in the patch " + "are different." }, + { GOT_ERR_NO_PATCH, "no patch found" }, }; /* blob - /dev/null blob + 3f56d45c54c3ff202d4e7db59288e3ec6717ed78 (mode 644) --- /dev/null +++ include/got_patch.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 Omar Polo <op@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Apply the (already opened) patch to the repository and register the + * status of the added and removed files. + * + * The patch file descriptor *must* be seekable. + */ +const struct got_error * +got_patch(int, struct got_worktree *, struct got_repository *, + got_worktree_delete_cb, got_worktree_checkout_cb); blob - 274e89878290befef48084afc0ae191cd5c36b16 blob + fef20e3a85c35f0faa4743d896a34ac04f0a4397 --- lib/got_lib_privsep.h +++ lib/got_lib_privsep.h @@ -44,6 +44,7 @@ #define GOT_PROG_READ_PACK got-read-pack #define GOT_PROG_READ_GITCONFIG got-read-gitconfig #define GOT_PROG_READ_GOTCONFIG got-read-gotconfig +#define GOT_PROG_READ_PATCH got-read-patch #define GOT_PROG_FETCH_PACK got-fetch-pack #define GOT_PROG_INDEX_PACK got-index-pack #define GOT_PROG_SEND_PACK got-send-pack @@ -68,6 +69,8 @@ GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_GITCONFIG) #define GOT_PATH_PROG_READ_GOTCONFIG \ GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_GOTCONFIG) +#define GOT_PATH_PROG_READ_PATCH \ + GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_PATCH) #define GOT_PATH_PROG_FETCH_PACK \ GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_FETCH_PACK) #define GOT_PATH_PROG_SEND_PACK \ @@ -179,6 +182,14 @@ enum got_imsg_type { GOT_IMSG_RAW_DELTA_OUTFD, GOT_IMSG_RAW_DELTA_REQUEST, GOT_IMSG_RAW_DELTA, + + /* Messages related to patch files. */ + GOT_IMSG_PATCH_FILE, + GOT_IMSG_PATCH_HUNK, + GOT_IMSG_PATCH_DONE, + GOT_IMSG_PATCH_LINE, + GOT_IMSG_PATCH, + GOT_IMSG_PATCH_EOF, }; /* Structure for GOT_IMSG_ERROR. */ @@ -510,6 +521,24 @@ struct got_imsg_remotes { int nremotes; /* This many GOT_IMSG_GITCONFIG_REMOTE messages follow. */ }; +/* + * Structure for GOT_IMSG_PATCH data. + */ +struct got_imsg_patch { + char old[PATH_MAX]; + char new[PATH_MAX]; +}; + +/* + * Structure for GOT_IMSG_PATCH_HUNK data. + */ +struct got_imsg_patch_hunk { + long oldfrom; + long oldlines; + long newfrom; + long newlines; +}; + struct got_remote_repo; struct got_pack; struct got_packidx; blob - /dev/null blob + 84226a57dca7da7a69187a76d804e8ceda7558ba (mode 644) --- /dev/null +++ lib/patch.c @@ -0,0 +1,596 @@ +/* + * Copyright (c) 2022 Omar Polo <op@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Apply patches. + * + * Things that are still missing: + * + "No final newline" handling + * + * Things that we may want to support: + * + support indented patches? + * + support other kinds of patches? + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <limits.h> +#include <sha1.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <imsg.h> + +#include "got_error.h" +#include "got_object.h" +#include "got_path.h" +#include "got_reference.h" +#include "got_cancel.h" +#include "got_worktree.h" +#include "got_opentemp.h" +#include "got_patch.h" + +#include "got_lib_delta.h" +#include "got_lib_object.h" +#include "got_lib_privsep.h" + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) + +struct got_patch_hunk { + STAILQ_ENTRY(got_patch_hunk) entries; + long old_from; + long old_lines; + long new_from; + long new_lines; + size_t len; + size_t cap; + char **lines; +}; + +struct got_patch { + char *old; + char *new; + STAILQ_HEAD(, got_patch_hunk) head; +}; + +static const struct got_error * +send_patch(struct imsgbuf *ibuf, int fd) +{ + const struct got_error *err = NULL; + + if (imsg_compose(ibuf, GOT_IMSG_PATCH_FILE, 0, 0, fd, + NULL, 0) == -1) { + err = got_error_from_errno( + "imsg_compose GOT_IMSG_PATCH_FILE"); + close(fd); + return err; + } + + if (imsg_flush(ibuf) == -1) { + err = got_error_from_errno("imsg_flush"); + imsg_clear(ibuf); + } + + return err; +} + +static void +patch_free(struct got_patch *p) +{ + struct got_patch_hunk *h; + size_t i; + + while (!STAILQ_EMPTY(&p->head)) { + h = STAILQ_FIRST(&p->head); + STAILQ_REMOVE_HEAD(&p->head, entries); + + for (i = 0; i < h->len; ++i) + free(h->lines[i]); + free(h->lines); + free(h); + } + + free(p->new); + free(p->old); +} + +static const struct got_error * +pushline(struct got_patch_hunk *h, const char *line) +{ + void *t; + size_t newcap; + + if (h->len == h->cap) { + if ((newcap = h->cap * 1.5) == 0) + newcap = 16; + t = recallocarray(h->lines, h->cap, newcap, + sizeof(h->lines[0])); + if (t == NULL) + return got_error_from_errno("recallocarray"); + h->lines = t; + h->cap = newcap; + } + + if ((t = strdup(line)) == NULL) + return got_error_from_errno("strdup"); + + h->lines[h->len++] = t; + return NULL; +} + +static const struct got_error * +recv_patch(struct imsgbuf *ibuf, int *done, struct got_patch *p) +{ + const struct got_error *err = NULL; + struct imsg imsg; + struct got_imsg_patch_hunk hdr; + struct got_imsg_patch patch; + struct got_patch_hunk *h = NULL; + size_t datalen; + + memset(p, 0, sizeof(*p)); + STAILQ_INIT(&p->head); + + err = got_privsep_recv_imsg(&imsg, ibuf, 0); + if (err) + return err; + if (imsg.hdr.type == GOT_IMSG_PATCH_EOF) { + *done = 1; + goto done; + } + if (imsg.hdr.type != GOT_IMSG_PATCH) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + if (datalen != sizeof(patch)) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + goto done; + } + memcpy(&patch, imsg.data, sizeof(patch)); + if (*patch.old != '\0' && (p->old = strdup(patch.old)) == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + if (*patch.new != '\0' && (p->new = strdup(patch.new)) == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + + imsg_free(&imsg); + + for (;;) { + char *t; + + err = got_privsep_recv_imsg(&imsg, ibuf, 0); + if (err) + return err; + + switch (imsg.hdr.type) { + case GOT_IMSG_PATCH_DONE: + goto done; + case GOT_IMSG_PATCH_HUNK: + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + if (datalen != sizeof(hdr)) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + goto done; + } + memcpy(&hdr, imsg.data, sizeof(hdr)); + if ((h = calloc(1, sizeof(*h))) == NULL) { + err = got_error_from_errno("calloc"); + goto done; + } + h->old_from = hdr.oldfrom; + h->old_lines = hdr.oldlines; + h->new_from = hdr.newfrom; + h->new_lines = hdr.newlines; + STAILQ_INSERT_TAIL(&p->head, h, entries); + break; + case GOT_IMSG_PATCH_LINE: + if (h == NULL) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + t = imsg.data; + /* at least one char plus newline */ + if (datalen < 2 || t[datalen-1] != '\0') { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + if (*t != ' ' && *t != '-' && *t != '+') { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + err = pushline(h, t); + if (err) + goto done; + break; + default: + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + + imsg_free(&imsg); + } + +done: + imsg_free(&imsg); + return err; +} + +/* + * Copy data from orig starting at copypos until pos into tmp. + * If pos is -1, copy until EOF. + */ +static const struct got_error * +copy(FILE *tmp, FILE *orig, off_t copypos, off_t pos) +{ + char buf[BUFSIZ]; + size_t len, r, w; + + if (fseek(orig, copypos, SEEK_SET) == -1) + return got_error_from_errno("fseek"); + + while (pos == -1 || copypos < pos) { + len = sizeof(buf); + if (pos > 0) + len = MIN(len, (size_t)pos - copypos); + r = fread(buf, 1, len, orig); + if (r != len && ferror(orig)) + return got_error_from_errno("fread"); + w = fwrite(buf, 1, r, tmp); + if (w != r) + return got_error_from_errno("fwrite"); + copypos += len; + if (r != len && feof(orig)) { + if (pos == -1) + return NULL; + return got_error(GOT_ERR_PATCH_DONT_APPLY); + } + } + return NULL; +} + +static const struct got_error * +locate_hunk(FILE *orig, struct got_patch_hunk *h, long *lineno) +{ + const struct got_error *err = NULL; + char *line = NULL; + char mode = *h->lines[0]; + size_t linesize = 0; + ssize_t linelen; + off_t match = -1; + long match_lineno = -1; + + for (;;) { + linelen = getline(&line, &linesize, orig); + if (linelen == -1) { + if (ferror(orig)) + err = got_error_from_errno("getline"); + else if (match == -1) + err = got_error(GOT_ERR_PATCH_DONT_APPLY); + break; + } + (*lineno)++; + + if ((mode == ' ' && !strcmp(h->lines[0]+1, line)) || + (mode == '-' && !strcmp(h->lines[0]+1, line)) || + (mode == '+' && *lineno == h->old_from)) { + match = ftello(orig); + if (match == -1) { + err = got_error_from_errno("ftello"); + break; + } + match -= linelen; + match_lineno = (*lineno)-1; + } + + if (*lineno >= h->old_from && match != -1) + break; + } + + if (err == NULL) { + *lineno = match_lineno; + if (fseek(orig, match, SEEK_SET) == -1) + err = got_error_from_errno("fseek"); + } + + free(line); + return err; +} + +static const struct got_error * +test_hunk(FILE *orig, struct got_patch_hunk *h) +{ + const struct got_error *err = NULL; + char *line = NULL; + size_t linesize = 0, i = 0; + ssize_t linelen; + + for (i = 0; i < h->len; ++i) { + switch (*h->lines[i]) { + case '+': + continue; + case ' ': + case '-': + linelen = getline(&line, &linesize, orig); + if (linelen == -1) { + if (ferror(orig)) + err = got_error_from_errno("getline"); + else + err = got_error( + GOT_ERR_PATCH_DONT_APPLY); + goto done; + } + if (strcmp(h->lines[i]+1, line)) { + err = got_error(GOT_ERR_PATCH_DONT_APPLY); + goto done; + } + break; + } + } + +done: + free(line); + return err; +} + +static const struct got_error * +apply_hunk(FILE *tmp, struct got_patch_hunk *h, long *lineno) +{ + size_t i = 0; + + for (i = 0; i < h->len; ++i) { + switch (*h->lines[i]) { + case ' ': + if (fprintf(tmp, "%s", h->lines[i]+1) < 0) + return got_error_from_errno("fprintf"); + /* fallthrough */ + case '-': + (*lineno)++; + break; + case '+': + if (fprintf(tmp, "%s", h->lines[i]+1) < 0) + return got_error_from_errno("fprintf"); + break; + } + } + return NULL; +} + +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, + got_worktree_checkout_cb add_cb) +{ + const struct got_error *err = NULL; + struct got_pathlist_head paths; + struct got_pathlist_entry *pe; + char *path = NULL, *tmppath = NULL; + FILE *orig = NULL, *tmp = NULL; + struct got_patch_hunk *h; + size_t i; + long lineno = 0; + off_t copypos, pos; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + + TAILQ_INIT(&paths); + + if (p->old == NULL && p->new == NULL) + return got_error(GOT_ERR_PATCH_MALFORMED); + + err = got_worktree_resolve_path(&path, worktree, + p->new != NULL ? p->new : p->old); + if (err) + return err; + err = got_pathlist_insert(&pe, &paths, path, NULL); + 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 = got_worktree_schedule_delete(worktree, &paths, + 0, NULL, delete_cb, NULL, repo, 0, 0); + goto done; + } else if (p->old != NULL && strcmp(p->old, p->new)) { + err = got_error(GOT_ERR_PATCH_PATHS_DIFFER); + goto done; + } + + err = got_opentemp_named(&tmppath, &tmp, + got_worktree_get_root_path(worktree)); + if (err) + goto done; + + if (p->old == NULL) { /* create */ + h = STAILQ_FIRST(&p->head); + if (h == NULL || STAILQ_NEXT(h, entries) != NULL) { + err = got_error(GOT_ERR_PATCH_MALFORMED); + goto done; + } + for (i = 0; i < h->len; ++i) { + if (fprintf(tmp, "%s", h->lines[i]+1) < 0) { + err = got_error_from_errno("fprintf"); + goto done; + } + } + goto rename; + } + + if ((orig = fopen(path, "r")) == NULL) { + err = got_error_from_errno2("fopen", path); + goto done; + } + + copypos = 0; + STAILQ_FOREACH(h, &p->head, entries) { + tryagain: + err = locate_hunk(orig, h, &lineno); + if (err != NULL) + goto done; + if ((pos = ftello(orig)) == -1) { + err = got_error_from_errno("ftello"); + goto done; + } + err = copy(tmp, orig, copypos, pos); + if (err != NULL) + goto done; + copypos = pos; + + err = test_hunk(orig, h); + if (err != NULL && err->code == GOT_ERR_PATCH_DONT_APPLY) { + /* + * try to apply the hunk again starting the search + * after the previous partial match. + */ + if (fseek(orig, pos, SEEK_SET) == -1) { + err = got_error_from_errno("fseek"); + goto done; + } + linelen = getline(&line, &linesize, orig); + if (linelen == -1) { + err = got_error_from_errno("getline"); + goto done; + } + lineno++; + goto tryagain; + } + if (err != NULL) + goto done; + + err = apply_hunk(tmp, h, &lineno); + if (err != NULL) + goto done; + + copypos = ftello(orig); + if (copypos == -1) { + err = got_error_from_errno("ftello"); + goto done; + } + } + + if (!feof(orig)) { + err = copy(tmp, orig, copypos, -1); + if (err) + goto done; + } + +rename: + if (rename(tmppath, path) == -1) { + err = got_error_from_errno3("rename", tmppath, path); + goto done; + } + + if (p->old == NULL) + err = got_worktree_schedule_add(worktree, &paths, + add_cb, NULL, repo, 1); + else + printf("M %s\n", path); /* XXX */ +done: + if (err != NULL && p->old == NULL && path != NULL) + unlink(path); + if (tmp != NULL) + fclose(tmp); + if (tmppath != NULL) + unlink(tmppath); + free(tmppath); + if (orig != NULL) { + if (p->old == NULL && err != NULL) + unlink(path); + fclose(orig); + } + free(path); + free(line); + got_pathlist_free(&paths); + return err; +} + +const struct got_error * +got_patch(int fd, struct got_worktree *worktree, struct got_repository *repo, + got_worktree_delete_cb delete_cb, got_worktree_checkout_cb add_cb) +{ + const struct got_error *err = NULL; + struct imsgbuf *ibuf; + int imsg_fds[2] = {-1, -1}; + int done = 0; + pid_t pid; + + ibuf = calloc(1, sizeof(*ibuf)); + if (ibuf == NULL) { + err = got_error_from_errno("calloc"); + goto done; + } + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, imsg_fds) == -1) { + err = got_error_from_errno("socketpair"); + goto done; + } + + pid = fork(); + if (pid == -1) { + err = got_error_from_errno("fork"); + goto done; + } else if (pid == 0) { + got_privsep_exec_child(imsg_fds, GOT_PATH_PROG_READ_PATCH, + NULL); + /* not reached */ + } + + if (close(imsg_fds[1]) == -1) { + err = got_error_from_errno("close"); + goto done; + } + imsg_fds[1] = -1; + imsg_init(ibuf, imsg_fds[0]); + + err = send_patch(ibuf, fd); + fd = -1; + if (err) + goto done; + + while (!done && err == NULL) { + struct got_patch p; + + err = recv_patch(ibuf, &done, &p); + if (err || done) + break; + + err = apply_patch(worktree, repo, &p, delete_cb, add_cb); + patch_free(&p); + if (err) + break; + } + +done: + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (ibuf != NULL) + imsg_clear(ibuf); + if (imsg_fds[0] != -1 && close(imsg_fds[0]) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (imsg_fds[1] != -1 && close(imsg_fds[1]) == -1 && err == NULL) + err = got_error_from_errno("close"); + return err; +} blob - 4cbc40e6f66cdf7a166db6612c052858ffaba1b6 blob + 67b0e54997c29a12feaca8d74bc968633b1711b4 --- lib/privsep.c +++ lib/privsep.c @@ -2842,6 +2842,7 @@ got_privsep_unveil_exec_helpers(void) GOT_PATH_PROG_READ_TAG, GOT_PATH_PROG_READ_GITCONFIG, GOT_PATH_PROG_READ_GOTCONFIG, + GOT_PATH_PROG_READ_PATCH, GOT_PATH_PROG_FETCH_PACK, GOT_PATH_PROG_INDEX_PACK, GOT_PATH_PROG_SEND_PACK, blob - 3783b56689f6ab58fbacbd8f0f990a7154d90f61 blob + cfd4876a2dfa135816bb51fb862396c0cd6a4331 --- libexec/Makefile +++ libexec/Makefile @@ -1,5 +1,6 @@ SUBDIR = got-read-blob got-read-commit got-read-object got-read-tree \ got-read-tag got-fetch-pack got-index-pack got-read-pack \ - got-read-gitconfig got-read-gotconfig got-send-pack + got-read-gitconfig got-read-gotconfig got-send-pack \ + got-read-patch .include <bsd.subdir.mk> blob - /dev/null blob + 9eddbae60cbd3e82dc3178ffebc9903391caa40c (mode 644) --- /dev/null +++ libexec/got-read-patch/Makefile @@ -0,0 +1,13 @@ +.PATH:${.CURDIR}/../../lib + +.include "../../got-version.mk" + +PROG= got-read-patch +SRCS= got-read-patch.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c + +CPPFLAGS = -I${.CURDIR}/../../include -I${.CURDIR}/../../lib +LDADD = -lz -lutil +DPADD = ${LIBZ} ${LIBUTIL} + +.include <bsd.prog.mk> blob - /dev/null blob + ed5eb50b17c3c73f369b043bdf1ea72f54f5ff88 (mode 644) --- /dev/null +++ libexec/got-read-patch/got-read-patch.c @@ -0,0 +1,480 @@ +/* + * Copyright 1986, Larry Wall + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following condition is met: + * 1. Redistributions of source code must retain the above copyright notice, + * this condition and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * Copyright (c) 2022 Omar Polo <op@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/uio.h> + +#include <ctype.h> +#include <limits.h> +#include <paths.h> +#include <sha1.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <imsg.h> + +#include "got_error.h" +#include "got_object.h" + +#include "got_lib_delta.h" +#include "got_lib_object.h" +#include "got_lib_privsep.h" + +struct imsgbuf ibuf; + +static const struct got_error * +send_patch(const char *oldname, const char *newname) +{ + struct got_imsg_patch p; + + memset(&p, 0, sizeof(p)); + + if (oldname != NULL) + strlcpy(p.old, oldname, sizeof(p.old)); + if (newname != NULL) + strlcpy(p.new, newname, sizeof(p.new)); + + if (imsg_compose(&ibuf, GOT_IMSG_PATCH, 0, 0, -1, + &p, sizeof(p)) == -1) + return got_error_from_errno("imsg_compose GOT_IMSG_PATCH"); + return NULL; +} + +static const struct got_error * +send_patch_done(void) +{ + if (imsg_compose(&ibuf, GOT_IMSG_PATCH_DONE, 0, 0, -1, + NULL, 0) == -1) + return got_error_from_errno("imsg_compose GOT_IMSG_PATCH_EOF"); + if (imsg_flush(&ibuf) == -1) + return got_error_from_errno("imsg_flush"); + return NULL; +} + +/* based on fetchname from usr.bin/patch/util.c */ +static const struct got_error * +filename(const char *at, char **name, int strip) +{ + char *fullname, *t; + int l, tab; + + *name = NULL; + if (*at == '\0') + return NULL; + + while (isspace((unsigned char)*at)) + at++; + + /* files can be created or removed by diffing against /dev/null */ + if (!strncmp(at, _PATH_DEVNULL, sizeof(_PATH_DEVNULL)-1)) + return NULL; + + t = strdup(at); + if (t == NULL) + return got_error_from_errno("strdup"); + *name = fullname = t; + tab = strchr(t, '\t') != NULL; + + /* strip off path components and NUL-terminate */ + for (l = strip; + *t != '\0' && ((tab && *t != '\t') || !isspace((unsigned char)*t)); + ++t) { + if (t[0] == '/' && t[1] != '/' && t[1] != '\0') + if (--l >= 0) + *name = t+1; + } + *t = '\0'; + + *name = strdup(*name); + free(fullname); + if (*name == NULL) + return got_error_from_errno("strdup"); + return NULL; +} + +static const struct got_error * +find_patch(FILE *fp) +{ + const struct got_error *err = NULL; + char *old = NULL, *new = NULL; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + int create, git = 0; + + while ((linelen = getline(&line, &linesize, fp)) != -1) { + /* + * Ignore the Index name like GNU and larry' patch, + * we don't have to follow POSIX. + */ + + if (git && !strncmp(line, "--- a/", 6)) { + free(old); + err = filename(line+6, &old, 0); + } else if (!strncmp(line, "--- ", 4)) { + free(old); + err = filename(line+4, &old, 0); + } else if (git && !strncmp(line, "+++ b/", 6)) { + free(new); + err = filename(line+6, &new, 0); + } else if (!strncmp(line, "+++ ", 4)) { + free(new); + err = filename(line+4, &new, 0); + } else if (!strncmp(line, "diff --git a/", 13)) + git = 1; + + if (err) + break; + + if (!strncmp(line, "@@ -", 4)) { + create = !strncmp(line+4, "0,0", 3); + if ((old == NULL && new == NULL) || + (!create && old == NULL)) + err = got_error(GOT_ERR_PATCH_MALFORMED); + else + err = send_patch(old, new); + + free(old); + free(new); + + if (err) + break; + + /* rewind to previous line */ + if (fseek(fp, linelen * -1, SEEK_CUR) == -1) + err = got_error_from_errno("fseek"); + break; + } + } + + free(line); + if (ferror(fp) && err == NULL) + err = got_error_from_errno("getline"); + if (feof(fp) && err == NULL) + err = got_error(GOT_ERR_NO_PATCH); + return err; +} + +static const struct got_error * +strtolnum(char **str, long *n) +{ + char *p, c; + const char *errstr; + + for (p = *str; isdigit((unsigned char)*p); ++p) + /* nop */; + + c = *p; + *p = '\0'; + + *n = strtonum(*str, 0, LONG_MAX, &errstr); + if (errstr != NULL) + return got_error(GOT_ERR_PATCH_MALFORMED); + + *p = c; + *str = p; + return NULL; +} + +static const struct got_error * +parse_hdr(char *s, int *ok, struct got_imsg_patch_hunk *hdr) +{ + static const struct got_error *err = NULL; + + *ok = 1; + if (strncmp(s, "@@ -", 4)) { + *ok = 0; + return NULL; + } + + s += 4; + if (!*s) + return NULL; + err = strtolnum(&s, &hdr->oldfrom); + if (err) + return err; + if (*s == ',') { + s++; + err = strtolnum(&s, &hdr->oldlines); + if (err) + return err; + } else + hdr->oldlines = 1; + + if (*s == ' ') + s++; + + if (*s != '+' || !*++s) + return got_error(GOT_ERR_PATCH_MALFORMED); + err = strtolnum(&s, &hdr->newfrom); + if (err) + return err; + if (*s == ',') { + s++; + err = strtolnum(&s, &hdr->newlines); + if (err) + return err; + } else + hdr->newlines = 1; + + if (*s == ' ') + s++; + + if (*s != '@') + return got_error(GOT_ERR_PATCH_MALFORMED); + + if (hdr->oldfrom >= LONG_MAX - hdr->oldlines || + hdr->newfrom >= LONG_MAX - hdr->newlines || + /* not so sure about this one */ + hdr->oldlines >= LONG_MAX - hdr->newlines - 1) + return got_error(GOT_ERR_PATCH_MALFORMED); + + if (hdr->oldlines == 0) { + /* larry says to "do append rather than insert"; I don't + * quite get it, but i trust him. + */ + hdr->oldfrom++; + } + + if (imsg_compose(&ibuf, GOT_IMSG_PATCH_HUNK, 0, 0, -1, + hdr, sizeof(*hdr)) == -1) + return got_error_from_errno( + "imsg_compose GOT_IMSG_PATCH_HUNK"); + return NULL; +} + +static const struct got_error * +send_line(const char *line) +{ + static const struct got_error *err = NULL; + char *p = NULL; + + if (*line != '+' && *line != '-' && *line != ' ') { + if (asprintf(&p, " %s", line) == -1) + return got_error_from_errno("asprintf"); + line = p; + } + + if (imsg_compose(&ibuf, GOT_IMSG_PATCH_LINE, 0, 0, -1, + line, strlen(line)+1) == -1) + err = got_error_from_errno( + "imsg_compose GOT_IMSG_PATCH_LINE"); + + free(p); + return err; +} + +static const struct got_error * +parse_hunk(FILE *fp, int *ok) +{ + static const struct got_error *err = NULL; + struct got_imsg_patch_hunk hdr; + char *line = NULL, ch; + size_t linesize = 0; + ssize_t linelen; + long leftold, leftnew; + + linelen = getline(&line, &linesize, fp); + if (linelen == -1) { + *ok = 0; + goto done; + } + + err = parse_hdr(line, ok, &hdr); + if (err) + goto done; + if (!*ok) { + if (fseek(fp, linelen * -1, SEEK_CUR) == -1) + err = got_error_from_errno("fseek"); + goto done; + } + + leftold = hdr.oldlines; + leftnew = hdr.newlines; + + while (leftold > 0 || leftnew > 0) { + linelen = getline(&line, &linesize, fp); + if (linelen == -1) { + if (ferror(fp)) { + err = got_error_from_errno("getline"); + goto done; + } + + /* trailing newlines may be chopped */ + if (leftold < 3 && leftnew < 3) { + *ok = 0; + break; + } + + err = got_error(GOT_ERR_PATCH_TRUNCATED); + goto done; + } + + /* usr.bin/patch allows '=' as context char */ + if (*line == '=') + *line = ' '; + + ch = *line; + if (ch == '\t' || ch == '\n') + ch = ' '; /* the space got eaten */ + + switch (ch) { + case '-': + leftold--; + break; + case ' ': + leftold--; + leftnew--; + break; + case '+': + leftnew--; + break; + default: + err = got_error(GOT_ERR_PATCH_MALFORMED); + goto done; + } + + if (leftold < 0 || leftnew < 0) { + err = got_error(GOT_ERR_PATCH_MALFORMED); + goto done; + } + + err = send_line(line); + if (err) + goto done; + } + +done: + free(line); + return err; +} + +static const struct got_error * +read_patch(struct imsgbuf *ibuf, int fd) +{ + const struct got_error *err = NULL; + FILE *fp; + int ok, patch_found = 0; + + if ((fp = fdopen(fd, "r")) == NULL) { + err = got_error_from_errno("fdopen"); + close(fd); + return err; + } + + while (!feof(fp)) { + err = find_patch(fp); + if (err) + goto done; + + patch_found = 1; + for (;;) { + err = parse_hunk(fp, &ok); + if (err) + goto done; + if (!ok) { + err = send_patch_done(); + if (err) + goto done; + break; + } + } + } + +done: + fclose(fp); + + /* ignore trailing gibberish */ + if (err != NULL && err->code == GOT_ERR_NO_PATCH && patch_found) + err = NULL; + + return err; +} + +int +main(int argc, char **argv) +{ + const struct got_error *err = NULL; + struct imsg imsg; +#if 0 + static int attached; + while (!attached) + sleep(1); +#endif + + imsg_init(&ibuf, GOT_IMSG_FD_CHILD); +#ifndef PROFILE + /* revoke access to most system calls */ + if (pledge("stdio recvfd", NULL) == -1) { + err = got_error_from_errno("pledge"); + got_privsep_send_error(&ibuf, err); + return 1; + } +#endif + + err = got_privsep_recv_imsg(&imsg, &ibuf, 0); + if (err) + goto done; + if (imsg.hdr.type != GOT_IMSG_PATCH_FILE || imsg.fd == -1) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + + err = read_patch(&ibuf, imsg.fd); + if (err) + goto done; + if (imsg_compose(&ibuf, GOT_IMSG_PATCH_EOF, 0, 0, -1, + NULL, 0) == -1) { + err = got_error_from_errno("imsg_compose GOT_IMSG_PATCH_EOF"); + goto done; + } + err = got_privsep_flush_imsg(&ibuf); +done: + imsg_free(&imsg); + if (err != NULL) { + got_privsep_send_error(&ibuf, err); + err = NULL; + } + if (close(GOT_IMSG_FD_CHILD) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (err && err->code != GOT_ERR_PRIVSEP_PIPE) + fprintf(stderr, "%s: %s\n", getprogname(), err->msg); + return err ? 1 : 0; +} blob - 54055c09da65df95bc8676121ad774abaed5f07c blob + a1b33c05a7dbcb845170a3d4eabcc5a6cbc68802 --- regress/cmdline/Makefile +++ regress/cmdline/Makefile @@ -1,6 +1,7 @@ REGRESS_TARGETS=checkout update status log add rm diff blame branch tag \ ref commit revert cherrypick backout rebase import histedit \ - integrate merge stage unstage cat clone fetch tree pack cleanup + integrate merge stage unstage cat clone fetch tree patch pack \ + cleanup NOOBJ=Yes GOT_TEST_ROOT=/tmp @@ -86,6 +87,9 @@ send: tree: ./tree.sh -q -r "$(GOT_TEST_ROOT)" +patch: + ./patch.sh -q -r "$(GOT_TEST_ROOT)" + pack: ./pack.sh -q -r "$(GOT_TEST_ROOT)" blob - /dev/null blob + 8d62a59817a8465d55c8c4770a983504c28e76fa (mode 755) --- /dev/null +++ regress/cmdline/patch.sh @@ -0,0 +1,638 @@ +#!/bin/sh +# +# Copyright (c) 2022 Omar Polo <op@openbsd.org> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +. ./common.sh + +test_patch_simple_add_file() { + local testroot=`test_init patch_simple_add_file` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- /dev/null ++++ eta +@@ -0,0 +1 @@ ++eta +EOF + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + echo "A eta" > $testroot/stdout.expected + cmp -s $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 eta > $testroot/wt/eta.expected + cmp -s $testroot/wt/eta.expected $testroot/wt/eta + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/wt/eta.expected $testroot/wt/eta + fi + test_done $testroot $ret +} + +test_patch_simple_rm_file() { + local testroot=`test_init patch_simple_rm_file` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- alpha ++++ /dev/null +@@ -1 +0,0 @@ +-alpha +EOF + + echo "D alpha" > $testroot/stdout.expected + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + if [ -f $testroot/wt/alpha ]; then + ret=1 + echo "alpha still exists!" + fi + test_done $testroot $ret +} + +test_patch_simple_edit_file() { + local testroot=`test_init patch_simple_edit_file` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- alpha ++++ alpha +@@ -1 +1 @@ +-alpha ++alpha is my favourite character +EOF + + echo "M alpha" > $testroot/stdout.expected + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cmp -s $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 'alpha is my favourite character' > $testroot/wt/alpha.expected + cmp -s $testroot/wt/alpha.expected $testroot/wt/alpha + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/wt/alpha.expected $testroot/wt/alpha + fi + test_done $testroot $ret +} + +test_patch_prepend_line() { + local testroot=`test_init patch_prepend_line` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- alpha ++++ alpha +@@ -1 +1,2 @@ ++hatsuseno + alpha +EOF + + echo "M alpha" > $testroot/stdout.expected + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cmp -s $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 hatsuseno > $testroot/wt/alpha.expected + echo alpha >> $testroot/wt/alpha.expected + cmp -s $testroot/wt/alpha.expected $testroot/wt/alpha + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/wt/alpha.expected $testroot/wt/alpha + fi + test_done $testroot $ret +} + +test_patch_replace_line() { + local testroot=`test_init patch_replace_line` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + jot 10 > $testroot/wt/numbers + (cd $testroot/wt/ && got add numbers && got ci -m 'add numbers') \ + >/dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- numbers ++++ numbers +@@ -3,7 +3,7 @@ + 3 + 4 + 5 +-6 ++foo + 7 + 8 + 9 +EOF + + echo "M numbers" > $testroot/stdout.expected + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + jot 10 | sed 's/6/foo/' > $testroot/wt/numbers.expected + cmp -s $testroot/wt/numbers.expected $testroot/wt/numbers + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/wt/numbers.expected $testroot/wt/numbers + fi + test_done $testroot $ret +} + +test_patch_multiple_hunks() { + local testroot=`test_init patch_replace_multiple_lines` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + jot 100 > $testroot/wt/numbers + (cd $testroot/wt/ && got add numbers && got ci -m 'add numbers') \ + >/dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- numbers ++++ numbers +@@ -3,7 +3,7 @@ + 3 + 4 + 5 +-6 ++foo + 7 + 8 + 9 +@@ -57,7 +57,7 @@ + 57 + 58 + 59 +-60 ++foo foo + 61 + 62 + 63 +@@ -98,3 +98,6 @@ + 98 + 99 + 100 ++101 ++102 ++... +EOF + + echo "M numbers" > $testroot/stdout.expected + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + jot 100 | sed -e 's/^6$/foo/' -e 's/^60$/foo foo/' \ + > $testroot/wt/numbers.expected + echo "101" >> $testroot/wt/numbers.expected + echo "102" >> $testroot/wt/numbers.expected + echo "..." >> $testroot/wt/numbers.expected + + cmp -s $testroot/wt/numbers.expected $testroot/wt/numbers + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/wt/numbers.expected $testroot/wt/numbers + fi + test_done $testroot $ret +} + +test_patch_multiple_files() { + local testroot=`test_init patch_multiple_files` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- alpha Mon Mar 7 19:02:07 2022 ++++ alpha Mon Mar 7 19:01:53 2022 +@@ -1 +1,3 @@ ++new + alpha ++available +--- beta Mon Mar 7 19:02:11 2022 ++++ beta Mon Mar 7 19:01:46 2022 +@@ -1 +1,3 @@ + beta ++was ++improved +--- gamma/delta Mon Mar 7 19:02:17 2022 ++++ gamma/delta Mon Mar 7 19:01:37 2022 +@@ -1 +1 @@ +-delta ++delta new +EOF + + echo "M alpha" > $testroot/stdout.expected + echo "M beta" >> $testroot/stdout.expected + echo "M gamma/delta" >> $testroot/stdout.expected + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testrot $ret + return 1 + fi + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + printf 'new\nalpha\navailable\n' > $testroot/wt/alpha.expected + printf 'beta\nwas\nimproved\n' > $testroot/wt/beta.expected + printf 'delta new\n' > $testroot/wt/gamma/delta.expected + + for f in alpha beta gamma/delta; do + cmp -s $testroot/wt/$f.expected $testroot/wt/$f + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/wt/$f.expected $testroot/wt/$f + test_done $testroot $ret + fi + done + + test_done $testroot 0 +} + +test_patch_dont_apply() { + local testroot=`test_init patch_dont_apply` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- alpha ++++ alpha +@@ -1 +1,2 @@ ++hatsuseno + alpha something +EOF + + echo -n > $testroot/stdout.expected + echo "got: patch doesn't apply" > $testroot/stderr.expected + + (cd $testroot/wt && got patch patch) \ + > $testroot/stdout \ + 2> $testroot/stderr + ret=$? + if [ $ret == 0 ]; then # should fail + test_done $testroot 1 + return 1 + fi + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + cmp -s $testroot/stderr.expected $testroot/stderr + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done $testroot $ret + return 1 + fi + + test_done $testroot $ret +} + +test_patch_malformed() { + local testroot=`test_init patch_malformed` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + # missing "@@" + cat <<EOF > $testroot/wt/patch +--- alpha ++++ alpha +@@ -1 +1,2 ++hatsuseno + alpha +EOF + + echo -n > $testroot/stdout.expected + echo "got: malformed patch" > $testroot/stderr.expected + + (cd $testroot/wt && got patch patch) \ + > $testroot/stdout \ + 2> $testroot/stderr + ret=$? + if [ $ret == 0 ]; then + echo "got managed to apply an invalid patch" + test_done $testroot 1 + return 1 + fi + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + cmp -s $testroot/stderr.expected $testroot/stderr + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done $testroot $ret + return 1 + fi + + # wrong first character + cat <<EOF > $testroot/wt/patch +--- alpha ++++ alpha +@@ -1 +1,2 @@ ++hatsuseno +alpha +EOF + + (cd $testroot/wt && got patch patch) \ + > $testroot/stdout \ + 2> $testroot/stderr + ret=$? + if [ $ret == 0 ]; then + echo "got managed to apply an invalid patch" + test_done $testroot 1 + return 1 + fi + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + cmp -s $testroot/stderr.expected $testroot/stderr + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done $testroot $ret + return 1 + fi + + test_done $testroot $ret +} + +test_patch_no_patch() { + local testroot=`test_init patch_no_patch` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +hello world! +... + +some other nonsense +... + +there's no patch in here! +EOF + + echo -n > $testroot/stdout.expected + echo "got: no patch found" > $testroot/stderr.expected + + (cd $testroot/wt && got patch patch) \ + > $testroot/stdout \ + 2> $testroot/stderr + ret=$? + if [ $ret == 0 ]; then # should fail + test_done $testroot 1 + return 1 + fi + + + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done $testroot $ret + return 1 + fi + + cmp -s $testroot/stderr.expected $testroot/stderr + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done $testroot $ret + return 1 + fi + + test_done $testroot $ret +} + +test_patch_equals_for_context() { + local testroot=`test_init patch_prepend_line` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cat <<EOF > $testroot/wt/patch +--- alpha ++++ alpha +@@ -1 +1,2 @@ ++hatsuseno +=alpha +EOF + + echo "M alpha" > $testroot/stdout.expected + + (cd $testroot/wt && got patch patch) > $testroot/stdout + ret=$? + if [ $ret != 0 ]; then + test_done $testroot $ret + return 1 + fi + + cmp -s $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 hatsuseno > $testroot/wt/alpha.expected + echo alpha >> $testroot/wt/alpha.expected + cmp -s $testroot/wt/alpha.expected $testroot/wt/alpha + ret=$? + if [ $ret != 0 ]; then + diff -u $testroot/wt/alpha.expected $testroot/wt/alpha + fi + test_done $testroot $ret +} + +test_parseargs "$@" +run_test test_patch_simple_add_file +run_test test_patch_simple_rm_file +run_test test_patch_simple_edit_file +run_test test_patch_prepend_line +run_test test_patch_replace_line +run_test test_patch_multiple_hunks +run_test test_patch_multiple_files +run_test test_patch_dont_apply +run_test test_patch_malformed +run_test test_patch_no_patch +run_test test_patch_equals_for_context
add got patch subcommand