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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
Re: add 'got send' command
To:
gameoftrees@openbsd.org
Date:
Thu, 26 Aug 2021 09:17:42 +0200

Download raw body.

Thread
On Wed, Aug 25, 2021 at 09:10:15PM +0200, Stefan Sperling wrote:
> Some code from got-fetch-pack is duplicated in got-send-pack.
> I intend to factor out common code into a new file in the lib/ directory
> later. But my main focus for now is to have this new feature working.

Updated diff which ports two fixes I have just committed to got-fetch-pack
over to got-send-pack:

In tokenize_refline() do not read past the refline buffer if the
server doesn't terminate the refline with whitespace or \0.

Account for my_capabilities being NULL. Not really the case here since
we always announce the report-status capability to work around an issue
with Github. Added anyway for consistency with got-fetch-pack.

diff refs/heads/main refs/heads/send
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 + 107ed8f094e31e980483426f5aee481ff5d9935e
--- 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
+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" },
@@ -7353,6 +7357,498 @@ done:
 }
 
 __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;
+}
+
+__dead static void
 usage_cherrypick(void)
 {
 	fprintf(stderr, "usage: %s cherrypick commit-id\n", getprogname());
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 <ori@openbsd.org>
+ * Copyright (c) 2021 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* 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,
@@ -335,6 +343,41 @@ struct got_imsg_fetch_download_progress {
 	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,16 +1284,17 @@ 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;
 	}
+	if (nmeta > 0) {
+		err = pick_deltas(meta, nmeta, nours, repo,
+		    progress_cb, progress_arg, cancel_cb, cancel_arg);
+		if (err)
+			goto done;
+	}
 
-	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);
 	if (err)
blob - 274f3c3fcbe691037dcbe34aba718d4c708a4af5
blob + ecad96e05628c923f414e7613a59a1e54b82bba0
--- lib/privsep.c
+++ lib/privsep.c
@@ -862,7 +862,250 @@ done:
 	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 <ori@openbsd.org>
+ * Copyright (c) 2021 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+
+#include <endian.h>
+#include <errno.h>
+#include <err.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <sha1.h>
+#include <unistd.h>
+#include <zlib.h>
+#include <ctype.h>
+#include <limits.h>
+#include <imsg.h>
+#include <time.h>
+#include <uuid.h>
+#include <netdb.h>
+#include <netinet/in.h>
+
+#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 <bsd.subdir.mk>
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 <bsd.prog.mk>
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 <ori@openbsd.org>
+ * Copyright (c) 2021 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+#include <sys/time.h>
+#include <sys/stat.h>
+
+#include <stdint.h>
+#include <errno.h>
+#include <imsg.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <sha1.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <zlib.h>
+#include <err.h>
+
+#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 <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+test_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 <<EOF
+remote "origin" {
+	protocol ssh
+	server 127.0.0.1
+	repository "$testroot/repo-clone"
+}
+EOF
+	got tag -r $testroot/repo -m '1.0' 1.0 >/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 <<EOF
+remote "origin" {
+	protocol ssh
+	server 127.0.0.1
+	repository "$testroot/repo-clone"
+}
+EOF
+	echo "modified alpha" > $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 <<EOF
+remote "foobar" {
+	protocol ssh
+	server 127.0.0.1
+	repository "$testroot/repo-clone"
+}
+EOF
+	echo "modified alpha" > $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 <<EOF
+remote "origin" {
+	protocol ssh
+	server 127.0.0.1
+	repository "$testroot/repo-clone"
+}
+EOF
+	# branch2 exists only in the remote repository
+	got branch -r $testroot/repo-clone branch2
+
+	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/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 <<EOF
+remote "origin" {
+	protocol ssh
+	server 127.0.0.1
+	repository "$testroot/repo-clone"
+}
+EOF
+	got tag -r $testroot/repo -m '1.0' 1.0 >/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 <<EOF
+remote "origin" {
+	protocol ssh
+	server 127.0.0.1
+	repository "$testroot/repo2"
+}
+EOF
+	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
+
+	# 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