Commit db5915fa14
Verified · cmc
Layout: unified · split
cmd/gitbay/main.go +1
| @@ -310,6 +310,7 @@ func orgCmd() *cobra.Command { | |||
| 310 | pass("create", "create an organization", passOpts{server: []string{"org", "create"}}), | 310 | pass("create", "create an organization", passOpts{server: []string{"org", "create"}}), |
| 311 | pass("list", "list organizations you belong to", passOpts{server: []string{"org", "list"}}), | 311 | pass("list", "list organizations you belong to", passOpts{server: []string{"org", "list"}}), |
| 312 | pass("show", "show an organization and its members", passOpts{server: []string{"org", "show"}}), | 312 | pass("show", "show an organization and its members", passOpts{server: []string{"org", "show"}}), |
| 313 | pass("rename", "rename an organization: <old> <new>", passOpts{server: []string{"org", "rename"}}), | ||
| 313 | pass("delete", "delete an empty organization (--yes)", passOpts{server: []string{"org", "delete"}}), | 314 | pass("delete", "delete an empty organization (--yes)", passOpts{server: []string{"org", "delete"}}), |
| 314 | group("members", "manage members", | 315 | group("members", "manage members", |
| 315 | pass("add", "add or update a member: <org> <user> [--role member|admin]", passOpts{server: []string{"org", "members", "add"}}), | 316 | pass("add", "add or update a member: <org> <user> [--role member|admin]", passOpts{server: []string{"org", "members", "add"}}), |
e2e/org_test.go +24
| @@ -113,6 +113,30 @@ func TestOrganizations(t *testing.T) { | |||
| 113 | t.Fatalf("org delete: %s", errOut) | 113 | t.Fatalf("org delete: %s", errOut) |
| 114 | } | 114 | } |
| 115 | 115 | ||
| 116 | // Rename: clone works at the new path, old path is gone, collisions | ||
| 117 | // with users and existing orgs are refused. | ||
| 118 | if _, _, code = inst.ssh(t, aliceKey, "", "org", "create", "oldname"); code != 0 { | ||
| 119 | t.Fatal("org create oldname failed") | ||
| 120 | } | ||
| 121 | if _, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "oldname/thing"); code != 0 { | ||
| 122 | t.Fatal("repo under oldname failed") | ||
| 123 | } | ||
| 124 | rnWork := t.TempDir() | ||
| 125 | mustGit(t, rnWork, inst.gitEnv(aliceKey), "clone", inst.sshURL("oldname/thing"), "w1") | ||
| 126 | if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "bob"); code != 2 || !strings.Contains(errOut, "taken") { | ||
| 127 | t.Fatalf("rename onto user name: %d %s", code, errOut) | ||
| 128 | } | ||
| 129 | if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "newname"); code != 0 { | ||
| 130 | t.Fatalf("rename: %s", errOut) | ||
| 131 | } | ||
| 132 | mustGit(t, rnWork, inst.gitEnv(aliceKey), "clone", inst.sshURL("newname/thing"), "w2") | ||
| 133 | if out, code := gitRun(t, t.TempDir(), inst.gitEnv(aliceKey), "clone", inst.sshURL("oldname/thing")); code == 0 { | ||
| 134 | t.Fatalf("old org path still clones:\n%s", out) | ||
| 135 | } | ||
| 136 | if out, _, _ := inst.ssh(t, aliceKey, "", "repo", "list"); !strings.Contains(out, "newname/thing") { | ||
| 137 | t.Fatalf("renamed org missing from repo list:\n%s", out) | ||
| 138 | } | ||
| 139 | |||
| 116 | // Public org repos appear on the anonymous web index. | 140 | // Public org repos appear on the anonymous web index. |
| 117 | if _, _, code = inst.ssh(t, aliceKey, "", "org", "create", "puborg"); code != 0 { | 141 | if _, _, code = inst.ssh(t, aliceKey, "", "org", "create", "puborg"); code != 0 { |
| 118 | t.Fatal("org create failed") | 142 | t.Fatal("org create failed") |
internal/control/org.go +37
| @@ -4,6 +4,8 @@ import ( | |||
| 4 | "errors" | 4 | "errors" |
| 5 | "fmt" | 5 | "fmt" |
| 6 | "io" | 6 | "io" |
| 7 | "os" | ||
| 8 | "path/filepath" | ||
| 7 | 9 | ||
| 8 | "gitbay.org/gitbay/internal/policy" | 10 | "gitbay.org/gitbay/internal/policy" |
| 9 | "gitbay.org/gitbay/internal/protocol" | 11 | "gitbay.org/gitbay/internal/protocol" |
| @@ -17,6 +19,8 @@ func init() { | |||
| 17 | Summary: "list organizations you belong to", ReadOnly: true, Run: runOrgList}) | 19 | Summary: "list organizations you belong to", ReadOnly: true, Run: runOrgList}) |
| 18 | register(Command{Path: []string{"org", "show"}, | 20 | register(Command{Path: []string{"org", "show"}, |
| 19 | Summary: "show an organization and its members: org show <name>", ReadOnly: true, Run: runOrgShow}) | 21 | Summary: "show an organization and its members: org show <name>", ReadOnly: true, Run: runOrgShow}) |
| 22 | register(Command{Path: []string{"org", "rename"}, | ||
| 23 | Summary: "rename an organization: org rename <old> <new> (clone URLs change)", Run: runOrgRename}) | ||
| 20 | register(Command{Path: []string{"org", "delete"}, | 24 | register(Command{Path: []string{"org", "delete"}, |
| 21 | Summary: "delete an empty organization: org delete <name> --yes", Run: runOrgDelete}) | 25 | Summary: "delete an empty organization: org delete <name> --yes", Run: runOrgDelete}) |
| 22 | register(Command{Path: []string{"org", "members", "add"}, | 26 | register(Command{Path: []string{"org", "members", "add"}, |
| @@ -116,6 +120,39 @@ func runOrgShow(c *Ctx, args []string) int { | |||
| 116 | }) | 120 | }) |
| 117 | } | 121 | } |
| 118 | 122 | ||
| 123 | func runOrgRename(c *Ctx, args []string) int { | ||
| 124 | if len(args) != 2 { | ||
| 125 | return c.fail(protocol.ExitUsage, "usage: org rename <old> <new>") | ||
| 126 | } | ||
| 127 | org, code := orgAdmin(c, args[0]) | ||
| 128 | if code >= 0 { | ||
| 129 | return code | ||
| 130 | } | ||
| 131 | newName := args[1] | ||
| 132 | if err := policy.ValidateOwnerName(newName); err != nil { | ||
| 133 | return c.fail(protocol.ExitUsage, "%v", err) | ||
| 134 | } | ||
| 135 | oldDir := filepath.Join(c.Cfg.Server.Root, "repos", org.Name) | ||
| 136 | newDir := filepath.Join(c.Cfg.Server.Root, "repos", newName) | ||
| 137 | if _, err := os.Stat(newDir); err == nil { | ||
| 138 | return c.fail(protocol.ExitFailure, "repository directory %s already exists", newName) | ||
| 139 | } | ||
| 140 | if err := c.Store.RenameOrg(org.ID, newName); err != nil { | ||
| 141 | return c.fail(protocol.ExitUsage, "%v", err) | ||
| 142 | } | ||
| 143 | // Repo paths on disk derive from the owner name; move the tree. If the | ||
| 144 | // move fails, revert the database so name and disk stay consistent. | ||
| 145 | if _, err := os.Stat(oldDir); err == nil { | ||
| 146 | if err := os.Rename(oldDir, newDir); err != nil { | ||
| 147 | c.Store.RenameOrg(org.ID, org.Name) | ||
| 148 | return c.fail(protocol.ExitFailure, "moving repositories: %v", err) | ||
| 149 | } | ||
| 150 | } | ||
| 151 | return c.emit(map[string]string{"org": newName, "was": org.Name}, func(w io.Writer) { | ||
| 152 | fmt.Fprintf(w, "renamed %s to %s — clone URLs now use %s/<repo>\n", org.Name, newName, newName) | ||
| 153 | }) | ||
| 154 | } | ||
| 155 | |||
| 119 | func runOrgDelete(c *Ctx, args []string) int { | 156 | func runOrgDelete(c *Ctx, args []string) int { |
| 120 | var name string | 157 | var name string |
| 121 | yes := false | 158 | yes := false |
internal/store/orgs.go +25
| @@ -190,3 +190,28 @@ func (s *Store) DeleteOrg(orgID int64) error { | |||
| 190 | _, err := s.DB.Exec("DELETE FROM orgs WHERE id = ?", orgID) | 190 | _, err := s.DB.Exec("DELETE FROM orgs WHERE id = ?", orgID) |
| 191 | return err | 191 | return err |
| 192 | } | 192 | } |
| 193 | |||
| 194 | // RenameOrg changes an org's name, holding the shared owner-namespace | ||
| 195 | // invariant. The caller moves the on-disk repos directory afterward. | ||
| 196 | func (s *Store) RenameOrg(orgID int64, newName string) error { | ||
| 197 | tx, err := s.DB.Begin() | ||
| 198 | if err != nil { | ||
| 199 | return err | ||
| 200 | } | ||
| 201 | defer tx.Rollback() | ||
| 202 | taken, err := ownerNameTaken(tx, newName) | ||
| 203 | if err != nil { | ||
| 204 | return err | ||
| 205 | } | ||
| 206 | if taken { | ||
| 207 | return fmt.Errorf("the name %q is taken", newName) | ||
| 208 | } | ||
| 209 | res, err := tx.Exec("UPDATE orgs SET name = ? WHERE id = ?", newName, orgID) | ||
| 210 | if err != nil { | ||
| 211 | return err | ||
| 212 | } | ||
| 213 | if n, _ := res.RowsAffected(); n == 0 { | ||
| 214 | return ErrNotFound | ||
| 215 | } | ||
| 216 | return tx.Commit() | ||
| 217 | } | ||