From bb0bdf8da8bc407bda1c34c7080559f83ec455b3 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 14 Apr 2026 12:06:18 +0300 Subject: [PATCH] appview: handlers, state, and templates for more record types w/ repoDID Lewis: May this revision serve well! appview,knotserver: validate git repo ownership according to knot Lewis: May this revision serve well! --- appview/db/notifications.go | 9 +- appview/issues/issues.go | 6 +- appview/labels/labels.go | 2 +- appview/middleware/middleware.go | 56 +++++++++- appview/models/repo.go | 5 + appview/pages/repoinfo/repoinfo.go | 4 +- .../templates/goodfirstissues/index.html | 4 +- appview/pages/templates/knots/dashboard.html | 2 +- appview/pages/templates/layouts/repobase.html | 2 +- .../notifications/fragments/item.html | 6 +- appview/pages/templates/repo/empty.html | 2 +- .../repo/fragments/cloneDropdown.html | 8 +- appview/pages/templates/repo/index.html | 1 + .../repo/pulls/fragments/pullActions.html | 2 +- .../repo/pulls/fragments/pullHeader.html | 2 +- appview/pages/templates/repo/pulls/pull.html | 2 +- .../pages/templates/repo/settings/hooks.html | 10 ++ .../pages/templates/repo/settings/sites.html | 6 +- .../pages/templates/spindles/dashboard.html | 2 +- .../templates/timeline/fragments/preview.html | 8 +- .../timeline/fragments/timeline.html | 8 +- .../templates/user/fragments/issueEvent.html | 5 +- .../templates/user/fragments/pullEvent.html | 5 +- .../templates/user/fragments/repoCard.html | 4 +- appview/pages/templates/user/overview.html | 2 +- appview/pipelines/pipelines.go | 8 +- appview/pulls/compose_helpers_test.go | 2 +- appview/repo/artifact.go | 9 +- appview/repo/feed.go | 6 +- appview/repo/index.go | 7 +- appview/repo/opengraph.go | 2 +- appview/repo/repo.go | 39 +++---- appview/repo/repo_util.go | 2 +- appview/repo/router.go | 1 + appview/repo/settings.go | 23 ++-- appview/repo/tags.go | 4 +- appview/repo/webhooks.go | 38 ++++--- appview/reporesolver/resolver.go | 16 +-- appview/settings/settings.go | 4 +- appview/sites/sites.go | 2 +- appview/state/knotstream.go | 43 +++----- appview/state/profile.go | 6 +- appview/state/router.go | 2 +- appview/state/state.go | 101 +++++++----------- appview/strings/strings.go | 5 +- 45 files changed, 258 insertions(+), 225 deletions(-) diff --git a/appview/db/notifications.go b/appview/db/notifications.go index c8fcafbc..baf3f9f6 100644 --- a/appview/db/notifications.go +++ b/appview/db/notifications.go @@ -135,7 +135,7 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm select n.id, n.recipient_did, n.actor_did, n.type, n.entity_type, n.entity_id, n.read, n.created, n.repo_id, n.issue_id, n.pull_id, - r.id as r_id, r.did as r_did, r.name as r_name, r.description as r_description, r.website as r_website, r.topics as r_topics, + r.id as r_id, r.did as r_did, r.rkey as r_rkey, r.name as r_name, r.description as r_description, r.website as r_website, r.topics as r_topics, i.id as i_id, i.did as i_did, i.issue_id as i_issue_id, i.title as i_title, i.open as i_open, p.id as p_id, p.owner_did as p_owner_did, p.pull_id as p_pull_id, p.title as p_title, p.state as p_state from notifications n @@ -164,7 +164,7 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm var issue models.Issue var pull models.Pull var rId, iId, pId sql.NullInt64 - var rDid, rName, rDescription, rWebsite, rTopicStr sql.NullString + var rDid, rRkey, rName, rDescription, rWebsite, rTopicStr sql.NullString var iDid sql.NullString var iIssueId sql.NullInt64 var iTitle sql.NullString @@ -177,7 +177,7 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm err := rows.Scan( &n.ID, &n.RecipientDid, &n.ActorDid, &typeStr, &n.EntityType, &n.EntityId, &n.Read, &createdStr, &n.RepoId, &n.IssueId, &n.PullId, - &rId, &rDid, &rName, &rDescription, &rWebsite, &rTopicStr, + &rId, &rDid, &rRkey, &rName, &rDescription, &rWebsite, &rTopicStr, &iId, &iDid, &iIssueId, &iTitle, &iOpen, &pId, &pOwnerDid, &pPullId, &pTitle, &pState, ) @@ -199,6 +199,9 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm if rDid.Valid { repo.Did = rDid.String } + if rRkey.Valid { + repo.Rkey = rRkey.String + } if rName.Valid { repo.Name = rName.String } diff --git a/appview/issues/issues.go b/appview/issues/issues.go index a96a0185..d4ce37f6 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -885,7 +885,7 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { searchOpts := models.IssueSearchOptions{ Keywords: tf.Keywords, Phrases: tf.Phrases, - RepoAt: f.RepoAt().String(), + RepoDid: f.RepoDid, IsOpen: isOpen, AuthorDid: authorDid, Labels: labels, @@ -945,7 +945,7 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { } } else { filters := []orm.Filter{ - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), } if isOpen != nil { openInt := 0 @@ -1039,7 +1039,7 @@ func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) issue := &models.Issue{ - RepoAt: f.RepoAt(), + RepoDid: syntax.DID(f.RepoDid), Rkey: tid.TID(), Title: r.FormValue("title"), Body: body, diff --git a/appview/labels/labels.go b/appview/labels/labels.go index 216a68ad..c63c5678 100644 --- a/appview/labels/labels.go +++ b/appview/labels/labels.go @@ -100,7 +100,7 @@ func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { } // find all the labels that this repo subscribes to - repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt)) + repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_did", repo.RepoDid)) if err != nil { fail("Failed to get labels for this repository.", err) return diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go index 9040d0f2..0f59a4e9 100644 --- a/appview/middleware/middleware.go +++ b/appview/middleware/middleware.go @@ -2,6 +2,8 @@ package middleware import ( "context" + "database/sql" + "errors" "fmt" "log/slog" "net/http" @@ -234,6 +236,7 @@ func (mw Middleware) ResolveRepo() middlewareFunc { l := mw.logger.With("middleware", "ResolveRepo") repoName := chi.URLParam(req, "repo") repoName = strings.TrimSuffix(repoName, ".git") + rkey := strings.ToLower(repoName) id, ok := req.Context().Value("resolvedId").(identity.Identity) if !ok { @@ -245,10 +248,53 @@ func (mw Middleware) ResolveRepo() middlewareFunc { repo, err := db.GetRepo( mw.db, orm.FilterEq("did", id.DID.String()), - orm.FilterEq("name", repoName), + orm.FilterEq("rkey", rkey), ) if err != nil { - l.Error("failed to resolve repo", "err", err) + if !errors.Is(err, sql.ErrNoRows) { + l.Error("failed to resolve repo", "err", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + hint, hintErr := db.LookupRepoRename(mw.db, id.DID.String(), rkey) + if hintErr != nil && !errors.Is(hintErr, sql.ErrNoRows) { + l.Error("failed to lookup repo rename hint", "err", hintErr) + } + if hint != nil { + parts := strings.SplitN(strings.TrimPrefix(req.URL.Path, "/"), "/", 3) + target := "/" + parts[0] + "/" + hint.Rkey + if len(parts) == 3 { + target += "/" + parts[2] + } + if req.URL.RawQuery != "" { + target += "?" + req.URL.RawQuery + } + http.Redirect(w, req, target, http.StatusMovedPermanently) + return + } + nameRepos, nameErr := db.GetRepos( + mw.db, + orm.FilterEq("did", id.DID.String()), + orm.FilterEq("name", repoName), + ) + if nameErr == nil && len(nameRepos) == 1 && nameRepos[0].RepoDid != "" { + nameRepo := &nameRepos[0] + if _, tidErr := syntax.ParseTID(nameRepo.Rkey); tidErr == nil { + ctx := context.WithValue(req.Context(), "repo", nameRepo) + next.ServeHTTP(w, req.WithContext(ctx)) + return + } + parts := strings.SplitN(strings.TrimPrefix(req.URL.Path, "/"), "/", 3) + target := "/" + nameRepo.RepoDid + if len(parts) == 3 { + target += "/" + parts[2] + } + if req.URL.RawQuery != "" { + target += "?" + req.URL.RawQuery + } + http.Redirect(w, req, target, http.StatusFound) + return + } w.WriteHeader(http.StatusNotFound) mw.pages.ErrorKnot404(w) return @@ -281,7 +327,7 @@ func (mw Middleware) ResolvePull() middlewareFunc { return } - pr, err := db.GetPull(mw.db, orm.FilterEq("repo_at", f.RepoAt()), orm.FilterEq("pull_id", prIdInt)) + pr, err := db.GetPull(mw.db, orm.FilterEq("repo_did", f.RepoDid), orm.FilterEq("pull_id", prIdInt)) if err != nil { l.Error("failed to get pull and comments", "err", err) mw.pages.Error404(w) @@ -324,7 +370,7 @@ func (mw Middleware) ResolveIssue(next http.Handler) http.Handler { return } - issue, err := db.GetIssue(mw.db, f.RepoAt(), issueId) + issue, err := db.GetIssue(mw.db, f.RepoDid, issueId) if err != nil { l.Error("failed to get issues", "err", err) mw.pages.Error404(w) @@ -360,7 +406,7 @@ func (mw Middleware) GoImport() middlewareFunc { if r.URL.Query().Get("go-get") == "1" { modulePath := userutil.FlattenDid(fullName) if strings.Contains(modulePath, ":") { - modulePath = userutil.FlattenDid(f.Did) + "/" + f.Name + modulePath = userutil.FlattenDid(f.Did) + "/" + f.Rkey } html := fmt.Sprintf( ` diff --git a/appview/models/repo.go b/appview/models/repo.go index 6e655e55..1e46fcd9 100644 --- a/appview/models/repo.go +++ b/appview/models/repo.go @@ -136,14 +136,17 @@ func ValidateRepoName(name string) error { return fmt.Errorf("Repository name must be 100 characters or fewer") } + // check for path traversal attempts if strings.Contains(name, "/") || strings.Contains(name, "\\") { return fmt.Errorf("Repository name contains invalid path characters") } + // check for sequences that could be used for traversal when normalized if strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { return fmt.Errorf("Repository name contains invalid path sequence") } + // then continue with character validation for _, char := range name { if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || @@ -153,6 +156,7 @@ func ValidateRepoName(name string) error { } } + // additional check to prevent multiple sequential dots if strings.Contains(name, "..") { return fmt.Errorf("Repository name cannot contain sequential dots") } @@ -161,6 +165,7 @@ func ValidateRepoName(name string) error { return fmt.Errorf("Repository name %q is reserved", name) } + // if all checks pass return nil } diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go index 61835efd..89a93c18 100644 --- a/appview/pages/repoinfo/repoinfo.go +++ b/appview/pages/repoinfo/repoinfo.go @@ -20,7 +20,7 @@ func (r RepoInfo) owner() string { } func (r RepoInfo) FullName() string { - return path.Join(r.owner(), r.Name) + return path.Join(r.owner(), r.Rkey) } func (r RepoInfo) RepoIdentifier() string { @@ -39,7 +39,7 @@ func (r RepoInfo) ownerWithoutAt() string { } func (r RepoInfo) FullNameWithoutAt() string { - return path.Join(r.ownerWithoutAt(), r.Name) + return path.Join(r.ownerWithoutAt(), r.Rkey) } func (r RepoInfo) GetTabs() [][]string { diff --git a/appview/pages/templates/goodfirstissues/index.html b/appview/pages/templates/goodfirstissues/index.html index b4fa8dd1..d0ca6af0 100644 --- a/appview/pages/templates/goodfirstissues/index.html +++ b/appview/pages/templates/goodfirstissues/index.html @@ -46,7 +46,7 @@ {{ i "book-marked" "w-4 h-4 mr-1.5 shrink-0" }} {{ end }} {{ $repoOwner := resolve .Repo.Did }} - {{ $repoOwner }}/{{ .Repo.Name }} + {{ $repoOwner }}/{{ .Repo.Name }} @@ -90,7 +90,7 @@ {{ if gt (len .Issues) 0 }}
{{ range .Issues }} - +
diff --git a/appview/pages/templates/knots/dashboard.html b/appview/pages/templates/knots/dashboard.html index 1f58655a..9ae6987f 100644 --- a/appview/pages/templates/knots/dashboard.html +++ b/appview/pages/templates/knots/dashboard.html @@ -77,7 +77,7 @@ {{ range $repos }} diff --git a/appview/pages/templates/layouts/repobase.html b/appview/pages/templates/layouts/repobase.html index 34ce6ff4..e71d0593 100644 --- a/appview/pages/templates/layouts/repobase.html +++ b/appview/pages/templates/layouts/repobase.html @@ -78,7 +78,7 @@
{{ i "git-fork" "w-3 h-3 shrink-0" }} forked from - + {{ $sourceOwner }}/{{ .RepoInfo.Source.Name }}
diff --git a/appview/pages/templates/notifications/fragments/item.html b/appview/pages/templates/notifications/fragments/item.html index eeadac0b..a1f76046 100644 --- a/appview/pages/templates/notifications/fragments/item.html +++ b/appview/pages/templates/notifications/fragments/item.html @@ -76,11 +76,11 @@ {{ define "notificationUrl" }} {{ $url := "" }} {{ if eq .Type "repo_starred" }} - {{$url = printf "/%s/%s" (resolve .Repo.Did) .Repo.Name}} + {{$url = printf "/%s/%s" (resolve .Repo.Did) .Repo.Rkey}} {{ else if .Issue }} - {{$url = printf "/%s/%s/issues/%d" (resolve .Repo.Did) .Repo.Name .Issue.IssueId}} + {{$url = printf "/%s/%s/issues/%d" (resolve .Repo.Did) .Repo.Rkey .Issue.IssueId}} {{ else if .Pull }} - {{$url = printf "/%s/%s/pulls/%d" (resolve .Repo.Did) .Repo.Name .Pull.PullId}} + {{$url = printf "/%s/%s/pulls/%d" (resolve .Repo.Did) .Repo.Rkey .Pull.PullId}} {{ else if eq .Type "followed" }} {{$url = printf "/%s" (resolve .ActorDid)}} {{ else }} diff --git a/appview/pages/templates/repo/empty.html b/appview/pages/templates/repo/empty.html index 46ffe7ee..10a6aca5 100644 --- a/appview/pages/templates/repo/empty.html +++ b/appview/pages/templates/repo/empty.html @@ -35,7 +35,7 @@

1First, generate a new SSH key pair.

2Then add the public key to your account from the keys page in your settings.

-

3Configure your remote to git@{{ $knot | stripPort }}:{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}

+

3Configure your remote to git@{{ $knot | stripPort }}:{{ .RepoInfo.RepoDid }}

4Push!

diff --git a/appview/pages/templates/repo/fragments/cloneDropdown.html b/appview/pages/templates/repo/fragments/cloneDropdown.html index 40455b9c..c005dea9 100644 --- a/appview/pages/templates/repo/fragments/cloneDropdown.html +++ b/appview/pages/templates/repo/fragments/cloneDropdown.html @@ -38,15 +38,15 @@ {{ template "cloneUrlItem" ( dict "Label" "HTTPS" - "HandleUrl" (printf "https://tangled.org/%s/%s" $repoOwnerHandle .RepoInfo.Name) - "PermaUrl" (printf "https://tangled.org/%s/%s" .RepoInfo.OwnerDid .RepoInfo.Name) + "HandleUrl" (printf "https://tangled.org/%s/%s" $repoOwnerHandle .RepoInfo.Rkey) + "PermaUrl" (printf "https://tangled.org/%s" .RepoInfo.RepoDid) ) }} {{ template "cloneUrlItem" ( dict "Label" "SSH" - "HandleUrl" (printf "git@%s:%s/%s" (stripPort $knot) $repoOwnerHandle .RepoInfo.Name) - "PermaUrl" (printf "git@%s:%s/%s" (stripPort $knot) .RepoInfo.OwnerDid .RepoInfo.Name) + "HandleUrl" (printf "git@%s:%s/%s" (stripPort $knot) $repoOwnerHandle .RepoInfo.Rkey) + "PermaUrl" (printf "git@%s:%s" (stripPort $knot) .RepoInfo.RepoDid) ) }}

diff --git a/appview/pages/templates/repo/index.html b/appview/pages/templates/repo/index.html index fb166028..80ea9937 100644 --- a/appview/pages/templates/repo/index.html +++ b/appview/pages/templates/repo/index.html @@ -361,3 +361,4 @@ {{ template "repo/fragments/readme" . }} {{- end -}} {{ end }} + diff --git a/appview/pages/templates/repo/pulls/fragments/pullActions.html b/appview/pages/templates/repo/pulls/fragments/pullActions.html index 248694d2..f6d4b697 100644 --- a/appview/pages/templates/repo/pulls/fragments/pullActions.html +++ b/appview/pages/templates/repo/pulls/fragments/pullActions.html @@ -34,7 +34,7 @@ {{ if .BranchDeleteStatus }}

+
+ + Repository renamed +

Additional event types (pull requests, issues) will be available in future updates.

@@ -294,13 +298,19 @@
{{ $hasPush := false }} + {{ $hasRepoRenamed := false }} {{ range $webhook.Events }} {{ if eq . "push" }}{{ $hasPush = true }}{{ end }} + {{ if eq . "repository:renamed" }}{{ $hasRepoRenamed = true }}{{ end }} {{ end }}
Push events
+
+ + Repository renamed +

Additional event types (pull requests, issues) will be available in future updates.

diff --git a/appview/pages/templates/repo/settings/sites.html b/appview/pages/templates/repo/settings/sites.html index 1ac98026..96d7cb6d 100644 --- a/appview/pages/templates/repo/settings/sites.html +++ b/appview/pages/templates/repo/settings/sites.html @@ -32,7 +32,7 @@ {{ else }}
{{ i "circle-check" "size-4 shrink-0" }} - live at {{ .OwnerClaim.Domain }}/{{ .RepoInfo.Name }} + live at {{ .OwnerClaim.Domain }}/{{ .RepoInfo.Rkey }}
{{ end }} {{ else if and .SiteConfig (not .OwnerClaim) }} @@ -157,9 +157,9 @@ sub-path site

{{ if .OwnerClaim }} - {{ .OwnerClaim.Domain }}/{{ $.RepoInfo.Name }} + {{ .OwnerClaim.Domain }}/{{ $.RepoInfo.Rkey }} {{ else }} - e.g. you.tngl.page/{{ $.RepoInfo.Name }} + e.g. you.tngl.page/{{ $.RepoInfo.Rkey }} {{ end }}

diff --git a/appview/pages/templates/spindles/dashboard.html b/appview/pages/templates/spindles/dashboard.html index 32fa8fd8..230e07c4 100644 --- a/appview/pages/templates/spindles/dashboard.html +++ b/appview/pages/templates/spindles/dashboard.html @@ -69,7 +69,7 @@ {{ range $repos }}
{{ i "book-marked" "size-4" }} - + {{ .Name }}
diff --git a/appview/pages/templates/timeline/fragments/preview.html b/appview/pages/templates/timeline/fragments/preview.html index 1fb84bd6..5aa01163 100644 --- a/appview/pages/templates/timeline/fragments/preview.html +++ b/appview/pages/templates/timeline/fragments/preview.html @@ -121,14 +121,14 @@ {{ with $source }} {{ $sourceDid := resolve .Did }} forked - + {{ $sourceDid }}/{{ .Name }} to - {{ $repo.Name }} + {{ $repo.Name }} {{ else }} created - + {{ $repo.Name }} {{ end }} @@ -159,7 +159,7 @@ starred {{ end }} {{ template "user/fragments/pic" (list $repoOwnerHandle "size-6") }} - + {{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }}
diff --git a/appview/pages/templates/timeline/fragments/timeline.html b/appview/pages/templates/timeline/fragments/timeline.html index c89e9480..e0dc354d 100644 --- a/appview/pages/templates/timeline/fragments/timeline.html +++ b/appview/pages/templates/timeline/fragments/timeline.html @@ -39,14 +39,14 @@ {{ with $source }} {{ $sourceDid := resolve .Did }} forked - + {{ $sourceDid }}/{{ .Name }} to - {{ $repo.Name }} + {{ $repo.Name }} {{ else }} created - + {{ $repo.Name }} {{ end }} @@ -74,7 +74,7 @@ {{ else }} starred {{ end }} - + {{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }} {{ template "repo/fragments/time" .Created }} diff --git a/appview/pages/templates/user/fragments/issueEvent.html b/appview/pages/templates/user/fragments/issueEvent.html index 65f954d6..a9f27202 100644 --- a/appview/pages/templates/user/fragments/issueEvent.html +++ b/appview/pages/templates/user/fragments/issueEvent.html @@ -1,6 +1,7 @@ {{ define "user/fragments/issueEvent" }} {{ $repoOwner := resolve .Repo.Did }} - {{ $repoUrl := printf "%s/%s" $repoOwner .Repo.Name }} + {{ $repoUrl := printf "%s/%s" $repoOwner .Repo.Rkey }} + {{ $repoDisplay := printf "%s/%s" $repoOwner .Repo.Name }}
{{ if .Open }} @@ -15,7 +16,7 @@ {{- .Title -}} on - {{ $repoUrl }} + {{ $repoDisplay }}
{{ end }} diff --git a/appview/pages/templates/user/fragments/pullEvent.html b/appview/pages/templates/user/fragments/pullEvent.html index 45b0fb6a..25b5ead7 100644 --- a/appview/pages/templates/user/fragments/pullEvent.html +++ b/appview/pages/templates/user/fragments/pullEvent.html @@ -1,6 +1,7 @@ {{ define "user/fragments/pullEvent" }} {{ $repoOwner := resolve .Repo.Did }} - {{ $repoUrl := printf "%s/%s" $repoOwner .Repo.Name }} + {{ $repoUrl := printf "%s/%s" $repoOwner .Repo.Rkey }} + {{ $repoDisplay := printf "%s/%s" $repoOwner .Repo.Name }}
{{ if .State.IsOpen }} @@ -19,7 +20,7 @@ {{- .Title -}} on - {{ $repoUrl }} + {{ $repoDisplay }}
{{ end }} diff --git a/appview/pages/templates/user/fragments/repoCard.html b/appview/pages/templates/user/fragments/repoCard.html index 7d487b71..b940c152 100644 --- a/appview/pages/templates/user/fragments/repoCard.html +++ b/appview/pages/templates/user/fragments/repoCard.html @@ -27,9 +27,9 @@ {{ end }} {{ $repoOwner := resolve .Did }} {{- if $fullName -}} - {{ $repoOwner }}/{{ .Name }} + {{ $repoOwner }}/{{ .Name }} {{- else -}} - {{ .Name }} + {{ .Name }} {{- end -}} {{ if and $starButton $root.LoggedInUser }} diff --git a/appview/pages/templates/user/overview.html b/appview/pages/templates/user/overview.html index 1d89d08d..30428171 100644 --- a/appview/pages/templates/user/overview.html +++ b/appview/pages/templates/user/overview.html @@ -79,7 +79,7 @@ {{ i "book-plus" "w-4 h-4" }} {{ end }} - + {{- .Repo.Name -}} diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go index 094c40bc..17b77097 100644 --- a/appview/pipelines/pipelines.go +++ b/appview/pipelines/pipelines.go @@ -89,7 +89,7 @@ func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) { filterKind := r.URL.Query().Get("trigger") filters := []orm.Filter{ orm.FilterEq("p.repo_owner", f.Did), - orm.FilterEq("p.repo_name", f.Name), + orm.FilterEq("p.repo_name", f.Rkey), orm.FilterEq("p.knot", f.Knot), } switch filterKind { @@ -153,7 +153,7 @@ func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) { p.db, 1, orm.FilterEq("p.repo_owner", f.Did), - orm.FilterEq("p.repo_name", f.Name), + orm.FilterEq("p.repo_name", f.Rkey), orm.FilterEq("p.knot", f.Knot), orm.FilterEq("p.id", pipelineId), ) @@ -220,7 +220,7 @@ func (p *Pipelines) Logs(w http.ResponseWriter, r *http.Request) { p.db, 1, orm.FilterEq("p.repo_owner", f.Did), - orm.FilterEq("p.repo_name", f.Name), + orm.FilterEq("p.repo_name", f.Rkey), orm.FilterEq("p.knot", f.Knot), orm.FilterEq("p.id", pipelineId), ) @@ -369,7 +369,7 @@ func (p *Pipelines) Cancel(w http.ResponseWriter, r *http.Request) { p.db, 1, orm.FilterEq("p.repo_owner", f.Did), - orm.FilterEq("p.repo_name", f.Name), + orm.FilterEq("p.repo_name", f.Rkey), orm.FilterEq("p.knot", f.Knot), orm.FilterEq("p.id", pipelineId), ) diff --git a/appview/pulls/compose_helpers_test.go b/appview/pulls/compose_helpers_test.go index 8f94c30a..17c85211 100644 --- a/appview/pulls/compose_helpers_test.go +++ b/appview/pulls/compose_helpers_test.go @@ -181,7 +181,7 @@ func TestSortBranchesByRecency(t *testing.T) { } func TestComposeCanonicalURL(t *testing.T) { - repo := repoinfo.RepoInfo{OwnerDid: "did:plc:abc", Name: "demo"} + repo := repoinfo.RepoInfo{OwnerDid: "did:plc:abc", Name: "demo", Rkey: "demo"} cases := []struct { name string p pages.RepoNewPullParams diff --git a/appview/repo/artifact.go b/appview/repo/artifact.go index e4b28bee..4f29fa50 100644 --- a/appview/repo/artifact.go +++ b/appview/repo/artifact.go @@ -20,6 +20,7 @@ import ( "tangled.org/core/xrpc" comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/dustin/go-humanize" @@ -102,7 +103,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { artifact := models.Artifact{ Did: user.Did, Rkey: rkey, - RepoAt: f.RepoAt(), + RepoDid: syntax.DID(f.RepoDid), Tag: tag.Tag.Hash, CreatedAt: createdAt, BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), @@ -154,7 +155,7 @@ func (rp *Repo) DownloadArtifact(w http.ResponseWriter, r *http.Request) { artifacts, err := db.GetArtifact( rp.db, - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), orm.FilterEq("tag", tag.Tag.Hash[:]), orm.FilterEq("name", filename), ) @@ -235,7 +236,7 @@ func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { artifacts, err := db.GetArtifact( rp.db, - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), orm.FilterEq("tag", tag[:]), orm.FilterEq("name", filename), ) @@ -277,7 +278,7 @@ func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { defer tx.Rollback() err = db.DeleteArtifact(tx, - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), orm.FilterEq("tag", artifact.Tag[:]), orm.FilterEq("name", filename), ) diff --git a/appview/repo/feed.go b/appview/repo/feed.go index 434fde03..07a10ba5 100644 --- a/appview/repo/feed.go +++ b/appview/repo/feed.go @@ -74,7 +74,7 @@ func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRe // fetch and add pull requests if requested if opts.IncludePulls { - pulls, err := db.GetPullsPaginated(rp.db, feedPagePerType, orm.FilterEq("repo_at", repo.RepoAt())) + pulls, err := db.GetPullsPaginated(rp.db, feedPagePerType, orm.FilterEq("repo_did", repo.RepoDid)) if err != nil { return nil, err } @@ -93,7 +93,7 @@ func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRe issues, err := db.GetIssuesPaginated( rp.db, feedPagePerType, - orm.FilterEq("repo_at", repo.RepoAt()), + orm.FilterEq("repo_did", repo.RepoDid), ) if err != nil { return nil, err @@ -315,7 +315,7 @@ func (rp *Repo) AtomFeed(w http.ResponseWriter, r *http.Request) { rp.logger.Error("failed to get resolved repo owner id") return } - ownerSlashRepo := repoOwnerId.Handle.String() + "/" + f.Name + ownerSlashRepo := repoOwnerId.Handle.String() + "/" + f.Rkey opts := parseFeedOpts(r) feed, err := rp.getRepoFeed(r.Context(), f, ownerSlashRepo, opts) diff --git a/appview/repo/index.go b/appview/repo/index.go index f5824c27..d8a5515b 100644 --- a/appview/repo/index.go +++ b/appview/repo/index.go @@ -15,6 +15,7 @@ import ( "context" "encoding/json" + "github.com/bluesky-social/indigo/atproto/syntax" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/go-git/go-git/v5/plumbing" "tangled.org/core/api/tangled" @@ -158,7 +159,7 @@ func (rp *Repo) getLanguageInfo( // first attempt to fetch from db langs, err := db.GetRepoLanguages( rp.db, - orm.FilterEq("repo_at", repo.RepoAt()), + orm.FilterEq("repo_did", repo.RepoDid), orm.FilterEq("ref", currentRef), ) @@ -181,7 +182,7 @@ func (rp *Repo) getLanguageInfo( for _, lang := range ls.Languages { langs = append(langs, models.RepoLanguage{ - RepoAt: repo.RepoAt(), + RepoDid: syntax.DID(repo.RepoDid), Ref: currentRef, IsDefaultRef: isDefaultRef, Language: lang.Name, @@ -196,7 +197,7 @@ func (rp *Repo) getLanguageInfo( defer tx.Rollback() // update appview's cache - err = db.UpdateRepoLanguages(tx, repo.RepoAt(), currentRef, langs) + err = db.UpdateRepoLanguages(tx, syntax.DID(repo.RepoDid), currentRef, langs) if err != nil { // non-fatal l.Error("failed to cache lang results", "err", err) diff --git a/appview/repo/opengraph.go b/appview/repo/opengraph.go index e7da3cd8..8e2edf9c 100644 --- a/appview/repo/opengraph.go +++ b/appview/repo/opengraph.go @@ -27,7 +27,7 @@ func (rp *Repo) Opengraph(w http.ResponseWriter, r *http.Request) { var languageStats []types.RepoLanguageDetails langs, err := db.GetRepoLanguages( rp.db, - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), orm.FilterEq("is_default_ref", 1), ) if err != nil { diff --git a/appview/repo/repo.go b/appview/repo/repo.go index ae729e08..c5248c85 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -140,7 +140,7 @@ func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { } // optimistic update - err = db.UpdateSpindle(rp.db, newRepo.RepoAt().String(), spindlePtr) + err = db.UpdateSpindle(rp.db, newRepo.RepoDid, spindlePtr) if err != nil { fail("Failed to update spindle. Try again later.", err) return @@ -320,7 +320,7 @@ func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) { } if err = db.SubscribeLabel(tx, &models.RepoLabel{ - RepoAt: f.RepoAt(), + RepoDid: syntax.DID(f.RepoDid), LabelAt: label.AtUri(), }); err != nil { fail("Failed to subscribe to label.", err) @@ -423,7 +423,7 @@ func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) { err = db.UnsubscribeLabel( tx, - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), orm.FilterEq("label_at", removedAt), ) if err != nil { @@ -515,7 +515,7 @@ func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) { for _, l := range labelAts { err = db.SubscribeLabel(tx, &models.RepoLabel{ - RepoAt: f.RepoAt(), + RepoDid: syntax.DID(f.RepoDid), LabelAt: syntax.ATURI(l), }) if err != nil { @@ -596,7 +596,7 @@ func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) { err = db.UnsubscribeLabel( rp.db, - orm.FilterEq("repo_at", f.RepoAt()), + orm.FilterEq("repo_did", f.RepoDid), orm.FilterIn("label_at", labelAts), ) if err != nil { @@ -805,7 +805,7 @@ func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { Did: syntax.DID(currentUser.Did), Rkey: rkey, SubjectDid: collaboratorIdent.DID, - RepoAt: f.RepoAt(), + RepoDid: syntax.DID(f.RepoDid), Created: createdAt, }) if err != nil { @@ -921,7 +921,7 @@ func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { } // remove repo from db - err = db.RemoveRepo(tx, f.Did, f.Name) + err = db.RemoveRepo(tx, f.Did, f.Rkey) if err != nil { rp.pages.Notice(w, noticeId, "Failed to update appview") return @@ -1040,7 +1040,7 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { } // choose a name for a fork - forkName := r.FormValue("repo_name") + forkName := strings.ToLower(r.FormValue("repo_name")) if forkName == "" { rp.pages.Notice(w, "repo", "Repository name cannot be empty.") return @@ -1074,7 +1074,7 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { forkSourceUrl := fmt.Sprintf("%s://%s/%s", uri, f.Knot, f.RepoIdentifier()) l = l.With("cloneUrl", forkSourceUrl) - rkey := tid.TID() + rkey := strings.ToLower(forkName) // TODO: this could coordinate better with the knot to receive a clone status client, err := rp.oauth.ServiceClient( @@ -1092,7 +1092,7 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { forkInput := &tangled.RepoCreate_Input{ Rkey: rkey, - Name: forkName, + Name: rkey, Source: &forkSourceUrl, } createResp, err := tangled.RepoCreate( @@ -1123,7 +1123,7 @@ func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { repo := &models.Repo{ Did: user.Did, - Name: forkName, + Name: rkey, Knot: targetKnot, Rkey: rkey, Source: forkSource, @@ -1276,15 +1276,15 @@ func (rp *Repo) Stars(w http.ResponseWriter, r *http.Request) { page.Limit = 30 } - starrers, err := db.GetStars(rp.db, f.RepoAt(), page) + starrers, err := db.GetStars(rp.db, string(f.RepoDid), page) if err != nil { - l.Error("failed to fetch starrers", "err", err, "repoAt", f.RepoAt()) + l.Error("failed to fetch starrers", "err", err, "repoDid", f.RepoDid) return } - totalCount, err := db.GetStarCount(rp.db, f.RepoAt()) + totalCount, err := db.GetStarCount(rp.db, models.StarSubjectRepo, string(f.RepoDid)) if err != nil { - l.Error("failed to fetch star count", "err", err, "repoAt", f.RepoAt()) + l.Error("failed to fetch star count", "err", err, "repoDid", f.RepoDid) return } @@ -1320,14 +1320,9 @@ func rollbackRecord(ctx context.Context, aturi string, client *atclient.APIClien } func repoCollaboratorRecord(f *models.Repo, subject string, createdAt time.Time) *tangled.RepoCollaborator { - rec := &tangled.RepoCollaborator{ + return &tangled.RepoCollaborator{ Subject: subject, CreatedAt: createdAt.Format(time.RFC3339), + Repo: f.RepoDid, } - s := string(f.RepoAt()) - rec.Repo = &s - if f.RepoDid != "" { - rec.RepoDid = &f.RepoDid - } - return rec } diff --git a/appview/repo/repo_util.go b/appview/repo/repo_util.go index 8bf020b8..71f64e35 100644 --- a/appview/repo/repo_util.go +++ b/appview/repo/repo_util.go @@ -104,7 +104,7 @@ func getPipelineStatuses( d, len(shas), orm.FilterEq("p.repo_owner", repo.Did), - orm.FilterEq("p.repo_name", repo.Name), + orm.FilterEq("p.repo_name", repo.Rkey), orm.FilterEq("p.knot", repo.Knot), orm.FilterIn("p.sha", shas), ) diff --git a/appview/repo/router.go b/appview/repo/router.go index 42590422..0fe2f653 100644 --- a/appview/repo/router.go +++ b/appview/repo/router.go @@ -88,6 +88,7 @@ func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/label/unsubscribe", rp.UnsubscribeLabel) r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator) r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo) + r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/rename", rp.RenameRepo) r.Put("/branches/default", rp.SetDefaultBranch) r.Put("/secrets", rp.Secrets) r.Delete("/secrets", rp.Secrets) diff --git a/appview/repo/settings.go b/appview/repo/settings.go index 8157ab2d..6cacb958 100644 --- a/appview/repo/settings.go +++ b/appview/repo/settings.go @@ -22,6 +22,7 @@ import ( "tangled.org/core/types" comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" indigoxrpc "github.com/bluesky-social/indigo/xrpc" ) @@ -195,7 +196,7 @@ func (rp *Repo) sitesSettings(w http.ResponseWriter, r *http.Request) { host := fmt.Sprintf("%s://%s", scheme, f.Knot) xrpcc := &indigoxrpc.Client{Host: host} - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) + repo := fmt.Sprintf("%s/%s", f.Did, f.Rkey) xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { l.Error("failed to call XRPC repo.branches", "xrpcerr", xrpcerr, "err", err) @@ -210,7 +211,7 @@ func (rp *Repo) sitesSettings(w http.ResponseWriter, r *http.Request) { return } - siteConfig, err := db.GetRepoSiteConfig(rp.db, f.RepoAt().String()) + siteConfig, err := db.GetRepoSiteConfig(rp.db, f.RepoDid) if err != nil { l.Error("failed to get site config", "err", err) rp.pages.Error503(w) @@ -224,14 +225,14 @@ func (rp *Repo) sitesSettings(w http.ResponseWriter, r *http.Request) { ownerClaim = nil } - deploys, err := db.GetSiteDeploys(rp.db, f.RepoAt().String(), 20) + deploys, err := db.GetSiteDeploys(rp.db, f.RepoDid, 20) if err != nil { l.Error("failed to get site deploys", "err", err) // non-fatal deploys = nil } - indexSiteTakenBy, err := db.GetIndexRepoAtForDid(rp.db, f.Did, f.RepoAt().String()) + indexSiteTakenBy, err := db.GetIndexRepoDidForDid(rp.db, f.Did, f.RepoDid) if err != nil { l.Error("failed to get index site owner", "err", err) // non-fatal @@ -281,7 +282,7 @@ func (rp *Repo) SaveRepoSiteConfig(w http.ResponseWriter, r *http.Request) { isIndex := r.FormValue("is_index") == "true" - if err := db.SetRepoSiteConfig(rp.db, f.RepoAt().String(), branch, dir, isIndex); err != nil { + if err := db.SetRepoSiteConfig(rp.db, f.RepoDid, branch, dir, isIndex); err != nil { l.Error("failed to save site config", "err", err) rp.pages.Notice(w, noticeId, "Failed to save site configuration.") return @@ -297,7 +298,7 @@ func (rp *Repo) SaveRepoSiteConfig(w http.ResponseWriter, r *http.Request) { ctx := context.Background() deploy := &models.SiteDeploy{ - RepoAt: f.RepoAt().String(), + RepoDid: syntax.DID(f.RepoDid), Branch: branch, Dir: dir, Trigger: models.SiteDeployTriggerConfigChange, @@ -317,7 +318,7 @@ func (rp *Repo) SaveRepoSiteConfig(w http.ResponseWriter, r *http.Request) { } if deployErr == nil { - if err := sites.PutDomainMapping(ctx, rp.cfClient, ownerClaim.Domain, f.Did, f.Name, isIndex); err != nil { + if err := sites.PutDomainMapping(ctx, rp.cfClient, ownerClaim.Domain, f.Did, f.Rkey, isIndex); err != nil { l.Error("sites: KV write failed", "domain", ownerClaim.Domain, "err", err) } rp.logger.Info("site deployed to r2", "repo", f.RepoIdentifier(), "is_index", isIndex) @@ -344,9 +345,9 @@ func (rp *Repo) DeleteRepoSiteConfig(w http.ResponseWriter, r *http.Request) { // Fetch the current config before deleting so we know the isIndex flag for // the KV key and the domain mapping to clean up. - existingConfig, _ := db.GetRepoSiteConfig(rp.db, f.RepoAt().String()) + existingConfig, _ := db.GetRepoSiteConfig(rp.db, f.RepoDid) - if err := db.DeleteRepoSiteConfig(rp.db, f.RepoAt().String()); err != nil { + if err := db.DeleteRepoSiteConfig(rp.db, f.RepoDid); err != nil { l.Error("failed to delete site config", "err", err) rp.pages.Notice(w, noticeId, "Failed to remove site configuration.") return @@ -358,11 +359,11 @@ func (rp *Repo) DeleteRepoSiteConfig(w http.ResponseWriter, r *http.Request) { go func() { ctx := context.Background() - if err := sites.Delete(ctx, rp.cfClient, f.Did, f.Name); err != nil { + if err := sites.Delete(ctx, rp.cfClient, f.Did, f.Rkey); err != nil { l.Error("sites: R2 delete failed", "repo", f.RepoIdentifier(), "err", err) } if ownerClaim != nil { - if err := sites.DeleteDomainMapping(ctx, rp.cfClient, ownerClaim.Domain, f.Name); err != nil { + if err := sites.DeleteDomainMapping(ctx, rp.cfClient, ownerClaim.Domain, f.Rkey); err != nil { l.Error("sites: KV delete failed", "domain", ownerClaim.Domain, "err", err) } } diff --git a/appview/repo/tags.go b/appview/repo/tags.go index fa3574b3..a7d4d7e4 100644 --- a/appview/repo/tags.go +++ b/appview/repo/tags.go @@ -40,7 +40,7 @@ func (rp *Repo) Tags(w http.ResponseWriter, r *http.Request) { rp.pages.Error503(w) return } - artifacts, err := db.GetArtifact(rp.db, orm.FilterEq("repo_at", f.RepoAt())) + artifacts, err := db.GetArtifact(rp.db, orm.FilterEq("repo_did", f.RepoDid)) if err != nil { l.Error("failed grab artifacts", "err", err) return @@ -124,7 +124,7 @@ func (rp *Repo) Tag(w http.ResponseWriter, r *http.Request) { return } - filters := []orm.Filter{orm.FilterEq("repo_at", f.RepoAt())} + filters := []orm.Filter{orm.FilterEq("repo_did", f.RepoDid)} if result.Tag.Tag != nil { filters = append(filters, orm.FilterEq("tag", result.Tag.Tag.Hash[:])) } diff --git a/appview/repo/webhooks.go b/appview/repo/webhooks.go index 2680a1fb..3fe3783f 100644 --- a/appview/repo/webhooks.go +++ b/appview/repo/webhooks.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" "tangled.org/core/appview/db" "tangled.org/core/appview/models" @@ -24,7 +25,7 @@ func (rp *Repo) Webhooks(w http.ResponseWriter, r *http.Request) { user := rp.oauth.GetMultiAccountUser(r) - webhooks, err := db.GetWebhooksForRepo(rp.db, f.RepoAt()) + webhooks, err := db.GetWebhooksForRepo(rp.db, f.RepoDid) if err != nil { l.Error("failed to get webhooks", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to load webhooks") @@ -82,18 +83,21 @@ func (rp *Repo) AddWebhook(w http.ResponseWriter, r *http.Request) { if r.FormValue("event_push") == "on" { events = append(events, string(models.WebhookEventPush)) } + if r.FormValue("event_repo_renamed") == "on" { + events = append(events, string(models.WebhookEventRepoRenamed)) + } if len(events) == 0 { - rp.pages.Notice(w, "webhooks-error", "Push events must be enabled") + rp.pages.Notice(w, "webhooks-error", "At least one event must be enabled") return } webhook := &models.Webhook{ - RepoAt: f.RepoAt(), - Url: url, - Secret: secret, - Active: active, - Events: events, + RepoDid: syntax.DID(f.RepoDid), + Url: url, + Secret: secret, + Active: active, + Events: events, } tx, err := rp.db.Begin() @@ -146,8 +150,8 @@ func (rp *Repo) UpdateWebhook(w http.ResponseWriter, r *http.Request) { } // Verify webhook belongs to this repo - if webhook.RepoAt != f.RepoAt() { - l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt()) + if string(webhook.RepoDid) != f.RepoDid { + l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) w.WriteHeader(http.StatusForbidden) return } @@ -168,11 +172,13 @@ func (rp *Repo) UpdateWebhook(w http.ResponseWriter, r *http.Request) { webhook.Active = r.FormValue("active") == "on" - // Parse events - only push events are supported for now events := []string{} if r.FormValue("event_push") == "on" { events = append(events, string(models.WebhookEventPush)) } + if r.FormValue("event_repo_renamed") == "on" { + events = append(events, string(models.WebhookEventRepoRenamed)) + } if len(events) > 0 { webhook.Events = events @@ -228,8 +234,8 @@ func (rp *Repo) DeleteWebhook(w http.ResponseWriter, r *http.Request) { } // Verify webhook belongs to this repo - if webhook.RepoAt != f.RepoAt() { - l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt()) + if string(webhook.RepoDid) != f.RepoDid { + l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) w.WriteHeader(http.StatusForbidden) return } @@ -284,8 +290,8 @@ func (rp *Repo) ToggleWebhook(w http.ResponseWriter, r *http.Request) { } // Verify webhook belongs to this repo - if webhook.RepoAt != f.RepoAt() { - l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt()) + if string(webhook.RepoDid) != f.RepoDid { + l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) w.WriteHeader(http.StatusForbidden) return } @@ -343,8 +349,8 @@ func (rp *Repo) WebhookDeliveries(w http.ResponseWriter, r *http.Request) { } // Verify webhook belongs to this repo - if webhook.RepoAt != f.RepoAt() { - l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoAt, "current_repo", f.RepoAt()) + if string(webhook.RepoDid) != f.RepoDid { + l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) w.WriteHeader(http.StatusForbidden) return } diff --git a/appview/reporesolver/resolver.go b/appview/reporesolver/resolver.go index 588de353..322ebb44 100644 --- a/appview/reporesolver/resolver.go +++ b/appview/reporesolver/resolver.go @@ -76,27 +76,27 @@ func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.MultiAccountUse currentDir := extractCurrentDir(r.URL.EscapedPath()) ref := chi.URLParam(r, "ref") - repoAt := repo.RepoAt() + repoDid := repo.RepoDid isStarred := false roles := repoinfo.RolesInRepo{} if user != nil { - isStarred = db.GetStarStatus(rr.execer, user.Did, repoAt) + isStarred = db.GetStarStatus(rr.execer, user.Did, repoDid) roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.RepoIdentifier()) } stats := repo.RepoStats if stats == nil { - starCount, starErr := db.GetStarCount(rr.execer, repoAt) + starCount, starErr := db.GetStarCount(rr.execer, models.StarSubjectRepo, repoDid) if starErr != nil { - log.Println("failed to get star count for ", repoAt) + log.Println("failed to get star count for ", repoDid) } - issueCount, err := db.GetIssueCount(rr.execer, repoAt) + issueCount, err := db.GetIssueCount(rr.execer, repoDid) if err != nil { - log.Println("failed to get issue count for ", repoAt) + log.Println("failed to get issue count for ", repoDid) } - pullCount, err := db.GetPullCount(rr.execer, repoAt) + pullCount, err := db.GetPullCount(rr.execer, repoDid) if err != nil { - log.Println("failed to get pull count for ", repoAt) + log.Println("failed to get pull count for ", repoDid) } stats = &models.RepoStats{ StarCount: starCount, diff --git a/appview/settings/settings.go b/appview/settings/settings.go index 6a078884..4df8bdc6 100644 --- a/appview/settings/settings.go +++ b/appview/settings/settings.go @@ -224,8 +224,8 @@ func (s *Settings) releaseSitesDomain(w http.ResponseWriter, r *http.Request) { // Delete each repo's R2 objects. for _, sc := range siteConfigs { - if err := sites.Delete(ctx, s.CfClient, user.Did, sc.RepoName); err != nil { - s.Logger.Error("releaseSitesDomain: R2 delete failed", "did", user.Did, "repo", sc.RepoName, "err", err) + if err := sites.Delete(ctx, s.CfClient, user.Did, sc.RepoRkey); err != nil { + s.Logger.Error("releaseSitesDomain: R2 delete failed", "did", user.Did, "repo", sc.RepoRkey, "err", err) } } diff --git a/appview/sites/sites.go b/appview/sites/sites.go index 07b87e8a..350ea5c1 100644 --- a/appview/sites/sites.go +++ b/appview/sites/sites.go @@ -158,7 +158,7 @@ func Deploy( return fmt.Errorf("walking deploy dir: %w", err) } - if err := cf.SyncFiles(ctx, prefix(f.Did, f.Name), files); err != nil { + if err := cf.SyncFiles(ctx, prefix(f.Did, f.Rkey), files); err != nil { return fmt.Errorf("syncing files to R2: %w", err) } diff --git a/appview/state/knotstream.go b/appview/state/knotstream.go index 5e38552b..49877e3e 100644 --- a/appview/state/knotstream.go +++ b/appview/state/knotstream.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "slices" + "strings" "time" "tangled.org/core/appview/cloudflare" @@ -72,7 +73,7 @@ func resolveRepo(d *db.DB, repoDid *string, ownerDid, repoName string) (*models. if repoDid != nil && *repoDid != "" { return db.GetRepoByDid(d, *repoDid) } - repos, err := db.GetRepos(d, orm.FilterEq("did", ownerDid), orm.FilterEq("name", repoName)) + repos, err := db.GetRepos(d, orm.FilterEq("did", ownerDid), orm.FilterEq("rkey", strings.ToLower(repoName))) if err != nil { return nil, err } @@ -114,17 +115,11 @@ func ingestRefUpdate(ctx context.Context, d *db.DB, enforcer *rbac.Enforcer, pc return fmt.Errorf("%s does not belong to %s, something is fishy", record.CommitterDid, source.Key()) } - ownerDid := "" - if record.OwnerDid != nil { - ownerDid = *record.OwnerDid - } else { - // handle legacy event - if record.RepoDid != nil { - ownerDid = *record.RepoDid - } + if record.Repo == "" { + return fmt.Errorf("gitRefUpdate from %s missing repo", source.Key()) } - repo, lookupErr := resolveRepo(d, record.RepoDid, ownerDid, record.RepoName) + repo, lookupErr := db.GetRepoByDid(d, record.Repo) if lookupErr != nil { return fmt.Errorf("failed to look up repo: %w", lookupErr) } @@ -167,17 +162,12 @@ func triggerSitesDeployIfNeeded(ctx context.Context, d *db.DB, cfClient *cloudfl } pushedBranch := ref.Short() - ownerDid := "" - if record.OwnerDid != nil { - ownerDid = *record.OwnerDid - } - - repo, err := resolveRepo(d, record.RepoDid, ownerDid, record.RepoName) + repo, err := db.GetRepoByDid(d, record.Repo) if err != nil { return } - siteConfig, err := db.GetRepoSiteConfig(d, repo.RepoAt().String()) + siteConfig, err := db.GetRepoSiteConfig(d, repo.RepoDid) if err != nil || siteConfig == nil { return } @@ -186,7 +176,7 @@ func triggerSitesDeployIfNeeded(ctx context.Context, d *db.DB, cfClient *cloudfl } deploy := &models.SiteDeploy{ - RepoAt: repo.RepoAt().String(), + RepoDid: syntax.DID(repo.RepoDid), Branch: siteConfig.Branch, Dir: siteConfig.Dir, CommitSHA: record.NewSha, @@ -249,15 +239,10 @@ func populatePunchcard(d *db.DB, record tangled.GitRefUpdate) error { func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { if record.Meta == nil || record.Meta.LangBreakdown == nil || record.Meta.LangBreakdown.Inputs == nil { - return fmt.Errorf("empty language data for repo: %v/%s", record.OwnerDid, record.RepoName) - } - - ownerDid := "" - if record.OwnerDid != nil { - ownerDid = *record.OwnerDid + return fmt.Errorf("empty language data for repo: %s", record.Repo) } - r, lookupErr := resolveRepo(d, record.RepoDid, ownerDid, record.RepoName) + r, lookupErr := db.GetRepoByDid(d, record.Repo) if lookupErr != nil { return fmt.Errorf("failed to look up repo: %w", lookupErr) } @@ -275,7 +260,7 @@ func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { } langs = append(langs, models.RepoLanguage{ - RepoAt: repo.RepoAt(), + RepoDid: syntax.DID(repo.RepoDid), Ref: ref.Short(), IsDefaultRef: record.Meta.IsDefaultRef, Language: l.Lang, @@ -290,7 +275,7 @@ func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { defer tx.Rollback() // update appview's cache - err = db.UpdateRepoLanguages(tx, repo.RepoAt(), ref.Short(), langs) + err = db.UpdateRepoLanguages(tx, syntax.DID(repo.RepoDid), ref.Short(), langs) if err != nil { fmt.Printf("failed; %s\n", err) // non-fatal @@ -398,7 +383,7 @@ func ingestDIDAssign(d *db.DB, enforcer *rbac.Enforcer, source ec.Source, msg ec repos, err := db.GetRepos(d, orm.FilterEq("did", record.OwnerDid), - orm.FilterEq("name", record.RepoName), + orm.FilterEq("rkey", strings.ToLower(record.RepoName)), ) if err != nil || len(repos) == 0 { logger.Warn("didAssign for unknown repo, skipping", @@ -443,7 +428,7 @@ func ingestDIDAssign(d *db.DB, enforcer *rbac.Enforcer, source ec.Source, msg ec return fmt.Errorf("add RBAC policies for %s: %w", record.RepoDid, err) } - collabs, collabErr := db.GetCollaborators(d, orm.FilterEq("repo_at", repoAtUri)) + collabs, collabErr := db.GetCollaborators(d, orm.FilterEq("repo_did", record.RepoDid)) if collabErr != nil { return fmt.Errorf("get collaborators for RBAC update: %w", collabErr) } diff --git a/appview/state/profile.go b/appview/state/profile.go index 71f095ba..d97492aa 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -725,7 +725,7 @@ func (s *State) addRepoItems(ctx context.Context, feed *feeds.Feed, repos []mode func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { return &feeds.Item{ Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.BaseUrl(), owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.BaseUrl(), owner.Handle, pull.Repo.Rkey, pull.PullId), Type: "text/html", Rel: "alternate"}, Created: pull.Created, Author: author, } @@ -734,7 +734,7 @@ func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identit func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { return &feeds.Item{ Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name), - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.BaseUrl(), owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.BaseUrl(), owner.Handle, issue.Repo.Rkey, issue.IssueId), Type: "text/html", Rel: "alternate"}, Created: issue.Created, Author: author, } @@ -754,7 +754,7 @@ func (s *State) createRepoItem(ctx context.Context, repo models.RepoEvent, autho return &feeds.Item{ Title: title, - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.BaseUrl(), author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.BaseUrl(), author.Name[1:], repo.Repo.Rkey), Type: "text/html", Rel: "alternate"}, // Remove @ prefix Created: repo.Repo.Created, Author: author, }, nil diff --git a/appview/state/router.go b/appview/state/router.go index 61e4a8e6..8b0e5ec1 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -63,7 +63,7 @@ func (s *State) Router() http.Handler { if len(pathParts) > 1 { remaining = "/" + pathParts[1] } - rewritten := "/" + repo.Did + "/" + repo.Name + remaining + rewritten := "/" + repo.Did + "/" + repo.Rkey + remaining r2 := r.Clone(r.Context()) r2.URL.Path = rewritten r2.URL.RawPath = rewritten diff --git a/appview/state/state.go b/appview/state/state.go index 1138027e..779c10fa 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -39,7 +39,6 @@ import ( tlog "tangled.org/core/log" "tangled.org/core/orm" "tangled.org/core/rbac" - "tangled.org/core/tid" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/atclient" @@ -134,6 +133,7 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { tangled.RepoArtifactNSID, tangled.RepoIssueCommentNSID, tangled.RepoIssueNSID, + tangled.RepoNSID, tangled.RepoPullNSID, tangled.SpindleMemberNSID, tangled.SpindleNSID, @@ -156,20 +156,6 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { return nil, fmt.Errorf("failed to backfill default label defs: %w", err) } - ingester := appview.Ingester{ - Db: wrapper, - Enforcer: enforcer, - IdResolver: res, - Cache: rdb, - Config: config, - Logger: log.SubLogger(logger, "ingester"), - Validator: validator, - } - err = jc.StartJetstream(ctx, ingester.Ingest()) - if err != nil { - return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) - } - var notifiers []notify.Notifier // Always add the database notifier @@ -186,6 +172,21 @@ func Make(ctx context.Context, config *config.Config) (*State, error) { notifier := notify.NewMergedNotifier(notifiers) notifier = lognotify.NewLoggingNotifier(notifier, tlog.SubLogger(logger, "notify")) + ingester := appview.Ingester{ + Db: d, + Enforcer: enforcer, + IdResolver: res, + Cache: rdb, + Config: config, + Logger: log.SubLogger(logger, "ingester"), + Validator: validator, + Notifier: notifier, + } + err = jc.StartJetstream(ctx, ingester.Ingest()) + if err != nil { + return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) + } + var cfClient *cloudflare.Client if config.Cloudflare.ApiToken != "" { cfClient, err = cloudflare.New(config) @@ -423,42 +424,6 @@ func (s *State) Keys(w http.ResponseWriter, r *http.Request) { } } -func validateRepoName(name string) error { - // check for path traversal attempts - if name == "." || name == ".." || - strings.Contains(name, "/") || strings.Contains(name, "\\") { - return fmt.Errorf("Repository name contains invalid path characters") - } - - // check for sequences that could be used for traversal when normalized - if strings.Contains(name, "./") || strings.Contains(name, "../") || - strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { - return fmt.Errorf("Repository name contains invalid path sequence") - } - - // then continue with character validation - for _, char := range name { - if !((char >= 'a' && char <= 'z') || - (char >= 'A' && char <= 'Z') || - (char >= '0' && char <= '9') || - char == '-' || char == '_' || char == '.') { - return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") - } - } - - // additional check to prevent multiple sequential dots - if strings.Contains(name, "..") { - return fmt.Errorf("Repository name cannot contain sequential dots") - } - - // if all checks pass - return nil -} - -func stripGitExt(name string) string { - return strings.TrimSuffix(name, ".git") -} - func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: @@ -494,12 +459,13 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { return } - if err := validateRepoName(repoName); err != nil { + if err := models.ValidateRepoName(repoName); err != nil { s.pages.Notice(w, "repo", err.Error()) return } - repoName = stripGitExt(repoName) - l = l.With("repoName", repoName) + repoName = models.StripGitExt(repoName) + rkey := strings.ToLower(repoName) + l = l.With("repoName", repoName, "rkey", rkey) defaultBranch := r.FormValue("branch") if defaultBranch == "" { @@ -525,7 +491,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { existingRepo, err := db.GetRepo( s.db, orm.FilterEq("did", user.Did), - orm.FilterEq("name", repoName), + orm.FilterEq("rkey", rkey), ) if err == nil && existingRepo != nil { l.Info("repo exists") @@ -533,8 +499,6 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { return } - rkey := tid.TID() - client, err := s.oauth.ServiceClient( r, oauth.WithService(domain), @@ -549,7 +513,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { input := &tangled.RepoCreate_Input{ Rkey: rkey, - Name: repoName, + Name: rkey, DefaultBranch: &defaultBranch, } createResp, err := tangled.RepoCreate( @@ -603,7 +567,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{ Did: user.Did, - Name: repoName, + Name: rkey, Rkey: rkey, }); dErr != nil { cancel() @@ -627,7 +591,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { return } - atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ + _, err = comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoNSID, Repo: user.Did, Rkey: rkey, @@ -638,11 +602,15 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { if err != nil { l.Info("PDS write failed", "err", err) cleanupKnot() - s.pages.Notice(w, "repo", "Failed to announce repository creation.") + if rkeyOccupied(r.Context(), atpClient, user.Did, rkey) { + s.pages.Notice(w, "repo", fmt.Sprintf("You already have a repository named %q.", rkey)) + } else { + s.pages.Notice(w, "repo", "Failed to announce repository creation.") + } return } - aturi := atresp.Uri + aturi := fmt.Sprintf("at://%s/%s/%s", user.Did, tangled.RepoNSID, rkey) l = l.With("aturi", aturi) l.Info("wrote to PDS") @@ -709,11 +677,18 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) default: handle := s.pages.DisplayHandle(r.Context(), user.Did) - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", handle, repoName)) + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", handle, rkey)) } } } +func rkeyOccupied(ctx context.Context, client *atclient.APIClient, did, rkey string) bool { + probeCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + resp, err := comatproto.RepoGetRecord(probeCtx, client, "", tangled.RepoNSID, did, rkey) + return err == nil && resp != nil +} + // this is used to rollback changes made to the PDS // // it is a no-op if the provided ATURI is empty diff --git a/appview/strings/strings.go b/appview/strings/strings.go index dff3c64f..b8478345 100644 --- a/appview/strings/strings.go +++ b/appview/strings/strings.go @@ -149,14 +149,15 @@ func (s *Strings) contents(w http.ResponseWriter, r *http.Request) { showRendered = r.URL.Query().Get("code") != "true" } - starCount, err := db.GetStarCount(s.Db, string.AtUri()) + stringUri := string.AtUri().String() + starCount, err := db.GetStarCount(s.Db, models.StarSubjectString, stringUri) if err != nil { l.Error("failed to get star count", "err", err) } user := s.OAuth.GetMultiAccountUser(r) isStarred := false if user != nil { - isStarred = db.GetStarStatus(s.Db, user.Did, string.AtUri()) + isStarred = db.GetStarStatus(s.Db, user.Did, stringUri) } s.Pages.SingleString(w, pages.SingleStringParams{ -- 2.51.2