commit e9ce266e31923cc339954b331d273d9bba543f6f from: Omar Polo date: Mon Mar 07 22:26:08 2022 UTC add `got patch' command for applying unified diffs commit - 805253d5155091691f7cf36e54134cc87b2ea91a commit + e9ce266e31923cc339954b331d273d9bba543f6f 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 + 3e66e251220b096cd82830703d4481ddfc399307 --- 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" }, @@ -7102,6 +7106,133 @@ done: TAILQ_FOREACH(pe, &paths, entry) free((char *)pe->path); got_pathlist_free(&paths); + free(cwd); + return error; +} + +__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; } @@ -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 + * + * 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 + * + * 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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 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 + * + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + cb9ff81d665f65b182c6a4ecb6bd3b4185b0ad6d (mode 755) --- /dev/null +++ regress/cmdline/patch.sh @@ -0,0 +1,639 @@ +#!/bin/sh +# +# Copyright (c) 2022 Omar Polo +# +# 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 < $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 < $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 < $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 < $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 < $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 < $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 < $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 + return 1 + 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 < $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 < $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 < $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 < $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 < $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