1package tinygit23import (4 "context"5 "errors"6 "fmt"7 "strings"8 "time"9)1011const collaborationDiffBytes = 512 * 102412const collaborationDiffTimeout = 5 * time.Second1314// CollaborationDiff is the result of rendering a pull request's local Git15// objects. Status is "ok" when Diff is authoritative and16// "diff_unavailable" when one or both commits are not present locally.17type CollaborationDiff struct {18 Status string `json:"status"`19 Base string `json:"base,omitempty"`20 Tip string `json:"tip,omitempty"`21 MergeBase string `json:"merge_base,omitempty"`22 Diff string `json:"diff,omitempty"`23 Truncated bool `json:"truncated,omitempty"`24 Reason string `json:"reason,omitempty"`25}2627// PRDiff renders a bounded diff from commits already present in the28// repository. It never fetches or resolves a remote ref. A supplied base is29// used only after it is verified as a local commit; when it is empty, Git's30// merge-base with the repository target is used.31func (g *GitRelay) PRDiff(ctx context.Context, r Repository, base, tip string) CollaborationDiff {32 ctx, cancel := context.WithTimeout(ctx, collaborationDiffTimeout)33 defer cancel()34 result := CollaborationDiff{Status: "diff_unavailable", Base: base, Tip: tip}35 if !isObjectID(base) && base != "" {36 result.Reason = "invalid base commit"37 return result38 }39 if !isObjectID(tip) {40 result.Reason = "invalid tip commit"41 return result42 }43 if err := g.localCommit(ctx, r, tip); err != nil {44 result.Reason = "tip commit is unavailable locally"45 return result46 }47 if base != "" {48 if err := g.localCommit(ctx, r, base); err != nil {49 result.Reason = "base commit is unavailable locally"50 return result51 }52 }53 target, err := g.targetCommit(ctx, r)54 if err != nil {55 result.Reason = "repository target commit is unavailable locally"56 return result57 }58 if base != "" {59 // A PR base is meaningful only when it is reachable from both sides of60 // the comparison. Checking the current target as well prevents an old61 // or malicious PR-only ancestor from being presented as the repository62 // diff base.63 if err := g.browseCommand(ctx, r, "merge-base", "--is-ancestor", base, tip).Run(); err != nil {64 result.Reason = "base is not an ancestor of tip"65 return result66 }67 if err := g.browseCommand(ctx, r, "merge-base", "--is-ancestor", base, target).Run(); err != nil {68 result.Reason = "base is not an ancestor of repository target"69 return result70 }71 result.MergeBase = base72 } else {73 b, err := g.browseCommand(ctx, r, "merge-base", "--", tip, target).Output()74 if err != nil || !isObjectID(strings.TrimSpace(string(b))) {75 result.Reason = "merge base is unavailable locally"76 return result77 }78 result.MergeBase = strings.TrimSpace(string(b))79 result.Base = result.MergeBase80 }8182 b, truncated, err := readBrowseOutput(g.browseCommand(ctx, r, "diff", "--format=", "--no-ext-diff", "--no-textconv", "--no-renames", "--binary", result.MergeBase, tip, "--"), collaborationDiffBytes)83 if err != nil {84 result.Reason = "unable to render local diff"85 return result86 }87 result.Status = "ok"88 result.Diff = strings.ToValidUTF8(string(b), "�")89 result.Truncated = truncated90 return result91}9293func (g *GitRelay) targetCommit(ctx context.Context, r Repository) (string, error) {94 target := strings.TrimPrefix(r.Head, "ref: ")95 if target == "" || !validBrowsePath(target) {96 return "", errors.New("missing repository target")97 }98 b, err := g.browseCommand(ctx, r, "rev-parse", "--verify", "--end-of-options", target+"^{commit}").Output()99 if err != nil {100 return "", err101 }102 target = strings.TrimSpace(string(b))103 if !isObjectID(target) || g.localCommit(ctx, r, target) != nil {104 return "", errors.New("target commit unavailable")105 }106 return target, nil107}108109func (g *GitRelay) localCommit(ctx context.Context, r Repository, oid string) error {110 if !isObjectID(oid) {111 return errors.New("invalid object id")112 }113 b, err := g.browseCommand(ctx, r, "cat-file", "-t", oid).Output()114 if err != nil || strings.TrimSpace(string(b)) != "commit" {115 return fmt.Errorf("commit unavailable: %s", oid)116 }117 return nil118}