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