Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532package pulls
import ( "context" "errors" "fmt" "io" "log/slog" "net/http" "strconv" "strings"
"github.com/bluesky-social/indigo/atproto/syntax" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/go-chi/chi/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "golang.org/x/sync/errgroup" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" "tangled.org/core/appview/models" "tangled.org/core/appview/oauth" "tangled.org/core/appview/pages" gitmirrorv1 "tangled.org/core/gitmirror/proto/gen" "tangled.org/core/hostutil" "tangled.org/core/orm" "tangled.org/core/types")
func (s *Pulls) RedirectLatestVersion(w http.ResponseWriter, r *http.Request) { pull, ok := r.Context().Value("pull").(*models.Pull) if !ok { s.logger.Error("failed to get pull") s.pages.Error500(w) return } u := r.URL.JoinPath(strconv.Itoa(pull.LatestVersionNumber())) http.Redirect(w, r, u.String(), http.StatusFound)}
func (s *Pulls) PullSingle(w http.ResponseWriter, r *http.Request) { if strings.Contains(chi.URLParam(r, "version"), "..") { s.PullInterDiff(w, r) } else { s.PullDiff(w, r) }}
// PullDiff is router for /pulls/{pull}/{version}/{commit}..{commit}//// Examples:// - /pulls/123/latest// - /pulls/123/2/head// - /pulls/123/2/base..head// - /pulls/123/2/a53ab251e..d8add468c// - /pulls/123/2/d8add468cfunc (s *Pulls) PullDiff(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "PullDiff") ctx := r.Context()
user := s.oauth.GetMultiAccountUser(r) if user != nil { l = l.With("user", user.Did) }
f, err := s.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) return }
pull, ok := r.Context().Value("pull").(*models.Pull) if !ok { l.Error("failed to get pull") s.pages.Error500(w) return }
var version models.PullVersion var versionIdRaw = chi.URLParam(r, "version") if versionIdRaw == "latest" { version = pull.LatestVersion() } else { versionId, err := strconv.Atoi(versionIdRaw) if err != nil { // invalid version number. redirect http.Redirect(w, r, fmt.Sprintf("/%s/pulls/%d/latest", pull.RepoDid, pull.PullId), http.StatusSeeOther, ) return } var ok bool version, ok = pull.GetVersion(versionId) if !ok { // invalid version number. redirect http.Redirect(w, r, fmt.Sprintf("/%s/pulls/%d/latest", pull.RepoDid, pull.PullId), http.StatusSeeOther, ) return } }
diffBase, diffHead, err := parseRange(chi.URLParam(r, "revspec")) if err != nil { http.Redirect(w, r, fmt.Sprintf("/%s/pulls/%d/%s", pull.RepoDid, pull.PullId, versionIdRaw), http.StatusSeeOther, ) return }
// defer render var params pages.PullDiffParams params.PullPageBaseParams = s.makePullPageBaseParams(r, user, f, pull) params.VersionId = version.ID defer func() { if err := s.pages.PullDiff(w, params); err != nil { l.Error("Failed to render", "err", err) } }()
// special cases // default to {target}..{current.head} if diffBase == "" || diffBase == "base" { diffBase = version.Base } if diffHead == "" || diffHead == "head" { diffHead = version.Head } params.DiffParams.Base = diffBase params.DiffParams.Head = diffHead
// TODO: Ideally we should show diff between <target-branch>..<pr-head>, // - but we can't find target branch's commit from forked repo. // - and we will get 0 diff when PR is merged. // so for now, we show diff between <pr-base>..<pr-head> // // resolve target branch -> (branch, commit) // xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} // branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) // if err != nil { // l.Warn("Failed to resolve target branch", "branch", pull.TargetBranch, "err", err) // params.ErrorMsg = fmt.Sprintf("Failed to resolve target branch %q", pull.TargetBranch) // return // } commits, err := s.listCommits(ctx, pull.SourceRepo, version.Base, version.Head) if err != nil { l.Error("failed to list commits", "err", err) params.ErrorMsg = "Failed to list commits. Try again later." return } params.Commits = commits
// commitId -> latest pipeline shas := make([]string, len(params.Commits)) for i, commit := range params.Commits { shas[i] = commit.Hash.String() } params.Pipelines = fetchPipelines(ctx, l, f, shas)}
// PullInterDiff is router for /pulls/{pull}/{version}..{version}/{change}//// Examples:// - /pulls/123/0..2/all// - /pulls/123/0..2/nrpytyzwfunc (s *Pulls) PullInterDiff(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "PullInterDiff") ctx := r.Context()
user := s.oauth.GetMultiAccountUser(r) if user != nil { l = l.With("user", user.Did) }
f, err := s.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) return }
pull, ok := r.Context().Value("pull").(*models.Pull) if !ok { s.logger.Error("failed to get pull") s.pages.Error500(w) return }
version1Raw, version2Raw, err := parseRange(chi.URLParam(r, "version")) if err != nil { http.Redirect(w, r, fmt.Sprintf("/%s/pulls/%d/0", pull.RepoDid, pull.PullId), http.StatusSeeOther, ) return } version1, err := strconv.Atoi(version1Raw) version2, err := strconv.Atoi(version2Raw) if err != nil { http.Redirect(w, r, fmt.Sprintf("/%s/pulls/%d/latest", pull.RepoDid, pull.PullId), http.StatusSeeOther, ) return }
changeId := chi.URLParam(r, "revspec") if changeId == "all" { changeId = "" }
// defer render var params pages.PullInterdiffParams params.PullPageBaseParams = s.makePullPageBaseParams(r, user, f, pull) params.Version1 = version1 params.Version2 = version2 params.ChangeId = changeId defer func() { if err := s.pages.PullInterdiff(w, params); err != nil { l.Error("Failed to render", "err", err) } }()
// TODO: Ideally we should show diff between <target-branch>..<pr-head>, // - but we can't find target branch's commit from forked repo. // - and we will get 0 diff when PR is merged. // so for now, we show diff between <pr-base>..<pr-head> // // resolve target branch -> (branch, commit) // xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} // branch, err := tangled.GitTempGetBranch(ctx, xrpcc, pull.TargetBranch, pull.RepoDid.String()) // if err != nil { // l.Warn("Failed to resolve target branch", "branch", pull.TargetBranch, "err", err) // params.ErrorMsg = fmt.Sprintf("Failed to resolve target branch %q", pull.TargetBranch) // return // } var commits1, commits2 []types.Commit g, gctx := errgroup.WithContext(ctx) if changeId != "" { g.Go(func() error { commits1, err = s.listCommits(gctx, pull.SourceRepo, pull.Versions[version1].Base, pull.Versions[version1].Head) return err }) } g.Go(func() error { commits2, err = s.listCommits(gctx, pull.SourceRepo, pull.Versions[version2].Base, pull.Versions[version2].Head) return err }) if err := g.Wait(); err != nil { l.Error("failed to list commits", "err", err) params.ErrorMsg = "Failed to list commits. Try again later." return } params.Commits = commits2
// commitId -> latest pipeline shas := make([]string, len(params.Commits)) for i, commit := range params.Commits { shas[i] = commit.Hash.String() } params.Pipelines = fetchPipelines(ctx, l, f, shas)
if changeId != "" { // interdiff by change-id var from, to *types.Commit for _, commit := range commits1 { if commit.ChangeId == changeId { from = &commit break } } for _, commit := range commits2 { if commit.ChangeId == changeId { to = &commit break } } l.Debug("commits", "old", from, "new", to)
switch { case to == nil: // can't find change-id from v2 branch. // NOTE: This can't happen because user selected from v2's commits params.ErrorMsg = "Can't find commit with given change-id." case from == nil: // new commit -> diff <parent1>..<new> params.ActiveCommitId = to.Hash.String() params.DiffParams.Diff = &pages.DiffParams_Diff{ Base: to.FirstParentHash().String(), Head: to.Hash.String(), } default: // interdiff params.ActiveCommitId = to.Hash.String() // TODO: use merged tree of all parents params.DiffParams.Interdiff = &pages.DiffParams_Interdiff{ From: pages.DiffParams_Diff{ Base: from.FirstParentHash().String(), Head: from.Hash.String(), }, To: pages.DiffParams_Diff{ Base: to.FirstParentHash().String(), Head: to.Hash.String(), }, } } } else { // interdiff of two versions params.DiffParams.Interdiff = &pages.DiffParams_Interdiff{ From: pages.DiffParams_Diff{ Base: pull.Versions[version1].Base, Head: pull.Versions[version1].Head, }, To: pages.DiffParams_Diff{ Base: pull.Versions[version2].Base, Head: pull.Versions[version2].Head, }, }
// TODO: if any of them is "", show error message }}
func (s *Pulls) PullPatchRaw(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "RepoPullPatchRaw")
pull, ok := r.Context().Value("pull").(*models.Pull) if !ok { l.Error("failed to get pull") s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") return } l = l.With("pull_id", pull.PullId)
var version models.PullVersion var versionIdRaw = chi.URLParam(r, "version") if versionIdRaw == "latest" { version = pull.LatestVersion() } else { versionId, err := strconv.Atoi(versionIdRaw) if err != nil { http.Error(w, "bad version id", http.StatusBadRequest) return } var ok bool version, ok = pull.GetVersion(versionId) if !ok { http.Error(w, "unknown version", http.StatusNotFound) return } }
xrpcc := &indigoxrpc.Client{Host: s.config.KnotMirror.Url} rawOut, err := tangled.GitTempFormatPatch(r.Context(), xrpcc, version.Base, pull.RepoDid.String(), version.Head) if err != nil { http.Error(w, "Failed to compute patch", http.StatusInternalServerError) return }
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write(rawOut)}
func (s *Pulls) makePullPageBaseParams(r *http.Request, user *oauth.MultiAccountUser, f *models.Repo, pull *models.Pull) pages.PullPageBaseParams { l := s.logger ctx := r.Context()
entities := []syntax.ATURI{pull.AtUri()} for _, v := range pull.Versions { for _, c := range v.Comments { entities = append(entities, c.FeedCommentAtUri()) } } reactions, err := db.ListReactionDisplayDataMap(s.db, entities, 20) if err != nil { l.Error("failed to get reactions", "err", err) }
var userReactions map[syntax.ATURI]map[models.ReactionKind]bool if user != nil { userReactions, err = db.ListReactionStatusMap(s.db, entities, syntax.DID(user.Did)) if err != nil { s.logger.Error("failed to get user reactions", "err", err) } }
labelDefs, err := db.GetLabelDefinitions( s.db, orm.FilterIn("at_uri", f.Labels), orm.FilterContains("scope", tangled.RepoPullNSID), ) if err != nil { l.Error("failed to fetch labels", "err", err) } defs := make(map[string]*models.LabelDefinition) for _, l := range labelDefs { defs[l.AtUri().String()] = &l }
vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) vouchSkips := make(map[syntax.DID]bool) if user != nil { participants := pull.Participants() vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), participants) if err != nil { l.Error("failed to fetch vouch relationships", "err", err) } ownerDid := syntax.DID(pull.OwnerDid) skipped, err := db.IsVouchSkipped(s.db, user.Did, pull.OwnerDid.String()) if err != nil { l.Error("failed to check vouch skip", "err", err) } vouchSkips[ownerDid] = skipped }
params := pages.PullPageBaseParams{} params.BaseParams = pages.BaseParamsFromContext(ctx) params.RepoInfo = s.repoResolver.GetRepoInfo(r, user) params.Pull = pull params.Backlinks = nil params.LabelDefs = defs params.Reactions = reactions params.UserReacted = userReactions params.VouchRelationships = vouchRelationships params.VouchSkips = vouchSkips return params}
func (s *Pulls) listCommits(ctx context.Context, repo syntax.DID, base, head string) ([]types.Commit, error) { s.logger.Debug("logging commits", "repo", repo, "base", base, "head", head) stream, err := s.gitmirror.CommitLog(ctx, &gitmirrorv1.CommitLogRequest{ Repo: repo.String(), Ranges: [][]byte{fmt.Appendf(nil, "%s..%s", base, head)}, AllRefs: false, }) if err != nil { return nil, err } var commits []types.Commit for { res, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { return nil, err } for _, commit := range res.Commits { commits = append(commits, types.Commit{ Hash: plumbing.NewHash(commit.Oid), // TODO: change-id Author: object.Signature{ Name: string(commit.Author.GetName()), Email: string(commit.Author.GetEmail()), When: commit.Author.Date.AsTime(), }, Committer: object.Signature{ Name: string(commit.Committer.GetName()), Email: string(commit.Committer.GetEmail()), When: commit.Committer.Date.AsTime(), }, Message: string(commit.Message), ParentHashes: func() []plumbing.Hash { var parents []plumbing.Hash for _, hash := range commit.Parents { parents = append(parents, plumbing.NewHash(hash)) } return parents }(), ChangeId: commit.ExtraHeaders["change-id"], }) } } return commits, err}
func fetchPipelines(ctx context.Context, l *slog.Logger, f *models.Repo, shas []string) map[string]types.Pipeline { m := make(map[string]types.Pipeline) if f.Spindle == "" || len(shas) == 0 { return m } spindleUrl, err := hostutil.EnsureHttpScheme(f.Spindle) if err != nil { l.Error("invalid spindle host", "host", f.Spindle, "err", err) return m } xrpcc := &indigoxrpc.Client{Host: spindleUrl} out, err := tangled.CiQueryPipelines(ctx, xrpcc, shas, "", 0, f.RepoDid) if err != nil { l.Error("failed to fetch pipelines", "err", err) return m }
for _, pipeline := range out.Pipelines { if pipeline == nil { continue } m[pipeline.Commit] = types.Pipeline{CiPipeline: pipeline} } return m}
// parseRange parses <base>..<base> stringfunc parseRange(input string) (base string, head string, err error) { input = strings.TrimSpace(input) if input == "" { return "", "", nil }
if strings.Count(input, "..") > 1 || strings.Contains(input, "...") { return "", "", fmt.Errorf("invalid revspec format: %q", input) }
if !strings.Contains(input, "..") { return "", input, nil }
parts := strings.SplitN(input, "..", 2) base = strings.TrimSpace(parts[0]) head = strings.TrimSpace(parts[1]) if base == "" && head == "" { return "", "", fmt.Errorf("invalid empty range: \"..\"") }
if head == "" { head = base base = "" }
return base, head, nil}