Download raw body.
gotd protected references
This patch adds a "protect" directive to gotd.conf which can be used
to forbid 'got send -f' on selected branches/tags.
There will always be accidents with 'got send -f' at some point in a
project's life cycle so gotd should provide safeguards against this.
ok?
-----------------------------------------------
add support for protecting references against 'got send -f' to gotd
diff e9e0377f452e9d3f600011e0714cc6c779f10bab 18abd72047de1eac9a8ced01a253a737f8ae6d81
commit - e9e0377f452e9d3f600011e0714cc6c779f10bab
commit + 18abd72047de1eac9a8ced01a253a737f8ae6d81
blob - 38e4349026233f676c860654e1ebed2cc32c13f3
blob + 02cc7ea7b999fff68dd0fd76a0015622e5c99b68
--- gotctl/gotctl.c
+++ gotctl/gotctl.c
@@ -33,6 +33,7 @@
#include "got_error.h"
#include "got_version.h"
+#include "got_path.h"
#include "got_lib_gitproto.h"
blob - 794c48b1569cbd4214a7d34d9e2e81ac05ff789c
blob + aa23d5a751490771ef96a646b52a1a40f8332ade
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -1700,6 +1700,7 @@ main(int argc, char **argv)
enum gotd_procid proc_id = PROC_GOTD;
struct event evsigint, evsigterm, evsighup, evsigusr1;
int *pack_fds = NULL, *temp_fds = NULL;
+ struct gotd_repo *repo = NULL;
log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */
@@ -1897,7 +1898,11 @@ main(int argc, char **argv)
err(1, "pledge");
#endif
apply_unveil_repo_readonly(repo_path);
- repo_write_main(title, repo_path, pack_fds, temp_fds);
+ repo = gotd_find_repo_by_path(repo_path, &gotd);
+ if (repo == NULL)
+ fatalx("no repository for path %s", repo_path);
+ repo_write_main(title, repo_path, pack_fds, temp_fds,
+ &repo->protected_refs, &repo->protected_namespaces);
/* NOTREACHED */
exit(0);
default:
blob - 42253ba60317aec22ed6f18dd8917d823ce845ed
blob + f15c866dfcd0adaa409ac969e215043f3cb67296
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
@@ -172,7 +172,60 @@ Numeric IDs are also accepted.
to
.Ar identity .
Numeric IDs are also accepted.
+.It Ic protect Brq Ar ...
+The
+.Cm protect
+directive may be used to protect existing references in a repository
+from being overwritten by potentially destructive client-side commands,
+such as
+.Cm got send -f
+and
+.Cm git push -f .
+.Pp
+To build a set of protected references, 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:
+.Pp
+.Bl -tag -width Ds
+.It Ic branch Ar name
+Protect the named branch.
+The
+.Ar name
+will be looked up in the
+.Dq refs/heads/
+reference namespace.
+.It Ic reference Ar name
+Protect the named reference.
+The reference
+.Ar name
+must be absolute, starting with
+.Dq refs/ .
+.It Ic namespace Ar namespace
+Protect all references in a given reference namespace.
+The
+.Ar namespace
+must be absolute, starting with
+.Dq refs/ .
+While new references may be created in a protected namespace, any attempts
+to modify or delete existing references from the namespace will be denied.
+.Pp
+The special 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.
.El
+.El
.Sh FILES
.Bl -tag -width Ds -compact
.It Pa /etc/gotd.conf
@@ -194,6 +247,9 @@ repository "src" {
permit rw flan_hacker
permit rw :developers
permit ro anonymous
+
+ protect branch "main"
+ protect namespace "refs/tags/"
}
# This repository can be accessed via
@@ -203,6 +259,11 @@ repository "openbsd/ports" {
permit rw :porters
permit ro anonymous
deny flan_hacker
+
+ protect {
+ branch "main"
+ namespace "refs/tags/"
+ }
}
# Use a larger request timeout value:
blob - 1f9b40a97a0e8ed68f190efc0abc407e5fbc5fde
blob + c21f06879b2872e426dcfd05ea08d1ccf1e7502c
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -85,6 +85,8 @@ struct gotd_repo {
char path[PATH_MAX];
struct gotd_access_rule_list rules;
+ struct got_pathlist_head protected_refs;
+ struct got_pathlist_head protected_namespaces;
};
TAILQ_HEAD(gotd_repolist, gotd_repo);
@@ -448,6 +450,7 @@ struct gotd_repo *gotd_find_repo_by_name(const char *,
int parse_config(const char *, enum gotd_procid, struct gotd *, int);
struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd *);
+struct gotd_repo *gotd_find_repo_by_path(const char *, struct gotd *);
/* imsg.c */
const struct got_error *gotd_imsg_flush(struct imsgbuf *);
blob - 3813dd6071aae8355cf41ade517cd479db0cd5af
blob + 86b16615f22802ae9f3a4f249265458eb1392651
--- gotd/imsg.c
+++ gotd/imsg.c
@@ -30,6 +30,7 @@
#include <unistd.h>
#include "got_error.h"
+#include "got_path.h"
#include "got_lib_poll.h"
blob - a0953584fa86b2eef4d45cc9ccb6d5823eb55f00
blob + b51f18490312837f6627991cf323dc1e5bc2d7a6
--- gotd/listen.c
+++ gotd/listen.c
@@ -34,6 +34,7 @@
#include <unistd.h>
#include "got_error.h"
+#include "got_path.h"
#include "gotd.h"
#include "log.h"
blob - b7d7e708dc1650571a862cb8f93f26798d1f1037
blob + 09cffe648d2c60f82fabdd79d7908f55dd8e9a90
--- gotd/parse.y
+++ gotd/parse.y
@@ -44,6 +44,7 @@
#include "got_error.h"
#include "got_path.h"
+#include "got_reference.h"
#include "log.h"
#include "gotd.h"
@@ -92,6 +93,9 @@ static enum gotd_procid gotd_proc_id;
static struct gotd_repo *conf_new_repo(const char *);
static void conf_new_access_rule(struct gotd_repo *,
enum gotd_access, int, char *);
+static int conf_protect_ref(struct gotd_repo *, char *);
+static int conf_protect_namespace(struct gotd_repo *, char *);
+static int conf_protect_branch(struct gotd_repo *, char *);
static enum gotd_procid gotd_proc_id;
typedef struct {
@@ -107,6 +111,7 @@ typedef struct {
%token PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY
%token RO RW CONNECTION LIMIT REQUEST TIMEOUT
+%token PROTECT NAMESPACE BRANCH REFERENCE
%token <v.string> STRING
%token <v.number> NUMBER
@@ -229,6 +234,43 @@ repository : REPOSITORY STRING {
}
;
+protect : PROTECT '{' optnl protectflags_l '}'
+ | PROTECT protectflags
+
+protectflags_l : protectflags optnl protectflags_l
+ | protectflags optnl
+ ;
+
+protectflags : REFERENCE STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_REPO_WRITE) {
+ if (conf_protect_ref(new_repo, $2)) {
+ free($2);
+ YYERROR;
+ }
+ }
+ }
+ | NAMESPACE STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_REPO_WRITE) {
+ if (conf_protect_namespace(new_repo, $2)) {
+ free($2);
+ YYERROR;
+ }
+ free($2);
+ }
+ }
+ | BRANCH STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_REPO_WRITE) {
+ if (conf_protect_branch(new_repo, $2)) {
+ free($2);
+ YYERROR;
+ }
+ }
+ }
+ ;
+
repository : REPOSITORY STRING {
struct gotd_repo *repo;
@@ -241,7 +283,8 @@ repository : REPOSITORY STRING {
}
if (gotd_proc_id == PROC_GOTD ||
- gotd_proc_id == PROC_AUTH) {
+ gotd_proc_id == PROC_AUTH ||
+ gotd_proc_id == PROC_REPO_WRITE) {
new_repo = conf_new_repo($2);
}
free($2);
@@ -251,7 +294,8 @@ repoopts1 : PATH STRING {
repoopts1 : PATH STRING {
if (gotd_proc_id == PROC_GOTD ||
- gotd_proc_id == PROC_AUTH) {
+ gotd_proc_id == PROC_AUTH ||
+ gotd_proc_id == PROC_REPO_WRITE) {
if (!got_path_is_absolute($2)) {
yyerror("%s: path %s is not absolute",
__func__, $2);
@@ -285,6 +329,7 @@ repoopts1 : PATH STRING {
GOTD_ACCESS_DENIED, 0, $2);
}
}
+ | protect
;
repoopts2 : repoopts2 repoopts1 nl
@@ -332,13 +377,17 @@ lookup(char *s)
{
/* This has to be sorted always. */
static const struct keywords keywords[] = {
+ { "branch", BRANCH },
{ "connection", CONNECTION },
{ "deny", DENY },
{ "limit", LIMIT },
{ "listen", LISTEN },
+ { "namespace", NAMESPACE },
{ "on", ON },
{ "path", PATH },
{ "permit", PERMIT },
+ { "protect", PROTECT },
+ { "reference", REFERENCE },
{ "repository", REPOSITORY },
{ "request", REQUEST },
{ "ro", RO },
@@ -811,6 +860,8 @@ conf_new_repo(const char *name)
fatalx("%s: calloc", __func__);
STAILQ_INIT(&repo->rules);
+ TAILQ_INIT(&repo->protected_refs);
+ TAILQ_INIT(&repo->protected_namespaces);
if (strlcpy(repo->name, name, sizeof(repo->name)) >=
sizeof(repo->name))
@@ -839,6 +890,82 @@ int
STAILQ_INSERT_TAIL(&repo->rules, rule, entry);
}
+static int
+conf_protect_ref(struct gotd_repo *repo, char *refname)
+{
+ const struct got_error *error;
+
+ if (!got_ref_name_is_valid(refname)) {
+ yyerror("invalid reference name: %s", refname);
+ return -1;
+ }
+
+ if (strlen(refname) < 5 || strncmp(refname, "refs/", 5) != 0) {
+ yyerror("reference name must begin with \"refs/\": %s",
+ refname);
+ return -1;
+ }
+
+ error = got_pathlist_insert(NULL, &repo->protected_refs,
+ refname, NULL);
+ if (error) {
+ yyerror("got_pathlist_insert: %s", error->msg);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+conf_protect_namespace(struct gotd_repo *repo, char *namespace)
+{
+ const struct got_error *error;
+ char *s;
+
+ got_path_strip_trailing_slashes(namespace);
+ if (!got_ref_name_is_valid(namespace)) {
+ yyerror("invalid reference namespace: %s", namespace);
+ return -1;
+ }
+
+ if (strlen(namespace) < 5 || strncmp(namespace, "refs/", 5) != 0) {
+ yyerror("reference namespace must begin with \"refs/\": %s",
+ namespace);
+ return -1;
+ }
+
+ if (asprintf(&s, "%s/", namespace) == -1) {
+ yyerror("asprintf: %s", strerror(errno));
+ return -1;
+ }
+
+ error = got_pathlist_insert(NULL, &repo->protected_namespaces,
+ s, NULL);
+ if (error) {
+ yyerror("got_pathlist_insert: %s", error->msg);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+conf_protect_branch(struct gotd_repo *repo, char *branchname)
+{
+ char *refname;
+ int ret;
+
+ if (asprintf(&refname, "refs/heads/%s", branchname) == -1) {
+ yyerror("asprintf: %s", strerror(errno));
+ return -1;
+ }
+
+ ret = conf_protect_ref(repo, refname);
+ if (ret)
+ free(refname);
+ return ret;
+}
+
int
symset(const char *nam, const char *val, int persist)
{
@@ -911,3 +1038,16 @@ gotd_find_repo_by_name(const char *repo_name, struct g
return NULL;
}
+
+struct gotd_repo *
+gotd_find_repo_by_path(const char *repo_path, struct gotd *gotd)
+{
+ struct gotd_repo *repo;
+
+ TAILQ_FOREACH(repo, &gotd->repos, entry) {
+ if (strcmp(repo->path, repo_path) == 0)
+ return repo;
+ }
+
+ return NULL;
+}
blob - a4d19987c878bbc0be5a08026bc1ba88bf3a3f11
blob + 77af3223695f08a4c263ad27a9d0d0333b0307e6
--- gotd/repo_imsg.c
+++ gotd/repo_imsg.c
@@ -30,6 +30,7 @@
#include "got_error.h"
#include "got_object.h"
+#include "got_path.h"
#include "got_lib_hash.h"
blob - 7775b1f5018ccb890406b37a3815e460b7cb8244
blob + b038586cc341ac1f637b059595471f402c3a54aa
--- gotd/repo_read.c
+++ gotd/repo_read.c
@@ -37,6 +37,7 @@
#include "got_repository.h"
#include "got_reference.h"
#include "got_repository_admin.h"
+#include "got_path.h"
#include "got_lib_delta.h"
#include "got_lib_object.h"
blob - 558b5bdb3d78eb3e3f997f05a681c61ec2f73414
blob + ca79147f555269b4bb74bde78dfbef5e5a6b1e65
--- gotd/repo_write.c
+++ gotd/repo_write.c
@@ -69,6 +69,8 @@ static struct repo_write {
int *temp_fds;
int session_fd;
struct gotd_imsgev session_iev;
+ struct got_pathlist_head *protected_refs;
+ struct got_pathlist_head *protected_namespaces;
} repo_write;
struct gotd_ref_update {
@@ -329,6 +331,22 @@ recv_ref_update(struct imsg *imsg)
}
static const struct got_error *
+protect_ref(struct got_reference *ref, const char *refname)
+{
+ size_t len = strlen(refname);
+
+ if (len < 5 || strncmp("refs/", refname, 5) != 0) {
+ return got_error_fmt(GOT_ERR_BAD_REF_NAME,
+ "reference '%s'", refname);
+ }
+
+ if (strncmp(refname, got_ref_get_name(ref), len) == 0)
+ return got_error_fmt(GOT_ERR_REF_PROTECTED, "%s", refname);
+
+ return NULL;
+}
+
+static const struct got_error *
recv_ref_update(struct imsg *imsg)
{
static const char zero_id[SHA1_DIGEST_LENGTH];
@@ -341,6 +359,7 @@ recv_ref_update(struct imsg *imsg)
struct got_object_id *id = NULL;
struct imsgbuf ibuf;
struct gotd_ref_update *ref_update = NULL;
+ struct got_pathlist_entry *pe;
log_debug("ref-update received");
@@ -397,6 +416,17 @@ recv_ref_update(struct imsg *imsg)
goto done;
if (!ref_update->ref_is_new) {
+ TAILQ_FOREACH(pe, repo_write.protected_namespaces, entry) {
+ err = protect_ref_namespace(ref, pe->path);
+ if (err)
+ goto done;
+ }
+ TAILQ_FOREACH(pe, repo_write.protected_refs, entry) {
+ err = protect_ref(ref, pe->path);
+ if (err)
+ goto done;
+ }
+
/*
* Ensure the client's idea of this update is still valid.
* At this point we can only return an error, to prevent
@@ -1437,7 +1467,9 @@ repo_write_main(const char *title, const char *repo_pa
void
repo_write_main(const char *title, const char *repo_path,
- int *pack_fds, int *temp_fds)
+ int *pack_fds, int *temp_fds,
+ struct got_pathlist_head *protected_refs,
+ struct got_pathlist_head *protected_namespaces)
{
const struct got_error *err = NULL;
struct repo_write_client *client = &repo_write_client;
@@ -1454,6 +1486,8 @@ repo_write_main(const char *title, const char *repo_pa
repo_write.temp_fds = temp_fds;
repo_write.session_fd = -1;
repo_write.session_iev.ibuf.fd = -1;
+ repo_write.protected_refs = protected_refs;
+ repo_write.protected_namespaces = protected_namespaces;
STAILQ_INIT(&repo_write_client.ref_updates);
blob - cb5ff4a606c537ef026d2f095e10c280b2ebe87b
blob + a8986215fe7c762d9e6092d4717a394f937d7578
--- gotd/repo_write.h
+++ gotd/repo_write.h
@@ -14,5 +14,6 @@ void repo_write_main(const char *, const char *, int *
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-void repo_write_main(const char *, const char *, int *, int *);
+void repo_write_main(const char *, const char *, int *, int *,
+ struct got_pathlist_head *, struct got_pathlist_head *);
void repo_write_shutdown(void);
blob - beb02307a41d585965a6d2853eb38b69adeedf87
blob + 2baef036bd213663876faf58ee76e34028149d55
--- gotsh/gotsh.c
+++ gotsh/gotsh.c
@@ -31,6 +31,7 @@
#include "got_error.h"
#include "got_serve.h"
+#include "got_path.h"
#include "gotd.h"
blob - 9722f2b79685f6dcd81c590ca357729edac35a25
blob + c2f4f1a9967a94dd15840bfbd9aae72dd498332a
--- lib/error.c
+++ lib/error.c
@@ -227,7 +227,7 @@ static const struct got_error got_errors[] = {
{ GOT_ERR_CLIENT_ID, "unknown client identifier" },
{ GOT_ERR_REPO_TEMPFILE, "no repository tempfile available" },
{ GOT_ERR_REFS_PROTECTED, "reference namespace may not be modified" },
- { GOT_ERR_REF_PROTECTED," reference may not be modified" },
+ { GOT_ERR_REF_PROTECTED, "reference may not be modified" },
{ GOT_ERR_REF_BUSY, "reference cannot be updated; please try again" },
{ GOT_ERR_COMMIT_BAD_AUTHOR, "commit author formatting would "
"make Git unhappy" },
blob - 43f08a42a8c29c7ff51d53a6167ba9fdd8ff9e37
blob + ebb50439f7901807fcf2e02d6999a6b3cc4fb0cd
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
@@ -3,7 +3,8 @@ REGRESS_TARGETS=test_repo_read test_repo_read_group \
REGRESS_TARGETS=test_repo_read test_repo_read_group \
test_repo_read_denied_user test_repo_read_denied_group \
test_repo_read_bad_user test_repo_read_bad_group \
- test_repo_write test_repo_write_empty test_request_bad
+ test_repo_write test_repo_write_empty test_request_bad \
+ test_repo_write_protected
NOOBJ=Yes
CLEANFILES=gotd.conf
@@ -134,6 +135,19 @@ prepare_test_repo: ensure_root
@$(GOTD_TRAP); $(GOTD_START_CMD)
@$(GOTD_TRAP); sleep .5
+start_gotd_rw_protected: ensure_root
+ @echo 'listen on "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
+ @echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
+ @echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
+ @echo ' path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
+ @echo ' permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
+ @echo ' protect branch "foo"' >> $(PWD)/gotd.conf
+ @echo ' protect namespace "refs/tags/"' >> $(PWD)/gotd.conf
+ @echo ' protect reference "refs/heads/main"' >> $(PWD)/gotd.conf
+ @echo "}" >> $(PWD)/gotd.conf
+ @$(GOTD_TRAP); $(GOTD_START_CMD)
+ @$(GOTD_TRAP); sleep .5
+
prepare_test_repo: ensure_root
@chown ${GOTD_USER} "${GOTD_TEST_REPO}"
@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./prepare_test_repo.sh'
@@ -189,6 +203,12 @@ test_repo_write_empty: prepare_test_repo_empty start_g
'env $(GOTD_TEST_ENV) sh ./repo_write_empty.sh'
@$(GOTD_STOP_CMD) 2>/dev/null
@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
+
+test_repo_write_protected: prepare_test_repo start_gotd_rw_protected
+ @-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
+ 'env $(GOTD_TEST_ENV) sh ./repo_write_protected.sh'
+ @$(GOTD_STOP_CMD) 2>/dev/null
+ @su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
test_request_bad: prepare_test_repo_empty start_gotd_ro
@-$(GOTD_TRAP); su -m ${GOTD_TEST_USER} -c \
blob - /dev/null
blob + 388de4405e9a94dcdb45d2fcf65e8027655ed7b7 (mode 644)
--- /dev/null
+++ regress/gotd/repo_write_protected.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+#
+# Copyright (c) 2023 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.
+
+. ../cmdline/common.sh
+. ./common.sh
+
+test_create_protected_branch() {
+ local testroot=`test_init create_protected_branch 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ (cd $testroot/wt && got branch foo) >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got branch failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ echo modified alpha > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'edit alpha') >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got commit failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+ local commit_id=`git_show_branch_head $testroot/repo-clone foo`
+
+ # Creating a new branch should succeed.
+ got send -q -r $testroot/repo-clone -b foo 2> $testroot/stderr
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ # Verify that the send operation worked fine.
+ got clone -l ${GOTD_TEST_REPO_URL} | grep foo > $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone -l failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "refs/heads/foo: $commit_id" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+
+ test_done "$testroot" $ret
+}
+
+test_modify_protected_namespace() {
+ local testroot=`test_init modify_protected_namespace`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got tag -r $testroot/repo-clone -m "1.0" 1.0 >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got tag failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ # Creating a new tag should succeed.
+ got send -q -r $testroot/repo-clone -t 1.0 2> $testroot/stderr
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got ref -r $testroot/repo-clone -d refs/tags/1.0 > /dev/null
+ got tag -r $testroot/repo-clone -m "another 1.0" 1.0 >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got tag failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ # Overwriting an existing tag should fail.
+ got send -q -f -r $testroot/repo-clone -t 1.0 2> $testroot/stderr
+ ret=$?
+ if [ $ret == 0 ]; then
+ echo "got send succeeded unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ if ! egrep -q '(gotsh|got-send-pack): refs/tags/: reference namespace may not be modified' \
+ $testroot/stderr; then
+ echo -n "error message unexpected or missing: " >&2
+ cat $testroot/stderr >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ test_done "$testroot" 0
+}
+
+test_delete_protected_ref() {
+ local testroot=`test_init delete_protected_ref`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ if got send -q -r $testroot/repo-clone -d main 2> $testroot/stderr; then
+ echo "got send succeeded unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ if ! egrep -q '(gotsh|got-send-pack): refs/heads/main: reference may not be modified' \
+ $testroot/stderr; then
+ echo -n "error message unexpected or missing: " >&2
+ cat $testroot/stderr >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ test_done "$testroot" 0
+}
+
+
+test_parseargs "$@"
+run_test test_create_protected_branch
+run_test test_modify_protected_namespace
+run_test test_delete_protected_ref
gotd protected references