diff --git a/packages/code-storage-go/README.md b/packages/code-storage-go/README.md index f03d7d0..abe6eb7 100644 --- a/packages/code-storage-go/README.md +++ b/packages/code-storage-go/README.md @@ -222,6 +222,25 @@ if err != nil { fmt.Println(result.CommitSHA) ``` +### Inspect commit parents + +`ListCommits` and `GetCommit` expose parent SHAs in Git parent order. Root +commits return an empty slice. + +```go +commits, err := repo.ListCommits(context.Background(), storage.ListCommitsOptions{ + Branch: "main", + Limit: 20, +}) +if err != nil { + log.Fatal(err) +} + +for _, commit := range commits.Commits { + fmt.Println(commit.SHA, commit.ParentSHAs) +} +``` + TTL fields use `time.Duration` values (for example `time.Hour`). ### Hydrate a repo without an API request diff --git a/packages/code-storage-go/repo.go b/packages/code-storage-go/repo.go index c15ca5b..d60a001 100644 --- a/packages/code-storage-go/repo.go +++ b/packages/code-storage-go/repo.go @@ -514,6 +514,7 @@ func (r *Repo) ListCommits(ctx context.Context, options ListCommitsOptions) (Lis for _, commit := range payload.Commits { result.Commits = append(result.Commits, CommitInfo{ SHA: commit.SHA, + ParentSHAs: commit.ParentSHAs, Message: commit.Message, AuthorName: commit.AuthorName, AuthorEmail: commit.AuthorEmail, @@ -556,6 +557,7 @@ func (r *Repo) GetCommit(ctx context.Context, options GetCommitOptions) (GetComm commit := CommitInfo{ SHA: payload.Commit.SHA, + ParentSHAs: payload.Commit.ParentSHAs, Message: payload.Commit.Message, AuthorName: payload.Commit.AuthorName, AuthorEmail: payload.Commit.AuthorEmail, diff --git a/packages/code-storage-go/repo_test.go b/packages/code-storage-go/repo_test.go index 02cda05..529d5fa 100644 --- a/packages/code-storage-go/repo_test.go +++ b/packages/code-storage-go/repo_test.go @@ -1937,7 +1937,7 @@ func TestListCommitsDateParsing(t *testing.T) { t.Fatalf("unexpected path: %s", r.URL.Path) } w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"commits":[{"sha":"abc123","message":"feat: add endpoint","author_name":"Jane Doe","author_email":"jane@example.com","committer_name":"Jane Doe","committer_email":"jane@example.com","date":"2024-01-15T14:32:18Z"}],"has_more":false}`)) + _, _ = w.Write([]byte(`{"commits":[{"sha":"abc123","parent_shas":["def456","789abc"],"message":"feat: add endpoint","author_name":"Jane Doe","author_email":"jane@example.com","committer_name":"Jane Doe","committer_email":"jane@example.com","date":"2024-01-15T14:32:18Z"}],"has_more":false}`)) })) defer server.Close() @@ -1955,6 +1955,9 @@ func TestListCommitsDateParsing(t *testing.T) { t.Fatalf("expected one commit") } commit := result.Commits[0] + if !reflect.DeepEqual([]string{"def456", "789abc"}, commit.ParentSHAs) { + t.Fatalf("unexpected parent SHAs: %#v", commit.ParentSHAs) + } if commit.RawDate != "2024-01-15T14:32:18Z" { t.Fatalf("unexpected raw date") } @@ -2054,7 +2057,7 @@ func TestGetCommit(t *testing.T) { t.Fatalf("unexpected sha query: %q", got) } w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"commit":{"sha":"abc123","message":"feat: add endpoint","author_name":"Jane Doe","author_email":"jane@example.com","committer_name":"Jane Doe","committer_email":"jane@example.com","date":"2024-01-15T14:32:18Z","signature":"-----BEGIN PGP SIGNATURE-----\nABC\n-----END PGP SIGNATURE-----\n","payload":"tree deadbeef\nauthor Jane Doe 1700000000 +0000\n"}}`)) + _, _ = w.Write([]byte(`{"commit":{"sha":"abc123","parent_shas":["def456"],"message":"feat: add endpoint","author_name":"Jane Doe","author_email":"jane@example.com","committer_name":"Jane Doe","committer_email":"jane@example.com","date":"2024-01-15T14:32:18Z","signature":"-----BEGIN PGP SIGNATURE-----\nABC\n-----END PGP SIGNATURE-----\n","payload":"tree deadbeef\nauthor Jane Doe 1700000000 +0000\n"}}`)) })) defer server.Close() @@ -2071,6 +2074,9 @@ func TestGetCommit(t *testing.T) { if result.Commit.SHA != "abc123" { t.Fatalf("unexpected sha: %q", result.Commit.SHA) } + if !reflect.DeepEqual([]string{"def456"}, result.Commit.ParentSHAs) { + t.Fatalf("unexpected parent SHAs: %#v", result.Commit.ParentSHAs) + } if result.Commit.Message != "feat: add endpoint" { t.Fatalf("unexpected message: %q", result.Commit.Message) } @@ -2091,7 +2097,7 @@ func TestGetCommit(t *testing.T) { func TestGetCommitUnsigned(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"commit":{"sha":"abc123","message":"chore: noop","author_name":"Jane Doe","author_email":"jane@example.com","committer_name":"Jane Doe","committer_email":"jane@example.com","date":"2024-01-15T14:32:18Z"}}`)) + _, _ = w.Write([]byte(`{"commit":{"sha":"abc123","parent_shas":[],"message":"chore: noop","author_name":"Jane Doe","author_email":"jane@example.com","committer_name":"Jane Doe","committer_email":"jane@example.com","date":"2024-01-15T14:32:18Z"}}`)) })) defer server.Close() @@ -2108,6 +2114,9 @@ func TestGetCommitUnsigned(t *testing.T) { if result.Commit.Signature != "" || result.Commit.Payload != "" { t.Fatalf("expected empty signature/payload for unsigned commit, got %+v", result.Commit) } + if result.Commit.ParentSHAs == nil || len(result.Commit.ParentSHAs) != 0 { + t.Fatalf("expected non-nil empty parent SHAs for root commit, got %#v", result.Commit.ParentSHAs) + } } func TestGetCommitRequiresSHA(t *testing.T) { diff --git a/packages/code-storage-go/responses.go b/packages/code-storage-go/responses.go index c41f257..007d936 100644 --- a/packages/code-storage-go/responses.go +++ b/packages/code-storage-go/responses.go @@ -82,13 +82,14 @@ type blameLineRaw struct { } type commitInfoRaw struct { - SHA string `json:"sha"` - Message string `json:"message"` - AuthorName string `json:"author_name"` - AuthorEmail string `json:"author_email"` - CommitterName string `json:"committer_name"` - CommitterEmail string `json:"committer_email"` - Date string `json:"date"` + SHA string `json:"sha"` + ParentSHAs []string `json:"parent_shas"` + Message string `json:"message"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + CommitterName string `json:"committer_name"` + CommitterEmail string `json:"committer_email"` + Date string `json:"date"` } // commitInfoWithSignatureRaw extends commitInfoRaw with the signature details diff --git a/packages/code-storage-go/types.go b/packages/code-storage-go/types.go index 6f95f91..81e13a3 100644 --- a/packages/code-storage-go/types.go +++ b/packages/code-storage-go/types.go @@ -610,7 +610,10 @@ type ListCommitsOptions struct { // CommitInfo describes a commit entry. type CommitInfo struct { - SHA string + SHA string + // ParentSHAs contains parent commit SHAs in Git parent order. It is empty + // for root commits. + ParentSHAs []string Message string AuthorName string AuthorEmail string diff --git a/packages/code-storage-go/version.go b/packages/code-storage-go/version.go index 5e3366b..0712dca 100644 --- a/packages/code-storage-go/version.go +++ b/packages/code-storage-go/version.go @@ -2,7 +2,7 @@ package storage const ( PackageName = "code-storage-go-sdk" - PackageVersion = "0.11.0" + PackageVersion = "0.12.0" ) func userAgent() string { diff --git a/packages/code-storage-python/README.md b/packages/code-storage-python/README.md index 5c17c8a..6a31633 100644 --- a/packages/code-storage-python/README.md +++ b/packages/code-storage-python/README.md @@ -283,10 +283,16 @@ commits = await repo.list_commits( cursor=None, # for pagination ) print(commits["commits"]) +for commit in commits["commits"]: + print(commit["sha"], commit["parent_shas"]) # Git parent order; [] for a root commit # Get a single commit's metadata (no diff) result = await repo.get_commit(sha="abc123...") -print(result["commit"]["message"], result["commit"]["author_name"]) +print( + result["commit"]["message"], + result["commit"]["author_name"], + result["commit"]["parent_shas"], +) # Signed commits also include the armored signature plus the exact signed # payload (raw commit object minus the gpgsig header) so you can verify it # yourself. Both keys are absent for unsigned commits. diff --git a/packages/code-storage-python/pierre_storage/repo.py b/packages/code-storage-python/pierre_storage/repo.py index 90af67a..87b0252 100644 --- a/packages/code-storage-python/pierre_storage/repo.py +++ b/packages/code-storage-python/pierre_storage/repo.py @@ -1400,6 +1400,7 @@ async def list_commits( commits.append( { "sha": c["sha"], + "parent_shas": c["parent_shas"], "message": c["message"], "author_name": c["author_name"], "author_email": c["author_email"], @@ -1460,6 +1461,7 @@ async def get_commit( date = datetime.fromisoformat(commit_raw["date"].replace("Z", "+00:00")) commit: CommitInfo = { "sha": commit_raw["sha"], + "parent_shas": commit_raw["parent_shas"], "message": commit_raw["message"], "author_name": commit_raw["author_name"], "author_email": commit_raw["author_email"], diff --git a/packages/code-storage-python/pierre_storage/types.py b/packages/code-storage-python/pierre_storage/types.py index ec6a464..366ef6d 100644 --- a/packages/code-storage-python/pierre_storage/types.py +++ b/packages/code-storage-python/pierre_storage/types.py @@ -318,6 +318,7 @@ class CommitInfo(TypedDict): """ sha: str + parent_shas: List[str] message: str author_name: str author_email: str diff --git a/packages/code-storage-python/pierre_storage/version.py b/packages/code-storage-python/pierre_storage/version.py index accb437..7a0f928 100644 --- a/packages/code-storage-python/pierre_storage/version.py +++ b/packages/code-storage-python/pierre_storage/version.py @@ -1,7 +1,7 @@ """Version information for Pierre Storage SDK.""" PACKAGE_NAME = "code-storage-py-sdk" -PACKAGE_VERSION = "1.11.0" +PACKAGE_VERSION = "1.13.0" def get_user_agent() -> str: diff --git a/packages/code-storage-python/pyproject.toml b/packages/code-storage-python/pyproject.toml index 8196507..a69010e 100644 --- a/packages/code-storage-python/pyproject.toml +++ b/packages/code-storage-python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pierre-storage" -version = "1.12.0" +version = "1.13.0" description = "Pierre Git Storage SDK for Python" readme = "README.md" license = "MIT" diff --git a/packages/code-storage-python/tests/test_repo.py b/packages/code-storage-python/tests/test_repo.py index 551bf97..069c613 100644 --- a/packages/code-storage-python/tests/test_repo.py +++ b/packages/code-storage-python/tests/test_repo.py @@ -1767,6 +1767,7 @@ async def test_list_commits(self, git_storage_options: dict) -> None: "commits": [ { "sha": "abc123", + "parent_shas": [], "message": "Initial commit", "author_name": "Test User", "author_email": "test@example.com", @@ -1776,6 +1777,7 @@ async def test_list_commits(self, git_storage_options: dict) -> None: }, { "sha": "def456", + "parent_shas": ["abc123"], "message": "Second commit", "author_name": "Test User", "author_email": "test@example.com", @@ -1803,7 +1805,9 @@ async def test_list_commits(self, git_storage_options: dict) -> None: assert "commits" in result assert len(result["commits"]) == 2 assert result["commits"][0]["sha"] == "abc123" + assert result["commits"][0]["parent_shas"] == [] assert result["commits"][0]["message"] == "Initial commit" + assert result["commits"][1]["parent_shas"] == ["abc123"] @pytest.mark.asyncio async def test_list_commits_ephemeral_query_param(self, git_storage_options: dict) -> None: @@ -1889,6 +1893,7 @@ async def test_get_commit(self, git_storage_options: dict) -> None: commit_response.json.return_value = { "commit": { "sha": "abc123", + "parent_shas": ["def456"], "message": "feat: add endpoint", "author_name": "Jane Doe", "author_email": "jane@example.com", @@ -1910,6 +1915,7 @@ async def test_get_commit(self, git_storage_options: dict) -> None: commit = result["commit"] assert commit["sha"] == "abc123" + assert commit["parent_shas"] == ["def456"] assert commit["message"] == "feat: add endpoint" assert commit["author_name"] == "Jane Doe" assert commit["author_email"] == "jane@example.com" @@ -1952,6 +1958,7 @@ async def test_get_commit_unsigned_omits_signature(self, git_storage_options: di commit_response.json.return_value = { "commit": { "sha": "abc123", + "parent_shas": [], "message": "chore: noop", "author_name": "Jane Doe", "author_email": "jane@example.com", @@ -1970,6 +1977,7 @@ async def test_get_commit_unsigned_omits_signature(self, git_storage_options: di result = await repo.get_commit(sha="abc123") commit = result["commit"] + assert commit["parent_shas"] == [] assert "signature" not in commit assert "payload" not in commit @@ -1990,6 +1998,7 @@ async def test_get_commit_trims_sha_and_honors_ttl(self, git_storage_options: di commit_response.json.return_value = { "commit": { "sha": "abc123", + "parent_shas": [], "message": "msg", "author_name": "A", "author_email": "a@example.com", diff --git a/packages/code-storage-python/uv.lock b/packages/code-storage-python/uv.lock index 070e4a9..5e600d2 100644 --- a/packages/code-storage-python/uv.lock +++ b/packages/code-storage-python/uv.lock @@ -915,7 +915,7 @@ wheels = [ [[package]] name = "pierre-storage" -version = "1.12.0" +version = "1.13.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, diff --git a/packages/code-storage-typescript/README.md b/packages/code-storage-typescript/README.md index d5b212d..88fe451 100644 --- a/packages/code-storage-typescript/README.md +++ b/packages/code-storage-typescript/README.md @@ -262,10 +262,11 @@ const commits = await repo.listCommits({ cursor: undefined, // for pagination }); console.log(commits.commits); +console.log(commits.commits[0]?.parentShas); // Git parent order; [] for a root commit // Get a single commit's metadata (no diff) const { commit } = await repo.getCommit({ sha: 'abc123...' }); -console.log(commit.message, commit.authorName); +console.log(commit.message, commit.authorName, commit.parentShas); // Signed commits also expose the armored signature plus the exact signed // payload (raw commit object minus the gpgsig header) so you can verify it // yourself. Both are undefined for unsigned commits. @@ -817,6 +818,8 @@ interface ListCommitsResult { interface CommitInfo { sha: string; + // Parent commit SHAs in Git parent order. Empty for root commits. + parentShas: string[]; message: string; authorName: string; authorEmail: string; diff --git a/packages/code-storage-typescript/package.json b/packages/code-storage-typescript/package.json index fd4592a..d586139 100644 --- a/packages/code-storage-typescript/package.json +++ b/packages/code-storage-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@pierre/storage", -"version": "1.13.0", + "version": "1.14.0", "description": "Pierre Git Storage SDK", "repository": { "type": "git", diff --git a/packages/code-storage-typescript/src/index.ts b/packages/code-storage-typescript/src/index.ts index 8ae330a..8f3496b 100644 --- a/packages/code-storage-typescript/src/index.ts +++ b/packages/code-storage-typescript/src/index.ts @@ -353,6 +353,7 @@ function transformCommitInfo(raw: RawCommitInfo): CommitInfo { const parsedDate = new Date(raw.date); return { sha: raw.sha, + parentShas: raw.parent_shas, message: raw.message, authorName: raw.author_name, authorEmail: raw.author_email, diff --git a/packages/code-storage-typescript/src/schemas.ts b/packages/code-storage-typescript/src/schemas.ts index 40cccc9..7aa70f6 100644 --- a/packages/code-storage-typescript/src/schemas.ts +++ b/packages/code-storage-typescript/src/schemas.ts @@ -58,6 +58,7 @@ export const listBranchesResponseSchema = z.object({ export const commitInfoRawSchema = z.object({ sha: z.string(), + parent_shas: z.array(z.string()), message: z.string(), author_name: z.string(), author_email: z.string(), diff --git a/packages/code-storage-typescript/src/types.ts b/packages/code-storage-typescript/src/types.ts index 0240ba4..440b74c 100644 --- a/packages/code-storage-typescript/src/types.ts +++ b/packages/code-storage-typescript/src/types.ts @@ -525,6 +525,8 @@ export type RawCommitInfo = SchemaRawCommitInfo; export interface CommitInfo { sha: string; + /** Parent commit SHAs in Git parent order. Empty for root commits. */ + parentShas: string[]; message: string; authorName: string; authorEmail: string; diff --git a/packages/code-storage-typescript/tests/index.test.ts b/packages/code-storage-typescript/tests/index.test.ts index 988e74d..a584119 100644 --- a/packages/code-storage-typescript/tests/index.test.ts +++ b/packages/code-storage-typescript/tests/index.test.ts @@ -193,6 +193,7 @@ describe('GitStorage', () => { json: async () => ({ commit: { sha: 'abc123', + parent_shas: [], message: 'msg', author_name: 'A', author_email: 'a@example.com', @@ -217,6 +218,7 @@ describe('GitStorage', () => { commits: [ { sha: 'abc123', + parent_shas: ['def456', '789abc'], message: 'feat: add endpoint', author_name: 'Jane Doe', author_email: 'jane@example.com', @@ -238,6 +240,7 @@ describe('GitStorage', () => { ); const commits = await repo.listCommits(); + expect(commits.commits[0].parentShas).toEqual(['def456', '789abc']); expect(commits.commits[0].rawDate).toBe('2024-01-15T14:32:18Z'); expect(commits.commits[0].date).toBeInstanceOf(Date); expect(commits.commits[0].date.toISOString()).toBe( @@ -333,6 +336,7 @@ describe('GitStorage', () => { json: async () => ({ commit: { sha: 'abc123', + parent_shas: ['def456'], message: 'feat: add endpoint', author_name: 'Jane Doe', author_email: 'jane@example.com', @@ -349,6 +353,7 @@ describe('GitStorage', () => { const result = await repo.getCommit({ sha: 'abc123' }); expect(result.commit.sha).toBe('abc123'); + expect(result.commit.parentShas).toEqual(['def456']); expect(result.commit.message).toBe('feat: add endpoint'); expect(result.commit.authorName).toBe('Jane Doe'); expect(result.commit.authorEmail).toBe('jane@example.com'); @@ -375,6 +380,7 @@ describe('GitStorage', () => { json: async () => ({ commit: { sha: 'abc123', + parent_shas: [], message: 'chore: noop', author_name: 'Jane Doe', author_email: 'jane@example.com', @@ -387,6 +393,7 @@ describe('GitStorage', () => { ); const result = await repo.getCommit({ sha: 'abc123' }); + expect(result.commit.parentShas).toEqual([]); expect(result.commit.signature).toBeUndefined(); expect(result.commit.payload).toBeUndefined(); }); @@ -412,6 +419,7 @@ describe('GitStorage', () => { json: async () => ({ commit: { sha: 'abc123', + parent_shas: [], message: 'msg', author_name: 'A', author_email: 'a@example.com', diff --git a/skills/code-storage/SKILL.md b/skills/code-storage/SKILL.md index 3e9d924..d50ca27 100644 --- a/skills/code-storage/SKILL.md +++ b/skills/code-storage/SKILL.md @@ -452,7 +452,8 @@ Params: only commits that touched that path are returned) - `cursor`, `limit` (default 20, max 100) -Response: `{ "commits": [{ "sha", "message", "author_name", "author_email", "date" }], "next_cursor", "has_more" }` +Response: `{ "commits": [{ "sha", "parent_shas", "message", "author_name", "author_email", "committer_name", "committer_email", "date" }], "next_cursor", "has_more" }` +`parent_shas` preserves Git parent order and is an empty array for root commits. ## GET /repos/commit — Get Commit @@ -463,7 +464,8 @@ curl "$CODE_STORAGE_BASE_URL/repos/commit?sha=COMMIT_SHA" \ Params: `sha` (required — full SHA, short SHA, branch name, or any revision Git can resolve). Returns commit metadata only; use `/repos/diff` for the diff. -Response: `{ "commit": { "sha", "message", "author_name", "author_email", "committer_name", "committer_email", "date", "signature"?, "payload"? } }` +Response: `{ "commit": { "sha", "parent_shas", "message", "author_name", "author_email", "committer_name", "committer_email", "date", "signature"?, "payload"? } }` +`parent_shas` preserves Git parent order and is an empty array for root commits. `signature` (armored OpenPGP/SSH block from the commit's gpgsig header) and `payload` (the exact signed bytes: the raw commit object with the gpgsig header removed) are present only for signed commits and omitted otherwise. Together