From 63871b5d78e5c0ceb1062ea1108c1cf2479fb1d9 Mon Sep 17 00:00:00 2001 From: Ian Ownbey Date: Fri, 17 Apr 2026 17:34:06 -0400 Subject: [PATCH] Deprecate base branch --- packages/code-storage-go/repo.go | 11 +- packages/code-storage-go/requests.go | 3 +- packages/code-storage-go/types.go | 2 + packages/code-storage-go/version.go | 2 +- packages/code-storage-python/QUICKSTART.md | 21 ++ packages/code-storage-python/README.md | 8 +- .../pierre_storage/repo.py | 56 ++++-- .../pierre_storage/types.py | 8 +- .../pierre_storage/version.py | 2 +- packages/code-storage-python/pyproject.toml | 2 +- .../code-storage-python/tests/test_repo.py | 187 ++++++++++++++++-- packages/code-storage-python/uv.lock | 2 +- packages/code-storage-typescript/README.md | 7 +- packages/code-storage-typescript/package.json | 2 +- packages/code-storage-typescript/src/index.ts | 13 +- packages/code-storage-typescript/src/types.ts | 4 +- .../tests/index.test.ts | 85 +++++++- 17 files changed, 356 insertions(+), 59 deletions(-) diff --git a/packages/code-storage-go/repo.go b/packages/code-storage-go/repo.go index 3f7a292..9e48de7 100644 --- a/packages/code-storage-go/repo.go +++ b/packages/code-storage-go/repo.go @@ -770,10 +770,11 @@ func (r *Repo) PullUpstream(ctx context.Context, options PullUpstreamOptions) er // CreateBranch creates a new branch. func (r *Repo) CreateBranch(ctx context.Context, options CreateBranchOptions) (CreateBranchResult, error) { + baseRef := strings.TrimSpace(options.BaseRef) baseBranch := strings.TrimSpace(options.BaseBranch) targetBranch := strings.TrimSpace(options.TargetBranch) - if baseBranch == "" { - return CreateBranchResult{}, errors.New("createBranch baseBranch is required") + if baseRef == "" && baseBranch == "" { + return CreateBranchResult{}, errors.New("createBranch baseRef or baseBranch is required") } if targetBranch == "" { return CreateBranchResult{}, errors.New("createBranch targetBranch is required") @@ -786,11 +787,15 @@ func (r *Repo) CreateBranch(ctx context.Context, options CreateBranchOptions) (C } body := &createBranchRequest{ - BaseBranch: baseBranch, TargetBranch: targetBranch, BaseIsEphemeral: options.BaseIsEphemeral, TargetIsEphemeral: options.TargetIsEphemeral, } + if baseRef != "" { + body.BaseRef = baseRef + } else { + body.BaseBranch = baseBranch + } resp, err := r.client.api.post(ctx, "repos/branches/create", nil, body, jwtToken, nil) if err != nil { diff --git a/packages/code-storage-go/requests.go b/packages/code-storage-go/requests.go index d0a920e..9c681f6 100644 --- a/packages/code-storage-go/requests.go +++ b/packages/code-storage-go/requests.go @@ -94,7 +94,8 @@ type archiveOptions struct { // createBranchRequest is the JSON body for CreateBranch. type createBranchRequest struct { - BaseBranch string `json:"base_branch"` + BaseRef string `json:"base_ref,omitempty"` + BaseBranch string `json:"base_branch,omitempty"` TargetBranch string `json:"target_branch"` BaseIsEphemeral bool `json:"base_is_ephemeral,omitempty"` TargetIsEphemeral bool `json:"target_is_ephemeral,omitempty"` diff --git a/packages/code-storage-go/types.go b/packages/code-storage-go/types.go index dfbc6b5..2558276 100644 --- a/packages/code-storage-go/types.go +++ b/packages/code-storage-go/types.go @@ -299,6 +299,8 @@ type ListBranchesResult struct { // CreateBranchOptions configures branch creation. type CreateBranchOptions struct { InvocationOptions + BaseRef string + // Deprecated: use BaseRef instead. BaseBranch string TargetBranch string BaseIsEphemeral bool diff --git a/packages/code-storage-go/version.go b/packages/code-storage-go/version.go index 36336fa..9955ce1 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.4.0" + PackageVersion = "0.4.1" ) func userAgent() string { diff --git a/packages/code-storage-python/QUICKSTART.md b/packages/code-storage-python/QUICKSTART.md index 63631c3..b7d1470 100644 --- a/packages/code-storage-python/QUICKSTART.md +++ b/packages/code-storage-python/QUICKSTART.md @@ -106,6 +106,27 @@ async def make_changes(): asyncio.run(make_changes()) ``` +### Creating a Branch + +```python +async def create_preview_branch(): + storage = GitStorage({"name": "your-name", "key": "your-key"}) + repo = await storage.find_one({"id": "repo-id"}) + + result = await repo.create_branch( + base_ref="main", + target_branch="preview/demo", + target_is_ephemeral=True, + ) + + print(result["target_branch"]) + +asyncio.run(create_preview_branch()) +``` + +Prefer `base_ref` for new code. `base_branch` is still accepted for backwards +compatibility, but it is deprecated. + ### Applying a Diff Directly If you already have a unified diff (for example, generated by `git diff`), you diff --git a/packages/code-storage-python/README.md b/packages/code-storage-python/README.md index e9a2f73..98b38c8 100644 --- a/packages/code-storage-python/README.md +++ b/packages/code-storage-python/README.md @@ -198,7 +198,7 @@ print(branches["branches"]) # Create or promote a branch (synchronous Temporal workflow) branch_result = await repo.create_branch( - base_branch="main", + base_ref="main", target_branch="feature/preview", base_is_ephemeral=False, # set True when the base lives in the ephemeral namespace target_is_ephemeral=True, # set True to create an ephemeral branch @@ -465,6 +465,9 @@ result = await repo.promote_ephemeral_branch( print(result["target_branch"]) # "feature/awesome-change" ``` +`promote_ephemeral_branch()` keeps its branch-oriented `base_branch` parameter. +Use `create_branch(base_ref=...)` for new code when you need to choose the base. + **Key points about ephemeral branches:** - Ephemeral branches are stored separately from regular branches @@ -669,7 +672,8 @@ class Repo: async def create_branch( self, *, - base_branch: str, + base_ref: Optional[str] = None, + base_branch: Optional[str] = None, # deprecated target_branch: str, base_is_ephemeral: bool = False, target_is_ephemeral: bool = False, diff --git a/packages/code-storage-python/pierre_storage/repo.py b/packages/code-storage-python/pierre_storage/repo.py index 57ec6e6..b161f8f 100644 --- a/packages/code-storage-python/pierre_storage/repo.py +++ b/packages/code-storage-python/pierre_storage/repo.py @@ -112,6 +112,15 @@ def normalize_diff_state(raw_state: str) -> DiffFileState: return state_map.get(leading, DiffFileState.UNKNOWN) +def normalize_optional_ref(value: Optional[str]) -> Optional[str]: + """Normalize optional branch/ref inputs by trimming blanks to None.""" + if value is None: + return None + + normalized = value.strip() + return normalized or None + + class RepoImpl: """Implementation of repository operations.""" @@ -514,21 +523,40 @@ async def list_branches( async def create_branch( self, *, - base_branch: str, + base_ref: Optional[str] = None, + base_branch: Optional[str] = None, target_branch: str, base_is_ephemeral: bool = False, target_is_ephemeral: bool = False, ttl: Optional[int] = None, ) -> CreateBranchResult: - """Create or promote a branch.""" - base_branch_clean = base_branch.strip() + """Create or promote a branch. + + Args: + base_ref: Preferred base ref (branch, tag, or commit SHA) + base_branch: Deprecated branch-only base name + target_branch: Target branch name + base_is_ephemeral: Whether the base ref lives in the ephemeral namespace + target_is_ephemeral: Whether to create the target in the ephemeral namespace + ttl: Token TTL in seconds + """ + base_ref_clean = normalize_optional_ref(base_ref) + base_branch_clean = normalize_optional_ref(base_branch) target_branch_clean = target_branch.strip() - if not base_branch_clean: - raise ValueError("create_branch base_branch is required") + effective_base = base_ref_clean or base_branch_clean + if effective_base is None: + raise ValueError("create_branch base_ref or base_branch is required") if not target_branch_clean: raise ValueError("create_branch target_branch is required") + if base_branch_clean is not None: + warnings.warn( + "create_branch base_branch is deprecated; use base_ref instead", + DeprecationWarning, + stacklevel=2, + ) + ttl_value = resolve_invocation_ttl_seconds({"ttl": ttl} if ttl is not None else None) jwt = self.generate_jwt( self._id, @@ -536,11 +564,14 @@ async def create_branch( ) payload: Dict[str, Any] = { - "base_branch": base_branch_clean, "target_branch": target_branch_clean, "base_is_ephemeral": bool(base_is_ephemeral), "target_is_ephemeral": bool(target_is_ephemeral), } + if base_ref_clean is not None: + payload["base_ref"] = base_ref_clean + else: + payload["base_branch"] = base_branch_clean url = f"{self.api_base_url}/api/v{self.api_version}/repos/branches/create" @@ -750,19 +781,14 @@ async def promote_ephemeral_branch( ttl: Optional[int] = None, ) -> CreateBranchResult: """Promote an ephemeral branch to a persistent target branch.""" - if base_branch is None: - raise ValueError("promote_ephemeral_branch base_branch is required") - - base_clean = base_branch.strip() - if not base_clean: + base_clean = normalize_optional_ref(base_branch) + if base_clean is None: raise ValueError("promote_ephemeral_branch base_branch is required") - target_clean = target_branch.strip() if target_branch is not None else base_clean - if not target_clean: - raise ValueError("promote_ephemeral_branch target_branch is required") + target_clean = normalize_optional_ref(target_branch) or base_clean return await self.create_branch( - base_branch=base_clean, + base_ref=base_clean, target_branch=target_clean, base_is_ephemeral=True, target_is_ephemeral=False, diff --git a/packages/code-storage-python/pierre_storage/types.py b/packages/code-storage-python/pierre_storage/types.py index 6f2febb..a32b0d0 100644 --- a/packages/code-storage-python/pierre_storage/types.py +++ b/packages/code-storage-python/pierre_storage/types.py @@ -599,13 +599,17 @@ async def list_branches( async def create_branch( self, *, - base_branch: str, + base_ref: Optional[str] = None, + base_branch: Optional[str] = None, target_branch: str, base_is_ephemeral: bool = False, target_is_ephemeral: bool = False, ttl: Optional[int] = None, ) -> CreateBranchResult: - """Create or promote a branch.""" + """Create or promote a branch. + + base_branch is deprecated; prefer base_ref. + """ ... async def list_tags( diff --git a/packages/code-storage-python/pierre_storage/version.py b/packages/code-storage-python/pierre_storage/version.py index 3d61286..4899ef7 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.3.3" +PACKAGE_VERSION = "1.5.2" def get_user_agent() -> str: diff --git a/packages/code-storage-python/pyproject.toml b/packages/code-storage-python/pyproject.toml index 4570956..e580280 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.5.1" +version = "1.5.2" 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 808f9c0..bb39bc6 100644 --- a/packages/code-storage-python/tests/test_repo.py +++ b/packages/code-storage-python/tests/test_repo.py @@ -286,9 +286,7 @@ async def test_list_files_ephemeral_flag(self, git_storage_options: dict) -> Non assert params.get("ref") == ["feature/demo"] @pytest.mark.asyncio - async def test_list_files_with_metadata_ephemeral_flag( - self, git_storage_options: dict - ) -> None: + async def test_list_files_with_metadata_ephemeral_flag(self, git_storage_options: dict) -> None: """Ensure ephemeral flag propagates to list files with metadata.""" storage = GitStorage(git_storage_options) @@ -392,9 +390,7 @@ async def test_list_files_with_metadata_invalid_commit_date_fallback( ) @pytest.mark.asyncio - async def test_list_files_with_metadata_custom_ttl( - self, git_storage_options: dict - ) -> None: + async def test_list_files_with_metadata_custom_ttl(self, git_storage_options: dict) -> None: """Ensure custom TTL propagates to list files with metadata JWT.""" storage = GitStorage(git_storage_options) custom_ttl = 900 @@ -624,8 +620,8 @@ async def test_list_branches_with_pagination(self, git_storage_options: dict) -> assert result["has_more"] is True @pytest.mark.asyncio - async def test_create_branch(self, git_storage_options: dict) -> None: - """Test creating a branch using the REST API.""" + async def test_create_branch_prefers_base_ref(self, git_storage_options: dict) -> None: + """Test creating a branch prefers the new base_ref payload.""" storage = GitStorage(git_storage_options) create_repo_response = MagicMock() @@ -651,7 +647,7 @@ async def test_create_branch(self, git_storage_options: dict) -> None: repo = await storage.create_repo(id="test-repo") result = await repo.create_branch( - base_branch="main", + base_ref="main", target_branch="feature/demo", target_is_ephemeral=True, ) @@ -661,15 +657,176 @@ async def test_create_branch(self, git_storage_options: dict) -> None: assert result["target_is_ephemeral"] is True assert result["commit_sha"] == "abc123" - # Ensure the API call was issued with the expected payload assert client_instance.post.await_count == 2 branch_call = client_instance.post.await_args_list[1] assert branch_call.args[0].endswith("/api/v1/repos/branches/create") payload = branch_call.kwargs["json"] - assert payload["base_branch"] == "main" + assert payload["base_ref"] == "main" + assert "base_branch" not in payload assert payload["target_branch"] == "feature/demo" assert payload["target_is_ephemeral"] is True + @pytest.mark.asyncio + async def test_create_branch_falls_back_to_deprecated_base_branch( + self, git_storage_options: dict + ) -> None: + """Test create_branch still supports deprecated base_branch.""" + storage = GitStorage(git_storage_options) + + create_repo_response = MagicMock() + create_repo_response.status_code = 200 + create_repo_response.is_success = True + create_repo_response.json.return_value = {"repo_id": "test-repo"} + + create_branch_response = MagicMock() + create_branch_response.status_code = 200 + create_branch_response.is_success = True + create_branch_response.json.return_value = { + "message": "branch created", + "target_branch": "feature/demo", + "target_is_ephemeral": False, + } + + with patch("httpx.AsyncClient") as mock_client: + client_instance = mock_client.return_value.__aenter__.return_value + client_instance.post = AsyncMock( + side_effect=[create_repo_response, create_branch_response] + ) + + repo = await storage.create_repo(id="test-repo") + with pytest.warns(DeprecationWarning, match="base_branch is deprecated"): + result = await repo.create_branch( + base_branch=" main ", + target_branch=" feature/demo ", + ) + + assert result["target_branch"] == "feature/demo" + branch_call = client_instance.post.await_args_list[1] + payload = branch_call.kwargs["json"] + assert payload["base_branch"] == "main" + assert "base_ref" not in payload + assert payload["target_branch"] == "feature/demo" + + @pytest.mark.asyncio + async def test_create_branch_prefers_base_ref_when_both_are_provided( + self, git_storage_options: dict + ) -> None: + """Test create_branch prefers base_ref over deprecated base_branch.""" + storage = GitStorage(git_storage_options) + + create_repo_response = MagicMock() + create_repo_response.status_code = 200 + create_repo_response.is_success = True + create_repo_response.json.return_value = {"repo_id": "test-repo"} + + create_branch_response = MagicMock() + create_branch_response.status_code = 200 + create_branch_response.is_success = True + create_branch_response.json.return_value = { + "message": "branch created", + "target_branch": "feature/demo", + "target_is_ephemeral": False, + } + + with patch("httpx.AsyncClient") as mock_client: + client_instance = mock_client.return_value.__aenter__.return_value + client_instance.post = AsyncMock( + side_effect=[create_repo_response, create_branch_response] + ) + + repo = await storage.create_repo(id="test-repo") + with pytest.warns(DeprecationWarning, match="base_branch is deprecated"): + await repo.create_branch( + base_ref="refs/tags/v1.2.3", + base_branch="main", + target_branch="feature/demo", + ) + + payload = client_instance.post.await_args_list[1].kwargs["json"] + assert payload["base_ref"] == "refs/tags/v1.2.3" + assert "base_branch" not in payload + + @pytest.mark.asyncio + async def test_create_branch_blank_base_ref_falls_back_to_base_branch( + self, git_storage_options: dict + ) -> None: + """Blank base_ref should be treated as missing after trimming.""" + storage = GitStorage(git_storage_options) + + create_repo_response = MagicMock() + create_repo_response.status_code = 200 + create_repo_response.is_success = True + create_repo_response.json.return_value = {"repo_id": "test-repo"} + + create_branch_response = MagicMock() + create_branch_response.status_code = 200 + create_branch_response.is_success = True + create_branch_response.json.return_value = { + "message": "branch created", + "target_branch": "feature/demo", + "target_is_ephemeral": False, + } + + with patch("httpx.AsyncClient") as mock_client: + client_instance = mock_client.return_value.__aenter__.return_value + client_instance.post = AsyncMock( + side_effect=[create_repo_response, create_branch_response] + ) + + repo = await storage.create_repo(id="test-repo") + with pytest.warns(DeprecationWarning, match="base_branch is deprecated"): + await repo.create_branch( + base_ref=" ", + base_branch="main", + target_branch="feature/demo", + ) + + payload = client_instance.post.await_args_list[1].kwargs["json"] + assert payload["base_branch"] == "main" + assert "base_ref" not in payload + + @pytest.mark.asyncio + async def test_create_branch_requires_effective_base(self, git_storage_options: dict) -> None: + """create_branch requires a non-blank base_ref or base_branch.""" + storage = GitStorage(git_storage_options) + + create_repo_response = MagicMock() + create_repo_response.status_code = 200 + create_repo_response.is_success = True + create_repo_response.json.return_value = {"repo_id": "test-repo"} + + with patch("httpx.AsyncClient") as mock_client: + client_instance = mock_client.return_value.__aenter__.return_value + client_instance.post = AsyncMock(return_value=create_repo_response) + + repo = await storage.create_repo(id="test-repo") + + with pytest.raises(ValueError, match="base_ref or base_branch is required"): + await repo.create_branch( + base_ref=" ", + base_branch=" ", + target_branch="feature/demo", + ) + + @pytest.mark.asyncio + async def test_create_branch_requires_target_branch(self, git_storage_options: dict) -> None: + """create_branch still requires a non-blank target branch.""" + storage = GitStorage(git_storage_options) + + create_repo_response = MagicMock() + create_repo_response.status_code = 200 + create_repo_response.is_success = True + create_repo_response.json.return_value = {"repo_id": "test-repo"} + + with patch("httpx.AsyncClient") as mock_client: + client_instance = mock_client.return_value.__aenter__.return_value + client_instance.post = AsyncMock(return_value=create_repo_response) + + repo = await storage.create_repo(id="test-repo") + + with pytest.raises(ValueError, match="target_branch is required"): + await repo.create_branch(base_ref="main", target_branch=" ") + @pytest.mark.asyncio async def test_promote_ephemeral_branch_defaults(self, git_storage_options: dict) -> None: """Test promoting an ephemeral branch with default target branch.""" @@ -706,7 +863,7 @@ async def test_promote_ephemeral_branch_defaults(self, git_storage_options: dict branch_call = client_instance.post.await_args_list[1] assert branch_call.args[0].endswith("/api/v1/repos/branches/create") payload = branch_call.kwargs["json"] - assert payload["base_branch"] == "ephemeral/demo" + assert payload["base_ref"] == "ephemeral/demo" assert payload["target_branch"] == "ephemeral/demo" assert payload["base_is_ephemeral"] is True assert payload["target_is_ephemeral"] is False @@ -749,7 +906,7 @@ async def test_promote_ephemeral_branch_custom_target( assert client_instance.post.await_count == 2 branch_call = client_instance.post.await_args_list[1] payload = branch_call.kwargs["json"] - assert payload["base_branch"] == "ephemeral/demo" + assert payload["base_ref"] == "ephemeral/demo" assert payload["target_branch"] == "feature/final-demo" assert payload["base_is_ephemeral"] is True assert payload["target_is_ephemeral"] is False @@ -777,7 +934,7 @@ async def test_create_branch_conflict(self, git_storage_options: dict) -> None: with pytest.raises(ApiError) as exc_info: await repo.create_branch( - base_branch="main", + base_ref="main", target_branch="feature/demo", ) @@ -1802,7 +1959,7 @@ async def capture_post(*args, **kwargs): mock_client.return_value.__aenter__.return_value.post = capture_post repo = await storage.create_repo(id="test-repo") - await repo.create_branch(base_branch="main", target_branch="feature/test") + await repo.create_branch(base_ref="main", target_branch="feature/test") # Verify headers include Code-Storage-Agent assert captured_headers is not None diff --git a/packages/code-storage-python/uv.lock b/packages/code-storage-python/uv.lock index e396429..87fc864 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.5.0" +version = "1.5.2" source = { editable = "." } dependencies = [ { name = "cryptography" }, diff --git a/packages/code-storage-typescript/README.md b/packages/code-storage-typescript/README.md index 127dc88..a02f516 100644 --- a/packages/code-storage-typescript/README.md +++ b/packages/code-storage-typescript/README.md @@ -276,15 +276,18 @@ const commitDiff = await repo.getCommitDiff({ console.log(commitDiff.stats); console.log(commitDiff.files); -// Create a new branch from an existing one +// Create a new branch from an existing ref const branch = await repo.createBranch({ - baseBranch: 'main', + baseRef: 'refs/heads/main', targetBranch: 'feature/demo', // baseIsEphemeral: true, // targetIsEphemeral: true, }); console.log(branch.targetBranch, branch.commitSha); +// `baseBranch` is still accepted for backwards compatibility, but deprecated. +// Prefer `baseRef` for new code. + // Create a commit using the streaming helper const fs = await import('node:fs/promises'); const result = await repo diff --git a/packages/code-storage-typescript/package.json b/packages/code-storage-typescript/package.json index aba0f08..94161b8 100644 --- a/packages/code-storage-typescript/package.json +++ b/packages/code-storage-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@pierre/storage", - "version": "1.4.1", + "version": "1.4.2", "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 bb45531..7b917d8 100644 --- a/packages/code-storage-typescript/src/index.ts +++ b/packages/code-storage-typescript/src/index.ts @@ -1255,9 +1255,10 @@ class RepoImpl implements Repo { async createBranch( options: CreateBranchOptions ): Promise { - const baseBranch = options?.baseBranch?.trim(); - if (!baseBranch) { - throw new Error('createBranch baseBranch is required'); + const baseRef = options?.baseRef?.trim() || undefined; + const baseBranch = options?.baseBranch?.trim() || undefined; + if (!baseRef && !baseBranch) { + throw new Error('createBranch baseRef or baseBranch is required'); } const targetBranch = options?.targetBranch?.trim(); if (!targetBranch) { @@ -1271,9 +1272,13 @@ class RepoImpl implements Repo { }); const body: Record = { - base_branch: baseBranch, target_branch: targetBranch, }; + if (baseRef) { + body.base_ref = baseRef; + } else { + body.base_branch = baseBranch; + } if (options.baseIsEphemeral === true) { body.base_is_ephemeral = true; diff --git a/packages/code-storage-typescript/src/types.ts b/packages/code-storage-typescript/src/types.ts index b826f48..56ef103 100644 --- a/packages/code-storage-typescript/src/types.ts +++ b/packages/code-storage-typescript/src/types.ts @@ -322,7 +322,9 @@ export interface ListBranchesResult { // Create Branch API types export interface CreateBranchOptions extends GitStorageInvocationOptions { - baseBranch: string; + baseRef?: string; + /** @deprecated Use baseRef instead. */ + baseBranch?: string; targetBranch: string; baseIsEphemeral?: boolean; targetIsEphemeral?: boolean; diff --git a/packages/code-storage-typescript/tests/index.test.ts b/packages/code-storage-typescript/tests/index.test.ts index f6340b4..65fc9c3 100644 --- a/packages/code-storage-typescript/tests/index.test.ts +++ b/packages/code-storage-typescript/tests/index.test.ts @@ -1178,7 +1178,7 @@ describe('GitStorage', () => { }); describe('Repo createBranch', () => { - it('posts to create branch endpoint and returns parsed result', async () => { + it('posts baseRef to create branch endpoint and returns parsed result', async () => { const store = new GitStorage({ name: 'v0', key }); const repo = await store.createRepo({ id: 'repo-create-branch' }); @@ -1196,7 +1196,7 @@ describe('GitStorage', () => { const body = JSON.parse(requestInit.body as string); expect(body).toEqual({ - base_branch: 'main', + base_ref: 'refs/heads/main', base_is_ephemeral: true, target_branch: 'feature/demo', target_is_ephemeral: true, @@ -1216,8 +1216,8 @@ describe('GitStorage', () => { }); const result = await repo.createBranch({ - baseBranch: 'main', - targetBranch: 'feature/demo', + baseRef: ' refs/heads/main ', + targetBranch: ' feature/demo ', baseIsEphemeral: true, targetIsEphemeral: true, }); @@ -1230,6 +1230,65 @@ describe('GitStorage', () => { }); }); + it('falls back to deprecated baseBranch when baseRef is absent', async () => { + const store = new GitStorage({ name: 'v0', key }); + const repo = await store.createRepo({ id: 'repo-create-branch-fallback' }); + + mockFetch.mockImplementationOnce((_url, init) => { + const body = JSON.parse((init as RequestInit).body as string); + expect(body).toEqual({ + base_branch: 'main', + target_branch: 'feature/demo', + }); + + return Promise.resolve({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ + message: 'branch created', + target_branch: 'feature/demo', + target_is_ephemeral: false, + }), + } as any); + }); + + await repo.createBranch({ + baseBranch: ' main ', + targetBranch: 'feature/demo', + }); + }); + + it('prefers baseRef when both baseRef and baseBranch are provided', async () => { + const store = new GitStorage({ name: 'v0', key }); + const repo = await store.createRepo({ id: 'repo-create-branch-precedence' }); + + mockFetch.mockImplementationOnce((_url, init) => { + const body = JSON.parse((init as RequestInit).body as string); + expect(body).toEqual({ + base_ref: 'refs/heads/main', + target_branch: 'feature/demo', + }); + + return Promise.resolve({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ + message: 'branch created', + target_branch: 'feature/demo', + target_is_ephemeral: false, + }), + } as any); + }); + + await repo.createBranch({ + baseRef: 'refs/heads/main', + baseBranch: 'main', + targetBranch: 'feature/demo', + }); + }); + it('honors ttl override when creating a branch', async () => { const store = new GitStorage({ name: 'v0', key }); const repo = await store.createRepo({ id: 'repo-create-branch-ttl' }); @@ -1254,7 +1313,7 @@ describe('GitStorage', () => { }); const result = await repo.createBranch({ - baseBranch: 'main', + baseRef: 'refs/heads/main', targetBranch: 'feature/demo', ttl: 600, }); @@ -1267,18 +1326,26 @@ describe('GitStorage', () => { }); }); - it('requires both base and target branches', async () => { + it('requires an effective base source and target branch', async () => { const store = new GitStorage({ name: 'v0', key }); const repo = await store.createRepo({ id: 'repo-create-branch-validation', }); await expect( - repo.createBranch({ baseBranch: '', targetBranch: 'feature/demo' }) - ).rejects.toThrow('createBranch baseBranch is required'); + repo.createBranch({ baseRef: '', targetBranch: 'feature/demo' }) + ).rejects.toThrow('createBranch baseRef or baseBranch is required'); + + await expect( + repo.createBranch({ + baseRef: ' ', + baseBranch: ' ', + targetBranch: 'feature/demo', + }) + ).rejects.toThrow('createBranch baseRef or baseBranch is required'); await expect( - repo.createBranch({ baseBranch: 'main', targetBranch: '' }) + repo.createBranch({ baseRef: 'refs/heads/main', targetBranch: '' }) ).rejects.toThrow('createBranch targetBranch is required'); }); });