commit f8a36e221091eb68b439ebe4eb07a5d03b335c28 from: Stefan Sperling date: Thu Aug 26 12:30:42 2021 UTC add 'got send' command for sending changes to remote repositories Known to work against git-daemon and github Git server implementations. Tests by abieber, naddy, jrick, and myself. Man page additions reviewed by Lucas. commit - 3379373c62efb02a719d4b6e040189f348470f0a commit + f8a36e221091eb68b439ebe4eb07a5d03b335c28 blob - 29c7f19e9b90a3fa15fae12f16e5e7654db9f42e blob + 14f769b331dbbbc7a4e4a948191670b97122457d --- got/Makefile +++ got/Makefile @@ -12,7 +12,7 @@ SRCS= got.c blame.c commit_graph.c delta.c diff.c \ gotconfig.c diff_main.c diff_atomize_text.c \ diff_myers.c diff_output.c diff_output_plain.c \ diff_output_unidiff.c diff_output_edscript.c \ - diff_patience.c + diff_patience.c send.c deltify.c pack_create.c MAN = ${PROG}.1 got-worktree.5 git-repository.5 got.conf.5 blob - 2fc788135c449d339f9e22c8ac5d28eaf3bb8047 blob + 6ccabbd90628971c00ab688ca261da4cece4f330 --- got/got.1 +++ got/got.1 @@ -1372,6 +1372,185 @@ in the repository. .It Cm ci Short alias for .Cm commit . +.It Cm send Oo Fl a Oc Oo Fl b Ar branch Oc Oo Fl d Ar branch Oc Oo Fl f Oc Oo Fl r Ar repository-path Oc Oo Fl t Ar tag Oc Oo Fl T Oc Oo Fl q Oc Oo Fl v Oc Op Ar remote-repository +Send new changes to a remote repository. +If no +.Ar remote-repository +is specified, +.Dq origin +will be used. +The remote repository's URL is obtained from the corresponding entry in +.Xr got.conf 5 +or Git's +.Pa config +file of the local repository, as created by +.Cm got clone . +.Pp +All objects corresponding to new changes will be written to a temporary +pack file which is then uploaded to the server. +Upon success, references in the +.Dq refs/remotes/ +reference namespace of the local repository will be updated to point at +the commits which have been sent. +.Pp +By default, changes will only be sent if they are based on up-to-date +copies of relevant branches in the remote repository. +If any changes to be sent are based on out-of-date copies, new changes +must be fetched from the server with +.Cm got fetch +and local branches must be rebased with +.Cm got rebase +before +.Cm got send +can succeed. +.Pp +The options for +.Cm got send +are as follows: +.Bl -tag -width Ds +.It Fl a +Send all branches from the local repository's +.Dq refs/heads/ +reference namespace. +The +.Fl a +option is equivalent to listing all branches with multiple +.Fl b +options. +Cannot be used together with the +.Fl b +option. +.It Fl b Ar branch +Send the specified +.Ar branch +from the local repository's +.Dq refs/heads/ +reference namespace. +This option may be specified multiple times to build a list of branches +to send. +If this option is not specified, default to the work tree's current branch +if invoked in a work tree, or to the repository's HEAD reference. +Cannot be used together with the +.Fl a +option. +.It Fl d Ar branch +Delete the specified +.Ar branch +from the remote repository's +.Dq refs/heads/ +reference namespace. +This option may be specified multiple times to build a list of branches +to delete. +.Pp +Only references are deleted. +Any commit, tree, tag, and blob objects belonging to deleted branches +may become subject to deletion by Git's garbage collector running on +the server. +.Pp +Requesting deletion of branches results in an error if the server +does not support this feature. +.It Fl f +Attempt to force the server to accept uploaded branches or tags in +spite of failing client-side sanity checks. +The server may reject forced requests regardless, depending on its +configuration. +.Pp +Any commit, tree, tag, and blob objects belonging to overwritten branches +or tags may become subject to deletion by Git's garbage collector running +on the server. +.Pp +The +.Dq refs/tags +reference namespace is globally shared between all repositories. +Use of the +.Fl f +option to overwrite tags is discouraged because it can lead to +inconsistencies between the tags present in different repositories. +In general, creating a new tag with a different name is recommended +instead of overwriting an existing tag. +.Pp +Use of the +.Fl f +option is particularly discouraged if changes being sent are based +on an out-of-date copy of a branch in the remote repository. +Instead of using the +.Fl f +option, new changes should +be fetched with +.Cm got fetch +and local branches should be rebased with +.Cm got rebase , +followed by another attempt to send the changes. +.Pp +The +.Fl f +option should only be needed in situations where the remote repository's +copy of a branch or tag is known to be out-of-date and is considered +disposable. +The risks of creating inconsistencies between different repositories +should also be taken into account. +.It Fl r Ar repository-path +Use the repository at the specified path. +If not specified, assume the repository is located at or above the current +working directory. +If this directory is a +.Nm +work tree, use the repository path associated with this work tree. +.It Fl t Ar tag +Send the specified +.Ar tag +from the local repository's +.Dq refs/tags/ +reference namespace, in addition to any branches that are being sent. +The +.Fl t +option may be specified multiple times to build a list of tags to send. +No tags will be sent if the +.Fl t +option is not used. +.Pp +Raise an error if the specified +.Ar tag +already exists in the remote repository, unless the +.Fl f +option is used to overwrite the sever's copy of the tag. +In general, creating a new tag with a different name is recommended +instead of overwriting an existing tag. +.Pp +Cannot be used together with the +.Fl T +option. +.It Fl T +Attempt to send all tags from the local repository's +.Dq refs/tags/ +reference namespace. +The +.Fl T +option is equivalent to listing all tags with multiple +.Fl t +options. +Cannot be used together with the +.Fl t +option. +.It Fl q +Suppress progress reporting output. +The same option will be passed to +.Xr ssh 1 +if applicable. +.It Fl v +Verbose mode. +Causes +.Cm got send +to print debugging messages to standard error output. +The same option will be passed to +.Xr ssh 1 +if applicable. +Multiple -v options increase the verbosity. +The maximum is 3. +.El +.It Cm se +Short alias for +.Cm send . .It Cm cherrypick Ar commit Merge changes from a single .Ar commit blob - 71cc16e4b52a05afc8991d3d0c566bd8c200b3d7 blob + 5f57676c71bfe6ae726a39f6cba1b9fb3dafb730 --- got/got.c +++ got/got.c @@ -51,6 +51,7 @@ #include "got_diff.h" #include "got_commit_graph.h" #include "got_fetch.h" +#include "got_send.h" #include "got_blame.h" #include "got_privsep.h" #include "got_opentemp.h" @@ -102,6 +103,7 @@ __dead static void usage_add(void); __dead static void usage_remove(void); __dead static void usage_revert(void); __dead static void usage_commit(void); +__dead static void usage_send(void); __dead static void usage_cherrypick(void); __dead static void usage_backout(void); __dead static void usage_rebase(void); @@ -130,6 +132,7 @@ static const struct got_error* cmd_add(int, char *[]) static const struct got_error* cmd_remove(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 *[]); static const struct got_error* cmd_cherrypick(int, char *[]); static const struct got_error* cmd_backout(int, char *[]); static const struct got_error* cmd_rebase(int, char *[]); @@ -159,6 +162,7 @@ static struct got_cmd got_commands[] = { { "remove", cmd_remove, usage_remove, "rm" }, { "revert", cmd_revert, usage_revert, "rv" }, { "commit", cmd_commit, usage_commit, "ci" }, + { "send", cmd_send, usage_send, "se" }, { "cherrypick", cmd_cherrypick, usage_cherrypick, "cy" }, { "backout", cmd_backout, usage_backout, "bo" }, { "rebase", cmd_rebase, usage_rebase, "rb" }, @@ -7349,6 +7353,498 @@ done: free(editor); free(author); free(prepared_logmsg); + return error; +} + +__dead static void +usage_send(void) +{ + fprintf(stderr, "usage: %s send [-a] [-b branch] [-d branch] [-f] " + "[-r repository-path] [-t tag] [-T] [-q] [-v] " + "[remote-repository]\n", getprogname()); + exit(1); +} + +struct got_send_progress_arg { + char last_scaled_packsize[FMT_SCALED_STRSIZE]; + int verbosity; + int last_ncommits; + int last_nobj_total; + int last_p_deltify; + int last_p_written; + int last_p_sent; + int printed_something; + int sent_something; + struct got_pathlist_head *delete_branches; +}; + +static const struct got_error * +send_progress(void *arg, off_t packfile_size, int ncommits, int nobj_total, + int nobj_deltify, int nobj_written, off_t bytes_sent, const char *refname, + int success) +{ + struct got_send_progress_arg *a = arg; + char scaled_packsize[FMT_SCALED_STRSIZE]; + char scaled_sent[FMT_SCALED_STRSIZE]; + int p_deltify = 0, p_written = 0, p_sent = 0; + int print_searching = 0, print_total = 0; + int print_deltify = 0, print_written = 0, print_sent = 0; + + if (a->verbosity < 0) + return NULL; + + if (refname) { + const char *status = success ? "accepted" : "rejected"; + + if (success) { + struct got_pathlist_entry *pe; + TAILQ_FOREACH(pe, a->delete_branches, entry) { + const char *branchname = pe->path; + if (got_path_cmp(branchname, refname, + strlen(branchname), strlen(refname)) == 0) { + status = "deleted"; + break; + } + } + } + + printf("\nServer has %s %s", status, refname); + a->printed_something = 1; + return NULL; + } + + if (fmt_scaled(packfile_size, scaled_packsize) == -1) + return got_error_from_errno("fmt_scaled"); + if (fmt_scaled(bytes_sent, scaled_sent) == -1) + return got_error_from_errno("fmt_scaled"); + + if (a->last_ncommits != ncommits) { + print_searching = 1; + a->last_ncommits = ncommits; + } + + if (a->last_nobj_total != nobj_total) { + print_searching = 1; + print_total = 1; + a->last_nobj_total = nobj_total; + } + + if (packfile_size > 0 && (a->last_scaled_packsize[0] == '\0' || + strcmp(scaled_packsize, a->last_scaled_packsize)) != 0) { + if (strlcpy(a->last_scaled_packsize, scaled_packsize, + FMT_SCALED_STRSIZE) >= FMT_SCALED_STRSIZE) + return got_error(GOT_ERR_NO_SPACE); + } + + if (nobj_deltify > 0 || nobj_written > 0) { + if (nobj_deltify > 0) { + p_deltify = (nobj_deltify * 100) / nobj_total; + if (p_deltify != a->last_p_deltify) { + a->last_p_deltify = p_deltify; + print_searching = 1; + print_total = 1; + print_deltify = 1; + } + } + if (nobj_written > 0) { + p_written = (nobj_written * 100) / nobj_total; + if (p_written != a->last_p_written) { + a->last_p_written = p_written; + print_searching = 1; + print_total = 1; + print_deltify = 1; + print_written = 1; + } + } + } + + if (bytes_sent > 0) { + p_sent = (bytes_sent * 100) / packfile_size; + if (p_sent != a->last_p_sent) { + a->last_p_sent = p_sent; + print_searching = 1; + print_total = 1; + print_deltify = 1; + print_written = 1; + print_sent = 1; + } + a->sent_something = 1; + } + + if (print_searching || print_total || print_deltify || print_written || + print_sent) + printf("\r"); + if (print_searching) + printf("packing %d reference%s", ncommits, + ncommits == 1 ? "" : "s"); + if (print_total) + printf("; %d object%s", nobj_total, + nobj_total == 1 ? "" : "s"); + if (print_deltify) + printf("; deltify: %d%%", p_deltify); + if (print_sent) + printf("; uploading pack: %*s %d%%", FMT_SCALED_STRSIZE, + scaled_packsize, p_sent); + else if (print_written) + printf("; writing pack: %*s %d%%", FMT_SCALED_STRSIZE, + scaled_packsize, p_written); + if (print_searching || print_total || print_deltify || + print_written || print_sent) { + a->printed_something = 1; + fflush(stdout); + } + return NULL; +} + +static const struct got_error * +cmd_send(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + char *cwd = NULL, *repo_path = NULL; + const char *remote_name; + char *proto = NULL, *host = NULL, *port = NULL; + char *repo_name = NULL, *server_path = NULL; + const struct got_remote_repo *remotes, *remote = NULL; + int nremotes, nbranches = 0, ntags = 0, ndelete_branches = 0; + struct got_repository *repo = NULL; + struct got_worktree *worktree = NULL; + const struct got_gotconfig *repo_conf = NULL, *worktree_conf = NULL; + struct got_pathlist_head branches; + struct got_pathlist_head tags; + struct got_reflist_head all_branches; + struct got_reflist_head all_tags; + struct got_pathlist_head delete_args; + struct got_pathlist_head delete_branches; + struct got_reflist_entry *re; + struct got_pathlist_entry *pe; + int i, ch, sendfd = -1, sendstatus; + pid_t sendpid = -1; + struct got_send_progress_arg spa; + int verbosity = 0, overwrite_refs = 0; + int send_all_branches = 0, send_all_tags = 0; + struct got_reference *ref = NULL; + + TAILQ_INIT(&branches); + TAILQ_INIT(&tags); + TAILQ_INIT(&all_branches); + TAILQ_INIT(&all_tags); + TAILQ_INIT(&delete_args); + TAILQ_INIT(&delete_branches); + + while ((ch = getopt(argc, argv, "ab:d:fr:t:Tvq")) != -1) { + switch (ch) { + case 'a': + send_all_branches = 1; + break; + case 'b': + error = got_pathlist_append(&branches, optarg, NULL); + if (error) + return error; + nbranches++; + break; + case 'd': + error = got_pathlist_append(&delete_args, optarg, NULL); + if (error) + return error; + break; + case 'f': + overwrite_refs = 1; + break; + case 'r': + repo_path = realpath(optarg, NULL); + if (repo_path == NULL) + return got_error_from_errno2("realpath", + optarg); + got_path_strip_trailing_slashes(repo_path); + break; + case 't': + error = got_pathlist_append(&tags, optarg, NULL); + if (error) + return error; + ntags++; + break; + case 'T': + send_all_tags = 1; + break; + case 'v': + if (verbosity < 0) + verbosity = 0; + else if (verbosity < 3) + verbosity++; + break; + case 'q': + verbosity = -1; + break; + default: + usage_send(); + /* NOTREACHED */ + } + } + argc -= optind; + argv += optind; + + if (send_all_branches && !TAILQ_EMPTY(&branches)) + option_conflict('a', 'b'); + if (send_all_tags && !TAILQ_EMPTY(&tags)) + option_conflict('T', 't'); + + + if (argc == 0) + remote_name = GOT_SEND_DEFAULT_REMOTE_NAME; + else if (argc == 1) + remote_name = argv[0]; + else + usage_send(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + if (repo_path == NULL) { + error = got_worktree_open(&worktree, cwd); + if (error && error->code != GOT_ERR_NOT_WORKTREE) + goto done; + else + error = NULL; + if (worktree) { + repo_path = + strdup(got_worktree_get_repo_path(worktree)); + if (repo_path == NULL) + error = got_error_from_errno("strdup"); + if (error) + goto done; + } else { + repo_path = strdup(cwd); + if (repo_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + } + + error = got_repo_open(&repo, repo_path, NULL); + if (error) + goto done; + + if (worktree) { + worktree_conf = got_worktree_get_gotconfig(worktree); + if (worktree_conf) { + got_gotconfig_get_remotes(&nremotes, &remotes, + worktree_conf); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + } + if (remote == NULL) { + repo_conf = got_repo_get_gotconfig(repo); + if (repo_conf) { + got_gotconfig_get_remotes(&nremotes, &remotes, + repo_conf); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + } + if (remote == NULL) { + got_repo_get_gitconfig_remotes(&nremotes, &remotes, repo); + for (i = 0; i < nremotes; i++) { + if (strcmp(remotes[i].name, remote_name) == 0) { + remote = &remotes[i]; + break; + } + } + } + if (remote == NULL) { + error = got_error_path(remote_name, GOT_ERR_NO_REMOTE); + goto done; + } + + error = got_fetch_parse_uri(&proto, &host, &port, &server_path, + &repo_name, remote->url); + if (error) + goto done; + + if (strcmp(proto, "git") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd dns inet unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "git+ssh") == 0 || + strcmp(proto, "ssh") == 0) { +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec " + "sendfd unveil", NULL) == -1) + err(1, "pledge"); +#endif + } else if (strcmp(proto, "http") == 0 || + strcmp(proto, "git+http") == 0) { + error = got_error_path(proto, GOT_ERR_NOT_IMPL); + goto done; + } else { + error = got_error_path(proto, GOT_ERR_BAD_PROTO); + goto done; + } + + if (strcmp(proto, "git+ssh") == 0 || strcmp(proto, "ssh") == 0) { + if (unveil(GOT_FETCH_PATH_SSH, "x") != 0) { + error = got_error_from_errno2("unveil", + GOT_FETCH_PATH_SSH); + goto done; + } + } + error = apply_unveil(got_repo_get_path(repo), 0, NULL); + if (error) + goto done; + + if (send_all_branches) { + error = got_ref_list(&all_branches, repo, "refs/heads", + got_ref_cmp_by_name, NULL); + if (error) + goto done; + TAILQ_FOREACH(re, &all_branches, entry) { + const char *branchname = got_ref_get_name(re->ref); + error = got_pathlist_append(&branches, + branchname, NULL); + if (error) + goto done; + nbranches++; + } + } + + if (send_all_tags) { + error = got_ref_list(&all_tags, repo, "refs/tags", + got_ref_cmp_by_name, NULL); + if (error) + goto done; + TAILQ_FOREACH(re, &all_tags, entry) { + const char *tagname = got_ref_get_name(re->ref); + error = got_pathlist_append(&tags, + tagname, NULL); + if (error) + goto done; + ntags++; + } + } + + /* + * To prevent accidents only branches in refs/heads/ can be deleted + * with 'got send -d'. + * Deleting anything else requires local repository access or Git. + */ + TAILQ_FOREACH(pe, &delete_args, entry) { + const char *branchname = pe->path; + char *s; + struct got_pathlist_entry *new; + if (strncmp(branchname, "refs/heads/", 11) == 0) { + s = strdup(branchname); + if (s == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else { + if (asprintf(&s, "refs/heads/%s", branchname) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + } + error = got_pathlist_insert(&new, &delete_branches, s, NULL); + if (error || new == NULL /* duplicate */) + free(s); + if (error) + goto done; + ndelete_branches++; + } + + if (nbranches == 0 && ndelete_branches == 0) { + struct got_reference *head_ref; + if (worktree) + error = got_ref_open(&head_ref, repo, + got_worktree_get_head_ref_name(worktree), 0); + else + error = got_ref_open(&head_ref, repo, GOT_REF_HEAD, 0); + if (error) + goto done; + if (got_ref_is_symbolic(head_ref)) { + error = got_ref_resolve_symbolic(&ref, repo, head_ref); + got_ref_close(head_ref); + if (error) + goto done; + } else + ref = head_ref; + error = got_pathlist_append(&branches, got_ref_get_name(ref), + NULL); + if (error) + goto done; + nbranches++; + } + + if (verbosity >= 0) + printf("Connecting to \"%s\" %s%s%s\n", remote->name, host, + port ? ":" : "", port ? port : ""); + + error = got_send_connect(&sendpid, &sendfd, proto, host, port, + server_path, verbosity); + if (error) + goto done; + + memset(&spa, 0, sizeof(spa)); + spa.last_scaled_packsize[0] = '\0'; + spa.last_p_deltify = -1; + spa.last_p_written = -1; + spa.verbosity = verbosity; + spa.delete_branches = &delete_branches; + error = got_send_pack(remote_name, &branches, &tags, &delete_branches, + verbosity, overwrite_refs, sendfd, repo, send_progress, &spa, + check_cancelled, NULL); + if (spa.printed_something) + putchar('\n'); + if (error) + goto done; + if (!spa.sent_something && verbosity >= 0) + printf("Already up-to-date\n"); +done: + if (sendpid > 0) { + if (kill(sendpid, SIGTERM) == -1) + error = got_error_from_errno("kill"); + if (waitpid(sendpid, &sendstatus, 0) == -1 && error == NULL) + error = got_error_from_errno("waitpid"); + } + if (sendfd != -1 && close(sendfd) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } + if (worktree) + got_worktree_close(worktree); + if (ref) + got_ref_close(ref); + got_pathlist_free(&branches); + got_pathlist_free(&tags); + got_ref_list_free(&all_branches); + got_ref_list_free(&all_tags); + got_pathlist_free(&delete_args); + TAILQ_FOREACH(pe, &delete_branches, entry) + free((char *)pe->path); + got_pathlist_free(&delete_branches); + free(cwd); + free(repo_path); + free(proto); + free(host); + free(port); + free(server_path); + free(repo_name); return error; } blob - eab64a846eec63193c520e1b192077f1c38f24c9 blob + 5eb5cc5ebde432af823128fc7227bf48b866369a --- include/got_error.h +++ include/got_error.h @@ -148,6 +148,13 @@ #define GOT_ERR_CANNOT_PACK 131 #define GOT_ERR_LONELY_PACKIDX 132 #define GOT_ERR_OBJ_CSUM 133 +#define GOT_ERR_SEND_BAD_REF 134 +#define GOT_ERR_SEND_FAILED 135 +#define GOT_ERR_SEND_EMPTY 136 +#define GOT_ERR_SEND_ANCESTRY 137 +#define GOT_ERR_CAPA_DELETE_REFS 138 +#define GOT_ERR_SEND_DELETE_REF 139 +#define GOT_ERR_SEND_TAG_EXISTS 140 static const struct got_error { int code; @@ -304,6 +311,13 @@ static const struct got_error { { GOT_ERR_LONELY_PACKIDX, "pack index has no corresponding pack file; " "pack file must be restored or 'gotadmin cleanup -p' must be run" }, { GOT_ERR_OBJ_CSUM, "bad object checksum" }, + { GOT_ERR_SEND_BAD_REF, "reference cannot be sent" }, + { GOT_ERR_SEND_FAILED, "could not send pack file" }, + { GOT_ERR_SEND_EMPTY, "no references to send" }, + { GOT_ERR_SEND_ANCESTRY, "fetch and rebase required" }, + { GOT_ERR_CAPA_DELETE_REFS, "server cannot delete references" }, + { GOT_ERR_SEND_DELETE_REF, "reference cannot be deleted" }, + { GOT_ERR_SEND_TAG_EXISTS, "tag already exists on server" }, }; /* blob - /dev/null blob + 2f0388ea400f048c63027a0b901c892f672248bc (mode 644) --- /dev/null +++ include/got_send.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018, 2019 Ori Bernstein + * Copyright (c) 2021 Stefan Sperling + * + * 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. + */ + +/* IANA assigned */ +#define GOT_DEFAULT_GIT_PORT 9418 +#define GOT_DEFAULT_GIT_PORT_STR "9418" + +#ifndef GOT_SEND_PATH_SSH +#define GOT_SEND_PATH_SSH "/usr/bin/ssh" +#endif + +#define GOT_SEND_DEFAULT_REMOTE_NAME "origin" + +#define GOT_SEND_PKTMAX 65536 + +/* + * Attempt to open a connection to a server using the provided protocol + * scheme, hostname port number (as a string) and server-side path. + * A verbosity level can be specified; it currently controls the amount + * of -v options passed to ssh(1). If the level is -1 ssh(1) will be run + * with the -q option. + * + * If successful return an open file descriptor for the connection which can + * be passed to other functions below, and must be disposed of with close(2). + * + * If an ssh(1) process was started return its PID as well, in which case + * the caller should eventually send SIGTERM to the procress and wait for + * the process to exit with waitpid(2). Otherwise, return PID -1. + */ +const struct got_error *got_send_connect(pid_t *, int *, const char *, + const char *, const char *, const char *, int); + +/* A callback function which gets invoked with progress information to print. */ +typedef const struct got_error *(*got_send_progress_cb)(void *, + off_t packfile_size, int ncommits, int nobj_total, + int nobj_deltify, int nobj_written, off_t bytes_sent, + const char *refname, int success); + +/* + * Attempt to generate a pack file and sent it to a server. + * This pack file will contain objects which are reachable in the local + * repository via the specified branches and tags. Any objects which are + * already present in the remote repository will be omitted from the + * pack file. + * + * If the server supports deletion of references, attempt to delete + * branches on the specified delete_branches list from the server. + * Such branches are not required to exist in the local repository. + * Requesting deletion of branches results in an error if the server + * does not support this feature. + */ +const struct got_error *got_send_pack(const char *remote_name, + struct got_pathlist_head *branch_names, + struct got_pathlist_head *tag_names, + struct got_pathlist_head *delete_branches, int verbosity, + int overwrite_refs, int sendfd, struct got_repository *repo, + got_send_progress_cb progress_cb, void *progress_arg, + got_cancel_cb cancel_cb, void *cancel_arg); blob - 180adaeec95923b909b3ce9e754eb670b166792d blob + 8a84ff3488e6ae227869888581b991d1ca4203c2 --- lib/got_lib_pack_create.h +++ lib/got_lib_pack_create.h @@ -24,6 +24,6 @@ const struct got_error *got_pack_create(uint8_t *pack_sha1, FILE *packfile, struct got_object_id **theirs, int ntheirs, struct got_object_id **ours, int nours, - struct got_repository *repo, int loose_obj_only, + struct got_repository *repo, int loose_obj_only, int allow_empty, got_pack_progress_cb progress_cb, void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg); blob - 6268cadc46c849d8772530b9e2fa38833090ea59 blob + daaf7f3639f1a1257da430a23bbc5529db74d685 --- lib/got_lib_privsep.h +++ lib/got_lib_privsep.h @@ -126,6 +126,14 @@ enum got_imsg_type { GOT_IMSG_IDXPACK_OUTFD, GOT_IMSG_IDXPACK_PROGRESS, GOT_IMSG_IDXPACK_DONE, + GOT_IMSG_SEND_REQUEST, + GOT_IMSG_SEND_REF, + GOT_IMSG_SEND_REMOTE_REF, + GOT_IMSG_SEND_REF_STATUS, + GOT_IMSG_SEND_PACK_REQUEST, + GOT_IMSG_SEND_PACKFD, + GOT_IMSG_SEND_UPLOAD_PROGRESS, + GOT_IMSG_SEND_DONE, /* Messages related to pack files. */ GOT_IMSG_PACKIDX, @@ -334,7 +342,42 @@ struct got_imsg_fetch_download_progress { /* Number of packfile data bytes downloaded so far. */ off_t packfile_bytes; }; + +/* Structure for GOT_IMSG_SEND_REQUEST data. */ +struct got_imsg_send_request { + int verbosity; + size_t nrefs; + /* Followed by nrefs GOT_IMSG_SEND_REF messages. */ +} __attribute__((__packed__)); + +/* Structure for GOT_IMSG_SEND_UPLOAD_PROGRESS data. */ +struct got_imsg_send_upload_progress { + /* Number of packfile data bytes uploaded so far. */ + off_t packfile_bytes; +}; + +/* Structure for GOT_IMSG_SEND_REF data. */ +struct got_imsg_send_ref { + uint8_t id[SHA1_DIGEST_LENGTH]; + int delete; + size_t name_len; + /* Followed by name_len data bytes. */ +} __attribute__((__packed__)); +/* Structure for GOT_IMSG_SEND_REMOTE_REF data. */ +struct got_imsg_send_remote_ref { + uint8_t id[SHA1_DIGEST_LENGTH]; + size_t name_len; + /* Followed by name_len data bytes. */ +} __attribute__((__packed__)); + +/* Structure for GOT_IMSG_SEND_REF_STATUS data. */ +struct got_imsg_send_ref_status { + int success; + size_t name_len; + /* Followed by name_len data bytes. */ +} __attribute__((__packed__)); + /* Structure for GOT_IMSG_IDXPACK_REQUEST data. */ struct got_imsg_index_pack_request { uint8_t pack_hash[SHA1_DIGEST_LENGTH]; @@ -466,6 +509,13 @@ const struct got_error *got_privsep_send_fetch_outfd(s const struct got_error *got_privsep_recv_fetch_progress(int *, struct got_object_id **, char **, struct got_pathlist_head *, char **, off_t *, uint8_t *, struct imsgbuf *); +const struct got_error *got_privsep_send_send_req(struct imsgbuf *, int, + struct got_pathlist_head *, struct got_pathlist_head *, int); +const struct got_error *got_privsep_recv_send_remote_refs( + struct got_pathlist_head *, struct imsgbuf *); +const struct got_error *got_privsep_send_packfd(struct imsgbuf *, int); +const struct got_error *got_privsep_recv_send_progress(int *, off_t *, + int *, char **, struct imsgbuf *); const struct got_error *got_privsep_get_imsg_obj(struct got_object **, struct imsg *, struct imsgbuf *); const struct got_error *got_privsep_recv_obj(struct got_object **, blob - 73b754dfefc5345d57bb8ffe4d81d0b0f8859745 blob + 1455b6df378eb19d397ce2e3d3588af5a0256490 --- lib/pack_create.c +++ lib/pack_create.c @@ -1271,7 +1271,7 @@ const struct got_error * got_pack_create(uint8_t *packsha1, FILE *packfile, struct got_object_id **theirs, int ntheirs, struct got_object_id **ours, int nours, - struct got_repository *repo, int loose_obj_only, + struct got_repository *repo, int loose_obj_only, int allow_empty, got_pack_progress_cb progress_cb, void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg) { @@ -1284,15 +1284,16 @@ got_pack_create(uint8_t *packsha1, FILE *packfile, if (err) return err; - if (nmeta == 0) { + if (nmeta == 0 && !allow_empty) { err = got_error(GOT_ERR_CANNOT_PACK); goto done; } - - err = pick_deltas(meta, nmeta, nours, repo, - progress_cb, progress_arg, cancel_cb, cancel_arg); - if (err) - goto done; + if (nmeta > 0) { + err = pick_deltas(meta, nmeta, nours, repo, + progress_cb, progress_arg, cancel_cb, cancel_arg); + if (err) + goto done; + } err = genpack(packsha1, packfile, meta, nmeta, nours, 1, repo, progress_cb, progress_arg, cancel_cb, cancel_arg); blob - 274f3c3fcbe691037dcbe34aba718d4c708a4af5 blob + ecad96e05628c923f414e7613a59a1e54b82bba0 --- lib/privsep.c +++ lib/privsep.c @@ -857,12 +857,255 @@ done: *id = NULL; free(*refname); *refname = NULL; + } + imsg_free(&imsg); + return err; +} + +static const struct got_error * +send_send_ref(const char *name, size_t name_len, struct got_object_id *id, + int delete, struct imsgbuf *ibuf) +{ + const struct got_error *err = NULL; + size_t len; + struct ibuf *wbuf; + + len = sizeof(struct got_imsg_send_ref) + name_len; + wbuf = imsg_create(ibuf, GOT_IMSG_SEND_REF, 0, 0, len); + if (wbuf == NULL) + return got_error_from_errno("imsg_create SEND_REF"); + + /* Keep in sync with struct got_imsg_send_ref! */ + if (imsg_add(wbuf, id->sha1, sizeof(id->sha1)) == -1) { + err = got_error_from_errno("imsg_add SEND_REF"); + ibuf_free(wbuf); + return err; + } + if (imsg_add(wbuf, &delete, sizeof(delete)) == -1) { + err = got_error_from_errno("imsg_add SEND_REF"); + ibuf_free(wbuf); + return err; + } + if (imsg_add(wbuf, &name_len, sizeof(name_len)) == -1) { + err = got_error_from_errno("imsg_add SEND_REF"); + ibuf_free(wbuf); + return err; + } + if (imsg_add(wbuf, name, name_len) == -1) { + err = got_error_from_errno("imsg_add SEND_REF"); + ibuf_free(wbuf); + return err; + } + + wbuf->fd = -1; + imsg_close(ibuf, wbuf); + return flush_imsg(ibuf); +} + +const struct got_error * +got_privsep_send_send_req(struct imsgbuf *ibuf, int fd, + struct got_pathlist_head *have_refs, + struct got_pathlist_head *delete_refs, + int verbosity) +{ + const struct got_error *err = NULL; + struct got_pathlist_entry *pe; + struct got_imsg_send_request sendreq; + struct got_object_id zero_id; + + memset(&zero_id, 0, sizeof(zero_id)); + memset(&sendreq, 0, sizeof(sendreq)); + sendreq.verbosity = verbosity; + TAILQ_FOREACH(pe, have_refs, entry) + sendreq.nrefs++; + TAILQ_FOREACH(pe, delete_refs, entry) + sendreq.nrefs++; + if (imsg_compose(ibuf, GOT_IMSG_SEND_REQUEST, 0, 0, fd, + &sendreq, sizeof(sendreq)) == -1) { + err = got_error_from_errno( + "imsg_compose FETCH_SERVER_PROGRESS"); + goto done; + } + + err = flush_imsg(ibuf); + if (err) + goto done; + fd = -1; + + TAILQ_FOREACH(pe, have_refs, entry) { + const char *name = pe->path; + size_t name_len = pe->path_len; + struct got_object_id *id = pe->data; + err = send_send_ref(name, name_len, id, 0, ibuf); + if (err) + goto done; + } + + TAILQ_FOREACH(pe, delete_refs, entry) { + const char *name = pe->path; + size_t name_len = pe->path_len; + err = send_send_ref(name, name_len, &zero_id, 1, ibuf); + if (err) + goto done; + } +done: + if (fd != -1 && close(fd) == -1 && err == NULL) + err = got_error_from_errno("close"); + return err; + +} + +const struct got_error * +got_privsep_recv_send_remote_refs(struct got_pathlist_head *remote_refs, + struct imsgbuf *ibuf) +{ + const struct got_error *err = NULL; + struct imsg imsg; + size_t datalen; + int done = 0; + struct got_imsg_send_remote_ref iremote_ref; + struct got_object_id *id = NULL; + char *refname = NULL; + struct got_pathlist_entry *new; + + while (!done) { + err = got_privsep_recv_imsg(&imsg, ibuf, 0); + if (err) + return err; + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + switch (imsg.hdr.type) { + case GOT_IMSG_ERROR: + if (datalen < sizeof(struct got_imsg_error)) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + goto done; + } + err = recv_imsg_error(&imsg, datalen); + goto done; + case GOT_IMSG_SEND_REMOTE_REF: + if (datalen < sizeof(iremote_ref)) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + memcpy(&iremote_ref, imsg.data, sizeof(iremote_ref)); + if (datalen != sizeof(iremote_ref) + + iremote_ref.name_len) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + id = malloc(sizeof(*id)); + if (id == NULL) { + err = got_error_from_errno("malloc"); + goto done; + } + memcpy(id->sha1, iremote_ref.id, SHA1_DIGEST_LENGTH); + refname = strndup(imsg.data + sizeof(iremote_ref), + datalen - sizeof(iremote_ref)); + if (refname == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + err = got_pathlist_insert(&new, remote_refs, + refname, id); + if (err) + goto done; + if (new == NULL) { /* duplicate which wasn't inserted */ + free(id); + free(refname); + } + id = NULL; + refname = NULL; + break; + case GOT_IMSG_SEND_PACK_REQUEST: + if (datalen != 0) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + /* got-send-pack is now waiting for a pack file. */ + done = 1; + break; + default: + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } } +done: + free(id); + free(refname); imsg_free(&imsg); return err; } const struct got_error * +got_privsep_send_packfd(struct imsgbuf *ibuf, int fd) +{ + return send_fd(ibuf, GOT_IMSG_SEND_PACKFD, fd); +} + +const struct got_error * +got_privsep_recv_send_progress(int *done, off_t *bytes_sent, + int *success, char **refname, struct imsgbuf *ibuf) +{ + const struct got_error *err = NULL; + struct imsg imsg; + size_t datalen; + struct got_imsg_send_ref_status iref_status; + + /* Do not reset the current value of 'bytes_sent', it accumulates. */ + *done = 0; + *success = 0; + *refname = NULL; + + err = got_privsep_recv_imsg(&imsg, ibuf, 0); + if (err) + return err; + + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + switch (imsg.hdr.type) { + case GOT_IMSG_ERROR: + if (datalen < sizeof(struct got_imsg_error)) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + break; + } + err = recv_imsg_error(&imsg, datalen); + break; + case GOT_IMSG_SEND_UPLOAD_PROGRESS: + if (datalen < sizeof(*bytes_sent)) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + memcpy(bytes_sent, imsg.data, sizeof(*bytes_sent)); + break; + case GOT_IMSG_SEND_REF_STATUS: + if (datalen < sizeof(iref_status)) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + memcpy(&iref_status, imsg.data, sizeof(iref_status)); + if (datalen != sizeof(iref_status) + iref_status.name_len) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + *success = iref_status.success; + *refname = strndup(imsg.data + sizeof(iref_status), + iref_status.name_len); + break; + case GOT_IMSG_SEND_DONE: + if (datalen != 0) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + *done = 1; + break; + default: + err = got_error(GOT_ERR_PRIVSEP_MSG); + break; + } + + imsg_free(&imsg); + return err; +} + +const struct got_error * got_privsep_send_index_pack_req(struct imsgbuf *ibuf, uint8_t *pack_sha1, int fd) { @@ -2451,6 +2694,7 @@ got_privsep_unveil_exec_helpers(void) GOT_PATH_PROG_READ_GOTCONFIG, GOT_PATH_PROG_FETCH_PACK, GOT_PATH_PROG_INDEX_PACK, + GOT_PATH_PROG_SEND_PACK, }; size_t i; blob - deb76e2c5553e4ba47753fc3862764b88217ec6a blob + cf1fd8aad18ee23db95c5c4e6417a650c9b5b15d --- lib/repository_admin.c +++ lib/repository_admin.c @@ -198,7 +198,7 @@ got_repo_pack_objects(FILE **packfile, struct got_obje } err = got_pack_create((*pack_hash)->sha1, *packfile, theirs, ntheirs, - ours, nours, repo, loose_obj_only, progress_cb, progress_arg, + ours, nours, repo, loose_obj_only, 0, progress_cb, progress_arg, cancel_cb, cancel_arg); if (err) goto done; blob - /dev/null blob + 52f062e8ad4e3182b002248a00f2c089435712f0 (mode 644) --- /dev/null +++ lib/send.c @@ -0,0 +1,841 @@ +/* + * Copyright (c) 2018, 2019 Ori Bernstein + * Copyright (c) 2021 Stefan Sperling + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" +#include "got_reference.h" +#include "got_repository.h" +#include "got_path.h" +#include "got_cancel.h" +#include "got_worktree.h" +#include "got_object.h" +#include "got_opentemp.h" +#include "got_send.h" +#include "got_repository_admin.h" +#include "got_commit_graph.h" + +#include "got_lib_delta.h" +#include "got_lib_inflate.h" +#include "got_lib_object.h" +#include "got_lib_object_parse.h" +#include "got_lib_object_create.h" +#include "got_lib_pack.h" +#include "got_lib_sha1.h" +#include "got_lib_privsep.h" +#include "got_lib_object_cache.h" +#include "got_lib_repository.h" +#include "got_lib_pack_create.h" + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +#ifndef ssizeof +#define ssizeof(_x) ((ssize_t)(sizeof(_x))) +#endif + +#ifndef MIN +#define MIN(_a,_b) ((_a) < (_b) ? (_a) : (_b)) +#endif + +static const struct got_error * +dial_ssh(pid_t *sendpid, int *sendfd, const char *host, const char *port, + const char *path, const char *direction, int verbosity) +{ + const struct got_error *error = NULL; + int pid, pfd[2]; + char cmd[64]; + char *argv[11]; + int i = 0, j; + + *sendpid = -1; + *sendfd = -1; + + argv[i++] = GOT_SEND_PATH_SSH; + if (port != NULL) { + argv[i++] = "-p"; + argv[i++] = (char *)port; + } + if (verbosity == -1) { + argv[i++] = "-q"; + } else { + /* ssh(1) allows up to 3 "-v" options. */ + for (j = 0; j < MIN(3, verbosity); j++) + argv[i++] = "-v"; + } + argv[i++] = "--"; + argv[i++] = (char *)host; + argv[i++] = (char *)cmd; + argv[i++] = (char *)path; + argv[i++] = NULL; + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pfd) == -1) + return got_error_from_errno("socketpair"); + + pid = fork(); + if (pid == -1) { + error = got_error_from_errno("fork"); + close(pfd[0]); + close(pfd[1]); + return error; + } else if (pid == 0) { + int n; + if (close(pfd[1]) == -1) + err(1, "close"); + if (dup2(pfd[0], 0) == -1) + err(1, "dup2"); + if (dup2(pfd[0], 1) == -1) + err(1, "dup2"); + n = snprintf(cmd, sizeof(cmd), "git-%s-pack", direction); + if (n < 0 || n >= ssizeof(cmd)) + err(1, "snprintf"); + if (execv(GOT_SEND_PATH_SSH, argv) == -1) + err(1, "execv"); + abort(); /* not reached */ + } else { + if (close(pfd[0]) == -1) + return got_error_from_errno("close"); + *sendpid = pid; + *sendfd = pfd[1]; + return NULL; + } +} + +static const struct got_error * +dial_git(int *sendfd, const char *host, const char *port, const char *path, + const char *direction) +{ + const struct got_error *err = NULL; + struct addrinfo hints, *servinfo, *p; + char *cmd = NULL; + int fd = -1, len, r, eaicode; + + *sendfd = -1; + + if (port == NULL) + port = GOT_DEFAULT_GIT_PORT_STR; + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + eaicode = getaddrinfo(host, port, &hints, &servinfo); + if (eaicode) { + char msg[512]; + snprintf(msg, sizeof(msg), "%s: %s", host, + gai_strerror(eaicode)); + return got_error_msg(GOT_ERR_ADDRINFO, msg); + } + + for (p = servinfo; p != NULL; p = p->ai_next) { + if ((fd = socket(p->ai_family, p->ai_socktype, + p->ai_protocol)) == -1) + continue; + if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) { + err = NULL; + break; + } + err = got_error_from_errno("connect"); + close(fd); + } + if (p == NULL) + goto done; + + if (asprintf(&cmd, "git-%s-pack %s", direction, path) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + len = 4 + strlen(cmd) + 1 + strlen("host=") + strlen(host) + 1; + r = dprintf(fd, "%04x%s%chost=%s%c", len, cmd, '\0', host, '\0'); + if (r < 0) + err = got_error_from_errno("dprintf"); +done: + free(cmd); + if (err) { + if (fd != -1) + close(fd); + } else + *sendfd = fd; + return err; +} + +const struct got_error * +got_send_connect(pid_t *sendpid, int *sendfd, const char *proto, + const char *host, const char *port, const char *server_path, int verbosity) +{ + const struct got_error *err = NULL; + + *sendpid = -1; + *sendfd = -1; + + if (strcmp(proto, "ssh") == 0 || strcmp(proto, "git+ssh") == 0) + err = dial_ssh(sendpid, sendfd, host, port, server_path, + "receive", verbosity); + else if (strcmp(proto, "git") == 0) + err = dial_git(sendfd, host, port, server_path, "receive"); + else if (strcmp(proto, "http") == 0 || strcmp(proto, "git+http") == 0) + err = got_error_path(proto, GOT_ERR_NOT_IMPL); + else + err = got_error_path(proto, GOT_ERR_BAD_PROTO); + return err; +} + +struct pack_progress_arg { + got_send_progress_cb progress_cb; + void *progress_arg; + + off_t packfile_size; + int ncommits; + int nobj_total; + int nobj_deltify; + int nobj_written; +}; + +static const struct got_error * +pack_progress(void *arg, off_t packfile_size, int ncommits, + int nobj_total, int nobj_deltify, int nobj_written) +{ + const struct got_error *err; + struct pack_progress_arg *a = arg; + + err = a->progress_cb(a->progress_arg, packfile_size, ncommits, + nobj_total, nobj_deltify, nobj_written, 0, NULL, 0); + if (err) + return err; + + a->packfile_size = packfile_size; + a->ncommits = ncommits; + a->nobj_total = nobj_total; + a->nobj_deltify = nobj_deltify; + a->nobj_written = nobj_written; + return NULL; +} + +static const struct got_error * +insert_ref(struct got_reflist_head *refs, const char *refname, + struct got_repository *repo) +{ + const struct got_error *err; + struct got_reference *ref; + struct got_reflist_entry *new; + + err = got_ref_open(&ref, repo, refname, 0); + if (err) + return err; + + err = got_reflist_insert(&new, refs, ref, got_ref_cmp_by_name, NULL); + if (err || new == NULL /* duplicate */) + got_ref_close(ref); + + return err; +} + +static const struct got_error * +check_linear_ancestry(const char *refname, struct got_object_id *my_id, + struct got_object_id *their_id, struct got_repository *repo, + got_cancel_cb cancel_cb, void *cancel_arg) +{ + const struct got_error *err = NULL; + struct got_object_id *yca_id; + int obj_type; + + err = got_object_get_type(&obj_type, repo, their_id); + if (err) + return err; + if (obj_type != GOT_OBJ_TYPE_COMMIT) + return got_error_fmt(GOT_ERR_OBJ_TYPE, + "bad object type on server for %s", refname); + + err = got_commit_graph_find_youngest_common_ancestor(&yca_id, + my_id, their_id, repo, cancel_cb, cancel_arg); + if (err) + return err; + if (yca_id == NULL) + return got_error_fmt(GOT_ERR_SEND_ANCESTRY, "%s", refname); + + /* + * Require a straight line of history between the two commits, + * with their commit being older than my commit. + * + * Non-linear situations such as this require a rebase: + * + * (theirs) D F (mine) + * \ / + * C E + * \ / + * B (yca) + * | + * A + */ + if (got_object_id_cmp(their_id, yca_id) != 0) + err = got_error_fmt(GOT_ERR_SEND_ANCESTRY, "%s", refname); + + free(yca_id); + return err; +} + +static const struct got_error * +realloc_ids(struct got_object_id ***ids, size_t *nalloc, size_t n) +{ + struct got_object_id **new; + const size_t alloc_chunksz = 256; + + if (*nalloc >= n + alloc_chunksz) + return NULL; + + new = recallocarray(*ids, *nalloc, *nalloc + alloc_chunksz, + sizeof(struct got_object_id)); + if (new == NULL) + return got_error_from_errno("recallocarray"); + + *ids = new; + *nalloc += alloc_chunksz; + return NULL; +} + +static struct got_reference * +find_ref(struct got_reflist_head *refs, const char *refname) +{ + struct got_reflist_entry *re; + + TAILQ_FOREACH(re, refs, entry) { + if (got_path_cmp(got_ref_get_name(re->ref), refname, + strlen(got_ref_get_name(re->ref)), + strlen(refname)) == 0) { + return re->ref; + } + } + + return NULL; +} + +static struct got_pathlist_entry * +find_their_ref(struct got_pathlist_head *their_refs, const char *refname) +{ + struct got_pathlist_entry *pe; + + TAILQ_FOREACH(pe, their_refs, entry) { + const char *their_refname = pe->path; + if (got_path_cmp(their_refname, refname, + strlen(their_refname), strlen(refname)) == 0) { + return pe; + } + } + + return NULL; +} + +static const struct got_error * +get_remote_refname(char **remote_refname, const char *remote_name, + const char *refname) +{ + if (strncmp(refname, "refs/", 5) == 0) + refname += 5; + if (strncmp(refname, "heads/", 6) == 0) + refname += 6; + + if (asprintf(remote_refname, "refs/remotes/%s/%s", + remote_name, refname) == -1) + return got_error_from_errno("asprintf"); + + return NULL; +} + +static const struct got_error * +update_remote_ref(struct got_reference *my_ref, const char *remote_name, + struct got_repository *repo) +{ + const struct got_error *err, *unlock_err; + struct got_object_id *my_id; + struct got_reference *ref = NULL; + char *remote_refname = NULL; + int ref_locked = 0; + + err = got_ref_resolve(&my_id, repo, my_ref); + if (err) + return err; + + err = get_remote_refname(&remote_refname, remote_name, + got_ref_get_name(my_ref)); + if (err) + goto done; + + err = got_ref_open(&ref, repo, remote_refname, 1 /* lock */); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + goto done; + err = got_ref_alloc(&ref, remote_refname, my_id); + if (err) + goto done; + } else { + ref_locked = 1; + err = got_ref_change_ref(ref, my_id); + if (err) + goto done; + } + + err = got_ref_write(ref, repo); +done: + if (ref) { + if (ref_locked) { + unlock_err = got_ref_unlock(ref); + if (unlock_err && err == NULL) + err = unlock_err; + } + got_ref_close(ref); + } + free(my_id); + free(remote_refname); + return err; +} + +const struct got_error* +got_send_pack(const char *remote_name, struct got_pathlist_head *branch_names, + struct got_pathlist_head *tag_names, + struct got_pathlist_head *delete_branches, + int verbosity, int overwrite_refs, int sendfd, + struct got_repository *repo, got_send_progress_cb progress_cb, + void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg) +{ + int imsg_sendfds[2]; + int npackfd = -1, nsendfd = -1; + int sendstatus, done = 0; + const struct got_error *err; + struct imsgbuf sendibuf; + pid_t sendpid = -1; + struct got_reflist_head refs; + struct got_pathlist_head have_refs; + struct got_pathlist_head their_refs; + struct got_pathlist_entry *pe; + struct got_reflist_entry *re; + struct got_object_id **our_ids = NULL; + struct got_object_id **their_ids = NULL; + struct got_object_id *my_id = NULL; + int i, nours = 0, ntheirs = 0; + size_t nalloc_ours = 0, nalloc_theirs = 0; + int refs_to_send = 0; + off_t bytes_sent = 0; + struct pack_progress_arg ppa; + uint8_t packsha1[SHA1_DIGEST_LENGTH]; + FILE *packfile = NULL; + + TAILQ_INIT(&refs); + TAILQ_INIT(&have_refs); + TAILQ_INIT(&their_refs); + + TAILQ_FOREACH(pe, branch_names, entry) { + const char *branchname = pe->path; + if (strncmp(branchname, "refs/heads/", 11) != 0) { + char *s; + if (asprintf(&s, "refs/heads/%s", branchname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + err = insert_ref(&refs, s, repo); + free(s); + } else { + err = insert_ref(&refs, branchname, repo); + } + if (err) + goto done; + } + + TAILQ_FOREACH(pe, delete_branches, entry) { + const char *branchname = pe->path; + struct got_reference *ref; + if (strncmp(branchname, "refs/heads/", 11) != 0) { + err = got_error_fmt(GOT_ERR_SEND_DELETE_REF, "%s", + branchname); + goto done; + } + ref = find_ref(&refs, branchname); + if (ref) { + err = got_error_fmt(GOT_ERR_SEND_DELETE_REF, + "changes on %s will be sent to server", + branchname); + goto done; + } + } + + TAILQ_FOREACH(pe, tag_names, entry) { + const char *tagname = pe->path; + if (strncmp(tagname, "refs/tags/", 10) != 0) { + char *s; + if (asprintf(&s, "refs/tags/%s", tagname) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + err = insert_ref(&refs, s, repo); + free(s); + } else { + err = insert_ref(&refs, tagname, repo); + } + if (err) + goto done; + } + + if (TAILQ_EMPTY(&refs) && TAILQ_EMPTY(delete_branches)) { + err = got_error(GOT_ERR_SEND_EMPTY); + goto done; + } + + TAILQ_FOREACH(re, &refs, entry) { + struct got_object_id *id; + int obj_type; + + if (got_ref_is_symbolic(re->ref)) { + err = got_error_fmt(GOT_ERR_BAD_REF_TYPE, + "cannot send symbolic reference %s", + got_ref_get_name(re->ref)); + goto done; + } + + err = got_ref_resolve(&id, repo, re->ref); + if (err) + goto done; + err = got_object_get_type(&obj_type, repo, id); + free(id); + if (err) + goto done; + switch (obj_type) { + case GOT_OBJ_TYPE_COMMIT: + case GOT_OBJ_TYPE_TAG: + break; + default: + err = got_error_fmt(GOT_ERR_OBJ_TYPE, + "cannot send %s", got_ref_get_name(re->ref)); + goto done; + } + } + + packfile = got_opentemp(); + if (packfile == NULL) { + err = got_error_from_errno("got_opentemp"); + goto done; + } + + err = realloc_ids(&our_ids, &nalloc_ours, 0); + if (err) + goto done; + err = realloc_ids(&their_ids, &nalloc_ours, 0); + if (err) + goto done; + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, imsg_sendfds) == -1) { + err = got_error_from_errno("socketpair"); + goto done; + } + + sendpid = fork(); + if (sendpid == -1) { + err = got_error_from_errno("fork"); + goto done; + } else if (sendpid == 0){ + got_privsep_exec_child(imsg_sendfds, + GOT_PATH_PROG_SEND_PACK, got_repo_get_path(repo)); + } + + if (close(imsg_sendfds[1]) == -1) { + err = got_error_from_errno("close"); + goto done; + } + imsg_init(&sendibuf, imsg_sendfds[0]); + nsendfd = dup(sendfd); + if (nsendfd == -1) { + err = got_error_from_errno("dup"); + goto done; + } + + /* + * Convert reflist to pathlist since the privsep layer + * is linked into helper programs which lack reference.c. + */ + TAILQ_FOREACH(re, &refs, entry) { + struct got_object_id *id; + err = got_ref_resolve(&id, repo, re->ref); + if (err) + goto done; + err = got_pathlist_append(&have_refs, + got_ref_get_name(re->ref), id); + if (err) + goto done; + /* + * Also prepare the array of our object IDs which + * will be needed for generating a pack file. + */ + err = realloc_ids(&our_ids, &nalloc_ours, nours); + if (err) + goto done; + our_ids[nours] = id; + nours++; + } + + err = got_privsep_send_send_req(&sendibuf, nsendfd, &have_refs, + delete_branches, verbosity); + if (err) + goto done; + nsendfd = -1; + + err = got_privsep_recv_send_remote_refs(&their_refs, &sendibuf); + if (err) + goto done; + + /* + * Process references reported by the server. + * Push appropriate object IDs onto the "their IDs" array. + * This array will be used to exclude objects which already + * exist on the server from our pack file. + */ + TAILQ_FOREACH(pe, &their_refs, entry) { + const char *refname = pe->path; + struct got_object_id *their_id = pe->data; + int have_their_id; + struct got_object *obj; + struct got_reference *my_ref = NULL; + int is_tag = 0; + + /* Don't blindly trust the server to send us valid names. */ + if (!got_ref_name_is_valid(refname)) + continue; + + /* + * Find out whether this is a reference we want to upload. + * Otherwise we can still use this reference as a hint to + * avoid uploading any objects the server already has. + */ + my_ref = find_ref(&refs, refname); + if (my_ref) { + err = got_ref_resolve(&my_id, repo, my_ref); + if (err) + goto done; + if (got_object_id_cmp(my_id, their_id) == 0) { + free(my_id); + my_id = NULL; + continue; + } + refs_to_send++; + + } + + if (strncmp(refname, "refs/tags/", 10) == 0) + is_tag = 1; + + /* Prevent tags from being overwritten by default. */ + if (!overwrite_refs && my_ref && is_tag) { + err = got_error_fmt(GOT_ERR_SEND_TAG_EXISTS, + "%s", refname); + goto done; + } + + /* Check if their object exists locally. */ + err = got_object_open(&obj, repo, their_id); + if (err) { + if (err->code != GOT_ERR_NO_OBJ) + goto done; + if (!overwrite_refs && my_ref != NULL) { + err = got_error_fmt(GOT_ERR_SEND_ANCESTRY, + "%s", refname); + goto done; + } + have_their_id = 0; + } else { + got_object_close(obj); + have_their_id = 1; + } + + err = realloc_ids(&their_ids, &nalloc_theirs, ntheirs); + if (err) + goto done; + + if (have_their_id) { + /* Enforce linear ancestry if required. */ + if (!overwrite_refs && my_ref && !is_tag) { + struct got_object_id *my_id; + err = got_ref_resolve(&my_id, repo, my_ref); + if (err) + goto done; + err = check_linear_ancestry(refname, my_id, + their_id, repo, cancel_cb, cancel_arg); + free(my_id); + my_id = NULL; + if (err) + goto done; + } + /* Exclude any objects reachable via their ID. */ + their_ids[ntheirs] = got_object_id_dup(their_id); + if (their_ids[ntheirs] == NULL) { + err = got_error_from_errno("got_object_id_dup"); + goto done; + } + ntheirs++; + } else if (!is_tag) { + char *remote_refname; + struct got_reference *ref; + /* + * Exclude any objects which exist on the server + * according to a locally cached remote reference. + */ + err = get_remote_refname(&remote_refname, + remote_name, refname); + if (err) + goto done; + err = got_ref_open(&ref, repo, remote_refname, 0); + free(remote_refname); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + goto done; + } else { + err = got_ref_resolve(&their_ids[ntheirs], + repo, ref); + got_ref_close(ref); + if (err) + goto done; + ntheirs++; + } + } + } + + /* Account for any new references we are going to upload. */ + TAILQ_FOREACH(re, &refs, entry) { + if (find_their_ref(&their_refs, + got_ref_get_name(re->ref)) == NULL) + refs_to_send++; + } + + if (refs_to_send == 0) { + got_privsep_send_stop(imsg_sendfds[0]); + goto done; + } + + memset(&ppa, 0, sizeof(ppa)); + ppa.progress_cb = progress_cb; + ppa.progress_arg = progress_arg; + err = got_pack_create(packsha1, packfile, their_ids, ntheirs, + our_ids, nours, repo, 0, 1, pack_progress, &ppa, + cancel_cb, cancel_arg); + if (err) + goto done; + + if (fflush(packfile) == -1) { + err = got_error_from_errno("fflush"); + goto done; + } + + npackfd = dup(fileno(packfile)); + if (npackfd == -1) { + err = got_error_from_errno("dup"); + goto done; + } + err = got_privsep_send_packfd(&sendibuf, npackfd); + if (err != NULL) + goto done; + npackfd = -1; + + while (!done) { + int success = 0; + char *refname = NULL; + off_t bytes_sent_cur = 0; + if (cancel_cb) { + err = (*cancel_cb)(cancel_arg); + if (err) + goto done; + } + err = got_privsep_recv_send_progress(&done, &bytes_sent, + &success, &refname, &sendibuf); + if (err) + goto done; + if (refname && got_ref_name_is_valid(refname) && success && + strncmp(refname, "refs/tags/", 10) != 0) { + struct got_reference *my_ref; + /* + * The server has accepted our changes. + * Update our reference in refs/remotes/ accordingly. + */ + my_ref = find_ref(&refs, refname); + if (my_ref) { + err = update_remote_ref(my_ref, remote_name, + repo); + if (err) + goto done; + } + } + if (refname != NULL || + bytes_sent_cur != bytes_sent) { + err = progress_cb(progress_arg, ppa.packfile_size, + ppa.ncommits, ppa.nobj_total, ppa.nobj_deltify, + ppa.nobj_written, bytes_sent, + refname, success); + if (err) { + free(refname); + goto done; + } + bytes_sent_cur = bytes_sent; + } + free(refname); + } +done: + if (sendpid != -1) { + if (err) + got_privsep_send_stop(imsg_sendfds[0]); + if (waitpid(sendpid, &sendstatus, 0) == -1 && err == NULL) + err = got_error_from_errno("waitpid"); + } + if (packfile && fclose(packfile) == EOF && err == NULL) + err = got_error_from_errno("fclose"); + if (nsendfd != -1 && close(nsendfd) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (npackfd != -1 && close(npackfd) == -1 && err == NULL) + err = got_error_from_errno("close"); + + got_ref_list_free(&refs); + got_pathlist_free(&have_refs); + got_pathlist_free(&their_refs); + for (i = 0; i < nours; i++) + free(our_ids[i]); + free(our_ids); + for (i = 0; i < ntheirs; i++) + free(their_ids[i]); + free(their_ids); + free(my_id); + return err; +} blob - 1e55c9808beb7f0984445d4aae36eaddd0ceb5d4 blob + 3783b56689f6ab58fbacbd8f0f990a7154d90f61 --- libexec/Makefile +++ libexec/Makefile @@ -1,5 +1,5 @@ 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-read-gitconfig got-read-gotconfig got-send-pack .include blob - /dev/null blob + ae3ef0f8e50b2387fd389b1553182be4454aecd9 (mode 644) --- /dev/null +++ libexec/got-send-pack/Makefile @@ -0,0 +1,13 @@ +.PATH:${.CURDIR}/../../lib + +.include "../../got-version.mk" + +PROG= got-send-pack +SRCS= got-send-pack.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c + +CPPFLAGS = -I${.CURDIR}/../../include -I${.CURDIR}/../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} + +.include blob - /dev/null blob + d114153710d8ca1d1fa45597373406f7d5fbe3fd (mode 644) --- /dev/null +++ libexec/got-send-pack/got-send-pack.c @@ -0,0 +1,1038 @@ +/* + * Copyright (c) 2019 Ori Bernstein + * Copyright (c) 2021 Stefan Sperling + * + * 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 +#include +#include +#include +#include +#include + +#include "got_error.h" +#include "got_object.h" +#include "got_path.h" +#include "got_version.h" +#include "got_fetch.h" +#include "got_reference.h" + +#include "got_lib_sha1.h" +#include "got_lib_delta.h" +#include "got_lib_object.h" +#include "got_lib_object_parse.h" +#include "got_lib_privsep.h" +#include "got_lib_pack.h" + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +struct got_object *indexed; +static int chattygot; + +static const struct got_error * +readn(ssize_t *off, int fd, void *buf, size_t n) +{ + ssize_t r; + + *off = 0; + while (*off != n) { + r = read(fd, buf + *off, n - *off); + if (r == -1) + return got_error_from_errno("read"); + if (r == 0) + return NULL; + *off += r; + } + return NULL; +} + +static const struct got_error * +flushpkt(int fd) +{ + ssize_t w; + + if (chattygot > 1) + fprintf(stderr, "%s: writepkt: 0000\n", getprogname()); + + w = write(fd, "0000", 4); + if (w == -1) + return got_error_from_errno("write"); + if (w != 4) + return got_error(GOT_ERR_IO); + return NULL; +} + +/* + * Packet header contains a 4-byte hexstring which specifies the length + * of data which follows. + */ +static const struct got_error * +read_pkthdr(int *datalen, int fd) +{ + static const struct got_error *err = NULL; + char lenstr[5]; + long len; + char *e; + int n, i; + ssize_t r; + + *datalen = 0; + + err = readn(&r, fd, lenstr, 4); + if (err) + return err; + if (r == 0) { + /* implicit "0000" */ + if (chattygot > 1) + fprintf(stderr, "%s: readpkt: 0000\n", getprogname()); + return NULL; + } + if (r != 4) + return got_error_msg(GOT_ERR_BAD_PACKET, + "wrong packet header length"); + + lenstr[4] = '\0'; + for (i = 0; i < 4; i++) { + if (!isprint((unsigned char)lenstr[i])) + return got_error_msg(GOT_ERR_BAD_PACKET, + "unprintable character in packet length field"); + } + for (i = 0; i < 4; i++) { + if (!isxdigit((unsigned char)lenstr[i])) { + if (chattygot) + fprintf(stderr, "%s: bad length: '%s'\n", + getprogname(), lenstr); + return got_error_msg(GOT_ERR_BAD_PACKET, + "packet length not specified in hex"); + } + } + errno = 0; + len = strtol(lenstr, &e, 16); + if (lenstr[0] == '\0' || *e != '\0') + return got_error(GOT_ERR_BAD_PACKET); + if (errno == ERANGE && (len == LONG_MAX || len == LONG_MIN)) + return got_error_msg(GOT_ERR_BAD_PACKET, "bad packet length"); + if (len > INT_MAX || len < INT_MIN) + return got_error_msg(GOT_ERR_BAD_PACKET, "bad packet length"); + n = len; + if (n == 0) + return NULL; + if (n <= 4) + return got_error_msg(GOT_ERR_BAD_PACKET, "packet too short"); + n -= 4; + + *datalen = n; + return NULL; +} + +static const struct got_error * +readpkt(int *outlen, int fd, char *buf, int buflen) +{ + const struct got_error *err = NULL; + int datalen, i; + ssize_t n; + + err = read_pkthdr(&datalen, fd); + if (err) + return err; + + if (datalen > buflen) + return got_error(GOT_ERR_NO_SPACE); + + err = readn(&n, fd, buf, datalen); + if (err) + return err; + if (n != datalen) + return got_error_msg(GOT_ERR_BAD_PACKET, "short packet"); + + if (chattygot > 1) { + fprintf(stderr, "%s: readpkt: %zd:\t", getprogname(), n); + for (i = 0; i < n; i++) { + if (isprint(buf[i])) + fputc(buf[i], stderr); + else + fprintf(stderr, "[0x%.2x]", buf[i]); + } + fputc('\n', stderr); + } + + *outlen = n; + return NULL; +} + +static const struct got_error * +writepkt(int fd, char *buf, int nbuf) +{ + char len[5]; + int i; + ssize_t w; + + if (snprintf(len, sizeof(len), "%04x", nbuf + 4) >= sizeof(len)) + return got_error(GOT_ERR_NO_SPACE); + w = write(fd, len, 4); + if (w == -1) + return got_error_from_errno("write"); + if (w != 4) + return got_error(GOT_ERR_IO); + w = write(fd, buf, nbuf); + if (w == -1) + return got_error_from_errno("write"); + if (w != nbuf) + return got_error(GOT_ERR_IO); + if (chattygot > 1) { + fprintf(stderr, "%s: writepkt: %s:\t", getprogname(), len); + for (i = 0; i < nbuf; i++) { + if (isprint(buf[i])) + fputc(buf[i], stderr); + else + fprintf(stderr, "[0x%.2x]", buf[i]); + } + fputc('\n', stderr); + } + return NULL; +} + +static const struct got_error * +tokenize_refline(char **tokens, char *line, int len, int maxtokens) +{ + const struct got_error *err = NULL; + char *p; + size_t i, n = 0; + + for (i = 0; i < maxtokens; i++) + tokens[i] = NULL; + + for (i = 0; n < len && i < maxtokens; i++) { + while (isspace(*line)) { + line++; + n++; + } + p = line; + while (*line != '\0' && n < len && + (!isspace(*line) || i == maxtokens - 1)) { + line++; + n++; + } + tokens[i] = strndup(p, line - p); + if (tokens[i] == NULL) { + err = got_error_from_errno("strndup"); + goto done; + } + /* Skip \0 field-delimiter at end of token. */ + while (line[0] == '\0' && n < len) { + line++; + n++; + } + } + if (i <= 2) + err = got_error(GOT_ERR_NOT_REF); +done: + if (err) { + int j; + for (j = 0; j < i; j++) { + free(tokens[j]); + tokens[j] = NULL; + } + } + return err; +} + +static const struct got_error * +parse_refline(char **id_str, char **refname, char **server_capabilities, + char *line, int len) +{ + const struct got_error *err = NULL; + char *tokens[3]; + + err = tokenize_refline(tokens, line, len, nitems(tokens)); + if (err) + return err; + + if (tokens[0]) + *id_str = tokens[0]; + if (tokens[1]) + *refname = tokens[1]; + if (tokens[2]) { + char *p; + *server_capabilities = tokens[2]; + p = strrchr(*server_capabilities, '\n'); + if (p) + *p = '\0'; + } + + return NULL; +} + +#define GOT_CAPA_AGENT "agent" +#define GOT_CAPA_OFS_DELTA "ofs-delta" +#define GOT_CAPA_SIDE_BAND_64K "side-band-64k" +#define GOT_CAPA_REPORT_STATUS "report-status" +#define GOT_CAPA_DELETE_REFS "delete-refs" + +#define GOT_SIDEBAND_PACKFILE_DATA 1 +#define GOT_SIDEBAND_PROGRESS_INFO 2 +#define GOT_SIDEBAND_ERROR_INFO 3 + + +struct got_capability { + const char *key; + const char *value; +}; +static const struct got_capability got_capabilities[] = { + { GOT_CAPA_AGENT, "got/" GOT_VERSION_STR }, + { GOT_CAPA_OFS_DELTA, NULL }, +#if 0 + { GOT_CAPA_SIDE_BAND_64K, NULL }, +#endif + { GOT_CAPA_REPORT_STATUS, NULL }, + { GOT_CAPA_DELETE_REFS, NULL }, +}; + +static const struct got_error * +match_capability(char **my_capabilities, const char *capa, + const struct got_capability *mycapa) +{ + char *equalsign; + char *s; + + equalsign = strchr(capa, '='); + if (equalsign) { + if (strncmp(capa, mycapa->key, equalsign - capa) != 0) + return NULL; + } else { + if (strcmp(capa, mycapa->key) != 0) + return NULL; + } + + if (asprintf(&s, "%s %s%s%s", + *my_capabilities != NULL ? *my_capabilities : "", + mycapa->key, + mycapa->value != NULL ? "=" : "", + mycapa->value != NULL? mycapa->value : "") == -1) + return got_error_from_errno("asprintf"); + + free(*my_capabilities); + *my_capabilities = s; + return NULL; +} + +static const struct got_error * +match_capabilities(char **my_capabilities, char *server_capabilities) +{ + const struct got_error *err = NULL; + char *capa; + size_t i; + + *my_capabilities = NULL; + do { + capa = strsep(&server_capabilities, " "); + for (i = 0; capa != NULL && i < nitems(got_capabilities); i++) { + err = match_capability(my_capabilities, + capa, &got_capabilities[i]); + if (err) + goto done; + } + } while (capa); + + if (*my_capabilities == NULL) { + *my_capabilities = strdup(""); + if (*my_capabilities == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + } + + /* + * Workaround for github. + * + * Github will accept the pack but fail to update the references + * if we don't have capabilities advertised. Report-status seems + * harmless to add, so we add it. + * + * Github doesn't advertise any capabilities, so we can't check + * for compatibility. We just need to add it blindly. + */ + if (strstr(*my_capabilities, GOT_CAPA_REPORT_STATUS) == NULL) { + char *s; + if (asprintf(&s, "%s %s", *my_capabilities, + GOT_CAPA_REPORT_STATUS) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + free(*my_capabilities); + *my_capabilities = s; + } +done: + if (err) { + free(*my_capabilities); + *my_capabilities = NULL; + } + return err; +} + +static const struct got_error * +send_upload_progress(struct imsgbuf *ibuf, off_t bytes) +{ + if (imsg_compose(ibuf, GOT_IMSG_SEND_UPLOAD_PROGRESS, 0, 0, -1, + &bytes, sizeof(bytes)) == -1) + return got_error_from_errno( + "imsg_compose SEND_UPLOAD_PROGRESS"); + + return got_privsep_flush_imsg(ibuf); +} + +static const struct got_error * +send_pack_request(struct imsgbuf *ibuf) +{ + if (imsg_compose(ibuf, GOT_IMSG_SEND_PACK_REQUEST, 0, 0, -1, + NULL, 0) == -1) + return got_error_from_errno("imsg_compose SEND_PACK_REQUEST"); + return got_privsep_flush_imsg(ibuf); +} + +static const struct got_error * +send_done(struct imsgbuf *ibuf) +{ + if (imsg_compose(ibuf, GOT_IMSG_SEND_DONE, 0, 0, -1, NULL, 0) == -1) + return got_error_from_errno("imsg_compose SEND_DONE"); + return got_privsep_flush_imsg(ibuf); +} + +static const struct got_error * +recv_packfd(int *packfd, struct imsgbuf *ibuf) +{ + const struct got_error *err; + struct imsg imsg; + + *packfd = -1; + + err = got_privsep_recv_imsg(&imsg, ibuf, 0); + if (err) + return err; + + if (imsg.hdr.type == GOT_IMSG_STOP) { + err = got_error(GOT_ERR_CANCELLED); + goto done; + } + + if (imsg.hdr.type != GOT_IMSG_SEND_PACKFD) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + + if (imsg.hdr.len - IMSG_HEADER_SIZE != 0) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + goto done; + } + + *packfd = imsg.fd; +done: + imsg_free(&imsg); + return err; +} + +static const struct got_error * +send_pack_file(int sendfd, int packfd, struct imsgbuf *ibuf) +{ + const struct got_error *err; + unsigned char buf[8192]; + ssize_t r, w; + off_t wtotal = 0; + + if (lseek(packfd, 0L, SEEK_SET) == -1) + return got_error_from_errno("lseek"); + + for (;;) { + r = read(packfd, buf, sizeof(buf)); + if (r == -1) + return got_error_from_errno("read"); + if (r == 0) + break; + w = write(sendfd, buf, r); + if (w == -1) + return got_error_from_errno("write"); + if (w != r) + return got_error(GOT_ERR_IO); + wtotal += w; + err = send_upload_progress(ibuf, wtotal); + if (err) + return err; + } + + return NULL; +} + +static const struct got_error * +send_error(const char *buf, size_t len) +{ + static char msg[1024]; + size_t i; + + for (i = 0; i < len && i < sizeof(msg) - 1; i++) { + if (!isprint(buf[i])) + return got_error_msg(GOT_ERR_BAD_PACKET, + "non-printable error message received from server"); + msg[i] = buf[i]; + } + msg[i] = '\0'; + return got_error_msg(GOT_ERR_SEND_FAILED, msg); +} + +static const struct got_error * +send_their_ref(struct imsgbuf *ibuf, struct got_object_id *refid, + const char *refname) +{ + const struct got_error *err = NULL; + struct ibuf *wbuf; + size_t len, reflen = strlen(refname); + + len = sizeof(struct got_imsg_send_remote_ref) + reflen; + if (len >= MAX_IMSGSIZE - IMSG_HEADER_SIZE) + return got_error(GOT_ERR_NO_SPACE); + + wbuf = imsg_create(ibuf, GOT_IMSG_SEND_REMOTE_REF, 0, 0, len); + if (wbuf == NULL) + return got_error_from_errno("imsg_create SEND_REMOTE_REF"); + + /* Keep in sync with struct got_imsg_send_remote_ref definition! */ + if (imsg_add(wbuf, refid->sha1, SHA1_DIGEST_LENGTH) == -1) { + err = got_error_from_errno("imsg_add SEND_REMOTE_REF"); + ibuf_free(wbuf); + return err; + } + if (imsg_add(wbuf, &reflen, sizeof(reflen)) == -1) { + err = got_error_from_errno("imsg_add SEND_REMOTE_REF"); + ibuf_free(wbuf); + return err; + } + if (imsg_add(wbuf, refname, reflen) == -1) { + err = got_error_from_errno("imsg_add SEND_REMOTE_REF"); + ibuf_free(wbuf); + return err; + } + + wbuf->fd = -1; + imsg_close(ibuf, wbuf); + return got_privsep_flush_imsg(ibuf); +} + +static const struct got_error * +send_ref_status(struct imsgbuf *ibuf, const char *refname, int success, + struct got_pathlist_head *refs, struct got_pathlist_head *delete_refs) + +{ + const struct got_error *err = NULL; + struct ibuf *wbuf; + size_t len, reflen = strlen(refname); + struct got_pathlist_entry *pe; + int ref_valid = 0; + char *eol; + + eol = strchr(refname, '\n'); + if (eol == NULL) { + return got_error_msg(GOT_ERR_BAD_PACKET, + "unexpected message from server"); + } + *eol = '\0'; + + TAILQ_FOREACH(pe, refs, entry) { + if (strcmp(refname, pe->path) == 0) { + ref_valid = 1; + break; + } + } + if (!ref_valid) { + TAILQ_FOREACH(pe, delete_refs, entry) { + if (strcmp(refname, pe->path) == 0) { + ref_valid = 1; + break; + } + } + } + if (!ref_valid) { + return got_error_msg(GOT_ERR_BAD_PACKET, + "unexpected message from server"); + } + + len = sizeof(struct got_imsg_send_ref_status) + reflen; + if (len >= MAX_IMSGSIZE - IMSG_HEADER_SIZE) + return got_error(GOT_ERR_NO_SPACE); + + wbuf = imsg_create(ibuf, GOT_IMSG_SEND_REF_STATUS, + 0, 0, len); + if (wbuf == NULL) + return got_error_from_errno("imsg_create SEND_REF_STATUS"); + + /* Keep in sync with struct got_imsg_send_ref_status definition! */ + if (imsg_add(wbuf, &success, sizeof(success)) == -1) { + err = got_error_from_errno("imsg_add SEND_REF_STATUS"); + ibuf_free(wbuf); + return err; + } + if (imsg_add(wbuf, &reflen, sizeof(reflen)) == -1) { + err = got_error_from_errno("imsg_add SEND_REF_STATUS"); + ibuf_free(wbuf); + return err; + } + if (imsg_add(wbuf, refname, reflen) == -1) { + err = got_error_from_errno("imsg_add SEND_REF_STATUS"); + ibuf_free(wbuf); + return err; + } + + wbuf->fd = -1; + imsg_close(ibuf, wbuf); + return got_privsep_flush_imsg(ibuf); +} + +static const struct got_error * +describe_refchange(int *n, int *sent_my_capabilites, + const char *my_capabilities, char *buf, size_t bufsize, + const char *refname, const char *old_hashstr, const char *new_hashstr) +{ + *n = snprintf(buf, bufsize, "%s %s %s", + old_hashstr, new_hashstr, refname); + if (*n >= bufsize) + return got_error(GOT_ERR_NO_SPACE); + + /* + * We must announce our capabilities along with the first + * reference. Unfortunately, the protocol requires an embedded + * NUL as a separator between reference name and capabilities, + * which we have to deal with here. + * It also requires a linefeed for terminating packet data. + */ + if (!*sent_my_capabilites && my_capabilities != NULL) { + int m; + if (*n >= bufsize - 1) + return got_error(GOT_ERR_NO_SPACE); + m = snprintf(buf + *n + 1, /* offset after '\0' */ + bufsize - (*n + 1), "%s\n", my_capabilities); + if (*n + m >= bufsize) + return got_error(GOT_ERR_NO_SPACE); + *n += m; + *sent_my_capabilites = 1; + } else { + *n = strlcat(buf, "\n", bufsize); + if (*n >= bufsize) + return got_error(GOT_ERR_NO_SPACE); + } + + return NULL; +} + +static const struct got_error * +send_pack(int fd, struct got_pathlist_head *refs, + struct got_pathlist_head *delete_refs, struct imsgbuf *ibuf) +{ + const struct got_error *err = NULL; + char buf[GOT_FETCH_PKTMAX]; + unsigned char zero_id[SHA1_DIGEST_LENGTH] = { 0 }; + char old_hashstr[SHA1_DIGEST_STRING_LENGTH]; + char new_hashstr[SHA1_DIGEST_STRING_LENGTH]; + struct got_pathlist_head their_refs; + int is_firstpkt = 1; + int n, nsent = 0; + int packfd = -1; + char *id_str = NULL, *refname = NULL; + struct got_object_id *id = NULL; + char *server_capabilities = NULL, *my_capabilities = NULL; + struct got_pathlist_entry *pe; + int sent_my_capabilites = 0; + + TAILQ_INIT(&their_refs); + + if (TAILQ_EMPTY(refs) && TAILQ_EMPTY(delete_refs)) + return got_error(GOT_ERR_SEND_EMPTY); + + while (1) { + err = readpkt(&n, fd, buf, sizeof(buf)); + if (err) + goto done; + if (n == 0) + break; + if (n >= 4 && strncmp(buf, "ERR ", 4) == 0) { + err = send_error(&buf[4], n - 4); + goto done; + } + err = parse_refline(&id_str, &refname, &server_capabilities, + buf, n); + if (err) + goto done; + if (is_firstpkt) { + if (chattygot && server_capabilities[0] != '\0') + fprintf(stderr, "%s: server capabilities: %s\n", + getprogname(), server_capabilities); + err = match_capabilities(&my_capabilities, + server_capabilities); + if (err) + goto done; + if (chattygot) + fprintf(stderr, "%s: my capabilities:%s\n", + getprogname(), my_capabilities); + is_firstpkt = 0; + } + if (strstr(refname, "^{}")) { + if (chattygot) { + fprintf(stderr, "%s: ignoring %s\n", + getprogname(), refname); + } + continue; + } + + id = malloc(sizeof(*id)); + if (id == NULL) { + err = got_error_from_errno("malloc"); + goto done; + } + if (!got_parse_sha1_digest(id->sha1, id_str)) { + err = got_error(GOT_ERR_BAD_OBJ_ID_STR); + goto done; + } + err = send_their_ref(ibuf, id, refname); + if (err) + goto done; + + err = got_pathlist_append(&their_refs, refname, id); + if (chattygot) + fprintf(stderr, "%s: remote has %s %s\n", + getprogname(), refname, id_str); + free(id_str); + id_str = NULL; + refname = NULL; /* do not free; owned by their_refs */ + id = NULL; /* do not free; owned by their_refs */ + } + + if (!TAILQ_EMPTY(delete_refs)) { + if (my_capabilities == NULL || + strstr(my_capabilities, GOT_CAPA_DELETE_REFS) == NULL) { + err = got_error(GOT_ERR_CAPA_DELETE_REFS); + goto done; + } + } + + TAILQ_FOREACH(pe, delete_refs, entry) { + const char *refname = pe->path; + struct got_pathlist_entry *their_pe; + struct got_object_id *their_id = NULL; + + TAILQ_FOREACH(their_pe, &their_refs, entry) { + const char *their_refname = their_pe->path; + if (got_path_cmp(refname, their_refname, + strlen(refname), strlen(their_refname)) == 0) { + their_id = their_pe->data; + break; + } + } + if (their_id == NULL) { + err = got_error_fmt(GOT_ERR_NOT_REF, + "%s does not exist in remote repository", + refname); + goto done; + } + + got_sha1_digest_to_str(their_id->sha1, old_hashstr, + sizeof(old_hashstr)); + got_sha1_digest_to_str(zero_id, new_hashstr, + sizeof(new_hashstr)); + err = describe_refchange(&n, &sent_my_capabilites, + my_capabilities, buf, sizeof(buf), refname, + old_hashstr, new_hashstr); + if (err) + goto done; + err = writepkt(fd, buf, n); + if (err) + goto done; + if (chattygot) { + fprintf(stderr, "%s: deleting %s %s\n", + getprogname(), refname, old_hashstr); + } + nsent++; + } + + TAILQ_FOREACH(pe, refs, entry) { + const char *refname = pe->path; + struct got_object_id *id = pe->data; + struct got_object_id *their_id = NULL; + struct got_pathlist_entry *their_pe; + + TAILQ_FOREACH(their_pe, &their_refs, entry) { + const char *their_refname = their_pe->path; + if (got_path_cmp(refname, their_refname, + strlen(refname), strlen(their_refname)) == 0) { + their_id = their_pe->data; + break; + } + } + if (their_id) { + if (got_object_id_cmp(id, their_id) == 0) { + if (chattygot) { + fprintf(stderr, + "%s: no change for %s\n", + getprogname(), refname); + } + continue; + } + got_sha1_digest_to_str(their_id->sha1, old_hashstr, + sizeof(old_hashstr)); + } else { + got_sha1_digest_to_str(zero_id, old_hashstr, + sizeof(old_hashstr)); + } + got_sha1_digest_to_str(id->sha1, new_hashstr, + sizeof(new_hashstr)); + err = describe_refchange(&n, &sent_my_capabilites, + my_capabilities, buf, sizeof(buf), refname, + old_hashstr, new_hashstr); + if (err) + goto done; + err = writepkt(fd, buf, n); + if (err) + goto done; + if (chattygot) { + if (their_id) { + fprintf(stderr, "%s: updating %s %s -> %s\n", + getprogname(), refname, old_hashstr, + new_hashstr); + } else { + fprintf(stderr, "%s: creating %s %s\n", + getprogname(), refname, new_hashstr); + } + } + nsent++; + } + err = flushpkt(fd); + if (err) + goto done; + + err = send_pack_request(ibuf); + if (err) + goto done; + + err = recv_packfd(&packfd, ibuf); + if (err) + goto done; + + err = send_pack_file(fd, packfd, ibuf); + if (err) + goto done; + + err = readpkt(&n, fd, buf, sizeof(buf)); + if (err) + goto done; + if (n >= 4 && strncmp(buf, "ERR ", 4) == 0) { + err = send_error(&buf[4], n - 4); + goto done; + } else if (n < 10 || strncmp(buf, "unpack ok\n", 10) != 0) { + err = got_error_msg(GOT_ERR_BAD_PACKET, + "unexpected message from server"); + goto done; + } + + while (nsent > 0) { + err = readpkt(&n, fd, buf, sizeof(buf)); + if (err) + goto done; + if (n < 3) { + err = got_error_msg(GOT_ERR_BAD_PACKET, + "unexpected message from server"); + goto done; + } else if (strncmp(buf, "ok ", 3) == 0) { + err = send_ref_status(ibuf, buf + 3, 1, + refs, delete_refs); + if (err) + goto done; + } else if (strncmp(buf, "ng ", 3) == 0) { + err = send_ref_status(ibuf, buf + 3, 0, + refs, delete_refs); + if (err) + goto done; + } else { + err = got_error_msg(GOT_ERR_BAD_PACKET, + "unexpected message from server"); + goto done; + } + nsent--; + } + + err = send_done(ibuf); +done: + TAILQ_FOREACH(pe, &their_refs, entry) { + free((void *)pe->path); + free(pe->data); + } + got_pathlist_free(&their_refs); + free(id_str); + free(id); + free(refname); + free(server_capabilities); + return err; +} + +int +main(int argc, char **argv) +{ + const struct got_error *err = NULL; + int sendfd, i; + struct imsgbuf ibuf; + struct imsg imsg; + struct got_pathlist_head refs; + struct got_pathlist_head delete_refs; + struct got_pathlist_entry *pe; + struct got_imsg_send_request send_req; + struct got_imsg_send_ref href; + size_t datalen; +#if 0 + static int attached; + while (!attached) + sleep (1); +#endif + + TAILQ_INIT(&refs); + TAILQ_INIT(&delete_refs); + + 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 + if ((err = got_privsep_recv_imsg(&imsg, &ibuf, 0)) != 0) { + if (err->code == GOT_ERR_PRIVSEP_PIPE) + err = NULL; + goto done; + } + if (imsg.hdr.type == GOT_IMSG_STOP) + goto done; + if (imsg.hdr.type != GOT_IMSG_SEND_REQUEST) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + if (datalen < sizeof(send_req)) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + goto done; + } + memcpy(&send_req, imsg.data, sizeof(send_req)); + sendfd = imsg.fd; + imsg_free(&imsg); + + if (send_req.verbosity > 0) + chattygot += send_req.verbosity; + + for (i = 0; i < send_req.nrefs; i++) { + struct got_object_id *id; + char *refname; + + if ((err = got_privsep_recv_imsg(&imsg, &ibuf, 0)) != 0) { + if (err->code == GOT_ERR_PRIVSEP_PIPE) + err = NULL; + goto done; + } + if (imsg.hdr.type == GOT_IMSG_STOP) + goto done; + if (imsg.hdr.type != GOT_IMSG_SEND_REF) { + err = got_error(GOT_ERR_PRIVSEP_MSG); + goto done; + } + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + if (datalen < sizeof(href)) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + goto done; + } + memcpy(&href, imsg.data, sizeof(href)); + if (datalen - sizeof(href) < href.name_len) { + err = got_error(GOT_ERR_PRIVSEP_LEN); + goto done; + } + refname = malloc(href.name_len + 1); + if (refname == NULL) { + err = got_error_from_errno("malloc"); + goto done; + } + memcpy(refname, imsg.data + sizeof(href), href.name_len); + refname[href.name_len] = '\0'; + + /* + * Prevent sending of references that won't make any + * sense outside the local repository's context. + */ + if (strncmp(refname, "refs/got/", 9) == 0 || + strncmp(refname, "refs/remotes/", 13) == 0) { + err = got_error_fmt(GOT_ERR_SEND_BAD_REF, + "%s", refname); + goto done; + } + + id = malloc(sizeof(*id)); + if (id == NULL) { + free(refname); + err = got_error_from_errno("malloc"); + goto done; + } + memcpy(id->sha1, href.id, SHA1_DIGEST_LENGTH); + if (href.delete) + err = got_pathlist_append(&delete_refs, refname, id); + else + err = got_pathlist_append(&refs, refname, id); + if (err) { + free(refname); + free(id); + goto done; + } + + imsg_free(&imsg); + } + + err = send_pack(sendfd, &refs, &delete_refs, &ibuf); +done: + TAILQ_FOREACH(pe, &refs, entry) { + free((char *)pe->path); + free(pe->data); + } + got_pathlist_free(&refs); + TAILQ_FOREACH(pe, &delete_refs, entry) { + free((char *)pe->path); + free(pe->data); + } + got_pathlist_free(&delete_refs); + if (sendfd != -1 && close(sendfd) == -1 && err == NULL) + err = got_error_from_errno("close"); + if (err != NULL && err->code != GOT_ERR_CANCELLED) { + fprintf(stderr, "%s: %s\n", getprogname(), err->msg); + got_privsep_send_error(&ibuf, err); + } + + exit(0); +} blob - ef0efe1dbbe320c62fff14e7f2e0b94db43f38ff blob + 68314f1e77bb35025aeb5401ca14f24b773350a5 --- regress/cmdline/Makefile +++ regress/cmdline/Makefile @@ -77,6 +77,9 @@ clone: fetch: ./fetch.sh -q -r "$(GOT_TEST_ROOT)" +send: + ./send.sh -q -r "$(GOT_TEST_ROOT)" + tree: ./tree.sh -q -r "$(GOT_TEST_ROOT)" blob - /dev/null blob + 9ac2cfc3c6e7719719f8547f7f5a1de2ece46017 (mode 755) --- /dev/null +++ regress/cmdline/send.sh @@ -0,0 +1,1042 @@ +#!/bin/sh +# +# Copyright (c) 2021 Stefan Sperling +# +# 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_send_basic() { + local testroot=`test_init send_basic` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + got clone -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + cat > $testroot/repo/.git/got.conf </dev/null + tag_id=`got ref -r $testroot/repo -l | grep "^refs/tags/1.0" \ + | tr -d ' ' | cut -d: -f2` + + echo "modified alpha" > $testroot/repo/alpha + git_commit $testroot/repo -m "modified alpha" + local commit_id2=`git_show_head $testroot/repo` + + got send -q -r $testroot/repo > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id2" \ + >> $testroot/stdout.expected + echo "refs/tags/1.0: $tag_id" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/HEAD: refs/remotes/origin/master" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got send -r $testroot/repo > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo 'Connecting to "origin" 127.0.0.1' > $testroot/stdout.expected + echo "Already up-to-date" >> $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +test_send_rebase_required() { + local testroot=`test_init send_rebase_required` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + got clone -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + cat > $testroot/repo/.git/got.conf < $testroot/repo/alpha + git_commit $testroot/repo -m "modified alpha" + local commit_id2=`git_show_head $testroot/repo` + + got checkout $testroot/repo-clone $testroot/wt-clone >/dev/null + echo "modified alpha, too" > $testroot/wt-clone/alpha + (cd $testroot/wt-clone && got commit -m 'change alpha' >/dev/null) + + got send -q -r $testroot/repo > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo "got: refs/heads/master: fetch and rebase required" \ + > $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + fi + test_done "$testroot" "$ret" +} + +test_send_rebase_required_overwrite() { + local testroot=`test_init send_rebase_required_overwrite` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + got clone -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + cat > $testroot/repo/.git/got.conf < $testroot/repo/alpha + git_commit $testroot/repo -m "modified alpha" + local commit_id2=`git_show_head $testroot/repo` + + got checkout $testroot/repo-clone $testroot/wt-clone >/dev/null + echo "modified alpha, too" > $testroot/wt-clone/alpha + (cd $testroot/wt-clone && got commit -m 'change alpha' >/dev/null) + local commit_id3=`git_show_head $testroot/repo-clone` + + # non-default remote requires an explicit argument + got send -q -r $testroot/repo -f > $testroot/stdout \ + 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + echo "got: origin: remote repository not found" \ + > $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + got send -q -r $testroot/repo -f foobar > $testroot/stdout \ + 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/foobar/master: $commit_id2" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + wt_uuid=`(cd $testroot/wt-clone && got info | grep 'UUID:' | \ + cut -d ':' -f 2 | tr -d ' ')` + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/got/worktree/base-$wt_uuid: $commit_id3" \ + >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/HEAD: refs/remotes/origin/master" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +test_send_delete() { + local testroot=`test_init send_delete` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + # branch1 exists in both repositories + got branch -r $testroot/repo branch1 + + got clone -a -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + cat > $testroot/repo/.git/got.conf < $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/branch1: $commit_id" >> $testroot/stdout.expected + echo "refs/heads/branch2: $commit_id" >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected + + # Sending changes for a branch and deleting it at the same + # time is not allowed. + got send -q -r $testroot/repo -d branch1 -b branch1 \ + > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + echo -n "got: changes on refs/heads/branch1 will be sent to server" \ + > $testroot/stderr.expected + echo ": reference cannot be deleted" >> $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + got send -q -r $testroot/repo -d refs/heads/branch1 origin \ + > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + got send -q -r $testroot/repo -d refs/heads/branch2 origin \ + > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + # branchX exists in neither repository + got send -q -r $testroot/repo -d refs/heads/branchX origin \ + > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + echo -n "got-send-pack: refs/heads/branchX does not exist in remote " \ + > $testroot/stderr.expected + echo "repository: no such reference found" >> $testroot/stderr.expected + echo "got: no such reference found" >> $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + # References outside of refs/heads/ cannot be deleted with 'got send'. + got send -q -r $testroot/repo -d refs/tags/1.0 origin \ + > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + echo -n "got-send-pack: refs/heads/refs/tags/1.0 does not exist " \ + > $testroot/stderr.expected + echo "in remote repository: no such reference found" \ + >> $testroot/stderr.expected + echo "got: no such reference found" >> $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/branch1: $commit_id" >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected + echo "refs/remotes/origin/HEAD: refs/remotes/origin/master" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/branch1: $commit_id" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +test_send_clone_and_send() { + local testroot=`test_init send_clone_and_send` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + (cd $testroot/repo && git config receive.denyCurrentBranch ignore) + + got clone -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + got checkout $testroot/repo-clone $testroot/wt >/dev/null + echo "modified alpha" > $testroot/wt/alpha + (cd $testroot/wt && got commit -m "modified alpha" >/dev/null) + local commit_id2=`git_show_head $testroot/repo-clone` + + (cd $testroot/wt && got send -q > $testroot/stdout 2> $testroot/stderr) + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + wt_uuid=`(cd $testroot/wt && got info | grep 'UUID:' | \ + cut -d ':' -f 2 | tr -d ' ')` + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/got/worktree/base-$wt_uuid: $commit_id2" \ + >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/HEAD: refs/remotes/origin/master" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id2" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +test_send_tags() { + local testroot=`test_init send_tags` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + got clone -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + cat > $testroot/repo/.git/got.conf </dev/null + tag_id=`got ref -r $testroot/repo -l | grep "^refs/tags/1.0" \ + | tr -d ' ' | cut -d: -f2` + + echo "modified alpha" > $testroot/repo/alpha + git_commit $testroot/repo -m "modified alpha" + local commit_id2=`git_show_head $testroot/repo` + + got tag -r $testroot/repo -m '2.0' 2.0 >/dev/null + tag_id2=`got ref -r $testroot/repo -l | grep "^refs/tags/2.0" \ + | tr -d ' ' | cut -d: -f2` + + got send -q -r $testroot/repo -T > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id2" \ + >> $testroot/stdout.expected + echo "refs/tags/1.0: $tag_id" >> $testroot/stdout.expected + echo "refs/tags/2.0: $tag_id2" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/HEAD: refs/remotes/origin/master" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id" \ + >> $testroot/stdout.expected + echo "refs/tags/1.0: $tag_id" >> $testroot/stdout.expected + echo "refs/tags/2.0: $tag_id2" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got tag -l -r $testroot/repo-clone | grep ^tag | sort > $testroot/stdout + echo "tag 1.0 $tag_id" > $testroot/stdout.expected + echo "tag 2.0 $tag_id2" >> $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # Overwriting an existing tag 'got send -f'. + got ref -r $testroot/repo -d refs/tags/1.0 >/dev/null + got tag -r $testroot/repo -m '1.0' 1.0 >/dev/null + tag_id3=`got ref -r $testroot/repo -l | grep "^refs/tags/1.0" \ + | tr -d ' ' | cut -d: -f2` + + got send -q -r $testroot/repo -t 1.0 > $testroot/stdout \ + 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "got: refs/tags/1.0: tag already exists on server" \ + > $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + # attempting the same with -T should fail, too + got send -q -r $testroot/repo -T > $testroot/stdout \ + 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "got: refs/tags/1.0: tag already exists on server" \ + > $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + got tag -l -r $testroot/repo-clone | grep ^tag | sort > $testroot/stdout + echo "tag 1.0 $tag_id" > $testroot/stdout.expected + echo "tag 2.0 $tag_id2" >> $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # overwrite the 1.0 tag only + got send -q -r $testroot/repo -t 1.0 -f > $testroot/stdout \ + 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + got tag -l -r $testroot/repo-clone | grep ^tag | sort > $testroot/stdout + echo "tag 1.0 $tag_id3" > $testroot/stdout.expected + echo "tag 2.0 $tag_id2" >> $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +test_send_new_branch() { + local testroot=`test_init send_new_branch` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + (cd $testroot/repo && git config receive.denyCurrentBranch ignore) + + got clone -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + got branch -r $testroot/repo-clone foo >/dev/null + got checkout -b foo $testroot/repo-clone $testroot/wt >/dev/null + echo "modified alpha" > $testroot/wt/alpha + (cd $testroot/wt && got commit -m "modified alpha" >/dev/null) + local commit_id2=`git_show_branch_head $testroot/repo-clone foo` + + (cd $testroot/wt && got send -q > $testroot/stdout 2> $testroot/stderr) + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/foo: $commit_id2" >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + wt_uuid=`(cd $testroot/wt && got info | grep 'UUID:' | \ + cut -d ':' -f 2 | tr -d ' ')` + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/got/worktree/base-$wt_uuid: $commit_id2" \ + >> $testroot/stdout.expected + echo "refs/heads/foo: $commit_id2" >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id" >> $testroot/stdout.expected + echo "refs/remotes/origin/HEAD: refs/remotes/origin/master" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/foo: $commit_id2" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +test_send_all_branches() { + local testroot=`test_init send_all_branches` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + (cd $testroot/repo && git config receive.denyCurrentBranch ignore) + + got clone -q $testurl/repo $testroot/repo-clone + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + got checkout $testroot/repo-clone $testroot/wt >/dev/null + echo "modified alpha" > $testroot/wt/alpha + (cd $testroot/wt && got commit -m "modified alpha" >/dev/null) + local commit_id2=`git_show_head $testroot/repo-clone` + + got branch -r $testroot/repo-clone foo >/dev/null + (cd $testroot/wt && got update -b foo >/dev/null) + echo "modified beta on new branch foo" > $testroot/wt/beta + (cd $testroot/wt && got commit -m "modified beta" >/dev/null) + local commit_id3=`git_show_branch_head $testroot/repo-clone foo` + + got branch -r $testroot/repo-clone bar >/dev/null + (cd $testroot/wt && got update -b bar >/dev/null) + echo "modified beta again on new branch bar" > $testroot/wt/beta + (cd $testroot/wt && got commit -m "modified beta" >/dev/null) + local commit_id4=`git_show_branch_head $testroot/repo-clone bar` + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/bar: $commit_id4" >> $testroot/stdout.expected + echo "refs/heads/foo: $commit_id3" >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + + got send -a -q -r $testroot/repo-clone -b master > $testroot/stdout \ + 2> $testroot/stderr + ret="$?" + if [ "$ret" == "0" ]; then + echo "got send command succeeded unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + echo "got: -a and -b options are mutually exclusive" \ + > $testroot/stderr.expected + cmp -s $testroot/stderr $testroot/stderr.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stderr.expected $testroot/stderr + test_done "$testroot" "$ret" + return 1 + fi + + got send -a -q -r $testroot/repo-clone > $testroot/stdout \ + 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/bar: $commit_id4" >> $testroot/stdout.expected + echo "refs/heads/foo: $commit_id3" >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo-clone > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + wt_uuid=`(cd $testroot/wt && got info | grep 'UUID:' | \ + cut -d ':' -f 2 | tr -d ' ')` + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/got/worktree/base-$wt_uuid: $commit_id4" \ + >> $testroot/stdout.expected + echo "refs/heads/bar: $commit_id4" >> $testroot/stdout.expected + echo "refs/heads/foo: $commit_id3" >> $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/HEAD: refs/remotes/origin/master" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/bar: $commit_id4" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/foo: $commit_id3" \ + >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id2" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +test_send_to_empty_repo() { + local testroot=`test_init send_to_empty_repo` + local testurl=ssh://127.0.0.1/$testroot + local commit_id=`git_show_head $testroot/repo` + + got init $testroot/repo2 + + ret="$?" + if [ "$ret" != "0" ]; then + echo "got clone command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + cat > $testroot/repo/.git/got.conf < $testroot/repo/alpha + git_commit $testroot/repo -m "modified alpha" + local commit_id2=`git_show_head $testroot/repo` + + got send -q -r $testroot/repo > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo -n > $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + # XXX Workaround: We cannot give the target for HEAD to 'got init' + got ref -r $testroot/repo2 -s refs/heads/master HEAD + + got ref -l -r $testroot/repo > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + echo "refs/remotes/origin/master: $commit_id2" \ + >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got ref -l -r $testroot/repo2 > $testroot/stdout + if [ "$ret" != "0" ]; then + echo "got ref command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo "HEAD: refs/heads/master" > $testroot/stdout.expected + echo "refs/heads/master: $commit_id2" >> $testroot/stdout.expected + + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got send -r $testroot/repo > $testroot/stdout 2> $testroot/stderr + ret="$?" + if [ "$ret" != "0" ]; then + echo "got send command failed unexpectedly" >&2 + test_done "$testroot" "$ret" + return 1 + fi + + echo 'Connecting to "origin" 127.0.0.1' > $testroot/stdout.expected + echo "Already up-to-date" >> $testroot/stdout.expected + cmp -s $testroot/stdout $testroot/stdout.expected + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + + +test_parseargs "$@" +run_test test_send_basic +run_test test_send_rebase_required +run_test test_send_rebase_required_overwrite +run_test test_send_delete +run_test test_send_clone_and_send +run_test test_send_tags +run_test test_send_new_branch +run_test test_send_all_branches +run_test test_send_to_empty_repo