Commit 47d3c4db23
Verified · cmc
e2e/mrweb_test.go +65
| @@ -388,3 +388,68 @@ func TestMRDiffEmptyExplained(t *testing.T) { | ||
| 388 | 388 | t.Fatalf("empty diff unexplained:\n%s", body) |
| 389 | 389 | } |
| 390 | 390 | } |
| 391 | ||
| 392 | // TestMRSupersedes closes one merge request in favour of another from the | |
| 393 | // web form, and checks both pages say so; clearing it over ssh removes | |
| 394 | // both lines again (#223). | |
| 395 | func TestMRSupersedes(t *testing.T) { | |
| 396 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") | |
| 397 | aliceKey := inst.newKey(t, "alice") | |
| 398 | inst.admin(t, "admin", "user", "create", "alice", | |
| 399 | "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") | |
| 400 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 { | |
| 401 | t.Fatalf("repo create: %s", errOut) | |
| 402 | } | |
| 403 | env := inst.gitEnv(aliceKey) | |
| 404 | work := t.TempDir() | |
| 405 | mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w") | |
| 406 | dir := filepath.Join(work, "w") | |
| 407 | os.WriteFile(filepath.Join(dir, "README"), []byte("base\n"), 0o644) | |
| 408 | mustGit(t, dir, env, "checkout", "-q", "-b", "main") | |
| 409 | mustGit(t, dir, env, "add", ".") | |
| 410 | mustGit(t, dir, env, "commit", "-q", "-m", "base") | |
| 411 | mustGit(t, dir, env, "push", "-q", "origin", "main") | |
| 412 | ||
| 413 | for _, branch := range []string{"one", "two"} { | |
| 414 | mustGit(t, dir, env, "checkout", "-q", "main") | |
| 415 | mustGit(t, dir, env, "checkout", "-q", "-b", branch) | |
| 416 | os.WriteFile(filepath.Join(dir, branch+".txt"), []byte(branch+"\n"), 0o644) | |
| 417 | mustGit(t, dir, env, "add", ".") | |
| 418 | mustGit(t, dir, env, "commit", "-q", "-m", branch) | |
| 419 | mustGit(t, dir, env, "push", "-q", "origin", branch) | |
| 420 | } | |
| 421 | if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/app", | |
| 422 | "--source", "one", "--target", "main", "--title", "one"); code != 0 { | |
| 423 | t.Fatalf("mr create one: %s", errOut) | |
| 424 | } | |
| 425 | if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/app", | |
| 426 | "--source", "two", "--target", "main", "--title", "two"); code != 0 { | |
| 427 | t.Fatalf("mr create two: %s", errOut) | |
| 428 | } | |
| 429 | ||
| 430 | alice := inst.login(t, aliceKey) | |
| 431 | if status, body := browserPost(t, alice, inst.base()+"/alice/app/mrs/1/close", url.Values{"by": {"2"}}); status != 200 { | |
| 432 | t.Fatalf("close post: %d\n%s", status, body) | |
| 433 | } | |
| 434 | ||
| 435 | _, body1 := browserGet(t, alice, inst.base()+"/alice/app/mrs/1") | |
| 436 | if !strings.Contains(body1, `in favour of <a href="/alice/app/mrs/2">!2</a>`) { | |
| 437 | t.Fatalf("!1 does not say it was superseded:\n%s", body1) | |
| 438 | } | |
| 439 | _, body2 := browserGet(t, alice, inst.base()+"/alice/app/mrs/2") | |
| 440 | if !strings.Contains(body2, `supersedes <a href="/alice/app/mrs/1">!1</a>`) { | |
| 441 | t.Fatalf("!2 does not say what it supersedes:\n%s", body2) | |
| 442 | } | |
| 443 | ||
| 444 | if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "edit", "alice/app", "1", "--superseded-by", "none"); code != 0 { | |
| 445 | t.Fatalf("mr edit --superseded-by none: %s", errOut) | |
| 446 | } | |
| 447 | _, body1 = browserGet(t, alice, inst.base()+"/alice/app/mrs/1") | |
| 448 | if strings.Contains(body1, "in favour of") { | |
| 449 | t.Fatalf("!1 still says it was superseded after clearing:\n%s", body1) | |
| 450 | } | |
| 451 | _, body2 = browserGet(t, alice, inst.base()+"/alice/app/mrs/2") | |
| 452 | if strings.Contains(body2, "supersedes") { | |
| 453 | t.Fatalf("!2 still says it supersedes after clearing:\n%s", body2) | |
| 454 | } | |
| 455 | } | |
internal/httpd/mractions.go +9 −1
| @@ -76,7 +76,15 @@ func (s *Server) mrMergeSubmit(w http.ResponseWriter, r *http.Request, u store.U | ||
| 76 | 76 | } |
| 77 | 77 | |
| 78 | 78 | func (s *Server) mrCloseSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 79 | _, msg, code := s.runControlCode(u, mrArgs(r, "close")) | |
| 79 | args := []string{} | |
| 80 | if by := strings.TrimSpace(r.FormValue("by")); by != "" { | |
| 81 | if _, err := strconv.ParseInt(by, 10, 64); err != nil { | |
| 82 | s.mrRedirect(w, r, "the superseding request is a number") | |
| 83 | return | |
| 84 | } | |
| 85 | args = append(args, "--by", by) | |
| 86 | } | |
| 87 | _, msg, code := s.runControlCode(u, mrArgs(r, "close", args...)) | |
| 80 | 88 | s.done(w, r, code, msg, s.mrRedirect) |
| 81 | 89 | } |
| 82 | 90 | |
internal/httpd/web.go +5 −1
| @@ -1957,6 +1957,9 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | ||
| 1957 | 1957 | stacked, _ = s.st.OpenMRsByTarget(p.Repo.ID, m.SourceRef) |
| 1958 | 1958 | } |
| 1959 | 1959 | } |
| 1960 | // The merge requests this one superseded when it was closed, so the | |
| 1961 | // page it points to can also say what it supersedes. | |
| 1962 | supersedes, _ := s.st.MRsSuperseding(p.Repo.ID, m.Number) | |
| 1960 | 1963 | s.render(w, "mr.html", struct { |
| 1961 | 1964 | repoPage |
| 1962 | 1965 | MR store.MR |
| @@ -1980,6 +1983,7 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | ||
| 1980 | 1983 | DetachedThreads []diffThread |
| 1981 | 1984 | StackedOn *store.MR |
| 1982 | 1985 | Stacked []store.MR |
| 1986 | Supersedes []store.MR | |
| 1983 | 1987 | Gates *control.GatesOut |
| 1984 | 1988 | SourceGone bool |
| 1985 | 1989 | HeadMerged bool |
| @@ -1987,7 +1991,7 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | ||
| 1987 | 1991 | Base string |
| 1988 | 1992 | }{p, m, view, md(m.Body, m.BodyFormat), checks, combined, renderComments(comments, md), |
| 1989 | 1993 | reviewRows, files, diffTruncated, stat, commits, commitsTotal, branches, s.canEditItem(r, p.Repo, m.Author), |
| 1990 | canWrite, unresolved, revisions, s.takeFlash(w, r), detachedThreads, stackedOn, stacked, gates, | |
| 1994 | canWrite, unresolved, revisions, s.takeFlash(w, r), detachedThreads, stackedOn, stacked, supersedes, gates, | |
| 1991 | 1995 | sourceGone(p, m), headMerged, headPruned, base}) |
| 1992 | 1996 | } |
| 1993 | 1997 | |
internal/web/templates/mr.html +4 −1
| @@ -5,12 +5,13 @@ | ||
| 5 | 5 | <h1 class="issuetitle">{{.MR.Title}} <span class="issuenumber">!{{.MR.Number}}</span></h1> |
| 6 | 6 | <p class="issuemeta"><span class="chip chip-{{.MR.State}}">{{if eq .MR.State "source_gone"}}source gone{{else}}{{.MR.State}}{{end}}</span>{{if .MR.Draft}} <span class="chip chip-neutral">draft</span>{{end}} |
| 7 | 7 | {{if and (eq .MR.State "merged") .MR.MergedBy}}merged by <a href="/{{.MR.MergedBy}}">{{.MR.MergedBy}}</a>{{with .MR.MergedAt}} on {{when .}}{{end}} |
| 8 | {{else if and (eq .MR.State "closed") .MR.ClosedBy}}closed without merging by <a href="/{{.MR.ClosedBy}}">{{.MR.ClosedBy}}</a>{{with .MR.ClosedAt}} on {{when .}}{{end}} | |
| 8 | {{else if and (eq .MR.State "closed") .MR.ClosedBy}}closed without merging by <a href="/{{.MR.ClosedBy}}">{{.MR.ClosedBy}}</a>{{with .MR.ClosedAt}} on {{when .}}{{end}}{{if .MR.SupersededBy}}, in favour of <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs/{{.MR.SupersededBy}}">!{{.MR.SupersededBy}}</a>{{end}} | |
| 9 | 9 | {{else if or (eq .MR.State "merged") (eq .MR.State "closed")}}{{if eq .MR.State "merged"}}merged{{else}}closed without merging{{end}} |
| 10 | 10 | {{else}}opened by <a href="/{{.MR.Author}}">{{.MR.Author}}</a> on {{when .MR.CreatedAt}}{{end}} |
| 11 | 11 | · <code>{{if .MR.SourcePath}}{{.MR.SourcePath}}:{{end}}{{.MR.SourceRef}}</code> into <code>{{.MR.TargetRef}}</code></p> |
| 12 | 12 | {{with field . "StackedOn"}}<p class="meta">Stacked on <a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{.Number}}">!{{.Number}} {{.Title}}</a>: merges into its branch until that lands, then onto its target.</p>{{end}} |
| 13 | 13 | {{with field . "Stacked"}}<p class="meta">Builds on this: {{range $i, $k := .}}{{if $i}}, {{end}}<a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{$k.Number}}">!{{$k.Number}} {{$k.Title}}</a>{{end}}. Merging with squash or rebase is refused while they are open.</p>{{end}} |
| 14 | {{with field . "Supersedes"}}<p class="meta">supersedes {{range $i, $m := .}}{{if $i}}, {{end}}<a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{$m.Number}}">!{{$m.Number}}</a>{{end}}</p>{{end}} | |
| 14 | 15 | |
| 15 | 16 | {{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}} |
| 16 | 17 | |
| @@ -103,6 +104,8 @@ | ||
| 103 | 104 | <button type="submit">Merge</button> |
| 104 | 105 | </form> |
| 105 | 106 | <form method="post" action="{{$base}}/close" class="actions"> |
| 107 | <label class="vh" for="by">Closed in favour of</label> | |
| 108 | <input type="text" id="by" name="by" inputmode="numeric" size="4" placeholder="!N"> | |
| 106 | 109 | <button type="submit" class="danger">Close without merging</button> |
| 107 | 110 | </form> |
| 108 | 111 | <form method="post" action="{{$base}}/draft" class="actions"> |