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: 2 additions & 0 deletions packages/code-storage-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ result, err := repo.Merge(context.Background(), storage.MergeOptions{
SourceBranch: "feature",
SourceIsEphemeral: true,
TargetBranch: "main",
// Leave ExpectedTargetSHA empty to merge into the current target tip.
// Set it to require TargetBranch to still point at that commit; moved targets return 409.
Strategy: storage.MergeStrategyMerge,
Author: &storage.CommitSignature{Name: "Merge Bot", Email: "merge@example.com"},
})
Expand Down
4 changes: 4 additions & 0 deletions packages/code-storage-go/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,10 @@ func (r *Repo) DeleteBranch(ctx context.Context, options DeleteBranchOptions) (D
}

// Merge merges a source branch into a target branch.
// Set ExpectedTargetSHA to require the target branch to still point at that commit.
// The server returns 409 if it moved. Leave ExpectedTargetSHA empty to merge into the
// current target tip. Native Code Storage targets may retry stale target/repository
// movement while preserving the resolved source commit.
func (r *Repo) Merge(ctx context.Context, options MergeOptions) (MergeResult, error) {
sourceBranch := strings.TrimSpace(options.SourceBranch)
if sourceBranch == "" {
Expand Down
4 changes: 2 additions & 2 deletions packages/code-storage-go/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ func TestCreateBranchTTL(t *testing.T) {
}
}

func TestMergeRequestAndResponse(t *testing.T) {
func TestMergeGuardedTargetTipRequestAndResponse(t *testing.T) {
var captured map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
Expand Down Expand Up @@ -903,7 +903,7 @@ func TestMergeRequestAndResponse(t *testing.T) {
}
}

func TestMergeOmitsOptionalFields(t *testing.T) {
func TestMergeCurrentTargetTipModeOmitsExpectedTargetSHA(t *testing.T) {
var captured map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/merge" {
Expand Down
10 changes: 6 additions & 4 deletions packages/code-storage-go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,10 +430,12 @@ const (
// MergeOptions configures branch merge operations.
type MergeOptions struct {
InvocationOptions
SourceBranch string
SourceIsEphemeral bool
TargetBranch string
TargetIsEphemeral bool
SourceBranch string
SourceIsEphemeral bool
TargetBranch string
TargetIsEphemeral bool
// ExpectedTargetSHA, when non-empty, requires the target branch to still point at
// that commit. Leave empty to merge into the current target tip.
ExpectedTargetSHA string
CommitMessage string
Author *CommitSignature
Expand Down
7 changes: 6 additions & 1 deletion packages/code-storage-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ merge_result = await repo.merge(
target_branch="main",
target_is_ephemeral=False, # optional; target branch can independently be ephemeral
strategy="merge", # one of: "merge", "ff_only", "ff_prefer"
expected_target_sha="abc123", # optional optimistic concurrency check
expected_target_sha="abc123", # optional; 409 if target moved
commit_message="Merge feature/preview", # optional
author={"name": "Bot", "email": "bot@example.com"}, # optional
committer={"name": "Bot", "email": "bot@example.com"}, # optional
Expand All @@ -246,6 +246,11 @@ merge_result = await repo.merge(
)
print(merge_result["result"], merge_result["commit_sha"])
print(merge_result["source"]["sha"], merge_result["target"]["new_sha"])
# Target-tip modes:
# - Provide expected_target_sha when target_branch must still point at that commit.
# - Omit expected_target_sha to merge into the current target tip. For native
# Code Storage targets, the gateway may retry stale target/repository movement
# internally while keeping the resolved source commit pinned.

# List tags
tags = await repo.list_tags(limit=10)
Expand Down
8 changes: 7 additions & 1 deletion packages/code-storage-python/pierre_storage/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,13 @@ async def merge(
ttl: Optional[int] = None,
ref_policies: Optional[Refs] = None,
) -> MergeBranchesResult:
"""Merge a source branch into a target branch."""
"""Merge a source branch into a target branch.

Provide expected_target_sha to require the target branch to still point at that
commit. Omit it to merge into the current target tip; native Code Storage
targets may retry stale target/repository movement while preserving the
resolved source commit.
"""
source_branch_clean = source_branch.strip()
target_branch_clean = target_branch.strip()
strategy_clean = strategy.strip()
Expand Down
2 changes: 2 additions & 0 deletions packages/code-storage-python/pierre_storage/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,8 @@ class MergeBranchesOptions(TypedDict, total=False):
source_is_ephemeral: bool
target_branch: str # required
target_is_ephemeral: bool
# When provided, target must still point at this SHA; omit to merge current tip.
# Native Code Storage targets may retry stale movement while preserving source commit.
expected_target_sha: str
commit_message: str
author: CommitSignature
Expand Down
8 changes: 4 additions & 4 deletions packages/code-storage-python/tests/test_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1252,8 +1252,10 @@ async def test_merge_posts_body_and_parses_response(self, git_storage_options: d
assert payload["exp"] - payload["iat"] == 900

@pytest.mark.asyncio
async def test_merge_omits_blank_optional_fields(self, git_storage_options: dict) -> None:
"""Blank optional merge fields should be omitted from the request body."""
async def test_merge_omits_expected_target_sha_for_current_target_tip(
self, git_storage_options: dict
) -> None:
"""Omitted expected_target_sha requests merge into the current target tip."""
storage = GitStorage(git_storage_options)

create_repo_response = MagicMock()
Expand Down Expand Up @@ -1287,8 +1289,6 @@ async def test_merge_omits_blank_optional_fields(self, git_storage_options: dict
source_branch="feature",
target_branch="main",
strategy="ff_prefer",
expected_target_sha=" ",
commit_message=None,
)

assert "merge_base_sha" not in result
Expand Down
8 changes: 7 additions & 1 deletion packages/code-storage-typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ const mergeResult = await repo.merge({
sourceIsEphemeral: true,
targetBranch: 'main',
targetIsEphemeral: false,
expectedTargetSha: '0123456789abcdef0123456789abcdef01234567', // optional guard
expectedTargetSha: '0123456789abcdef0123456789abcdef01234567', // optional; 409 if target moved
strategy: 'merge', // 'merge' | 'ff_only' | 'ff_prefer'
commitMessage: 'Merge feature/demo', // optional
author: { name: 'Merge Bot', email: 'merge@example.com' }, // optional
Expand All @@ -357,6 +357,11 @@ console.log(mergeResult.commitSha, mergeResult.target.newSha);
// reported), and number of promoted commits. A backend conflict response
// (HTTP 409) is surfaced as an API error with the response body preserved for
// callers that need conflict_paths or merge_base_sha.
// Target-tip modes:
// - Provide expectedTargetSha when targetBranch must still point at that commit.
// - Omit expectedTargetSha to merge into the current target tip. For native
// Code Storage targets, the gateway may retry stale target/repository
// movement internally while keeping the resolved source commit pinned.

// Create a commit using the streaming helper
const fs = await import('node:fs/promises');
Expand Down Expand Up @@ -899,6 +904,7 @@ interface MergeOptions {
sourceIsEphemeral?: boolean;
targetBranch: string;
targetIsEphemeral?: boolean;
// Optional target-tip guard. Omit to merge into the current target tip.
expectedTargetSha?: string;
commitMessage?: string;
author?: CommitSignature;
Expand Down
6 changes: 6 additions & 0 deletions packages/code-storage-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,12 @@ export interface MergeOptions
sourceIsEphemeral?: boolean;
targetBranch: string;
targetIsEphemeral?: boolean;
/**
* When provided, the target branch must still point at this commit; the merge
* fails with HTTP 409 if it moved. Omit to merge into the current target tip.
* Native Code Storage targets may retry stale target/repository movement while
* preserving the resolved source commit.
*/
expectedTargetSha?: string;
commitMessage?: string;
author?: CommitSignature;
Expand Down
6 changes: 2 additions & 4 deletions packages/code-storage-typescript/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2087,7 +2087,7 @@ describe('GitStorage', () => {
});

describe('Repo merge', () => {
it('posts merge request and returns transformed response', async () => {
it('posts guarded target-tip merge request and returns transformed response', async () => {
const store = new GitStorage({ name: 'v0', key });
const repo = store.repo({ id: 'repo-merge' });

Expand Down Expand Up @@ -2166,7 +2166,7 @@ describe('GitStorage', () => {
});
});

it('omits blank optional string fields from merge request', async () => {
it('omits expectedTargetSha for current target tip mode', async () => {
const store = new GitStorage({ name: 'v0', key });
const repo = store.repo({ id: 'repo-merge-minimal' });

Expand Down Expand Up @@ -2201,8 +2201,6 @@ describe('GitStorage', () => {
await repo.merge({
sourceBranch: 'feature',
targetBranch: 'main',
expectedTargetSha: ' ',
commitMessage: '',
strategy: 'ff_prefer',
});
});
Expand Down
10 changes: 9 additions & 1 deletion skills/code-storage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,14 @@ curl "$CODE_STORAGE_BASE_URL/repos/merge" -X POST \
Required: `source_branch`, `target_branch`, `strategy` (`merge` | `ff_only` | `ff_prefer`).
Optional: `source_is_ephemeral`, `target_is_ephemeral`, `expected_target_sha`,
`commit_message`, `author`, `committer`, `allow_unrelated_histories`, `squash`.

Target-tip modes:
- Set `expected_target_sha` when the target branch must still point at a specific
commit; movement returns HTTP 409.
- Omit `expected_target_sha` to merge into the current target tip. For native Code
Storage targets, the gateway may retry stale target/repository movement internally
while keeping the originally resolved source commit pinned.

Set `squash: true` to collapse the source into a single new commit whose only
parent is the current target tip. It is incompatible with `ff_only`.
Response: `{ "result": "merge_commit"|"fast_forward"|"no_op"|"squash"|"unknown",
Expand Down Expand Up @@ -1033,4 +1041,4 @@ git push origin feature-branch
| Blob data encoding | Always base64. Max 4 MiB per chunk. Use multiple chunks for large files. |
| `expected_head_sha` | Optimistic lock. Provide current branch tip SHA to enforce fast-forward semantics. |
| Policy ops | JWT-level guards via `refPolicies` (per-ref, first match wins, preferred). `no-force-push` (TS/Py `OP_NO_FORCE_PUSH`, Go `OpNoForcePush`) blocks non-FF updates. `no-push` (`OP_NO_PUSH`/`OpNoPush`) blocks pushes to matching refs. `verify-sig` (`OP_VERIFY_SIG`/`OpVerifySig`) blocks pushes introducing commits not signed by a registered signing key. Top-level `ops` is a legacy alias on URL-minting methods only. |
| Merge endpoint | `POST /repos/merge`. Strategies: `merge`, `ff_only`, `ff_prefer`. Optional `squash` (not with `ff_only`). 409 on conflict. |
| Merge endpoint | `POST /repos/merge`. Strategies: `merge`, `ff_only`, `ff_prefer`. Optional `expected_target_sha` guards the target tip (409 if moved); omit it to merge into the current target tip. Optional `squash` (not with `ff_only`). 409 on conflict. |
Loading