krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

b04b2221fd70c1d56776271e5ce33481d45c1d40

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-24T01:30:57Z

repo import: allow org owners

Same ownership rule as repo create — yourself, or an org you admin.
The self-only restriction predated orgs and was never relaxed; e2e
covers the org path and the updated refusal message.
 e2e/import_test.go         | 13 ++++++++++++-
 internal/control/import.go | 22 +++++++++++++++++++---
 2 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/e2e/import_test.go b/e2e/import_test.go
index 9eca5c7..8882ebd 100644
--- a/e2e/import_test.go
+++ b/e2e/import_test.go
@@ -89,6 +89,17 @@ func TestRepoImport(t *testing.T) {
 		t.Fatalf("git:// import: %s", errOut)
 	}
 
+	// Org-owned imports: allowed for org admins, refused for non-members.
+	if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 {
+		t.Fatalf("org create: %s", errOut)
+	}
+	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "import", "imports/mirror", "--from", httpURL); code != 0 {
+		t.Fatalf("org import: %s", errOut)
+	}
+	if out, _, code := inst.ssh(t, aliceKey, "", "repo", "log", "imports/mirror"); code != 0 || !strings.Contains(out, "first") {
+		t.Fatalf("org import log: %d\n%s", code, out)
+	}
+
 	// Refusals: bad scheme, credentials in URL, existing name, foreign owner.
 	cases := []struct {
 		args []string
@@ -97,7 +108,7 @@ func TestRepoImport(t *testing.T) {
 		{[]string{"repo", "import", "alice/x", "--from", "file:///etc"}, "https://, http://, and git://"},
 		{[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"},
 		{[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"},
-		{[]string{"repo", "import", "bob/x", "--from", httpURL}, "your own account"},
+		{[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"},
 	}
 	for _, tc := range cases {
 		_, errOut, code := inst.ssh(t, aliceKey, "", tc.args...)
diff --git a/internal/control/import.go b/internal/control/import.go
index c1dbafc..d40fa1f 100644
--- a/internal/control/import.go
+++ b/internal/control/import.go
@@ -58,12 +58,28 @@ func runRepoImport(c *Ctx, args []string) int {
 		return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url> [--private] [--token-stdin]")
 	}
 	owner, name, ok := strings.Cut(path, "/")
-	if !ok || owner != c.User.Username {
-		return c.fail(protocol.ExitDenied, "imports land under your own account: %s/<name>", c.User.Username)
+	if !ok {
+		return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>")
 	}
 	if err := policy.ValidateName(name); err != nil {
 		return c.fail(protocol.ExitUsage, "%v", err)
 	}
+	// Same ownership rule as repo create: yourself, or an org you admin.
+	ownerKind, ownerID := "user", c.User.ID
+	if owner != c.User.Username {
+		org, err := c.Store.OrgByName(owner)
+		if err != nil {
+			return c.fail(protocol.ExitDenied, "cannot import under %q: not you and not an organization you can see", owner)
+		}
+		role, err := c.Store.OrgRole(org.ID, c.User.ID)
+		if err != nil {
+			return c.fail(protocol.ExitFailure, "%v", err)
+		}
+		if role != "admin" {
+			return c.fail(protocol.ExitDenied, "only admins of %s can import repositories there", owner)
+		}
+		ownerKind, ownerID = "org", org.ID
+	}
 
 	// Scheme allowlist. file:// (and anything else local) would read the
 	// server's filesystem; ssh:// would use the server's own keys.
@@ -107,7 +123,7 @@ func runRepoImport(c *Ctx, args []string) int {
 	if private {
 		visibility = "private"
 	}
-	id, err := c.Store.CreateRepo("user", c.User.ID, name, visibility)
+	id, err := c.Store.CreateRepo(ownerKind, ownerID, name, visibility)
 	if err != nil {
 		return c.fail(protocol.ExitFailure, "%v", err)
 	}