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 }}
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 }}
{{ 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{