From 244ff651c3ad2b2a9766a4e9e224d13d9774333c Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 22 Mar 2026 14:53:03 +0000 Subject: [PATCH] feat(commits): add List/Get methods + tests (Codex) CommitService now has ListRepoCommits and GetCommit methods with full httptest coverage. Tests verify correct paths, response parsing, and error handling. Co-Authored-By: Virgil --- commits.go | 33 ++++++++++++++++++- commits_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/commits.go b/commits.go index 1c38c82..27b501e 100644 --- a/commits.go +++ b/commits.go @@ -3,21 +3,52 @@ package forge import ( "context" "fmt" + "iter" "dappco.re/go/core/forge/types" ) // CommitService handles commit-related operations such as commit statuses // and git notes. -// No Resource embedding — heterogeneous endpoints across status and note paths. +// No Resource embedding — collection and item commit paths differ, and the +// remaining endpoints are heterogeneous across status and note paths. type CommitService struct { client *Client } +const ( + commitCollectionPath = "/api/v1/repos/{owner}/{repo}/commits" + commitItemPath = "/api/v1/repos/{owner}/{repo}/git/commits/{sha}" +) + func newCommitService(c *Client) *CommitService { return &CommitService{client: c} } +// List returns a single page of commits for a repository. +func (s *CommitService) List(ctx context.Context, params Params, opts ListOptions) (*PagedResult[types.Commit], error) { + return ListPage[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil, opts) +} + +// ListAll returns all commits for a repository. +func (s *CommitService) ListAll(ctx context.Context, params Params) ([]types.Commit, error) { + return ListAll[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil) +} + +// Iter returns an iterator over all commits for a repository. +func (s *CommitService) Iter(ctx context.Context, params Params) iter.Seq2[types.Commit, error] { + return ListIter[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil) +} + +// Get returns a single commit by SHA or ref. +func (s *CommitService) Get(ctx context.Context, params Params) (*types.Commit, error) { + var out types.Commit + if err := s.client.Get(ctx, ResolvePath(commitItemPath, params), &out); err != nil { + return nil, err + } + return &out, nil +} + // GetCombinedStatus returns the combined status for a given ref (branch, tag, or SHA). func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error) { path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, ref) diff --git a/commits_test.go b/commits_test.go index 4c44a0f..82043f8 100644 --- a/commits_test.go +++ b/commits_test.go @@ -10,6 +10,91 @@ import ( "dappco.re/go/core/forge/types" ) +func TestCommitService_Good_List(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/repos/core/go-forge/commits" { + t.Errorf("wrong path: %s", r.URL.Path) + } + if r.URL.Query().Get("page") != "1" { + t.Errorf("got page=%q, want %q", r.URL.Query().Get("page"), "1") + } + if r.URL.Query().Get("limit") != "50" { + t.Errorf("got limit=%q, want %q", r.URL.Query().Get("limit"), "50") + } + w.Header().Set("X-Total-Count", "2") + json.NewEncoder(w).Encode([]types.Commit{ + { + SHA: "abc123", + Commit: &types.RepoCommit{ + Message: "first commit", + }, + }, + { + SHA: "def456", + Commit: &types.RepoCommit{ + Message: "second commit", + }, + }, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + result, err := f.Commits.List(context.Background(), Params{"owner": "core", "repo": "go-forge"}, DefaultList) + if err != nil { + t.Fatal(err) + } + if len(result.Items) != 2 { + t.Fatalf("got %d items, want 2", len(result.Items)) + } + if result.Items[0].SHA != "abc123" { + t.Errorf("got sha=%q, want %q", result.Items[0].SHA, "abc123") + } + if result.Items[1].Commit == nil { + t.Fatal("expected commit payload, got nil") + } + if result.Items[1].Commit.Message != "second commit" { + t.Errorf("got message=%q, want %q", result.Items[1].Commit.Message, "second commit") + } +} + +func TestCommitService_Good_Get(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/repos/core/go-forge/git/commits/abc123" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode(types.Commit{ + SHA: "abc123", + HTMLURL: "https://forge.example/core/go-forge/commit/abc123", + Commit: &types.RepoCommit{ + Message: "initial import", + }, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + commit, err := f.Commits.Get(context.Background(), Params{"owner": "core", "repo": "go-forge", "sha": "abc123"}) + if err != nil { + t.Fatal(err) + } + if commit.SHA != "abc123" { + t.Errorf("got sha=%q, want %q", commit.SHA, "abc123") + } + if commit.Commit == nil { + t.Fatal("expected commit payload, got nil") + } + if commit.Commit.Message != "initial import" { + t.Errorf("got message=%q, want %q", commit.Commit.Message, "initial import") + } +} + func TestCommitService_Good_ListStatuses(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet {