Skip to content
Open
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
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Exact branch and tag lookup

- Added exact branch and tag lookup methods to the TypeScript, Python, and Go SDKs.
- Added ephemeral branch lookup and the preferred repository-scoped REST routes.
- Exact results omit list cursors and the private tag object SHA.

### Preferred REST routes

- Changed preferred SDK calls to use the unversioned `/api` collection and
Expand Down
16 changes: 15 additions & 1 deletion packages/code-storage-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,29 @@ to blame the whole file. The top-level `CommitSHA` is the SHA `Ref` resolved
to; each `BlameLine` carries its authoring commit's metadata inline, with
`PreviousCommitSHA` empty when the line has no prior version.

### Manage tags
### Read branches and manage tags

```go
branch, err := repo.GetBranch(context.Background(), storage.GetBranchOptions{
Name: "feature/preview",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(branch.HeadSHA, branch.CreatedAt)

tags, err := repo.ListTags(context.Background(), storage.ListTagsOptions{Limit: 10})
if err != nil {
log.Fatal(err)
}
fmt.Println(tags.Tags)

tag, err := repo.GetTag(context.Background(), storage.GetTagOptions{Name: "v1.0.0"})
if err != nil {
log.Fatal(err)
}
fmt.Println(tag.SHA)

createdTag, err := repo.CreateTag(context.Background(), storage.CreateTagOptions{
Name: "v1.0.0",
Ref: "0123456789abcdef0123456789abcdef01234567",
Expand Down
159 changes: 159 additions & 0 deletions packages/code-storage-go/named_ref_lookup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package storage

import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestGetBranchRequestAndResult(t *testing.T) {
tests := []struct {
name string
ephemeral *bool
expectedQuery string
responseBranch string
}{
{
name: "true",
ephemeral: boolPtr(true),
expectedQuery: "ephemeral=true&name=attempt%2F7",
responseBranch: "attempt/7",
},
{
name: "false",
ephemeral: boolPtr(false),
expectedQuery: "ephemeral=false&name=attempt%2F7",
responseBranch: "attempt/7",
},
{
name: "omitted",
expectedQuery: "name=attempt%2F7",
responseBranch: "attempt/7",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatalf("unexpected method: %s", r.Method)
}
if r.URL.Path != "/api/repos/owner/repo/branch" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.RawQuery != test.expectedQuery {
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
}
if strings.Contains(r.URL.RawQuery, "%252F") {
t.Fatalf("name was encoded twice: %s", r.URL.RawQuery)
}
assertGitReadScope(t, r)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"branch":{"name":"attempt/7","head_sha":"abc123","created_at":"2026-08-29T10:00:00Z","cursor":"private-cursor"}}`))
}))
defer server.Close()

client, err := NewClient(Options{Name: "acme", Key: testKey, APIBaseURL: server.URL})
if err != nil {
t.Fatalf("client error: %v", err)
}
repo := &Repo{ID: "owner/repo", DefaultBranch: "main", client: client}

result, err := repo.GetBranch(context.Background(), GetBranchOptions{
Name: "attempt/7",
Ephemeral: test.ephemeral,
})
if err != nil {
t.Fatalf("get branch error: %v", err)
}
expected := GetBranchResult{
Name: test.responseBranch,
HeadSHA: "abc123",
CreatedAt: "2026-08-29T10:00:00Z",
}
if result != expected {
t.Fatalf("unexpected result: %+v", result)
}
})
}
}

func TestGetTagRequestAndResult(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatalf("unexpected method: %s", r.Method)
}
if r.URL.Path != "/api/repos/owner/repo/tag" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.URL.RawQuery != "name=releases%2Fv1.4.0" {
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
}
if strings.Contains(r.URL.RawQuery, "%252F") {
t.Fatalf("name was encoded twice: %s", r.URL.RawQuery)
}
assertGitReadScope(t, r)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"tag":{"name":"releases/v1.4.0","sha":"commit123","object_sha":"tag-object-123","cursor":"private-cursor"}}`))
}))
defer server.Close()

client, err := NewClient(Options{Name: "acme", Key: testKey, APIBaseURL: server.URL})
if err != nil {
t.Fatalf("client error: %v", err)
}
repo := &Repo{ID: "owner/repo", DefaultBranch: "main", client: client}

result, err := repo.GetTag(context.Background(), GetTagOptions{Name: "releases/v1.4.0"})
if err != nil {
t.Fatalf("get tag error: %v", err)
}
if result != (GetTagResult{Name: "releases/v1.4.0", SHA: "commit123"}) {
t.Fatalf("unexpected result: %+v", result)
}
}

func TestNamedRefLookupNotFound(t *testing.T) {
for _, resource := range []string{"branch", "tag"} {
t.Run(resource, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":"ref not found"}`))
}))
defer server.Close()

client, err := NewClient(Options{Name: "acme", Key: testKey, APIBaseURL: server.URL})
if err != nil {
t.Fatalf("client error: %v", err)
}
repo := &Repo{ID: "repo", DefaultBranch: "main", client: client}

if resource == "branch" {
_, err = repo.GetBranch(context.Background(), GetBranchOptions{Name: "missing/ref"})
} else {
_, err = repo.GetTag(context.Background(), GetTagOptions{Name: "missing/ref"})
}
var apiErr *APIError
if !errors.As(err, &apiErr) {
t.Fatalf("expected APIError, got %T", err)
}
if apiErr.Status != http.StatusNotFound || apiErr.Message != "ref not found" {
t.Fatalf("unexpected error: %+v", apiErr)
}
})
}
}

func assertGitReadScope(t *testing.T, r *http.Request) {
t.Helper()
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
claims := parseJWTFromToken(t, token)
scopes, ok := claims["scopes"].([]interface{})
if !ok || len(scopes) != 1 || scopes[0] != "git:read" {
t.Fatalf("unexpected scopes: %v", claims["scopes"])
}
}
54 changes: 54 additions & 0 deletions packages/code-storage-go/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,37 @@ func (r *Repo) ListBranches(ctx context.Context, options ListBranchesOptions) (L
return result, nil
}

// GetBranch gets one branch by its exact name.
func (r *Repo) GetBranch(ctx context.Context, options GetBranchOptions) (GetBranchResult, error) {
ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitRead}, TTL: ttl})
if err != nil {
return GetBranchResult{}, err
}

params := url.Values{}
params.Set("name", options.Name)
if options.Ephemeral != nil {
params.Set("ephemeral", strconv.FormatBool(*options.Ephemeral))
}

resp, err := r.client.api.get(ctx, r.apiPath("branch"), params, jwtToken, nil)
if err != nil {
return GetBranchResult{}, err
}
defer resp.Body.Close()

var payload getBranchResponse
if err := decodeJSON(resp, &payload); err != nil {
return GetBranchResult{}, err
}
return GetBranchResult{
Name: payload.Branch.Name,
HeadSHA: payload.Branch.HeadSHA,
CreatedAt: payload.Branch.CreatedAt,
}, nil
}

// ListTags lists tags.
func (r *Repo) ListTags(ctx context.Context, options ListTagsOptions) (ListTagsResult, error) {
ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
Expand Down Expand Up @@ -473,6 +504,29 @@ func (r *Repo) ListTags(ctx context.Context, options ListTagsOptions) (ListTagsR
return result, nil
}

// GetTag gets one tag by its exact name.
func (r *Repo) GetTag(ctx context.Context, options GetTagOptions) (GetTagResult, error) {
ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
jwtToken, err := r.client.generateJWT(r.ID, RemoteURLOptions{Permissions: []Permission{PermissionGitRead}, TTL: ttl})
if err != nil {
return GetTagResult{}, err
}

params := url.Values{}
params.Set("name", options.Name)
resp, err := r.client.api.get(ctx, r.apiPath("tag"), params, jwtToken, nil)
if err != nil {
return GetTagResult{}, err
}
defer resp.Body.Close()

var payload getTagResponse
if err := decodeJSON(resp, &payload); err != nil {
return GetTagResult{}, err
}
return GetTagResult{Name: payload.Tag.Name, SHA: payload.Tag.SHA}, nil
}

// ListCommits lists commits.
func (r *Repo) ListCommits(ctx context.Context, options ListCommitsOptions) (ListCommitsResult, error) {
ttl := resolveInvocationTTL(options.InvocationOptions, defaultTokenTTL)
Expand Down
15 changes: 15 additions & 0 deletions packages/code-storage-go/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ type branchInfoRaw struct {
CreatedAt string `json:"created_at"`
}

type getBranchResponse struct {
Branch struct {
Name string `json:"name"`
HeadSHA string `json:"head_sha"`
CreatedAt string `json:"created_at"`
} `json:"branch"`
}

type listCommitsResponse struct {
Commits []commitInfoRaw `json:"commits"`
NextCursor string `json:"next_cursor"`
Expand Down Expand Up @@ -274,6 +282,13 @@ type tagInfoRaw struct {
SHA string `json:"sha"`
}

type getTagResponse struct {
Tag struct {
Name string `json:"name"`
SHA string `json:"sha"`
} `json:"tag"`
}

type createTagResponse struct {
Name string `json:"name"`
SHA string `json:"sha"`
Expand Down
4 changes: 4 additions & 0 deletions packages/code-storage-go/route_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ func TestPreferredRESTRouteContract(t *testing.T) {
{name: "ListFiles", method: "GET", path: "/api/repos/owner%2Fname/files", invoke: func(_ *Client, r *Repo) { _, _ = r.ListFiles(ctx, ListFilesOptions{}) }},
{name: "ListFilesWithMetadata", method: "GET", path: "/api/repos/owner%2Fname/files/metadata", invoke: func(_ *Client, r *Repo) { _, _ = r.ListFilesWithMetadata(ctx, ListFilesWithMetadataOptions{}) }},
{name: "ListBranches", method: "GET", path: "/api/repos/owner%2Fname/branches", invoke: func(_ *Client, r *Repo) { _, _ = r.ListBranches(ctx, ListBranchesOptions{}) }},
{name: "GetBranch", method: "GET", path: "/api/repos/owner%2Fname/branch", query: url.Values{"name": {"feature/one"}, "ephemeral": {"false"}}, invoke: func(_ *Client, r *Repo) {
_, _ = r.GetBranch(ctx, GetBranchOptions{Name: "feature/one", Ephemeral: boolPtr(false)})
}},
{name: "ListTags", method: "GET", path: "/api/repos/owner%2Fname/tags", invoke: func(_ *Client, r *Repo) { _, _ = r.ListTags(ctx, ListTagsOptions{}) }},
{name: "GetTag", method: "GET", path: "/api/repos/owner%2Fname/tag", query: url.Values{"name": {"release/v1"}}, invoke: func(_ *Client, r *Repo) { _, _ = r.GetTag(ctx, GetTagOptions{Name: "release/v1"}) }},
{name: "ListCommits", method: "GET", path: "/api/repos/owner%2Fname/commits", invoke: func(_ *Client, r *Repo) { _, _ = r.ListCommits(ctx, ListCommitsOptions{}) }},
{name: "GetCommit", method: "GET", path: "/api/repos/owner%2Fname/commit", invoke: func(_ *Client, r *Repo) { _, _ = r.GetCommit(ctx, GetCommitOptions{Ref: "main"}) }},
{name: "GetBlame", method: "GET", path: "/api/repos/owner%2Fname/blame", invoke: func(_ *Client, r *Repo) { _, _ = r.GetBlame(ctx, BlameOptions{Path: "README.md"}) }},
Expand Down
26 changes: 26 additions & 0 deletions packages/code-storage-go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,20 @@ type ListBranchesResult struct {
HasMore bool
}

// GetBranchOptions configures an exact branch lookup.
type GetBranchOptions struct {
InvocationOptions
Name string
Ephemeral *bool
}

// GetBranchResult describes one branch.
type GetBranchResult struct {
Name string
HeadSHA string
CreatedAt string
}

// CreateBranchOptions configures branch creation.
type CreateBranchOptions struct {
InvocationOptions
Expand Down Expand Up @@ -593,6 +607,18 @@ type ListTagsResult struct {
HasMore bool
}

// GetTagOptions configures an exact tag lookup.
type GetTagOptions struct {
InvocationOptions
Name string
}

// GetTagResult describes one tag.
type GetTagResult struct {
Name string
SHA string
}

// CreateTagOptions configures tag creation.
type CreateTagOptions struct {
InvocationOptions
Expand Down
Loading