This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pulls / diff.go
5.4 kB 168 lines
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}