Commit Diff


commit - cf599b8e0000b9576e1ca1015f52f574f6df0f10
commit + 84a6474d63ab0958f05fbea78767ca7ad130af32
blob - 001117e83dbf60d55247f9d00c6ae708c726055f
blob + 5fa5bf526b069dec112a85cb6096f202814858e4
--- gotsys/gotsys.conf.5
+++ gotsys/gotsys.conf.5
@@ -295,70 +295,70 @@ The special user
 can be used when public read-only access to repositories over SSH is desired.
 The anonymous user has an empty password, cannot use an SSH public key, and
 can only be granted read-only access.
-.\".It Ic protect Brq Ar ...
-.\"The
-.\".Cm protect
-.\"directive may be used to protect branches and tags in a repository
-.\"from being overwritten by potentially destructive client-side commands,
-.\"such as when
-.\".Cm got send -f
-.\"and
-.\".Cm git push -f
-.\"are used to change the history of a branch.
-.\".Pp
-.\"To build a set of protected branches and tags, multiple
-.\".Ic protect
-.\"directives may be specified per repository and
-.\"multiple
-.\".Ic protect
-.\"directive parameters may be specified within curly braces.
-.\".Pp
-.\"The available
-.\".Cm protect
-.\"parameters are as follows:
-.\".Bl -tag -width Ds
-.\".It Ic branch Ar name
-.\"Protect the named branch.
-.\"The branch may be created if it does not exist yet.
-.\"Attempts to delete the branch or change its history will be denied.
-.\".Pp
-.\"If the
-.\".Ar name
-.\"does not already begin with
-.\".Dq refs/heads/
-.\"it will be looked up in the
-.\".Dq refs/heads/
-.\"reference namespace.
-.\".It Ic branch Ic namespace Ar namespace
-.\"Protect the given reference namespace, assuming that references in
-.\"this namespace represent branches.
-.\"New branches may be created in the namespace.
-.\"Attempts to change the history of branches or delete them will be denied.
-.\".Pp
-.\"The
-.\".Ar namespace
-.\"argument must be absolute, starting with
-.\".Dq refs/ .
-.\".It Ic tag Ic namespace Ar namespace
-.\"Protect the given reference namespace, assuming that references in
-.\"this namespace represent tags.
-.\"New tags may be created in the namespace.
-.\"Attempts to change or delete existing tags will be denied.
-.\".Pp
-.\"The
-.\".Ar namespace
-.\"argument must be absolute, starting with
-.\".Dq refs/ .
-.\".El
-.\".Pp
-.\"The special reference namespaces
-.\".Dq refs/got/
-.\"and
-.\".Dq refs/remotes/
-.\"do not need to be listed in
-.\".Nm .
-.\"These namespaces are always protected and even attempts to create new
-.\"references in these namespaces will always be denied.
+.It Ic protect Brq Ar ...
+The
+.Cm protect
+directive may be used to protect branches and tags in a repository
+from being overwritten by potentially destructive client-side commands,
+such as when
+.Cm got send -f
+and
+.Cm git push -f
+are used to change the history of a branch.
+.Pp
+To build a set of protected branches and tags, multiple
+.Ic protect
+directives may be specified per repository and
+multiple
+.Ic protect
+directive parameters may be specified within curly braces.
+.Pp
+The available
+.Cm protect
+parameters are as follows:
+.Bl -tag -width Ds
+.It Ic branch Ar name
+Protect the named branch.
+The branch may be created if it does not exist yet.
+Attempts to delete the branch or change its history will be denied.
+.Pp
+If the
+.Ar name
+does not already begin with
+.Dq refs/heads/
+it will be looked up in the
+.Dq refs/heads/
+reference namespace.
+.It Ic branch Ic namespace Ar namespace
+Protect the given reference namespace, assuming that references in
+this namespace represent branches.
+New branches may be created in the namespace.
+Attempts to change the history of branches or delete them will be denied.
+.Pp
+The
+.Ar namespace
+argument must be absolute, starting with
+.Dq refs/ .
+.It Ic tag Ic namespace Ar namespace
+Protect the given reference namespace, assuming that references in
+this namespace represent tags.
+New tags may be created in the namespace.
+Attempts to change or delete existing tags will be denied.
+.Pp
+The
+.Ar namespace
+argument must be absolute, starting with
+.Dq refs/ .
+.El
+.Pp
+The special reference namespaces
+.Dq refs/got/
+and
+.Dq refs/remotes/
+do not need to be listed in
+.Nm .
+These namespaces are always protected and even attempts to create new
+references in these namespaces will always be denied.
 .\".It Ic notify Brq Ar ...
 .\"The
 .\".Ic notify
blob - 707cce7d91deba214fd9735be9c66dd0b08d64fa
blob + 18b5a178440f33fea8c8aa22d775a03b49c959f0
--- gotsys/gotsys.h
+++ gotsys/gotsys.h
@@ -99,8 +99,11 @@ struct gotsys_repo {
 	struct gotsys_access_rule_list access_rules;
 
 	struct got_pathlist_head protected_tag_namespaces;
+	size_t nprotected_tag_namespaces;
 	struct got_pathlist_head protected_branch_namespaces;
+	size_t nprotected_branch_namespaces;
 	struct got_pathlist_head protected_branches;
+	size_t nprotected_branches;
 
 	struct got_pathlist_head notification_refs;
 	struct got_pathlist_head notification_ref_namespaces;
blob - 3b62d252431acad5bdf4e042b6138ebb898c4a88
blob + f3cd5c9f1c4ab2a6c16900bf39cb72143e710b50
--- gotsys/parse.y
+++ gotsys/parse.y
@@ -1334,6 +1334,7 @@ conf_protect_tag_namespace(struct gotsys_repo *repo, c
 	if (conf_protect_ref_namespace(&new, &repo->protected_tag_namespaces,
 	    namespace) == -1)
 		return -1;
+	repo->nprotected_tag_namespaces++;
 
 	RB_FOREACH(pe, got_pathlist_head, &repo->protected_branch_namespaces) {
 		if (strcmp(pe->path, new) == 0) {
@@ -1354,6 +1355,7 @@ conf_protect_branch_namespace(struct gotsys_repo *repo
 	if (conf_protect_ref_namespace(&new,
 	    &repo->protected_branch_namespaces, namespace) == -1)
 		return -1;
+	repo->nprotected_branch_namespaces++;
 
 	RB_FOREACH(pe, got_pathlist_head, &repo->protected_tag_namespaces) {
 		if (strcmp(pe->path, new) == 0) {
@@ -1400,6 +1402,7 @@ conf_protect_branch(struct gotsys_repo *repo, char *br
 			yyerror("duplicate protect branch %s", branchname);
 		return -1;
 	}
+	repo->nprotected_branches++;
 
 	return 0;
 }
blob - d9b5ab290a0fa6d2902a648ad02a8e43a6f9e7e3
blob + 6d37103e663e08e88c395b8e01b66320b5c4bc2f
--- gotsysd/gotsysd.h
+++ gotsysd/gotsysd.h
@@ -178,6 +178,13 @@ enum gotsysd_imsg_type {
 	GOTSYSD_IMSG_SYSCONF_REPOS_DONE,
 	GOTSYSD_IMSG_SYSCONF_ACCESS_RULE,
 	GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE,
+	GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES,
+	GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM,
+	GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES,
+	GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES_ELEM,
+	GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES,
+	GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM,
+	GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE,
 	GOTSYSD_IMSG_SYSCONF_PARSE_DONE,
 
 	/* Addition of users and groups. */
@@ -404,7 +411,37 @@ struct gotsysd_imsg_sysconf_access_rule {
 
 	/* Followed by identifier_len bytes. */
 };
+
+/*
+ * Structure for sending path lists over imsg. Used with:
+ * GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES
+ * GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES
+ * GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES
+ * GOTSYSD_IMSG_SYSCONF_NOTIFY_BRANCHES
+ * GOTSYSD_IMSG_SYSCONF_NOTIFY_REF_NAMESPACES
+ */
+struct gotsysd_imsg_pathlist {
+	size_t nelem;
 
+	/* Followed by nelem path list elements. */
+};
+
+/*
+ * Structure for a path list element. Used with:
+ * GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM
+ * GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES_ELEM
+ * GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM
+ * GOTSYSD_IMSG_SYSCONF_NOTIFY_BRANCHES_ELEM
+ * GOTSYSD_IMSG_SYSCONF_NOTIFY_REF_NAMESPACES_ELEM
+ */
+struct gotsysd_imsg_pathlist_elem {
+	size_t path_len;
+	size_t data_len;
+
+	/* Followed by path_len bytes. */
+	/* Followed by data_len bytes. */
+};
+
 #ifndef GOT_LIBEXECDIR
 #define GOT_LIBEXECDIR /usr/libexec
 #endif
@@ -498,6 +535,7 @@ struct gotsys_authorized_keys_list;
 struct gotsys_repolist;
 struct gotsys_repo;
 struct gotsys_access_rule;
+struct got_pathlist_head;
 
 const struct got_error *gotsys_imsg_send_users(struct gotsysd_imsgev *,
     struct gotsys_userlist *, int, int, int);
@@ -522,6 +560,9 @@ const struct got_error *gotsys_imsg_recv_repository(st
 const struct got_error *gotsys_imsg_recv_access_rule(
     struct gotsys_access_rule **, struct imsg *, struct gotsys_userlist *,
     struct gotsys_grouplist *);
+const struct got_error *gotsys_imsg_recv_pathlist(size_t *, struct imsg *);
+const struct got_error *gotsys_imsg_recv_pathlist_elem(struct imsg *,
+    struct got_pathlist_head *);
 
 struct gotsys_uidset_element;
 struct gotsys_uidset;
blob - 45491d7892cffae8f83ce3db7fe01350bbdaa863
blob + 156666bf66fc9c6047015be78b05d579669ba07e
--- gotsysd/helpers.c
+++ gotsysd/helpers.c
@@ -579,6 +579,13 @@ dispatch_helper_child(int fd, short event, void *arg)
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_PARSE_DONE:
 			if (proc->type != GOTSYSD_IMSG_START_PROG_READ_CONF) {
 				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
@@ -1005,6 +1012,13 @@ helpers_dispatch_sysconf(int fd, short event, void *ar
 		case GOTSYSD_IMSG_SYSCONF_REPO:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
 			proc = find_proc(GOTSYSD_IMSG_START_PROG_WRITE_CONF,
 			    1);
blob - bf8eba639405b64b6389a8f311c72d6404a7710e
blob + 4cf2c6bad5710597487e40327b1c1b47a9e65ba2
--- gotsysd/libexec/gotsys-write-conf/Makefile
+++ gotsysd/libexec/gotsys-write-conf/Makefile
@@ -4,7 +4,8 @@
 
 PROG=		gotsys-write-conf
 SRCS=		gotsys-write-conf.c error.c path.c hash.c pollfd.c \
-		log.c imsg.c gotsys_conf.c gotsys_imsg.c opentemp.c
+		log.c imsg.c gotsys_conf.c gotsys_imsg.c opentemp.c \
+		reference_parse.c
 
 CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
 	-I${.CURDIR}/../../../gotsys -I${.CURDIR}/../../ -I${.CURDIR}
blob - 2aa43851a7ef5318fbc9caa2089ab0e57439948f
blob + 3224ceb6ed71f00cb8c64518e60e0a64557c78f6
--- gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
+++ gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
@@ -36,6 +36,7 @@
 #include "got_path.h"
 #include "got_opentemp.h"
 #include "got_object.h"
+#include "got_reference.h"
 
 #include "gotsysd.h"
 #include "gotsys.h"
@@ -43,6 +44,9 @@
 static struct gotsys_conf gotsysconf;
 static struct gotsys_userlist *users_cur;
 static struct gotsys_repo *repo_cur;
+static struct got_pathlist_head *protected_refs_cur;
+static size_t nprotected_refs_needed;
+static size_t nprotected_refs_received;
 static int gotd_conf_tmpfd = -1;
 static char *gotd_conf_tmppath;
 
@@ -138,6 +142,119 @@ write_access_rules(struct gotsys_access_rule_list *rul
 }
 
 static const struct got_error *
+refname_is_valid(const char *refname)
+{
+	if (strncmp(refname, "refs/", 5) != 0) {
+		return got_error_fmt( GOT_ERR_BAD_REF_NAME,
+		    "reference name must begin with \"refs/\": %s", refname);
+	}
+
+	if (!got_ref_name_is_valid(refname))
+		return got_error_path(refname, GOT_ERR_BAD_REF_NAME);
+
+	return NULL;
+}
+
+static const struct got_error *
+write_protected_refs(struct gotsys_repo *repo)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_entry *pe;
+	int ret;
+	const char *opening = "protect {";
+	const char *closing = "}";
+	char *namespace = NULL;
+
+	if (RB_EMPTY(&repo->protected_tag_namespaces) &&
+	    RB_EMPTY(&repo->protected_branch_namespaces) &&
+	    RB_EMPTY(&repo->protected_branches))
+		return NULL;
+
+	ret = dprintf(gotd_conf_tmpfd, "\t%s\n", opening);
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
+	if (ret != 2 + strlen(opening))
+		return got_error_fmt(GOT_ERR_IO, "short write to %s",
+		    gotd_conf_tmppath);
+
+	RB_FOREACH(pe, got_pathlist_head, &repo->protected_tag_namespaces) {
+		namespace = strdup(pe->path);
+		if (namespace == NULL)
+			return got_error_from_errno("strdup");
+
+		got_path_strip_trailing_slashes(namespace);
+		err = refname_is_valid(namespace);
+		if (err)
+			goto done;
+
+		ret = dprintf(gotd_conf_tmpfd, "\t\ttag namespace \"%s\"\n",
+		    namespace);
+		if (ret == -1) {
+			err = got_error_from_errno2("dprintf",
+			    gotd_conf_tmppath);
+			goto done;
+		}
+		if (ret != 19 + strlen(namespace)) {
+			err = got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    gotd_conf_tmppath);
+			goto done;
+		}
+		free(namespace);
+		namespace = NULL;
+	}
+
+	RB_FOREACH(pe, got_pathlist_head, &repo->protected_branch_namespaces) {
+		namespace = strdup(pe->path);
+		if (namespace == NULL)
+			return got_error_from_errno("strdup");
+
+		got_path_strip_trailing_slashes(namespace);
+		err = refname_is_valid(namespace);
+		if (err)
+			goto done;
+
+		ret = dprintf(gotd_conf_tmpfd, "\t\tbranch namespace \"%s\"\n",
+		    namespace);
+		if (ret == -1) {
+			err = got_error_from_errno2("dprintf",
+			    gotd_conf_tmppath);
+			goto done;
+		}
+		if (ret != 22 + strlen(namespace)) {
+			err = got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    gotd_conf_tmppath);
+			goto done;
+		}
+		free(namespace);
+		namespace = NULL;
+	}
+
+	RB_FOREACH(pe, got_pathlist_head, &repo->protected_branches) {
+		err = refname_is_valid(pe->path);
+		if (err)
+			return err;
+		ret = dprintf(gotd_conf_tmpfd, "\t\tbranch \"%s\"\n", pe->path);
+		if (ret == -1) {
+			return got_error_from_errno2("dprintf",
+			    gotd_conf_tmppath);
+		}
+		if (ret != 12 + strlen(pe->path))
+			return got_error_fmt(GOT_ERR_IO, "short write to %s",
+			    gotd_conf_tmppath);
+	}
+
+	ret = dprintf(gotd_conf_tmpfd, "\t%s\n", closing);
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
+	if (ret != 2 + strlen(closing))
+		return got_error_fmt(GOT_ERR_IO, "short write to %s",
+		    gotd_conf_tmppath);
+done:
+	free(namespace);
+	return NULL;
+}
+
+static const struct got_error *
 write_gotd_conf(void)
 {
 	const struct got_error *err = NULL;
@@ -208,6 +325,10 @@ write_gotd_conf(void)
 		}
 
 		err = write_access_rules(&repo->access_rules);
+		if (err)
+			return err;
+
+		err = write_protected_refs(repo);
 		if (err)
 			return err;
 
@@ -244,6 +365,7 @@ dispatch_event(int fd, short event, void *arg)
 	struct imsgbuf *ibuf = &iev->ibuf;
 	struct imsg imsg;
 	ssize_t n;
+	size_t npaths;
 	int shut = 0;
 	static int flush_and_exit;
 
@@ -392,8 +514,89 @@ dispatch_event(int fd, short event, void *arg)
 				    "received unexpected imsg %d while in "
 				    "state %d\n", imsg.hdr.type,
 				    writeconf_state);
+				break;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES:
+			if (repo_cur == NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS ||
+			    protected_refs_cur != NULL ||
+			    nprotected_refs_needed != 0) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
 				break;
 			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			protected_refs_cur =
+			    &repo_cur->protected_tag_namespaces;
+			nprotected_refs_needed = npaths;
+			nprotected_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES:
+			if (repo_cur == NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS ||
+			    protected_refs_cur != NULL ||
+			    nprotected_refs_needed != 0) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			protected_refs_cur =
+			    &repo_cur->protected_branch_namespaces;
+			nprotected_refs_needed = npaths;
+			nprotected_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES:
+			if (repo_cur == NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS ||
+			    protected_refs_cur != NULL ||
+			    nprotected_refs_needed != 0) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			protected_refs_cur =
+			    &repo_cur->protected_branches;
+			nprotected_refs_needed = npaths;
+			nprotected_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM:
+			if (protected_refs_cur == NULL ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS ||
+			    nprotected_refs_needed == 0 ||
+			    nprotected_refs_received >=
+			    nprotected_refs_needed) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			/* TODO: validate refname validity */
+			err = gotsys_imsg_recv_pathlist_elem(&imsg,
+			    protected_refs_cur);
+			if (err)
+				break;
+			if (++nprotected_refs_received >=
+			    nprotected_refs_needed) {
+				protected_refs_cur = NULL;
+				nprotected_refs_needed = 0;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE:
+			if (repo_cur == NULL ||
+			    nprotected_refs_needed != 0 ||
+			    writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
 			repo_cur = NULL;
 			break;
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
blob - b9d26b56d86bb129468b4a03d916c5e108fb12fb
blob + e3f8794ab608fefd10b1564764fb55cb242295fa
--- gotsysd/sysconf.c
+++ gotsysd/sysconf.c
@@ -75,6 +75,10 @@ static struct gotsysd_sysconf {
 	uid_t uid_start;
 	uid_t uid_end;
 	int have_anonymous_user;
+	struct got_pathlist_head *protected_refs_cur;
+	size_t *nprotected_refs_cur;
+	size_t nprotected_refs_needed;
+	size_t nprotected_refs_received;
 } gotsysd_sysconf;
 
 static struct gotsys_conf gotsysconf;
@@ -152,6 +156,7 @@ sysconf_dispatch_libexec(int fd, short event, void *ar
 	struct imsgbuf *ibuf = &iev->ibuf;
 	struct imsg imsg;
 	ssize_t n;
+	size_t npaths;
 	int shut = 0;
 
 	if (event & EV_READ) {
@@ -390,6 +395,104 @@ sysconf_dispatch_libexec(int fd, short event, void *ar
 				break;
 			}
 			log_debug("done receiving access rules");
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.protected_refs_cur != NULL ||
+			    gotsysd_sysconf.nprotected_refs_needed != 0 ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			gotsysd_sysconf.protected_refs_cur =
+			    &gotsysd_sysconf.repo_cur->protected_tag_namespaces;
+			gotsysd_sysconf.nprotected_refs_cur =
+			    &gotsysd_sysconf.repo_cur->nprotected_tag_namespaces;
+			gotsysd_sysconf.nprotected_refs_needed = npaths;
+			gotsysd_sysconf.nprotected_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.protected_refs_cur != NULL ||
+			    gotsysd_sysconf.nprotected_refs_needed != 0 ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			gotsysd_sysconf.protected_refs_cur =
+			    &gotsysd_sysconf.repo_cur->protected_branch_namespaces;
+			gotsysd_sysconf.nprotected_refs_cur =
+			    &gotsysd_sysconf.repo_cur->nprotected_branch_namespaces;
+			gotsysd_sysconf.nprotected_refs_needed = npaths;
+			gotsysd_sysconf.nprotected_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.protected_refs_cur != NULL ||
+			    gotsysd_sysconf.nprotected_refs_needed != 0 ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist(&npaths, &imsg);
+			if (err)
+				break;
+			gotsysd_sysconf.protected_refs_cur =
+			    &gotsysd_sysconf.repo_cur->protected_branches;
+			gotsysd_sysconf.nprotected_refs_cur =
+			    &gotsysd_sysconf.repo_cur->nprotected_branches;
+			gotsysd_sysconf.nprotected_refs_needed = npaths;
+			gotsysd_sysconf.nprotected_refs_received = 0;
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES_ELEM:
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM:
+			if (gotsysd_sysconf.protected_refs_cur == NULL ||
+			    gotsysd_sysconf.nprotected_refs_cur == NULL ||
+			    gotsysd_sysconf.nprotected_refs_needed == 0 ||
+			    gotsysd_sysconf.nprotected_refs_received >=
+			    gotsysd_sysconf.nprotected_refs_needed ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			err = gotsys_imsg_recv_pathlist_elem(&imsg,
+			    gotsysd_sysconf.protected_refs_cur);
+			if (err)
+				break;
+			if (++gotsysd_sysconf.nprotected_refs_received >=
+			    gotsysd_sysconf.nprotected_refs_needed) {
+				gotsysd_sysconf.protected_refs_cur = NULL;
+				*gotsysd_sysconf.nprotected_refs_cur =
+				    gotsysd_sysconf.nprotected_refs_received;
+				gotsysd_sysconf.nprotected_refs_needed = 0;
+				gotsysd_sysconf.nprotected_refs_received = 0;
+			}
+			break;
+		case GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE:
+			if (gotsysd_sysconf.repo_cur == NULL ||
+			    gotsysd_sysconf.nprotected_refs_needed != 0 ||
+			    gotsysd_sysconf.protected_refs_cur != NULL ||
+			    gotsysd_sysconf.state !=
+			    SYSCONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    gotsysd_sysconf.state);
+				break;
+			}
+			log_debug("done receiving protected refs");
 			gotsysd_sysconf.repo_cur = NULL;
 			break;
 		case GOTSYSD_IMSG_SYSCONF_REPOS_DONE:
blob - 28bdc76a4a5c4aadfcb1362a99a3ed0a9f5c6c71
blob + 58b5c39c8106660a557c87b8ece84046cd2dbadf
--- lib/gotsys_imsg.c
+++ lib/gotsys_imsg.c
@@ -659,6 +659,99 @@ send_access_rule(struct gotsysd_imsgev *iev,
 }
 
 static const struct got_error *
+send_pathlist_elem(struct gotsysd_imsgev *iev, const char *refname,
+    int imsg_type)
+{
+	struct gotsysd_imsg_pathlist_elem ielem;
+	struct ibuf *wbuf = NULL;
+
+	memset(&ielem, 0, sizeof(ielem));
+	ielem.path_len = strlen(refname);
+
+	wbuf = imsg_create(&iev->ibuf, imsg_type, 0, 0,
+	    sizeof(ielem) + ielem.path_len);
+	if (wbuf == NULL)
+		return got_error_from_errno_fmt("imsg_create %d", imsg_type);
+
+	if (imsg_add(wbuf, &ielem, sizeof(ielem)) == -1)
+		return got_error_from_errno_fmt("imsg_add %d", imsg_type);
+	if (imsg_add(wbuf, refname, ielem.path_len) == -1)
+		return got_error_from_errno_fmt("imsg_add %d", imsg_type);
+
+	imsg_close(&iev->ibuf, wbuf);
+	return gotsysd_imsg_flush(&iev->ibuf);
+}
+
+static const struct got_error *
+send_protected_refs(struct gotsysd_imsgev *iev, struct gotsys_repo *repo)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_entry *pe;
+	struct gotsysd_imsg_pathlist ilist;
+
+	memset(&ilist, 0, sizeof(ilist));
+
+	ilist.nelem = repo->nprotected_tag_namespaces;
+	if (ilist.nelem > 0) {
+		if (gotsysd_imsg_compose_event(iev,
+		    GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES,
+		    0, -1, &ilist, sizeof(ilist)) == -1) {
+			return got_error_from_errno("imsg compose "
+			    "PROTECTED_TAG_NAMESPACES");
+		}
+
+		RB_FOREACH(pe, got_pathlist_head,
+		    &repo->protected_tag_namespaces) {
+			err = send_pathlist_elem(iev, pe->path,
+			    GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES_ELEM);
+			if (err)
+				return err;
+		}
+	}
+
+	ilist.nelem = repo->nprotected_branch_namespaces;
+	if (ilist.nelem > 0) {
+		if (gotsysd_imsg_compose_event(iev,
+		    GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES,
+		    0, -1, &ilist, sizeof(ilist)) == -1) {
+			return got_error_from_errno("imsg compose "
+			    "PROTECTED_BRANCH_NAMESPACES");
+		}
+
+		RB_FOREACH(pe, got_pathlist_head,
+		    &repo->protected_branch_namespaces) {
+			err = send_pathlist_elem(iev, pe->path,
+			    GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCH_NAMESPACES_ELEM);
+			if (err)
+				return err;
+		}
+	}
+
+	ilist.nelem = repo->nprotected_branches;
+	if (ilist.nelem > 0) {
+		if (gotsysd_imsg_compose_event(iev,
+		    GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES,
+		    0, -1, &ilist, sizeof(ilist)) == -1) {
+			return got_error_from_errno("imsg compose "
+			    "PROTECTED_BRANCH_NAMESPACES");
+		}
+
+		RB_FOREACH(pe, got_pathlist_head, &repo->protected_branches) {
+			err = send_pathlist_elem(iev, pe->path,
+			    GOTSYSD_IMSG_SYSCONF_PROTECTED_BRANCHES_ELEM);
+			if (err)
+				return err;
+		}
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_PROTECTED_REFS_DONE, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
+
+	return NULL;
+}
+
+static const struct got_error *
 send_repo(struct gotsysd_imsgev *iev, struct gotsys_repo *repo)
 {
 	const struct got_error *err;
@@ -701,7 +794,9 @@ send_repo(struct gotsysd_imsgev *iev, struct gotsys_re
 		return got_error_from_errno("gotsysd_imsg_compose_event");
 	}
 
-	/* TODO: send protected tags and branches */
+	err = send_protected_refs(iev, repo);
+	if (err)
+		return err;
 
 	/* TODO: send notification config */
 
@@ -850,3 +945,49 @@ gotsys_imsg_recv_access_rule(struct gotsys_access_rule
 	free(identifier);
 	return err;
 }
+
+const struct got_error *
+gotsys_imsg_recv_pathlist(size_t *npaths, struct imsg *imsg)
+{
+	struct gotsysd_imsg_pathlist ilist;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(ilist))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&ilist, imsg->data, sizeof(ilist));
+
+	if (ilist.nelem == 0)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	*npaths = ilist.nelem;
+	return NULL;
+}
+
+const struct got_error *
+gotsys_imsg_recv_pathlist_elem(struct imsg *imsg,
+    struct got_pathlist_head *paths)
+{
+	const struct got_error *err = NULL;
+	struct gotsysd_imsg_pathlist_elem ielem;
+	size_t datalen;
+	char *path;
+	struct got_pathlist_entry *pe;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(ielem))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&ielem, imsg->data, sizeof(ielem));
+
+	if (datalen != sizeof(ielem) + ielem.path_len)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	path = strndup(imsg->data + sizeof(ielem), ielem.path_len);
+	if (path == NULL)
+		return got_error_from_errno("strndup");
+
+	err = got_pathlist_insert(&pe, paths, path, NULL);
+	if (err || pe == NULL)
+		free(path);
+	return err;
+}
blob - 1e68e76f694a8b64e2cac3adcdd9e8140c38e3fc
blob + 9ab2945ef4f0d7684ff35c3c3ec60abe54b149c5
--- regress/gotsysd/test_gotsysd.sh
+++ regress/gotsysd/test_gotsysd.sh
@@ -1302,6 +1302,79 @@ EOF
 	fi
 
 	echo "HEAD: refs/heads/foo" > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "gotsysd failed to apply configuration" >&2
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_protect_refs() {
+	local testroot=`test_init protect_refs 1`
+
+	got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository "foo" {
+	permit rw ${GOTSYSD_DEV_USER}
+	permit ro anonymous
+	head foo
+	protect branch foo
+	protect {
+		tag namespace "refs/tags"
+	}
+}
+EOF
+	(cd ${testroot}/wt && \
+		got commit -m "protect branch and tags" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Wait for gotsysd to apply the new configuration.
+	echo "$commit_id" > $testroot/stdout.expected
+	for i in 1 2 3 4 5; do
+		sleep 1
+		ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+			cat /var/db/gotsysd/commit > $testroot/stdout
+		if cmp -s $testroot/stdout.expected $testroot/stdout; then
+			break;
+		fi
+	done
 	cmp -s $testroot/stdout.expected $testroot/stdout
 	ret=$?
 	if [ $ret -ne 0 ]; then
@@ -1311,6 +1384,50 @@ EOF
 		return 1
 	fi
 
+	# Create a new commit on branch "foo" and send it.
+	got clone -q -i ${GOTSYSD_SSH_KEY} -b foo \
+		${GOTSYSD_DEV_USER}@${VMIP}:foo.git $testroot/foo.git
+	got checkout -q $testroot/foo.git $testroot/wt-foo > /dev/null
+	echo "tweak alpha" > $testroot/wt-foo/alpha
+	(cd $testroot/wt-foo && got commit -m 'change alpha' > /dev/null)
+	got send -q -i ${GOTSYSD_SSH_KEY} -r $testroot/foo.git -b foo
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		return 1
+	fi
+
+	# Attempt to rewrite the history of branch "foo".
+	(cd $testroot/wt-foo && got update -q -c :head:-1 > /dev/null)
+	(cd $testroot/wt-foo && got histedit -d > /dev/null)
+	got send -q -i ${GOTSYSD_SSH_KEY} -r $testroot/foo.git -b foo -f \
+		> $testroot/stdout 2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "got send succeeded unexpectedly" >&2
+		return 1
+	fi
+
+	echo -n "" > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "gotsh: refs/heads/foo: reference is protected" \
+		> $testroot/stderr.expected
+	grep '^gotsh:' $testroot/stderr > $testroot/stderr.filtered
+	cmp -s $testroot/stderr.expected $testroot/stderr.filtered
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr.filtered
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
 	test_done "$testroot" "$ret"
 }
 
@@ -1324,3 +1441,4 @@ run_test test_repo_create
 run_test test_user_anonymous
 run_test test_bad_gotsysconf
 run_test test_set_head
+run_test test_protect_refs