Commit 6576de5182

6576de5182d9c1ce005956931d1be475343cac45

parent: a166de255d

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 21:44 UTC

control: repository delete, rename, transfer and org rename wait out a full backup

Ref #259

Layout: unified · split

internal/control/backuplock_test.go added +40
@@ -0,0 +1,40 @@
1package control
2
3import (
4 "strings"
5 "testing"
6
7 "gitbay.org/gitbay/internal/backuplock"
8 "gitbay.org/gitbay/internal/protocol"
9)
10
11func TestRepoDeleteAndRenameRefusedDuringBackup(t *testing.T) {
12 st, repo, uid := newQueueTestRepo(t)
13 owner, err := st.UserByID(uid)
14 if err != nil {
15 t.Fatal(err)
16 }
17 root := t.TempDir()
18 release, err := backuplock.Hold(root)
19 if err != nil {
20 t.Fatal(err)
21 }
22 for _, argv := range [][]string{
23 {"repo", "rename", repo.Path(), "renamed"},
24 {"repo", "delete", repo.Path(), "--yes"},
25 } {
26 c, errOut := pruneCtx(st, root, owner)
27 if code := Dispatch(c, argv); code != protocol.ExitFailure || !strings.Contains(errOut.String(), "a backup is running") {
28 t.Fatalf("%v during a backup: exit %d, %s", argv, code, errOut)
29 }
30 }
31 if got, err := st.RepoByID(repo.ID); err != nil || got.Name != repo.Name {
32 t.Fatalf("repository changed during a backup: %+v, %v", got, err)
33 }
34 release()
35
36 c, errOut := pruneCtx(st, root, owner)
37 if code := Dispatch(c, []string{"repo", "delete", repo.Path(), "--yes"}); code != protocol.ExitOK {
38 t.Fatalf("delete after the backup: exit %d, %s", code, errOut)
39 }
40}
internal/control/org.go +5
@@ -167,6 +167,11 @@ func runOrgRename(c *Ctx, args []string) int {
167167 if _, err := os.Stat(newDir); err == nil {
168168 return c.fail(protocol.ExitFailure, "repository directory %s already exists", newName)
169169 }
170 release, lockCode := holdOffBackup(c)
171 if lockCode >= 0 {
172 return lockCode
173 }
174 defer release()
170175 if err := c.Store.RenameOrg(org.ID, newName); err != nil {
171176 return c.failErr(err)
172177 }
internal/control/repo.go +28
@@ -11,6 +11,7 @@ import (
1111 "strconv"
1212 "strings"
1313
14 "gitbay.org/gitbay/internal/backuplock"
1415 "gitbay.org/gitbay/internal/gitutil"
1516 "gitbay.org/gitbay/internal/policy"
1617 "gitbay.org/gitbay/internal/protocol"
@@ -513,6 +514,11 @@ func runRepoTransfer(c *Ctx, args []string) int {
513514 if _, err := os.Stat(newDir); err == nil {
514515 return c.fail(protocol.ExitFailure, "repository directory already exists at %s/%s", newOwner, repo.Name)
515516 }
517 release, lockCode := holdOffBackup(c)
518 if lockCode >= 0 {
519 return lockCode
520 }
521 defer release()
516522 // The directory moves before the record changes: a move that fails
517523 // leaves nothing to undo, whereas the record's change into an org
518524 // folds labels and milestones into the org's rows, which a revert
@@ -557,6 +563,11 @@ func runRepoRename(c *Ctx, args []string) int {
557563 if _, err := os.Stat(newDir); err == nil {
558564 return c.fail(protocol.ExitFailure, "repository directory already exists at %s/%s", repo.OwnerName, newName)
559565 }
566 release, lockCode := holdOffBackup(c)
567 if lockCode >= 0 {
568 return lockCode
569 }
570 defer release()
560571 if err := c.Store.RenameRepo(repo.ID, newName); err != nil {
561572 return c.failErr(err)
562573 }
@@ -609,6 +620,11 @@ func runRepoDelete(c *Ctx, args []string) int {
609620// webhooks; an instance that needs to hear about it wants the audit log
610621// (#112).
611622func deleteRepo(c *Ctx, repo store.Repo) int {
623 release, lockCode := holdOffBackup(c)
624 if lockCode >= 0 {
625 return lockCode
626 }
627 defer release()
612628 // Open MRs sourced from this repo keep working (targets own the
613629 // objects) but must show that the source is gone.
614630 if err := c.Store.MarkSourceGoneForRepo(repo.ID); err != nil {
@@ -625,6 +641,18 @@ func deleteRepo(c *Ctx, repo store.Repo) int {
625641 })
626642}
627643
644// holdOffBackup keeps a full backup from starting while a repository
645// directory moves or goes, and refuses while one runs: the backup's
646// database snapshot names every repository its walk then archives
647// (#259). The caller defers the returned release.
648func holdOffBackup(c *Ctx) (func(), int) {
649 release, err := backuplock.TryShared(c.Cfg.Server.Root)
650 if err != nil {
651 return nil, c.fail(protocol.ExitFailure, "%v", err)
652 }
653 return release, -1
654}
655
628656func runAccessGrant(c *Ctx, args []string) int {
629657 if len(args) != 3 || !slices.Contains([]string{"read", "write", "admin"}, args[2]) {
630658 return c.usage()