| @@ -373,6 +373,36 @@ func runRepoList(c *Ctx, args []string) int { |
| 373 | 373 | }) |
| 374 | 374 | } |
| 375 | 375 | |
| 376 | // repoMirrorOut is one mirror as repo show emits it, for admins. |
| 377 | type repoMirrorOut struct { |
| 378 | Direction string `json:"direction"` |
| 379 | URL string `json:"url"` |
| 380 | Pending bool `json:"pending"` |
| 381 | LastSync string `json:"last_sync,omitempty"` |
| 382 | LastError string `json:"last_error,omitempty"` |
| 383 | } |
| 384 | |
| 385 | // repoShowOut is what repo show emits. |
| 386 | type repoShowOut struct { |
| 387 | Path string `json:"path"` |
| 388 | Description string `json:"description,omitempty"` |
| 389 | Website string `json:"website,omitempty"` |
| 390 | Visibility string `json:"visibility"` |
| 391 | DefaultBranch string `json:"default_branch"` |
| 392 | ProtectedBranches []string `json:"protected_branches,omitempty"` |
| 393 | Archived bool `json:"archived,omitempty"` |
| 394 | Topics []string `json:"topics,omitempty"` |
| 395 | Domains []string `json:"domains,omitempty"` |
| 396 | Mirrors []repoMirrorOut `json:"mirrors,omitempty"` |
| 397 | // ForkOf names the parent only when the caller can read it: a |
| 398 | // private parent is not confirmed to exist, here as anywhere. |
| 399 | ForkOf string `json:"fork_of,omitempty"` |
| 400 | // Watch and Bookmarked are the caller's own state, so a client |
| 401 | // can draw a toggle rather than two stateless buttons (#178). |
| 402 | Watch string `json:"watch,omitempty"` // watching, muted, or absent |
| 403 | Bookmarked bool `json:"bookmarked,omitempty"` |
| 404 | } |
| 405 | |
| 376 | 406 | func runRepoShow(c *Ctx, args []string) int { |
| 377 | 407 | if len(args) != 1 { |
| 378 | 408 | return c.usage() |
| @@ -381,32 +411,6 @@ func runRepoShow(c *Ctx, args []string) int { |
| 381 | 411 | if code >= 0 { |
| 382 | 412 | return code |
| 383 | 413 | } |
| 384 | | type mirrorOut struct { |
| 385 | | Direction string `json:"direction"` |
| 386 | | URL string `json:"url"` |
| 387 | | Pending bool `json:"pending"` |
| 388 | | LastSync string `json:"last_sync,omitempty"` |
| 389 | | LastError string `json:"last_error,omitempty"` |
| 390 | | } |
| 391 | | type out struct { |
| 392 | | Path string `json:"path"` |
| 393 | | Description string `json:"description,omitempty"` |
| 394 | | Website string `json:"website,omitempty"` |
| 395 | | Visibility string `json:"visibility"` |
| 396 | | DefaultBranch string `json:"default_branch"` |
| 397 | | ProtectedBranches []string `json:"protected_branches,omitempty"` |
| 398 | | Archived bool `json:"archived,omitempty"` |
| 399 | | Topics []string `json:"topics,omitempty"` |
| 400 | | Domains []string `json:"domains,omitempty"` |
| 401 | | Mirrors []mirrorOut `json:"mirrors,omitempty"` |
| 402 | | // ForkOf names the parent only when the caller can read it: a |
| 403 | | // private parent is not confirmed to exist, here as anywhere. |
| 404 | | ForkOf string `json:"fork_of,omitempty"` |
| 405 | | // Watch and Bookmarked are the caller's own state, so a client |
| 406 | | // can draw a toggle rather than two stateless buttons (#178). |
| 407 | | Watch string `json:"watch,omitempty"` // watching, muted, or absent |
| 408 | | Bookmarked bool `json:"bookmarked,omitempty"` |
| 409 | | } |
| 410 | 414 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)) |
| 411 | 415 | topics, err := c.Store.ListTopics(repo.ID) |
| 412 | 416 | if err != nil { |
| @@ -420,7 +424,7 @@ func runRepoShow(c *Ctx, args []string) int { |
| 420 | 424 | } |
| 421 | 425 | } |
| 422 | 426 | } |
| 423 | | d := out{Path: repo.Path(), Description: desc, Website: repo.Settings.Website, Visibility: repo.Visibility, |
| 427 | d := repoShowOut{Path: repo.Path(), Description: desc, Website: repo.Settings.Website, Visibility: repo.Visibility, |
| 424 | 428 | DefaultBranch: repo.DefaultBranch, ProtectedBranches: repo.Settings.ProtectedBranches, |
| 425 | 429 | Archived: repo.Settings.Archived, Topics: topics, Domains: domains} |
| 426 | 430 | if repo.ForkOf != 0 { |
| @@ -442,14 +446,26 @@ func runRepoShow(c *Ctx, args []string) int { |
| 442 | 446 | return c.fail(protocol.ExitFailure, "%v", err) |
| 443 | 447 | } |
| 444 | 448 | for _, m := range ms { |
| 445 | | d.Mirrors = append(d.Mirrors, mirrorOut{m.Direction, m.URL, m.Dirty, m.LastSync, m.LastError}) |
| 449 | d.Mirrors = append(d.Mirrors, repoMirrorOut{m.Direction, m.URL, m.Dirty, m.LastSync, m.LastError}) |
| 446 | 450 | } |
| 447 | 451 | } |
| 448 | 452 | var glance repoGlance |
| 453 | var mrs []store.MR |
| 454 | var issues []store.Issue |
| 455 | var commits []CommitOut |
| 449 | 456 | if c.Term.Cols > 0 && !c.JSON { |
| 450 | 457 | glance = repoAtAGlance(c, repo) |
| 458 | mrs, _ = c.Store.ListMRs(repo.ID, "open", 5, 0) |
| 459 | issues, _ = c.Store.QueryIssues(repo.ID, store.IssueFilter{State: "open", Limit: 5}) |
| 460 | dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) |
| 461 | if shas, err := gitutil.RevList(dir, "refs/heads/"+repo.DefaultBranch, 5); err == nil { |
| 462 | subjects := gitutil.Subjects(dir, shas) |
| 463 | for _, sha := range shas { |
| 464 | commits = append(commits, CommitOut{sha, subjects[sha]}) |
| 465 | } |
| 466 | } |
| 451 | 467 | } |
| 452 | | return c.emit(d, func(w io.Writer) { |
| 468 | return c.emitView(d, func(w io.Writer) { |
| 453 | 469 | bookmarked, archived := "", "" |
| 454 | 470 | if d.Bookmarked { |
| 455 | 471 | bookmarked = "yes" |
| @@ -458,41 +474,19 @@ func runRepoShow(c *Ctx, args []string) int { |
| 458 | 474 | archived = "yes" |
| 459 | 475 | } |
| 460 | 476 | v := c.view(w) |
| 461 | | if c.Term.Cols > 0 { |
| 462 | | v.title(d.Path, "", d.Visibility) |
| 463 | | v.text(d.Description) |
| 464 | | v.fields( |
| 465 | | "clone", glance.clone, |
| 466 | | "issues", glance.issues, |
| 467 | | "merge requests", glance.mrs, |
| 468 | | "release", glance.release, |
| 469 | | "checks", glance.checks, |
| 470 | | "default branch", d.DefaultBranch, |
| 471 | | "website", d.Website, |
| 472 | | "topics", strings.Join(d.Topics, ", "), |
| 473 | | "protected", strings.Join(d.ProtectedBranches, ", "), |
| 474 | | "pages domains", strings.Join(d.Domains, ", "), |
| 475 | | "fork of", d.ForkOf, |
| 476 | | "watch", d.Watch, |
| 477 | | "bookmarked", bookmarked, |
| 478 | | "archived", archived, |
| 479 | | "url", c.siteURL(d.Path), |
| 480 | | ) |
| 481 | | } else { |
| 482 | | v.title(d.Path, d.Description, d.Visibility) |
| 483 | | v.fields( |
| 484 | | "default branch", d.DefaultBranch, |
| 485 | | "website", d.Website, |
| 486 | | "topics", strings.Join(d.Topics, ", "), |
| 487 | | "protected", strings.Join(d.ProtectedBranches, ", "), |
| 488 | | "pages domains", strings.Join(d.Domains, ", "), |
| 489 | | "fork of", d.ForkOf, |
| 490 | | "watch", d.Watch, |
| 491 | | "bookmarked", bookmarked, |
| 492 | | "archived", archived, |
| 493 | | "url", c.siteURL(d.Path), |
| 494 | | ) |
| 495 | | } |
| 477 | v.title(d.Path, d.Description, d.Visibility) |
| 478 | v.fields( |
| 479 | "default branch", d.DefaultBranch, |
| 480 | "website", d.Website, |
| 481 | "topics", strings.Join(d.Topics, ", "), |
| 482 | "protected", strings.Join(d.ProtectedBranches, ", "), |
| 483 | "pages domains", strings.Join(d.Domains, ", "), |
| 484 | "fork of", d.ForkOf, |
| 485 | "watch", d.Watch, |
| 486 | "bookmarked", bookmarked, |
| 487 | "archived", archived, |
| 488 | "url", c.siteURL(d.Path), |
| 489 | ) |
| 496 | 490 | if len(d.Mirrors) > 0 { |
| 497 | 491 | v.section("mirror") |
| 498 | 492 | tb := c.table(w, "DIRECTION", "URL", "LAST SYNC", "STATUS") |
| @@ -508,13 +502,14 @@ func runRepoShow(c *Ctx, args []string) int { |
| 508 | 502 | } |
| 509 | 503 | tb.flush() |
| 510 | 504 | } |
| 511 | | }) |
| 505 | }, func() screen { return repoShowScreen(c, d, glance, mrs, issues, commits) }) |
| 512 | 506 | } |
| 513 | 507 | |
| 514 | 508 | // repoGlance is what repo show adds at a terminal: how to clone it and |
| 515 | 509 | // what is going on in it. |
| 516 | 510 | type repoGlance struct { |
| 517 | | clone, issues, mrs, release, checks string |
| 511 | clone, release, checks string |
| 512 | issuesN, mrsN int |
| 518 | 513 | } |
| 519 | 514 | |
| 520 | 515 | // repoAtAGlance reads the glance fields. Each is left blank when it |
| @@ -525,9 +520,7 @@ func repoAtAGlance(c *Ctx, repo store.Repo) repoGlance { |
| 525 | 520 | host += ":" + strconv.Itoa(c.Cfg.SSH.Port) |
| 526 | 521 | } |
| 527 | 522 | g := repoGlance{clone: "ssh://git@" + host + "/" + repo.Path() + ".git"} |
| 528 | | issues, mrs := c.Store.OpenCounts(repo.ID) |
| 529 | | g.issues = fmt.Sprintf("%d open", issues) |
| 530 | | g.mrs = fmt.Sprintf("%d open", mrs) |
| 523 | g.issuesN, g.mrsN = c.Store.OpenCounts(repo.ID) |
| 531 | 524 | if rs, err := c.Store.ListReleasesPage(repo.ID, 1, "", 0); err == nil && len(rs) > 0 { |
| 532 | 525 | g.release = rs[0].Tag + ", " + relAge(rs[0].CreatedAt, termNow()) |
| 533 | 526 | } |
| @@ -1374,3 +1367,59 @@ func runRepoDiff(c *Ctx, args []string) int { |
| 1374 | 1367 | } |
| 1375 | 1368 | return protocol.ExitOK |
| 1376 | 1369 | } |
| 1370 | |
| 1371 | // repoShowScreen is repo show at a terminal: how to clone it, where the |
| 1372 | // default branch stands, what is open, and the latest commits. |
| 1373 | func repoShowScreen(c *Ctx, d repoShowOut, g repoGlance, mrs []store.MR, issues []store.Issue, commits []CommitOut) screen { |
| 1374 | s := screen{body: d.Description, format: "text"} |
| 1375 | name := []cell{cLink(d.Path, c.siteURL(d.Path)), cState(d.Visibility)} |
| 1376 | if d.Archived { |
| 1377 | name[1].s += ", archived" |
| 1378 | } |
| 1379 | s.fields = append(s.fields, field{"Repo", name}) |
| 1380 | if g.clone != "" { |
| 1381 | s.fields = append(s.fields, field{"Clone", []cell{cText(g.clone)}}) |
| 1382 | } |
| 1383 | head := []cell{cText(d.DefaultBranch)} |
| 1384 | if g.checks != "" { |
| 1385 | head = []cell{cText(d.DefaultBranch), cText(strings.TrimSuffix(g.checks, " on "+d.DefaultBranch))} |
| 1386 | } |
| 1387 | s.fields = append(s.fields, field{"Head", head}) |
| 1388 | if g.release != "" { |
| 1389 | s.fields = append(s.fields, field{"Release", []cell{cText(g.release)}}) |
| 1390 | } |
| 1391 | if len(d.Topics) > 0 { |
| 1392 | s.fields = append(s.fields, field{"Topics", []cell{cText(strings.Join(d.Topics, ", "))}}) |
| 1393 | } |
| 1394 | if d.ForkOf != "" { |
| 1395 | s.fields = append(s.fields, field{"Fork of", []cell{cRef(d.ForkOf)}}) |
| 1396 | } |
| 1397 | for _, m := range d.Mirrors { |
| 1398 | if m.LastError != "" { |
| 1399 | s.fields = append(s.fields, field{"Mirror", []cell{cGlyph("failed"), cText(m.Direction + " " + m.URL + ": " + m.LastError)}}) |
| 1400 | } |
| 1401 | } |
| 1402 | if !c.Term.Links { |
| 1403 | s.fields = append(s.fields, field{"URL", []cell{cText(c.siteURL(d.Path))}}) |
| 1404 | } |
| 1405 | |
| 1406 | ms := section{title: "Open merge requests", n: g.mrsN, more: []string{"mr", "list", d.Path}} |
| 1407 | for _, m := range mrs { |
| 1408 | ms.rows = append(ms.rows, rowOf(cRef(fmt.Sprintf("!%d", m.Number)), cFlex(m.Title), cAge(m.UpdatedAt))) |
| 1409 | } |
| 1410 | is := section{title: "Open issues", n: g.issuesN, more: []string{"issue", "list", d.Path}} |
| 1411 | for _, i := range issues { |
| 1412 | is.rows = append(is.rows, rowOf(cRef(fmt.Sprintf("#%d", i.Number)), cFlex(i.Title), cAge(i.UpdatedAt))) |
| 1413 | } |
| 1414 | cs := section{title: "Recent commits", n: len(commits), more: []string{"repo", "log", d.Path}} |
| 1415 | for _, cm := range commits { |
| 1416 | cs.rows = append(cs.rows, rowOf(cRef(fmt.Sprintf("%.7s", cm.SHA)), cFlex(cm.Subject))) |
| 1417 | } |
| 1418 | s.sections = []section{ms, is, cs} |
| 1419 | s.actions = []action{ |
| 1420 | {"Contribute", []string{"mr", "create", d.Path}}, |
| 1421 | {"Contribute", []string{"issue", "create", d.Path}}, |
| 1422 | {"Read", []string{"repo", "log", d.Path}}, |
| 1423 | } |
| 1424 | return s |
| 1425 | } |