forked from tangled.org/core
Monorepo for Tangled

appview: state: introduce get route to check if fork is syncable

authored by brookjeynes.dev and committed by Tangled 65defd9c a26ee40d

Changed files
+79 -72
appview
knotclient
pages
state
knotserver
types
+23 -2
appview/knotclient/signer.go
··· 106 106 return s.client.Do(req) 107 107 } 108 108 109 + func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) { 110 + const ( 111 + Method = "GET" 112 + ) 113 + endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 114 + 115 + body, _ := json.Marshal(map[string]any{ 116 + "did": ownerDid, 117 + "source": source, 118 + "name": name, 119 + "hiddenref": hiddenRef, 120 + }) 121 + 122 + req, err := s.newRequest(Method, endpoint, body) 123 + if err != nil { 124 + return nil, err 125 + } 126 + 127 + return s.client.Do(req) 128 + } 129 + 109 130 func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) { 110 131 const ( 111 - Method = "POST" 132 + Method = "POST" 112 133 ) 113 - endpoint := fmt.Sprintf("/repo/fork/sync/%s", branch) 134 + endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 114 135 115 136 body, _ := json.Marshal(map[string]any{ 116 137 "did": ownerDid,
+1 -15
appview/pages/pages.go
··· 402 402 return p.executePlain("repo/fragments/repoDescription", w, params) 403 403 } 404 404 405 - type ForkStatus int 406 - 407 - const ( 408 - UpToDate ForkStatus = 0 409 - FastForwardable = 1 410 - Conflict = 2 411 - MissingBranch = 3 412 - ) 413 - 414 - type ForkInfo struct { 415 - IsFork bool 416 - Status ForkStatus 417 - } 418 - 419 405 type RepoIndexParams struct { 420 406 LoggedInUser *oauth.User 421 407 RepoInfo repoinfo.RepoInfo ··· 424 410 CommitsTrunc []*object.Commit 425 411 TagsTrunc []*types.TagReference 426 412 BranchesTrunc []types.Branch 427 - ForkInfo ForkInfo 413 + ForkInfo types.ForkInfo 428 414 types.RepoIndexResponse 429 415 HTMLReadme template.HTML 430 416 Raw bool
+21 -46
appview/state/repo.go
··· 25 25 "tangled.sh/tangled.sh/core/appview/pages/markup" 26 26 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 27 27 "tangled.sh/tangled.sh/core/appview/pagination" 28 - "tangled.sh/tangled.sh/core/knotserver" 29 28 "tangled.sh/tangled.sh/core/types" 30 29 31 30 "github.com/bluesky-social/indigo/atproto/data" ··· 124 123 user := s.oauth.GetUser(r) 125 124 repoInfo := f.RepoInfo(s, user) 126 125 127 - forkInfo, err := GetForkInfo(repoInfo, s, f, us, w, user) 126 + forkInfo, err := getForkInfo(repoInfo, s, f, us, w, user) 128 127 if err != nil { 129 128 log.Printf("Failed to fetch fork information: %v", err) 130 129 return ··· 144 143 return 145 144 } 146 145 147 - func GetForkInfo( 146 + func getForkInfo( 148 147 repoInfo repoinfo.RepoInfo, 149 148 s *State, 150 149 f *FullyResolvedRepo, 151 150 us *knotclient.UnsignedClient, 152 151 w http.ResponseWriter, 153 152 user *oauth.User, 154 - ) (*pages.ForkInfo, error) { 155 - forkInfo := pages.ForkInfo{ 153 + ) (*types.ForkInfo, error) { 154 + forkInfo := types.ForkInfo{ 156 155 IsFork: repoInfo.Source != nil, 157 - Status: pages.UpToDate, 156 + Status: types.UpToDate, 158 157 } 159 158 160 159 secret, err := db.GetRegistrationKey(s.db, f.Knot) ··· 187 186 return nil, err 188 187 } 189 188 190 - var contains = false 191 - for _, branch := range result.Branches { 192 - if branch.Name == f.Ref { 193 - contains = true 194 - break 195 - } 196 - } 197 - 198 - if contains == false { 199 - forkInfo.Status = pages.MissingBranch 189 + if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 190 + return branch.Name == f.Ref 191 + }) { 192 + forkInfo.Status = types.MissingBranch 200 193 return &forkInfo, nil 201 194 } 202 195 ··· 213 206 } 214 207 215 208 hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 216 - comparison, err := us.Compare(user.Did, repoInfo.Name, f.Ref, hiddenRef) 217 - if err != nil { 218 - log.Printf("failed to compare branches '%s' and '%s': %s", f.Ref, hiddenRef, err) 219 - return nil, err 220 - } 221 209 222 - if len(comparison.FormatPatch) == 0 { 223 - return &forkInfo, nil 224 - } 225 - 226 - var isAncestor types.AncestorCheckResponse 227 - forkSyncableResp, err := signedClient.RepoForkSyncable(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 210 + var status types.AncestorCheckResponse 211 + forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 228 212 if err != nil { 229 - log.Printf("failed to check if fork is syncable: %s", err) 213 + log.Printf("failed to check if fork is ahead/behind: %s", err) 230 214 return nil, err 231 215 } 232 216 233 - if err := json.NewDecoder(forkSyncableResp.Body).Decode(&isAncestor); err != nil { 234 - log.Printf("failed to decode 'isAncestor': %s", err) 217 + if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil { 218 + log.Printf("failed to decode fork status: %s", err) 235 219 return nil, err 236 220 } 237 221 238 - if isAncestor.IsAncestor { 239 - forkInfo.Status = pages.FastForwardable 240 - } else { 241 - forkInfo.Status = pages.Conflict 242 - } 243 - 222 + forkInfo.Status = status.Status 244 223 return &forkInfo, nil 245 224 } 246 225 ··· 1916 1895 } 1917 1896 1918 1897 func (s *State) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1919 - user := s.auth.GetUser(r) 1898 + user := s.oauth.GetUser(r) 1920 1899 f, err := s.fullyResolvedRepo(r) 1921 1900 if err != nil { 1922 1901 log.Printf("failed to resolve source repo: %v", err) 1923 1902 return 1924 1903 } 1925 1904 1926 - params := r.URL.Query() 1927 - knot := params.Get("knot") 1928 - branch := params.Get("branch") 1929 - 1930 1905 switch r.Method { 1931 1906 case http.MethodPost: 1932 - secret, err := db.GetRegistrationKey(s.db, knot) 1907 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 1933 1908 if err != nil { 1934 - s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1909 + s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) 1935 1910 return 1936 1911 } 1937 1912 1938 - client, err := NewSignedClient(knot, secret, s.config.Dev) 1913 + client, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1939 1914 if err != nil { 1940 1915 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1941 1916 return 1942 1917 } 1943 1918 1944 1919 var uri string 1945 - if s.config.Dev { 1920 + if s.config.Core.Dev { 1946 1921 uri = "http" 1947 1922 } else { 1948 1923 uri = "https" ··· 1950 1925 forkName := fmt.Sprintf("%s", f.RepoName) 1951 1926 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1952 1927 1953 - _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, branch) 1928 + _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1954 1929 if err != nil { 1955 1930 s.pages.Notice(w, "repo", "Failed to sync repository fork.") 1956 1931 return
+3 -1
appview/state/router.go
··· 113 113 r.Use(middleware.AuthMiddleware(s.oauth)) 114 114 r.Get("/", s.ForkRepo) 115 115 r.Post("/", s.ForkRepo) 116 - r.Post("/sync", s.SyncRepoFork) 116 + r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/sync", func(r chi.Router) { 117 + r.Post("/", s.SyncRepoFork) 118 + }) 117 119 }) 118 120 119 121 r.Route("/pulls", func(r chi.Router) {
+1 -1
knotserver/handler.go
··· 129 129 r.Route("/fork", func(r chi.Router) { 130 130 r.Post("/", h.RepoFork) 131 131 r.Post("/sync/{branch}", h.RepoForkSync) 132 - r.Get("/sync/{branch}", h.RepoForkSyncable) 132 + r.Get("/sync/{branch}", h.RepoForkAheadBehind) 133 133 }) 134 134 }) 135 135
+15 -6
knotserver/routes.go
··· 631 631 w.WriteHeader(http.StatusNoContent) 632 632 } 633 633 634 - func (h *Handle) RepoForkSyncable(w http.ResponseWriter, r *http.Request) { 634 + func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { 635 635 l := h.l.With("handler", "RepoForkSync") 636 636 637 637 data := struct { ··· 689 689 return 690 690 } 691 691 692 - isAncestor, err := forkCommit.IsAncestor(sourceCommit) 693 - if err != nil { 694 - log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 695 - return 692 + status := types.UpToDate 693 + if forkCommit.Hash.String() != sourceCommit.Hash.String() { 694 + isAncestor, err := forkCommit.IsAncestor(sourceCommit) 695 + if err != nil { 696 + log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 697 + return 698 + } 699 + 700 + if isAncestor { 701 + status = types.FastForwardable 702 + } else { 703 + status = types.Conflict 704 + } 696 705 } 697 706 698 707 w.Header().Set("Content-Type", "application/json") 699 - json.NewEncoder(w).Encode(types.AncestorCheckResponse{IsAncestor: isAncestor}) 708 + json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status}) 700 709 } 701 710 702 711 func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
+15 -1
types/repo.go
··· 91 91 SizeHint uint64 `json:"size_hint,omitempty"` 92 92 } 93 93 94 + type ForkStatus int 95 + 96 + const ( 97 + UpToDate ForkStatus = 0 98 + FastForwardable = 1 99 + Conflict = 2 100 + MissingBranch = 3 101 + ) 102 + 103 + type ForkInfo struct { 104 + IsFork bool 105 + Status ForkStatus 106 + } 107 + 94 108 type AncestorCheckResponse struct { 95 - IsAncestor bool `json:"isAncestor"` 109 + Status ForkStatus `json:"status"` 96 110 }