From 65defd9ca7366b31b9212b6492cea52532b34d3d Mon Sep 17 00:00:00 2001 From: BrookJeynes Date: Tue, 13 May 2025 17:29:50 +1000 Subject: [PATCH] appview: state: introduce get route to check if fork is syncable --- appview/knotclient/signer.go | 25 ++++++++++++-- appview/pages/pages.go | 16 +-------- appview/state/repo.go | 67 +++++++++++------------------------- appview/state/router.go | 4 ++- knotserver/handler.go | 2 +- knotserver/routes.go | 21 +++++++---- types/repo.go | 16 ++++++++- 7 files changed, 79 insertions(+), 72 deletions(-) diff --git a/appview/knotclient/signer.go b/appview/knotclient/signer.go index e878ae3..fbb7eeb 100644 --- a/appview/knotclient/signer.go +++ b/appview/knotclient/signer.go @@ -106,11 +106,32 @@ func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Respo return s.client.Do(req) } +func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) { + const ( + Method = "GET" + ) + endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) + + body, _ := json.Marshal(map[string]any{ + "did": ownerDid, + "source": source, + "name": name, + "hiddenref": hiddenRef, + }) + + req, err := s.newRequest(Method, endpoint, body) + if err != nil { + return nil, err + } + + return s.client.Do(req) +} + func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) { const ( - Method = "POST" + Method = "POST" ) - endpoint := fmt.Sprintf("/repo/fork/sync/%s", branch) + endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) body, _ := json.Marshal(map[string]any{ "did": ownerDid, diff --git a/appview/pages/pages.go b/appview/pages/pages.go index f74cddb..4deaaef 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -402,20 +402,6 @@ func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParam return p.executePlain("repo/fragments/repoDescription", w, params) } -type ForkStatus int - -const ( - UpToDate ForkStatus = 0 - FastForwardable = 1 - Conflict = 2 - MissingBranch = 3 -) - -type ForkInfo struct { - IsFork bool - Status ForkStatus -} - type RepoIndexParams struct { LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo @@ -424,7 +410,7 @@ type RepoIndexParams struct { CommitsTrunc []*object.Commit TagsTrunc []*types.TagReference BranchesTrunc []types.Branch - ForkInfo ForkInfo + ForkInfo types.ForkInfo types.RepoIndexResponse HTMLReadme template.HTML Raw bool diff --git a/appview/state/repo.go b/appview/state/repo.go index 791f909..d37d993 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -25,7 +25,6 @@ import ( "tangled.sh/tangled.sh/core/appview/pages/markup" "tangled.sh/tangled.sh/core/appview/pages/repoinfo" "tangled.sh/tangled.sh/core/appview/pagination" - "tangled.sh/tangled.sh/core/knotserver" "tangled.sh/tangled.sh/core/types" "github.com/bluesky-social/indigo/atproto/data" @@ -124,7 +123,7 @@ func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { user := s.oauth.GetUser(r) repoInfo := f.RepoInfo(s, user) - forkInfo, err := GetForkInfo(repoInfo, s, f, us, w, user) + forkInfo, err := getForkInfo(repoInfo, s, f, us, w, user) if err != nil { log.Printf("Failed to fetch fork information: %v", err) return @@ -144,17 +143,17 @@ func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { return } -func GetForkInfo( +func getForkInfo( repoInfo repoinfo.RepoInfo, s *State, f *FullyResolvedRepo, us *knotclient.UnsignedClient, w http.ResponseWriter, user *oauth.User, -) (*pages.ForkInfo, error) { - forkInfo := pages.ForkInfo{ +) (*types.ForkInfo, error) { + forkInfo := types.ForkInfo{ IsFork: repoInfo.Source != nil, - Status: pages.UpToDate, + Status: types.UpToDate, } secret, err := db.GetRegistrationKey(s.db, f.Knot) @@ -187,16 +186,10 @@ func GetForkInfo( return nil, err } - var contains = false - for _, branch := range result.Branches { - if branch.Name == f.Ref { - contains = true - break - } - } - - if contains == false { - forkInfo.Status = pages.MissingBranch + if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { + return branch.Name == f.Ref + }) { + forkInfo.Status = types.MissingBranch return &forkInfo, nil } @@ -213,34 +206,20 @@ func GetForkInfo( } hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) - comparison, err := us.Compare(user.Did, repoInfo.Name, f.Ref, hiddenRef) - if err != nil { - log.Printf("failed to compare branches '%s' and '%s': %s", f.Ref, hiddenRef, err) - return nil, err - } - - if len(comparison.FormatPatch) == 0 { - return &forkInfo, nil - } - var isAncestor types.AncestorCheckResponse - forkSyncableResp, err := signedClient.RepoForkSyncable(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) + var status types.AncestorCheckResponse + forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) if err != nil { - log.Printf("failed to check if fork is syncable: %s", err) + log.Printf("failed to check if fork is ahead/behind: %s", err) return nil, err } - if err := json.NewDecoder(forkSyncableResp.Body).Decode(&isAncestor); err != nil { - log.Printf("failed to decode 'isAncestor': %s", err) + if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil { + log.Printf("failed to decode fork status: %s", err) return nil, err } - if isAncestor.IsAncestor { - forkInfo.Status = pages.FastForwardable - } else { - forkInfo.Status = pages.Conflict - } - + forkInfo.Status = status.Status return &forkInfo, nil } @@ -1916,33 +1895,29 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { } func (s *State) SyncRepoFork(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Printf("failed to resolve source repo: %v", err) return } - params := r.URL.Query() - knot := params.Get("knot") - branch := params.Get("branch") - switch r.Method { case http.MethodPost: - secret, err := db.GetRegistrationKey(s.db, knot) + secret, err := db.GetRegistrationKey(s.db, f.Knot) if err != nil { - s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) + s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) return } - client, err := NewSignedClient(knot, secret, s.config.Dev) + client, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) if err != nil { s.pages.Notice(w, "repo", "Failed to reach knot server.") return } var uri string - if s.config.Dev { + if s.config.Core.Dev { uri = "http" } else { uri = "https" @@ -1950,7 +1925,7 @@ func (s *State) SyncRepoFork(w http.ResponseWriter, r *http.Request) { forkName := fmt.Sprintf("%s", f.RepoName) forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) - _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, branch) + _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) if err != nil { s.pages.Notice(w, "repo", "Failed to sync repository fork.") return diff --git a/appview/state/router.go b/appview/state/router.go index fe98dc7..fd3b072 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -113,7 +113,9 @@ func (s *State) UserRouter() http.Handler { r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/", s.ForkRepo) r.Post("/", s.ForkRepo) - r.Post("/sync", s.SyncRepoFork) + r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/sync", func(r chi.Router) { + r.Post("/", s.SyncRepoFork) + }) }) r.Route("/pulls", func(r chi.Router) { diff --git a/knotserver/handler.go b/knotserver/handler.go index 21d5f8a..f3b4239 100644 --- a/knotserver/handler.go +++ b/knotserver/handler.go @@ -129,7 +129,7 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j r.Route("/fork", func(r chi.Router) { r.Post("/", h.RepoFork) r.Post("/sync/{branch}", h.RepoForkSync) - r.Get("/sync/{branch}", h.RepoForkSyncable) + r.Get("/sync/{branch}", h.RepoForkAheadBehind) }) }) diff --git a/knotserver/routes.go b/knotserver/routes.go index 33d18ce..e517ec3 100644 --- a/knotserver/routes.go +++ b/knotserver/routes.go @@ -631,7 +631,7 @@ func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (h *Handle) RepoForkSyncable(w http.ResponseWriter, r *http.Request) { +func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { l := h.l.With("handler", "RepoForkSync") data := struct { @@ -689,14 +689,23 @@ func (h *Handle) RepoForkSyncable(w http.ResponseWriter, r *http.Request) { return } - isAncestor, err := forkCommit.IsAncestor(sourceCommit) - if err != nil { - log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) - return + status := types.UpToDate + if forkCommit.Hash.String() != sourceCommit.Hash.String() { + isAncestor, err := forkCommit.IsAncestor(sourceCommit) + if err != nil { + log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) + return + } + + if isAncestor { + status = types.FastForwardable + } else { + status = types.Conflict + } } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(types.AncestorCheckResponse{IsAncestor: isAncestor}) + json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status}) } func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { diff --git a/types/repo.go b/types/repo.go index 99a2692..daa3d13 100644 --- a/types/repo.go +++ b/types/repo.go @@ -91,6 +91,20 @@ type RepoBlobResponse struct { SizeHint uint64 `json:"size_hint,omitempty"` } +type ForkStatus int + +const ( + UpToDate ForkStatus = 0 + FastForwardable = 1 + Conflict = 2 + MissingBranch = 3 +) + +type ForkInfo struct { + IsFork bool + Status ForkStatus +} + type AncestorCheckResponse struct { - IsAncestor bool `json:"isAncestor"` + Status ForkStatus `json:"status"` } -- 2.51.2