List an MR's commits on the web page and in mr show !43
4 files changed, +84 −2
Layout: unified · split
e2e/mr_test.go +13
| @@ -267,4 +267,17 @@ func TestMergeRequests(t *testing.T) { | |||
| 267 | if !strings.Contains(body, "feature.txt") { | 267 | if !strings.Contains(body, "feature.txt") { |
| 268 | t.Fatalf("merged MR web diff empty:\n%s", body) | 268 | t.Fatalf("merged MR web diff empty:\n%s", body) |
| 269 | } | 269 | } |
| 270 | // The MR page lists the commits it carries, linked to commit pages. | ||
| 271 | if !strings.Contains(body, ">commits <") || !strings.Contains(body, "/alice/lib/commit/") { | ||
| 272 | t.Fatalf("mr commits section missing:\n%s", body) | ||
| 273 | } | ||
| 274 | // So does mr show, human and JSON. | ||
| 275 | showOut, _, code := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1") | ||
| 276 | if code != 0 || !strings.Contains(showOut, "commit: ") { | ||
| 277 | t.Fatalf("mr show missing commits: %d\n%s", code, showOut) | ||
| 278 | } | ||
| 279 | showJSON, _, _ := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1", "--json") | ||
| 280 | if !strings.Contains(showJSON, `"commits":[`) || !strings.Contains(showJSON, `"subject"`) { | ||
| 281 | t.Fatalf("mr show json missing commits: %s", showJSON) | ||
| 282 | } | ||
| 270 | } | 283 | } |
internal/control/mr.go +32 −1
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | "gitbay.org/gitbay/internal/gitutil" | 11 | "gitbay.org/gitbay/internal/gitutil" |
| 12 | "gitbay.org/gitbay/internal/policy" | 12 | "gitbay.org/gitbay/internal/policy" |
| 13 | "gitbay.org/gitbay/internal/protocol" | 13 | "gitbay.org/gitbay/internal/protocol" |
| 14 | "gitbay.org/gitbay/internal/sig" | ||
| 14 | "gitbay.org/gitbay/internal/store" | 15 | "gitbay.org/gitbay/internal/store" |
| 15 | ) | 16 | ) |
| 16 | 17 | ||
| @@ -414,19 +415,49 @@ func runMRShow(c *Ctx, args []string) int { | |||
| 414 | for _, r := range reviews { | 415 | for _, r := range reviews { |
| 415 | rs = append(rs, reviewOut{r.Reviewer, r.Verdict, r.Stale}) | 416 | rs = append(rs, reviewOut{r.Reviewer, r.Verdict, r.Stale}) |
| 416 | } | 417 | } |
| 418 | // The commits this MR carries: base..head, the diff's range. | ||
| 419 | type commitOut struct { | ||
| 420 | SHA string `json:"sha"` | ||
| 421 | Subject string `json:"subject"` | ||
| 422 | } | ||
| 423 | var commits []commitOut | ||
| 424 | dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) | ||
| 425 | base := mr.MergedBase | ||
| 426 | if base == "" { | ||
| 427 | if b, err := gitutil.MergeBase(dir, "refs/heads/"+mr.TargetRef, mrHeadRef(mr.Number)); err == nil { | ||
| 428 | base = b | ||
| 429 | } | ||
| 430 | } | ||
| 431 | if base != "" { | ||
| 432 | if shas, err := gitutil.RevListRange(dir, base, mrHeadRef(mr.Number)); err == nil { | ||
| 433 | for _, sha := range shas { | ||
| 434 | subject := "" | ||
| 435 | if raw, err := gitutil.ReadCommit(dir, sha); err == nil { | ||
| 436 | if parsed, err := sig.ParseCommit(raw); err == nil { | ||
| 437 | subject = parsed.Subject | ||
| 438 | } | ||
| 439 | } | ||
| 440 | commits = append(commits, commitOut{sha, subject}) | ||
| 441 | } | ||
| 442 | } | ||
| 443 | } | ||
| 417 | d := struct { | 444 | d := struct { |
| 418 | mrOut | 445 | mrOut |
| 419 | Checks []checkOut `json:"checks,omitempty"` | 446 | Checks []checkOut `json:"checks,omitempty"` |
| 420 | Combined string `json:"checks_combined,omitempty"` | 447 | Combined string `json:"checks_combined,omitempty"` |
| 421 | UnresolvedThreads int `json:"unresolved_threads,omitempty"` | 448 | UnresolvedThreads int `json:"unresolved_threads,omitempty"` |
| 449 | Commits []commitOut `json:"commits,omitempty"` | ||
| 422 | Comments []commentOut `json:"comments,omitempty"` | 450 | Comments []commentOut `json:"comments,omitempty"` |
| 423 | Reviews []reviewOut `json:"reviews,omitempty"` | 451 | Reviews []reviewOut `json:"reviews,omitempty"` |
| 424 | }{mrToOut(repo, mr, true), checks, store.CombinedStatus(statuses), unresolved, cs, rs} | 452 | }{mrToOut(repo, mr, true), checks, store.CombinedStatus(statuses), unresolved, commits, cs, rs} |
| 425 | return c.emit(d, func(w io.Writer) { | 453 | return c.emit(d, func(w io.Writer) { |
| 426 | fmt.Fprintf(w, "!%d %s [%s] by %s\n%s -> %s @ %.10s\n", d.Number, d.Title, d.State, d.Author, d.Source, d.TargetRef, d.HeadSHA) | 454 | fmt.Fprintf(w, "!%d %s [%s] by %s\n%s -> %s @ %.10s\n", d.Number, d.Title, d.State, d.Author, d.Source, d.TargetRef, d.HeadSHA) |
| 427 | if d.Body != "" { | 455 | if d.Body != "" { |
| 428 | fmt.Fprintf(w, "\n%s\n", d.Body) | 456 | fmt.Fprintf(w, "\n%s\n", d.Body) |
| 429 | } | 457 | } |
| 458 | for _, cm := range commits { | ||
| 459 | fmt.Fprintf(w, "commit: %.10s %s\n", cm.SHA, cm.Subject) | ||
| 460 | } | ||
| 430 | for _, x := range checks { | 461 | for _, x := range checks { |
| 431 | fmt.Fprintf(w, "check: %s %s\n", x.Context, x.State) | 462 | fmt.Fprintf(w, "check: %s %s\n", x.Context, x.State) |
| 432 | } | 463 | } |
internal/httpd/web.go +25 −1
| @@ -1345,6 +1345,29 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | |||
| 1345 | stat.Files++ | 1345 | stat.Files++ |
| 1346 | } | 1346 | } |
| 1347 | } | 1347 | } |
| 1348 | // The commits this MR carries: base..head, the same range as the diff. | ||
| 1349 | type commitRow struct { | ||
| 1350 | SHA, ShortSHA, Subject, AuthorName, Date string | ||
| 1351 | Sig sigView | ||
| 1352 | } | ||
| 1353 | var commits []commitRow | ||
| 1354 | if base != "" { | ||
| 1355 | const maxMRCommits = 100 | ||
| 1356 | shas, _ := gitutil.RevListRange(p.Dir, base, headRef) | ||
| 1357 | if len(shas) > maxMRCommits { | ||
| 1358 | shas = shas[:maxMRCommits] | ||
| 1359 | } | ||
| 1360 | for _, sha := range shas { | ||
| 1361 | v, parsed := s.sigFor(p.Repo, p.Dir, sha) | ||
| 1362 | cr := commitRow{SHA: sha, ShortSHA: sha[:10], Sig: v} | ||
| 1363 | if parsed != nil { | ||
| 1364 | cr.Subject = parsed.Subject | ||
| 1365 | cr.AuthorName = parsed.AuthorName | ||
| 1366 | cr.Date = time.Unix(parsed.AuthorUnix, 0).UTC().Format("2006-01-02") | ||
| 1367 | } | ||
| 1368 | commits = append(commits, cr) | ||
| 1369 | } | ||
| 1370 | } | ||
| 1348 | s.render(w, "mr.html", struct { | 1371 | s.render(w, "mr.html", struct { |
| 1349 | repoPage | 1372 | repoPage |
| 1350 | MR store.MR | 1373 | MR store.MR |
| @@ -1355,10 +1378,11 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | |||
| 1355 | Reviews []store.MRReview | 1378 | Reviews []store.MRReview |
| 1356 | DiffLines []diffLine | 1379 | DiffLines []diffLine |
| 1357 | Stat diffStat | 1380 | Stat diffStat |
| 1381 | Commits []commitRow | ||
| 1358 | CanEdit bool | 1382 | CanEdit bool |
| 1359 | DetachedThreads []diffThread | 1383 | DetachedThreads []diffThread |
| 1360 | }{p, m, md(m.Body), checks, store.CombinedStatus(checks), renderComments(comments, md), | 1384 | }{p, m, md(m.Body), checks, store.CombinedStatus(checks), renderComments(comments, md), |
| 1361 | reviews, lines, stat, s.canEditItem(r, p.Repo, m.Author), detachedThreads}) | 1385 | reviews, lines, stat, commits, s.canEditItem(r, p.Repo, m.Author), detachedThreads}) |
| 1362 | } | 1386 | } |
| 1363 | 1387 | ||
| 1364 | func (s *Server) refs(w http.ResponseWriter, r *http.Request) { | 1388 | func (s *Server) refs(w http.ResponseWriter, r *http.Request) { |
internal/web/templates/mr.html +14
| @@ -33,6 +33,20 @@ | |||
| 33 | <p><button type="submit">comment</button></p> | 33 | <p><button type="submit">comment</button></p> |
| 34 | </form> | 34 | </form> |
| 35 | {{end}} | 35 | {{end}} |
| 36 | {{if .Commits}}<h3>commits <span class="count">{{len .Commits}}</span></h3> | ||
| 37 | <ul class="loglist"> | ||
| 38 | {{range .Commits}}<li> | ||
| 39 | <div class="commitmain"> | ||
| 40 | <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p> | ||
| 41 | <p class="meta">{{.AuthorName}} · {{.Date}}</p> | ||
| 42 | </div> | ||
| 43 | <div class="commitside"> | ||
| 44 | {{template "sigbadge" .Sig}} | ||
| 45 | <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code> | ||
| 46 | </div> | ||
| 47 | </li> | ||
| 48 | {{end}} | ||
| 49 | </ul>{{end}} | ||
| 36 | <details class="difffold"> | 50 | <details class="difffold"> |
| 37 | <summary><h3>diff</h3><span class="meta">{{.Stat.Files}} file{{if ne .Stat.Files 1}}s{{end}} changed, <span class="add">+{{.Stat.Adds}}</span> <span class="del">−{{.Stat.Dels}}</span></span></summary> | 51 | <summary><h3>diff</h3><span class="meta">{{.Stat.Files}} file{{if ne .Stat.Files 1}}s{{end}} changed, <span class="add">+{{.Stat.Adds}}</span> <span class="del">−{{.Stat.Dels}}</span></span></summary> |
| 38 | <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span> | 52 | <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span> |