package pulls import ( "context" "errors" "fmt" "html/template" "io" "log/slog" "net/http" "strings" "github.com/bluesky-social/indigo/atproto/syntax" "golang.org/x/sync/errgroup" "tangled.org/core/appview/models" "tangled.org/core/appview/pages" gitmirrorv1 "tangled.org/core/gitmirror/proto/gen" ) // htmx fragment. render diff between commits func (s *Pulls) PullDiffFragment(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "PullDiffFragment") ctx := r.Context() var ( baseRepoRaw = r.URL.Query().Get("baseRepo") headRepoRaw = r.URL.Query().Get("headRepo") base = r.URL.Query().Get("base") // base commit SHA head = r.URL.Query().Get("head") // head commit SHA unified = r.URL.Query().Get("view") == "unified" ) baseRepo, err := syntax.ParseDID(baseRepoRaw) if err != nil { if err := s.pages.PullDiffFragment(w, pages.PullDiffFragmentParams{ ErrorMsg: "invalid base repository DID", }); err != nil { l.Error("failed to render", "err", err) } return } headRepo, err := syntax.ParseDID(headRepoRaw) if err != nil { if err := s.pages.PullDiffFragment(w, pages.PullDiffFragmentParams{ ErrorMsg: "invalid head repository DID", }); err != nil { l.Error("failed to render", "err", err) } return } l.Debug("diff fragment", "baseRepo", baseRepo, "base", base, "headRepo", headRepo, "head", head) var params pages.PullDiffFragmentParams params.BaseRepo = baseRepo params.HeadRepo = headRepo params.DiffBase = base params.DiffHead = head params.DiffUrl = r.URL.Path params.Unified = unified params.Files, params.ErrorMsg = s.diffFragmentParams(ctx, l, baseRepo, base, headRepo, head, unified) if err := s.pages.PullDiffFragment(w, params); err != nil { l.Error("failed to render", "err", err) } } func (s *Pulls) diffFragmentParams(ctx context.Context, l *slog.Logger, baseRepo syntax.DID, base string, headRepo syntax.DID, head string, unified bool) ([]pages.DiffFile, string) { var err error if base != "" && !models.IsHash(base) { l := l.With("rev", base, "repo", baseRepo) if base, err = s.resolveRev(ctx, baseRepo, base); err != nil { l.Error("failed to resolve base revision", "err", err) return nil, "Failed to resolve base revision." } } if head != "" && !models.IsHash(head) { l := l.With("rev", head, "repo", headRepo) if head, err = s.resolveRev(ctx, headRepo, head); err != nil { l.Error("failed to resolve head revision", "err", err) return nil, "Failed to resolve head revision." } } // a. drain the diff stream into one fileDiff per changed file. req := &gitmirrorv1.DiffRequest{ Head: &gitmirrorv1.RepoCommit{Repo: headRepo.String(), Commit: []byte(head)}, } if base != "" { req.Base = &gitmirrorv1.RepoCommit{Repo: baseRepo.String(), Commit: []byte(base)} } stream, err := s.gitmirror.Diff(ctx, req) if err != nil { l.Error("failed to diff", "err", err) return nil, "Failed to diff. Try again later." } var files []*fileDiff for { fd, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { l.Error("failed to drain diff response", "err", err) return nil, "Failed to diff. Try again later." } files = append(files, &fileDiff{diff: fd}) } // b. fetch each file's base/head blob in parallel and split into lines. 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 } baseBlob, err := s.getBlob(gctx, baseRepo, lhs.GetOid()) if err != nil { return err } headBlob, err := s.getBlob(gctx, headRepo, rhs.GetOid()) if err != nil { return err } // TODO: highlight each blobs // pass lhs_positions & rhs_positions so highlighter can apply diff highlights for line := range strings.SplitSeq(strings.TrimSuffix(string(baseBlob), "\n"), "\n") { f.baseLines = append(f.baseLines, template.HTML(fmt.Sprintf("