Commit 3e75710732
Verified · cmc ci/build: success ci/test: error
Layout: unified · split
.gitbay/wiki/Users.org +5
| @@ -154,6 +154,11 @@ gitbay repo fork other/project [--name mine] | ||
| 154 | 154 | gitbay repo delete you/project --yes |
| 155 | 155 | #+end_src |
| 156 | 156 | |
| 157 | =repo show= also reports your own state on the repository — =watch= | |
| 158 | (=watching= or =muted=) and =bookmarked= — and =fork_of= when it is a | |
| 159 | fork whose parent you can read, so a client draws a toggle rather than | |
| 160 | two blind buttons. Absent means none. | |
| 161 | ||
| 157 | 162 | Pushing is SSH-only. Public repositories are anonymously readable over |
| 158 | 163 | HTTPS (and =git://= where enabled); private repositories exist only over |
| 159 | 164 | SSH and answer "not found" to everyone without access. |
e2e/reposhowstate_test.go added +59
| @@ -0,0 +1,59 @@ | ||
| 1 | package e2e | |
| 2 | ||
| 3 | import ( | |
| 4 | "strings" | |
| 5 | "testing" | |
| 6 | ) | |
| 7 | ||
| 8 | // repo show carries the caller's own watch and bookmark state and, when | |
| 9 | // the caller can read it, what the repository was forked from — so a | |
| 10 | // client draws the real state without a second read per screen (#178). | |
| 11 | func TestRepoShowCarriesViewerState(t *testing.T) { | |
| 12 | inst := startInstance(t) | |
| 13 | aliceKey := inst.newKey(t, "alice") | |
| 14 | bobKey := inst.newKey(t, "bob") | |
| 15 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub") | |
| 16 | inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub") | |
| 17 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 { | |
| 18 | t.Fatalf("repo create: %s", errOut) | |
| 19 | } | |
| 20 | ||
| 21 | // Nothing yet: the fields are absent, not false or empty strings. | |
| 22 | out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "alice/app", "--json") | |
| 23 | for _, k := range []string{`"watch"`, `"bookmarked"`, `"fork_of"`} { | |
| 24 | if strings.Contains(out, k) { | |
| 25 | t.Errorf("%s present with no state:\n%s", k, out) | |
| 26 | } | |
| 27 | } | |
| 28 | ||
| 29 | inst.ssh(t, bobKey, "", "repo", "watch", "alice/app") | |
| 30 | inst.ssh(t, bobKey, "", "repo", "bookmark", "alice/app") | |
| 31 | out, _, _ = inst.ssh(t, bobKey, "", "repo", "show", "alice/app", "--json") | |
| 32 | if !strings.Contains(out, `"watch":"watching"`) || !strings.Contains(out, `"bookmarked":true`) { | |
| 33 | t.Fatalf("bob's state not reported:\n%s", out) | |
| 34 | } | |
| 35 | // It is the caller's state: alice sees her own, not bob's. | |
| 36 | if out, _, _ := inst.ssh(t, aliceKey, "", "repo", "show", "alice/app", "--json"); strings.Contains(out, `"bookmarked"`) { | |
| 37 | t.Errorf("alice sees bob's bookmark:\n%s", out) | |
| 38 | } | |
| 39 | ||
| 40 | // Plain output carries the same. | |
| 41 | if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "alice/app"); !strings.Contains(out, "watch: watching") || | |
| 42 | !strings.Contains(out, "bookmarked") { | |
| 43 | t.Errorf("plain output lacks the state:\n%s", out) | |
| 44 | } | |
| 45 | ||
| 46 | // A fork names its parent to anyone who can read the parent. | |
| 47 | if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "fork", "alice/app"); code != 0 { | |
| 48 | t.Fatalf("fork: %s", errOut) | |
| 49 | } | |
| 50 | if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "bob/app", "--json"); !strings.Contains(out, `"fork_of":"alice/app"`) { | |
| 51 | t.Fatalf("fork_of missing:\n%s", out) | |
| 52 | } | |
| 53 | // Once the parent is private and unreadable, the fork stops naming | |
| 54 | // it: a private repository is never confirmed to exist. | |
| 55 | inst.ssh(t, aliceKey, "", "repo", "settings", "visibility", "alice/app", "private") | |
| 56 | if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "bob/app", "--json"); strings.Contains(out, "alice/app") { | |
| 57 | t.Errorf("a fork named a parent the caller cannot read:\n%s", out) | |
| 58 | } | |
| 59 | } | |
internal/control/repo.go +30 −2
| @@ -289,6 +289,13 @@ func runRepoShow(c *Ctx, args []string) int { | ||
| 289 | 289 | Topics []string `json:"topics,omitempty"` |
| 290 | 290 | Domains []string `json:"domains,omitempty"` |
| 291 | 291 | Mirrors []mirrorOut `json:"mirrors,omitempty"` |
| 292 | // ForkOf names the parent only when the caller can read it: a | |
| 293 | // private parent is not confirmed to exist, here as anywhere. | |
| 294 | ForkOf string `json:"fork_of,omitempty"` | |
| 295 | // Watch and Bookmarked are the caller's own state, so a client | |
| 296 | // can draw a toggle rather than two stateless buttons (#178). | |
| 297 | Watch string `json:"watch,omitempty"` // watching, muted, or absent | |
| 298 | Bookmarked bool `json:"bookmarked,omitempty"` | |
| 292 | 299 | } |
| 293 | 300 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)) |
| 294 | 301 | topics, err := c.Store.ListTopics(repo.ID) |
| @@ -303,8 +310,20 @@ func runRepoShow(c *Ctx, args []string) int { | ||
| 303 | 310 | } |
| 304 | 311 | } |
| 305 | 312 | } |
| 306 | d := out{repo.Path(), desc, repo.Settings.Website, repo.Visibility, repo.DefaultBranch, | |
| 307 | repo.Settings.ProtectedBranches, repo.Settings.Archived, topics, domains, nil} | |
| 313 | d := out{Path: repo.Path(), Description: desc, Website: repo.Settings.Website, Visibility: repo.Visibility, | |
| 314 | DefaultBranch: repo.DefaultBranch, ProtectedBranches: repo.Settings.ProtectedBranches, | |
| 315 | Archived: repo.Settings.Archived, Topics: topics, Domains: domains} | |
| 316 | if repo.ForkOf != 0 { | |
| 317 | if parent, err := c.Store.RepoByID(repo.ForkOf); err == nil { | |
| 318 | if grant, err := c.Store.AccessRole(parent.ID, c.User.ID); err == nil && policy.CanRead(c.User, parent, grant) { | |
| 319 | d.ForkOf = parent.Path() | |
| 320 | } | |
| 321 | } | |
| 322 | } | |
| 323 | if c.User.ID != 0 { | |
| 324 | d.Watch = c.Store.RepoWatchState(repo.ID, c.User.ID) | |
| 325 | d.Bookmarked = c.Store.IsBookmarked(c.User.ID, repo.ID) | |
| 326 | } | |
| 308 | 327 | // Mirror status is admin-only, like repo mirror list. The token never |
| 309 | 328 | // leaves the server. |
| 310 | 329 | if grant, err := c.Store.AccessRole(repo.ID, c.User.ID); err == nil && policy.CanAdmin(c.User, repo, grant) { |
| @@ -337,6 +356,15 @@ func runRepoShow(c *Ctx, args []string) int { | ||
| 337 | 356 | if len(d.Domains) > 0 { |
| 338 | 357 | fmt.Fprintf(w, "pages domains: %s\n", strings.Join(d.Domains, ", ")) |
| 339 | 358 | } |
| 359 | if d.ForkOf != "" { | |
| 360 | fmt.Fprintf(w, "fork of: %s\n", d.ForkOf) | |
| 361 | } | |
| 362 | if d.Watch != "" { | |
| 363 | fmt.Fprintf(w, "watch: %s\n", d.Watch) | |
| 364 | } | |
| 365 | if d.Bookmarked { | |
| 366 | fmt.Fprintln(w, "bookmarked") | |
| 367 | } | |
| 340 | 368 | for _, m := range d.Mirrors { |
| 341 | 369 | status := "ok" |
| 342 | 370 | if m.Pending { |