Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Other tools accept a monitored branch or a full SHA. To work with any other bran
| Full 40-character SHA | An observed or retained version; otherwise `REVISION_NOT_ELIGIBLE` | `observed` |
| Any other branch or tag name, `refs/heads/<name>`, `refs/tags/<name>` | The repository's ref catalog when it is fresh and not truncated; otherwise the remote is asked for exactly these names and the matching ref is downloaded to a scratch directory, without file trees, to confirm its object type and commit details, then deleted | `catalog` or `remote` |

A bare name that is both a branch and a tag returns `REF_AMBIGUOUS` listing both; pass the full form. A missing name returns `REF_NOT_FOUND`, and a tag confirmed to end at a tree or blob returns `REF_NOT_COMMIT`. Resolution uses the ref as actually downloaded, so a branch that moves between listing and download resolves to the downloaded commit; an object that cannot be verified returns a retryable `REMOTE_UNAVAILABLE`, never `REF_NOT_COMMIT`. Annotated tags resolve to their commit; `created_at/author/title` describe the tag and `committer_time` the commit. Short SHAs, `HEAD` and revision expressions are still rejected. Identical concurrent remote lookups share one request, and results, including "not found", are cached for 30 seconds (at most 10,000 entries). Remote lookups run at most 2 per repository and 8 per instance, at most 64 may be running or waiting (beyond that, `REMOTE_UNAVAILABLE` with `Retry-After`), and they back off per repository after failures, from 10 seconds up to 5 minutes. Visibility is checked before every cached answer.
A bare name that is both a branch and a tag returns `REF_AMBIGUOUS` listing both; pass the full form. A missing name returns `REF_NOT_FOUND`, and a tag confirmed to end at a tree or blob returns `REF_NOT_COMMIT`. Resolution uses the ref as actually downloaded, so a branch that moves between listing and download resolves to the downloaded commit; an object that cannot be verified returns a retryable `REMOTE_UNAVAILABLE`, never `REF_NOT_COMMIT`. Annotated tags resolve to their commit; `created_at/author/title` describe the tag and `committer_time` the commit. Short SHAs, `HEAD`, revision expressions and branch or tag names with characters the service does not accept (such as parentheses or spaces) are rejected with `SHORT_SHA_UNSUPPORTED` or `INVALID_REVISION`; such names still appear in `refs`. Identical concurrent remote lookups share one request, and results, including "not found", are cached for 30 seconds (at most 10,000 entries). Remote lookups run at most 2 per repository and 8 per instance, at most 64 may be running or waiting (beyond that, `REMOTE_UNAVAILABLE` with `Retry-After`), and they back off per repository after failures, from 10 seconds up to 5 minutes. Visibility is checked before every cached answer.

`refs` lists a registered repository's branches or tags. Optional arguments: `kind` (`branch` default, `tag`, `all`); `query`, `author` and `title`, case-insensitive substrings of the name, the author or tagger, and the commit title or tag message first line; `since/until` (RFC 3339 or `YYYY-MM-DD`, compared with `created_at`); `sort` (`created`, newest first, the default, or `name`); `first` (1–100, default 30); `after` (the previous `next_cursor`, with the other arguments unchanged). It returns `repo/catalog/sort/refs/next_cursor/has_more` and sometimes a `hint`. Each entry has `name/ref/kind/monitored/indexed`; entries verified to end at a commit add `commit/created_at/committer_time/author/title`, annotated tags add `annotated`, and tags of trees, blobs or unverifiable objects carry `object/target_type` instead of `commit`. `created_at` is the commit time for branches and lightweight tags and the tagging time for annotated tags.

Expand Down
30 changes: 30 additions & 0 deletions internal/httpserver/plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,33 @@ func TestPluginDiscoveryRouteAndVisibility(t *testing.T) {
}
}
}

func TestRefsRouteAcceptsItsFilters(t *testing.T) {
store := &pluginRepoStore{public: true}
s, e := service.New(store, t.TempDir(), "", "")
if e != nil {
t.Fatal(e)
}
defer s.Close()
h := Runtime(config.Defaults(), slog.New(slog.NewTextHandler(io.Discard, nil)), s, "")
for _, tc := range []struct {
query, code string
}{
// Filters reach the service, which validates their values.
{"&kind=tag&query=v1&author=a&title=b&sort=name&first=5&until=2026-01-01&since=yesterday", "INVALID_TIME"},
{"&kind=tag&kind=branch", "INVALID_QUERY_PARAMETERS"},
{"&unknown=1", "INVALID_QUERY_PARAMETERS"},
} {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest("GET", "/sourcegraph/v1/repo/refs?repo=org/repo"+tc.query, nil))
if w.Code != 400 || !strings.Contains(w.Body.String(), tc.code) {
t.Fatalf("%s: %d %s", tc.query, w.Code, w.Body.String())
}
}
// A filter name is accepted only on the refs route.
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest("GET", "/sourcegraph/v1/repo/availability?repo=org/repo&kind=tag", nil))
if w.Code != 400 || !strings.Contains(w.Body.String(), "INVALID_QUERY_PARAMETERS") {
t.Fatalf("availability: %d %s", w.Code, w.Body.String())
}
}
2 changes: 1 addition & 1 deletion internal/httpserver/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func runtime(c config.Config, log *slog.Logger, s *service.Service, token string
}
if r.URL.RawQuery != "" {
for k, v := range r.URL.Query() {
if (k != "repo" && k != "after" && !(k == "revision" && r.URL.Path == Prefix+"/v1/repo/plugins") && !(k == "branch" && r.URL.Path == Prefix+"/api/admin/v1/repo/commit-options")) || len(v) != 1 {
if (k != "repo" && k != "after" && !(k == "revision" && r.URL.Path == Prefix+"/v1/repo/plugins") && !(k == "branch" && r.URL.Path == Prefix+"/api/admin/v1/repo/commit-options") && !(refsParameters[k] && r.URL.Path == Prefix+"/v1/repo/refs")) || len(v) != 1 {
status = 400
writeError(w, id, status, "INVALID_QUERY_PARAMETERS", "Unexpected or repeated query parameter.")
return
Expand Down
3 changes: 3 additions & 0 deletions internal/httpserver/website_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import (

// Public access is method/path specific. A future management route
// cannot become public merely by sharing the same URL prefix.
// refsParameters are the filters GET /v1/repo/refs accepts besides repo and after.
var refsParameters = map[string]bool{"kind": true, "query": true, "author": true, "title": true, "since": true, "until": true, "sort": true, "first": true}

func publicRead(r *http.Request) bool {
if r.Method == "GET" {
switch r.URL.Path {
Expand Down
2 changes: 1 addition & 1 deletion internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ func eligible(r control.Repository, revision string) (control.Version, error) {
return control.Version{}, contract.Fail("SHORT_SHA_UNSUPPORTED", "Abbreviated commit SHAs are not supported; obtain the original full 40-character lowercase SHA from trusted source metadata. Do not guess or switch branches.", 400)
}
if (revision == branch && isHexRevision(revision)) || gitstore.ValidateRevision(revision) != nil || (strings.HasPrefix(revision, "refs/") && revision == branch) {
return control.Version{}, contract.Fail("INVALID_REVISION", "Use a monitored branch or a full 40-character lowercase commit SHA; HEAD, tags and arbitrary refs are not supported.", 400)
return control.Version{}, contract.Fail("INVALID_REVISION", "Use a monitored branch or a full 40-character lowercase commit SHA; resolve also accepts other branch and tag names. HEAD, revision expressions, other ref namespaces and names with unsupported characters are rejected.", 400)
}
return control.Version{}, contract.Fail("BRANCH_NOT_MONITORED", "The requested branch is not monitored; inspect availability for configured branches without substituting a different baseline.", 404)
}
Expand Down
Loading