| @@ -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 | func runRepoShow(c *Ctx, args []string) int { |
406 | func runRepoShow(c *Ctx, args []string) int { |
| 377 | if len(args) != 1 { |
407 | if len(args) != 1 { |
| 378 | return c.usage() |
408 | return c.usage() |
| @@ -381,32 +411,6 @@ func runRepoShow(c *Ctx, args []string) int { |
| 381 | if code >= 0 { |
411 | if code >= 0 { |
| 382 | return code |
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 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)) |
414 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)) |
| 411 | topics, err := c.Store.ListTopics(repo.ID) |
415 | topics, err := c.Store.ListTopics(repo.ID) |
| 412 | if err != nil { |
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 | DefaultBranch: repo.DefaultBranch, ProtectedBranches: repo.Settings.ProtectedBranches, |
428 | DefaultBranch: repo.DefaultBranch, ProtectedBranches: repo.Settings.ProtectedBranches, |
| 425 | Archived: repo.Settings.Archived, Topics: topics, Domains: domains} |
429 | Archived: repo.Settings.Archived, Topics: topics, Domains: domains} |
| 426 | if repo.ForkOf != 0 { |
430 | if repo.ForkOf != 0 { |
| @@ -442,14 +446,26 @@ func runRepoShow(c *Ctx, args []string) int { |
| 442 | return c.fail(protocol.ExitFailure, "%v", err) |
446 | return c.fail(protocol.ExitFailure, "%v", err) |
| 443 | } |
447 | } |
| 444 | for _, m := range ms { |
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 | var glance repoGlance |
452 | var glance repoGlance |
| |
453 | var mrs []store.MR |
| |
454 | var issues []store.Issue |
| |
455 | var commits []CommitOut |
| 449 | if c.Term.Cols > 0 && !c.JSON { |
456 | if c.Term.Cols > 0 && !c.JSON { |
| 450 | glance = repoAtAGlance(c, repo) |
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 | bookmarked, archived := "", "" |
469 | bookmarked, archived := "", "" |
| 454 | if d.Bookmarked { |
470 | if d.Bookmarked { |
| 455 | bookmarked = "yes" |
471 | bookmarked = "yes" |
| @@ -458,41 +474,19 @@ func runRepoShow(c *Ctx, args []string) int { |
| 458 | archived = "yes" |
474 | archived = "yes" |
| 459 | } |
475 | } |
| 460 | v := c.view(w) |
476 | v := c.view(w) |
| 461 | if c.Term.Cols > 0 { |
477 | v.title(d.Path, d.Description, d.Visibility) |
| 462 | v.title(d.Path, "", d.Visibility) |
478 | v.fields( |
| 463 | v.text(d.Description) |
479 | "default branch", d.DefaultBranch, |
| 464 | v.fields( |
480 | "website", d.Website, |
| 465 | "clone", glance.clone, |
481 | "topics", strings.Join(d.Topics, ", "), |
| 466 | "issues", glance.issues, |
482 | "protected", strings.Join(d.ProtectedBranches, ", "), |
| 467 | "merge requests", glance.mrs, |
483 | "pages domains", strings.Join(d.Domains, ", "), |
| 468 | "release", glance.release, |
484 | "fork of", d.ForkOf, |
| 469 | "checks", glance.checks, |
485 | "watch", d.Watch, |
| 470 | "default branch", d.DefaultBranch, |
486 | "bookmarked", bookmarked, |
| 471 | "website", d.Website, |
487 | "archived", archived, |
| 472 | "topics", strings.Join(d.Topics, ", "), |
488 | "url", c.siteURL(d.Path), |
| 473 | "protected", strings.Join(d.ProtectedBranches, ", "), |
489 | ) |
| 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 | } |
| |
| 496 | if len(d.Mirrors) > 0 { |
490 | if len(d.Mirrors) > 0 { |
| 497 | v.section("mirror") |
491 | v.section("mirror") |
| 498 | tb := c.table(w, "DIRECTION", "URL", "LAST SYNC", "STATUS") |
492 | tb := c.table(w, "DIRECTION", "URL", "LAST SYNC", "STATUS") |
| @@ -508,13 +502,14 @@ func runRepoShow(c *Ctx, args []string) int { |
| 508 | } |
502 | } |
| 509 | tb.flush() |
503 | tb.flush() |
| 510 | } |
504 | } |
| 511 | }) |
505 | }, func() screen { return repoShowScreen(c, d, glance, mrs, issues, commits) }) |
| 512 | } |
506 | } |
| 513 | |
507 | |
| 514 | // repoGlance is what repo show adds at a terminal: how to clone it and |
508 | // repoGlance is what repo show adds at a terminal: how to clone it and |
| 515 | // what is going on in it. |
509 | // what is going on in it. |
| 516 | type repoGlance struct { |
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 | // repoAtAGlance reads the glance fields. Each is left blank when it |
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 | host += ":" + strconv.Itoa(c.Cfg.SSH.Port) |
520 | host += ":" + strconv.Itoa(c.Cfg.SSH.Port) |
| 526 | } |
521 | } |
| 527 | g := repoGlance{clone: "ssh://git@" + host + "/" + repo.Path() + ".git"} |
522 | g := repoGlance{clone: "ssh://git@" + host + "/" + repo.Path() + ".git"} |
| 528 | issues, mrs := c.Store.OpenCounts(repo.ID) |
523 | g.issuesN, g.mrsN = c.Store.OpenCounts(repo.ID) |
| 529 | g.issues = fmt.Sprintf("%d open", issues) |
| |
| 530 | g.mrs = fmt.Sprintf("%d open", mrs) |
| |
| 531 | if rs, err := c.Store.ListReleasesPage(repo.ID, 1, "", 0); err == nil && len(rs) > 0 { |
524 | if rs, err := c.Store.ListReleasesPage(repo.ID, 1, "", 0); err == nil && len(rs) > 0 { |
| 532 | g.release = rs[0].Tag + ", " + relAge(rs[0].CreatedAt, termNow()) |
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 | return protocol.ExitOK |
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 | } |