From: Cas Hariri Subject: gotd: branch namespace protection and forced push To: gameoftrees@openbsd.org Date: Mon, 26 Oct 2026 12:57:51 +0100 Hi, I just installed OpenBSD, beautiful system, love it. I'm setting up gotd/gotsysd 0.129 so my git repositories behave like a ledger: a push may add commits, but nobody, including someone with a stolen key, can rewrite or delete history. Setup Every repository in gotsys.conf had:     protect {         branch namespace "refs/heads/"         tag namespace "refs/tags/"     } What I tested In a throwaway repository, from a git client:   git push origin HEAD:refs/heads/ledger-test           # accepted   git push --force origin HEAD~1:refs/heads/ledger-test # accepted   git push origin --delete ledger-test                  # refused:     refs/heads/: reference namespace is protected The deletion was refused, but the forced push rewound the branch. gotsys.conf(5) says that for a branch namespace, "attempts to change the history of branches or delete them will be denied", so I expected the second push to be refused as well. How I worked around it I added the branches by name to each protect block:         branch "master"         branch "main"         branch "ledger-test" With that, the same test gives:   git push origin HEAD:refs/heads/ledger-test           # accepted   git push --force origin HEAD~1:refs/heads/ledger-test # refused:     refs/heads/ledger-test: reference is protected Question Is this expected behaviour, or a bug? In case it helps: the rewind sends an empty pack, so I think it goes through protect_refs_from_moving() in gotd/repo_write.c. There the namespace check is     if (strcmp(pe->path, refname) != 0)         continue; which never matches "refs/heads/" against "refs/heads/ledger-test". The other path, protect_branch_namespace(), uses a prefix match. If it is a bug, maybe this: --- gotd/repo_write.c +++ gotd/repo_write.c @@ -1565,7 +1565,7 @@          RB_FOREACH(pe, got_pathlist_head,              &repo_write.protected_branch_namespaces) { -            if (strcmp(pe->path, refname) != 0) +            if (strncmp(pe->path, refname, strlen(pe->path)) != 0)                  continue;              err = protect_require_yca(&ref_update->new_id, 0, Thanks, Cas