From: Stefan Sperling Subject: gotd protected references To: gameoftrees@openbsd.org Date: Fri, 31 Mar 2023 22:06:23 +0200 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 #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 #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 STRING %token 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 +# +# 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