This repository has no description
1package pulls
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "html/template"
8 "io"
9 "log/slog"
10 "net/http"
11 "strings"
12
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "golang.org/x/sync/errgroup"
15 "tangled.org/core/appview/models"
16 "tangled.org/core/appview/pages"
17 gitmirrorv1 "tangled.org/core/gitmirror/proto/gen"
18)
19
20// htmx fragment. render diff between commits
21func (s *Pulls) PullDiffFragment(w http.ResponseWriter, r *http.Request) {
22 l := s.logger.With("handler", "PullDiffFragment")
23 ctx := r.Context()
24
25 var (
26 baseRepoRaw = r.URL.Query().Get("baseRepo")
27 headRepoRaw = r.URL.Query().Get("headRepo")
28 base = r.URL.Query().Get("base") // base commit SHA
29 head = r.URL.Query().Get("head") // head commit SHA
30 unified = r.URL.Query().Get("view") == "unified"
31 )
32 baseRepo, err := syntax.ParseDID(baseRepoRaw)
33 if err != nil {
34 if err := s.pages.PullDiffFragment(w, pages.PullDiffFragmentParams{
35 ErrorMsg: "invalid base repository DID",
36 }); err != nil {
37 l.Error("failed to render", "err", err)
38 }
39 return
40 }
41 headRepo, err := syntax.ParseDID(headRepoRaw)
42 if err != nil {
43 if err := s.pages.PullDiffFragment(w, pages.PullDiffFragmentParams{
44 ErrorMsg: "invalid head repository DID",
45 }); err != nil {
46 l.Error("failed to render", "err", err)
47 }
48 return
49 }
50 l.Debug("diff fragment", "baseRepo", baseRepo, "base", base, "headRepo", headRepo, "head", head)
51
52 var params pages.PullDiffFragmentParams
53 params.BaseRepo = baseRepo
54 params.HeadRepo = headRepo
55 params.DiffBase = base
56 params.DiffHead = head
57 params.DiffUrl = r.URL.Path
58 params.Unified = unified
59 params.Files, params.ErrorMsg = s.diffFragmentParams(ctx, l, baseRepo, base, headRepo, head, unified)
60 if err := s.pages.PullDiffFragment(w, params); err != nil {
61 l.Error("failed to render", "err", err)
62 }
63}
64
65func (s *Pulls) diffFragmentParams(ctx context.Context, l *slog.Logger, baseRepo syntax.DID, base string, headRepo syntax.DID, head string, unified bool) ([]pages.DiffFile, string) {
66 var err error
67 if base != "" && !models.IsHash(base) {
68 l := l.With("rev", base, "repo", baseRepo)
69 if base, err = s.resolveRev(ctx, baseRepo, base); err != nil {
70 l.Error("failed to resolve base revision", "err", err)
71 return nil, "Failed to resolve base revision."
72 }
73 }
74 if head != "" && !models.IsHash(head) {
75 l := l.With("rev", head, "repo", headRepo)
76 if head, err = s.resolveRev(ctx, headRepo, head); err != nil {
77 l.Error("failed to resolve head revision", "err", err)
78 return nil, "Failed to resolve head revision."
79 }
80 }
81
82 // a. drain the diff stream into one fileDiff per changed file.
83 req := &gitmirrorv1.DiffRequest{
84 Head: &gitmirrorv1.RepoCommit{Repo: headRepo.String(), Commit: []byte(head)},
85 }
86 if base != "" {
87 req.Base = &gitmirrorv1.RepoCommit{Repo: baseRepo.String(), Commit: []byte(base)}
88 }
89 stream, err := s.gitmirror.Diff(ctx, req)
90 if err != nil {
91 l.Error("failed to diff", "err", err)
92 return nil, "Failed to diff. Try again later."
93 }
94 var files []*fileDiff
95 for {
96 fd, err := stream.Recv()
97 if errors.Is(err, io.EOF) {
98 break
99 }
100 if err != nil {
101 l.Error("failed to drain diff response", "err", err)
102 return nil, "Failed to diff. Try again later."
103 }
104 files = append(files, &fileDiff{diff: fd})
105 }
106
107 // b. fetch each file's base/head blob in parallel and split into lines.
108 g, gctx := errgroup.WithContext(ctx)
109 for _, f := range files {
110 g.Go(func() error {
111 lhs, rhs := f.diff.GetLhsSrc(), f.diff.GetRhsSrc()
112 // Binary/submodule files have no line content to fetch.
113 if isBinaryOrSubmodule(lhs) || isBinaryOrSubmodule(rhs) {
114 return nil
115 }
116 baseBlob, err := s.getBlob(gctx, baseRepo, lhs.GetOid())
117 if err != nil {
118 return err
119 }
120 headBlob, err := s.getBlob(gctx, headRepo, rhs.GetOid())
121 if err != nil {
122 return err
123 }
124
125 // TODO: highlight each blobs
126 // pass lhs_positions & rhs_positions so highlighter can apply diff highlights
127 for line := range strings.SplitSeq(strings.TrimSuffix(string(baseBlob), "\n"), "\n") {
128 f.baseLines = append(f.baseLines, template.HTML(fmt.Sprintf("<div><span>%s</span></div>", template.HTMLEscapeString(line))))
129 }
130 for line := range strings.SplitSeq(strings.TrimSuffix(string(headBlob), "\n"), "\n") {
131 f.headLines = append(f.headLines, template.HTML(fmt.Sprintf("<div><span>%s</span></div>", template.HTMLEscapeString(line))))
132 }
133
134 // f.baseLines = strings.Split(strings.TrimSuffix(string(baseBlob), "\n"), "\n")
135 // f.headLines = strings.Split(strings.TrimSuffix(string(headBlob), "\n"), "\n")
136 return nil
137 })
138 }
139 if err := g.Wait(); err != nil {
140 l.Error("failed to prepare html", "err", err)
141 return nil, "Failed to render diff. Try again later."
142 }
143
144 // c. build the render model
145 var outFiles []pages.DiffFile
146 for _, f := range files {
147 df := pages.DiffFile{Path: f.diff.GetRhsSrc().GetPath()}
148
149 if isBinaryOrSubmodule(f.diff.GetLhsSrc()) || isBinaryOrSubmodule(f.diff.GetRhsSrc()) {
150 df.Note = "binary or submodule"
151 outFiles = append(outFiles, df)
152 continue
153 }
154
155 for _, h := range buildHunks(f.baseLines, f.headLines, f.diff.Hunks) {
156 var dh pages.DiffHunk
157 if unified {
158 dh.Lines = buildUnifiedLines(h, f.baseLines, f.headLines)
159 } else {
160 dh.Rows = buildSplitRows(h, f.baseLines, f.headLines)
161 }
162 df.Hunks = append(df.Hunks, dh)
163 }
164 outFiles = append(outFiles, df)
165 }
166
167 return outFiles, ""
168}