package pulls import ( "errors" "fmt" "html/template" "io" "net/http" "strings" "github.com/bluesky-social/indigo/atproto/syntax" "golang.org/x/sync/errgroup" "tangled.org/core/appview/pages" gitmirrorv1 "tangled.org/core/gitmirror/proto/gen" ) // htmx fragment. render interdiff between changes func (s *Pulls) PullInterdiffFragment(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "PullInterdiffFragment") ctx := r.Context() var ( repoRaw = r.URL.Query().Get("repo") // source repo DID base1 = r.URL.Query().Get("base1") // base1 commit ID head1 = r.URL.Query().Get("head1") // head1 commit ID base2 = r.URL.Query().Get("base2") // base2 commit ID head2 = r.URL.Query().Get("head2") // head2 commit ID unified = r.URL.Query().Get("view") == "unified" ) repo, err := syntax.ParseDID(repoRaw) if err != nil { http.Error(w, "invalid repo DID", http.StatusBadRequest) return } l.Debug("interdiff", "base1", base1, "head1", head1, "base2", base2, "head2", head2) var params pages.PullDiffFragmentParams params.DiffUrl = r.URL.Path params.Unified = unified defer func() { s.pages.PullDiffFragment(w, params) }() stream, err := s.gitmirror.Interdiff(ctx, &gitmirrorv1.InterdiffRequest{ Repo: repo.String(), FromBase: []byte(base1), FromHead: []byte(head1), ToBase: []byte(base2), ToHead: []byte(head2), }) if err != nil { l.Error("failed to interdiff", "err", err) params.ErrorMsg = "Failed to interdiff. Try again later." return } var files []*fileDiff for { fd, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { l.Error("failed to drain interdiff response", "err", err) params.ErrorMsg = "Failed to interdiff. Try again later." return } files = append(files, &fileDiff{diff: fd}) } g, gctx := errgroup.WithContext(ctx) for _, f := range files { g.Go(func() error { lhs, rhs := f.diff.GetLhsSrc(), f.diff.GetRhsSrc() // Binary/submodule files have no line content to fetch. if isBinaryOrSubmodule(lhs) || isBinaryOrSubmodule(rhs) { return nil } headBlob, err := s.getBlob(gctx, syntax.DID(repo), rhs.GetOid()) if err != nil { return err } if f.diff.LhsSrc.Content != nil { for line := range strings.SplitSeq(strings.TrimSuffix(string(f.diff.LhsSrc.Content), "\n"), "\n") { f.baseLines = append(f.baseLines, template.HTML(fmt.Sprintf("
%s
", template.HTMLEscapeString(line)))) } } for line := range strings.SplitSeq(strings.TrimSuffix(string(headBlob), "\n"), "\n") { f.headLines = append(f.headLines, template.HTML(fmt.Sprintf("
%s
", template.HTMLEscapeString(line)))) } return nil }) } if err := g.Wait(); err != nil { l.Error("failed to prepare interdiff UI", "err", err) params.ErrorMsg = "Failed to render interdiff. Try again later." return } for _, f := range files { df := pages.DiffFile{Path: f.diff.GetRhsSrc().GetPath()} if isBinaryOrSubmodule(f.diff.GetLhsSrc()) || isBinaryOrSubmodule(f.diff.GetRhsSrc()) { df.Note = "binary or submodule" params.Files = append(params.Files, df) continue } for _, h := range buildHunks(f.baseLines, f.headLines, f.diff.Hunks) { var dh pages.DiffHunk if unified { dh.Lines = buildUnifiedLines(h, f.baseLines, f.headLines) } else { dh.Rows = buildSplitRows(h, f.baseLines, f.headLines) } df.Hunks = append(df.Hunks, dh) } params.Files = append(params.Files, df) } }