| @@ -670,6 +670,8 @@ func (s *Server) pull(w http.ResponseWriter, r *http.Request, repo Repo, rest st |
| 670 | 670 | hash, err = g.Squash(pull.TargetBranch, pull.Ref(), message, pull.Author.Name, pull.Author.Name+"@"+s.host, user.Name, user.Name+"@"+s.host) |
| 671 | 671 | case "rebase": |
| 672 | 672 | hash, err = g.Rebase(pull.TargetBranch, pull.Ref(), user.Name, user.Name+"@"+s.host) |
| 673 | + case "ff": |
| 674 | + hash, err = g.FastForward(pull.TargetBranch, pull.Ref(), pull.baseRef()) |
| 673 | 675 | default: |
| 674 | 676 | hash, err = g.Merge(pull.TargetBranch, pull.Ref(), message, user.Name, user.Name+"@"+s.host) |
| 675 | 677 | } |
| @@ -714,7 +716,7 @@ func (s *Server) pull(w http.ResponseWriter, r *http.Request, repo Repo, rest st |
| 714 | 716 | } |
| 715 | 717 | base := pull.TargetBranch |
| 716 | 718 | if pull.State == "merged" && pull.Merged != "" { |
| 717 | | - base = pull.Merged + "^1" |
| 719 | + base = g.mergedBase(pull) |
| 718 | 720 | } |
| 719 | 721 | if mb, err := g.MergeBase(base, head); err == nil { |
| 720 | 722 | data["Base"] = mb |
| @@ -771,6 +773,7 @@ func (s *Server) pull(w http.ResponseWriter, r *http.Request, repo Repo, rest st |
| 771 | 773 | data["Blocked"] = fmt.Sprintf("needs %d approving reviews of the current head and no change requests", prot.Approvals) |
| 772 | 774 | data["Mergeable"] = false |
| 773 | 775 | } |
| 776 | + data["FastForward"] = g.IsAncestor(pull.TargetBranch, head) |
| 774 | 777 | if isOwner && data["Mergeable"] == true && r.URL.Query().Get("merge") == "1" { |
| 775 | 778 | data["Confirm"] = true |
| 776 | 779 | data["MergeTitle"] = mergeTitle(pull) |
| @@ -880,7 +883,7 @@ func (s *Server) interdiff(w http.ResponseWriter, r *http.Request, rp repoPage, |
| 880 | 883 | } |
| 881 | 884 | target := pull.TargetBranch |
| 882 | 885 | if pull.State == "merged" && pull.Merged != "" { |
| 883 | | - target = pull.Merged + "^1" |
| 886 | + target = g.mergedBase(pull) |
| 884 | 887 | } |
| 885 | 888 | baseA, errA := g.MergeBase(target, revs[from-1]) |
| 886 | 889 | baseB, errB := g.MergeBase(target, revs[to-1]) |
| @@ -925,3 +928,40 @@ func (s *Server) interdiff(w http.ResponseWriter, r *http.Request, rp repoPage, |
| 925 | 928 | rp.Tab = "pulls" |
| 926 | 929 | s.render(w, r, "interdiff", page{Title: fmt.Sprintf("%s #%d v%d to v%d", rp.Repo.Path(), pull.Number, from, to), Data: map[string]any{"Page": rp, "Pull": pull, "Revisions": revs, "From": from, "To": to, "Lines": lines}}) |
| 927 | 930 | } |
| 931 | + |
| 932 | +func (p Pull) baseRef() string { |
| 933 | + return fmt.Sprintf("refs/pull/%d/base", p.Number) |
| 934 | +} |
| 935 | + |
| 936 | +func (g Git) mergedBase(p Pull) string { |
| 937 | + if base, err := g.Resolve(p.baseRef()); err == nil { |
| 938 | + return base |
| 939 | + } |
| 940 | + return p.Merged + "^1" |
| 941 | +} |
| 942 | + |
| 943 | +func (g Git) IsAncestor(a, b string) bool { |
| 944 | + _, err := g.run(nil, "merge-base", "--is-ancestor", "--end-of-options", a, b) |
| 945 | + return err == nil |
| 946 | +} |
| 947 | + |
| 948 | +func (g Git) FastForward(target, head, baseRef string) (string, error) { |
| 949 | + targetHash, err := g.Resolve("refs/heads/" + target) |
| 950 | + if err != nil { |
| 951 | + return "", errors.New("no such target branch") |
| 952 | + } |
| 953 | + headHash, err := g.Resolve(head) |
| 954 | + if err != nil { |
| 955 | + return "", err |
| 956 | + } |
| 957 | + if headHash == targetHash || !g.IsAncestor(targetHash, headHash) { |
| 958 | + return "", errors.New("the target has moved on, so this cannot fast forward") |
| 959 | + } |
| 960 | + if _, err := g.run(nil, "update-ref", baseRef, targetHash); err != nil { |
| 961 | + return "", err |
| 962 | + } |
| 963 | + if _, err := g.run(nil, "update-ref", "refs/heads/"+target, headHash, targetHash); err != nil { |
| 964 | + return "", errors.New("target branch moved, try again") |
| 965 | + } |
| 966 | + return headHash, nil |
| 967 | +} |