Skip to content

Commit 3b9a465

Browse files
author
Datata1
committed
feat(git): add git resource
1 parent d7dea84 commit 3b9a465

File tree

10 files changed

+273
-4
lines changed

10 files changed

+273
-4
lines changed
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
from .git import GitHead, WorkspaceGitManager
2+
from .resources import WorkspacesResource
13
from .schemas import (
4+
CommandInput,
5+
CommandOutput,
26
Workspace,
37
WorkspaceCreate,
4-
WorkspaceUpdate,
58
WorkspaceStatus,
6-
CommandInput,
7-
CommandOutput,
9+
WorkspaceUpdate,
810
)
9-
from .resources import WorkspacesResource
1011

1112
__all__ = [
1213
"Workspace",
@@ -16,4 +17,6 @@
1617
"WorkspacesResource",
1718
"CommandInput",
1819
"CommandOutput",
20+
"WorkspaceGitManager",
21+
"GitHead",
1922
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .models import WorkspaceGitManager
2+
from .schema import GitHead
3+
4+
__all__ = ["WorkspaceGitManager", "GitHead"]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import Optional
5+
6+
from ....core.handler import _APIOperationExecutor
7+
from ....http_client import APIHttpClient
8+
from .operations import (
9+
_GET_HEAD_OP,
10+
_PULL_OP,
11+
_PULL_WITH_REMOTE_AND_BRANCH_OP,
12+
_PULL_WITH_REMOTE_OP,
13+
)
14+
from .schema import GitHead
15+
16+
log = logging.getLogger(__name__)
17+
18+
19+
class WorkspaceGitManager(_APIOperationExecutor):
20+
"""Manager for git operations on a workspace."""
21+
22+
def __init__(self, http_client: APIHttpClient, workspace_id: int):
23+
self._http_client = http_client
24+
self._workspace_id = workspace_id
25+
self.id = workspace_id
26+
27+
async def get_head(self) -> GitHead:
28+
return await self._execute_operation(_GET_HEAD_OP)
29+
30+
async def pull(
31+
self,
32+
remote: Optional[str] = None,
33+
branch: Optional[str] = None,
34+
) -> None:
35+
if remote is not None and branch is not None:
36+
await self._execute_operation(
37+
_PULL_WITH_REMOTE_AND_BRANCH_OP, remote=remote, branch=branch
38+
)
39+
elif remote is not None:
40+
await self._execute_operation(_PULL_WITH_REMOTE_OP, remote=remote)
41+
else:
42+
await self._execute_operation(_PULL_OP)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
from ....core.operations import APIOperation
4+
from .schema import GitHead
5+
6+
_GET_HEAD_OP = APIOperation(
7+
method="GET",
8+
endpoint_template="/workspaces/{id}/git/head",
9+
response_model=GitHead,
10+
)
11+
12+
_PULL_OP = APIOperation(
13+
method="POST",
14+
endpoint_template="/workspaces/{id}/git/pull",
15+
response_model=type(None),
16+
)
17+
18+
_PULL_WITH_REMOTE_OP = APIOperation(
19+
method="POST",
20+
endpoint_template="/workspaces/{id}/git/pull/{remote}",
21+
response_model=type(None),
22+
)
23+
24+
_PULL_WITH_REMOTE_AND_BRANCH_OP = APIOperation(
25+
method="POST",
26+
endpoint_template="/workspaces/{id}/git/pull/{remote}/{branch}",
27+
response_model=type(None),
28+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from __future__ import annotations
2+
3+
from ....core.base import CamelModel
4+
5+
6+
class GitHead(CamelModel):
7+
head: str

src/codesphere/resources/workspace/pipeline/resources.py

Whitespace-only changes.

src/codesphere/resources/workspace/schemas.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from ...core.base import CamelModel
1010
from ...utils import update_model_fields
1111
from .envVars import EnvVar, WorkspaceEnvVarManager
12+
from .git import WorkspaceGitManager
1213
from .landscape import WorkspaceLandscapeManager
1314

1415
log = logging.getLogger(__name__)
@@ -137,3 +138,9 @@ def landscape(self) -> WorkspaceLandscapeManager:
137138
"""Manager for landscape operations (Multi Server Deployments)."""
138139
http_client = self.validate_http_client()
139140
return WorkspaceLandscapeManager(http_client, workspace_id=self.id)
141+
142+
@cached_property
143+
def git(self) -> WorkspaceGitManager:
144+
"""Manager for git operations (head, pull)."""
145+
http_client = self.validate_http_client()
146+
return WorkspaceGitManager(http_client, workspace_id=self.id)

tests/integration/test_git.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import AsyncGenerator
2+
3+
import pytest
4+
5+
from codesphere import CodesphereSDK
6+
from codesphere.resources.workspace import Workspace, WorkspaceCreate
7+
8+
pytestmark = pytest.mark.integration
9+
10+
11+
@pytest.fixture(scope="module")
12+
async def git_workspace(
13+
module_sdk_client: CodesphereSDK,
14+
test_team_id: int,
15+
test_plan_id: int,
16+
) -> AsyncGenerator[Workspace, None]:
17+
payload = WorkspaceCreate(
18+
team_id=test_team_id,
19+
name="sdk-git-integration-test",
20+
plan_id=test_plan_id,
21+
git_url="https://github.com/octocat/Hello-World.git",
22+
)
23+
24+
workspace = await module_sdk_client.workspaces.create(payload=payload)
25+
26+
try:
27+
await workspace.wait_until_running(timeout=120.0)
28+
yield workspace
29+
finally:
30+
try:
31+
await workspace.delete()
32+
except Exception:
33+
pass
34+
35+
36+
class TestGitIntegration:
37+
@pytest.mark.asyncio
38+
async def test_get_head(self, git_workspace: Workspace):
39+
result = await git_workspace.git.get_head()
40+
41+
assert result.head is not None
42+
assert len(result.head) > 0
43+
44+
@pytest.mark.asyncio
45+
async def test_pull_default(self, git_workspace: Workspace):
46+
# This should not raise an exception
47+
await git_workspace.git.pull()
48+
49+
@pytest.mark.asyncio
50+
async def test_pull_with_remote(self, git_workspace: Workspace):
51+
await git_workspace.git.pull(remote="origin")
52+
53+
@pytest.mark.asyncio
54+
async def test_pull_with_remote_and_branch(self, git_workspace: Workspace):
55+
await git_workspace.git.pull(remote="origin", branch="master")
File renamed without changes.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import pytest
2+
3+
from codesphere.resources.workspace.git import GitHead, WorkspaceGitManager
4+
5+
6+
class TestWorkspaceGitManager:
7+
@pytest.fixture
8+
def git_manager(self, mock_http_client_for_resource):
9+
def _create(response_data):
10+
mock_client = mock_http_client_for_resource(response_data)
11+
manager = WorkspaceGitManager(http_client=mock_client, workspace_id=72678)
12+
return manager, mock_client
13+
14+
return _create
15+
16+
@pytest.mark.asyncio
17+
async def test_get_head(self, git_manager):
18+
manager, mock_client = git_manager({"head": "abc123def456"})
19+
20+
result = await manager.get_head()
21+
22+
assert isinstance(result, GitHead)
23+
assert result.head == "abc123def456"
24+
mock_client.request.assert_awaited_once()
25+
call_args = mock_client.request.call_args
26+
assert call_args.kwargs.get("method") == "GET"
27+
assert call_args.kwargs.get("endpoint") == "/workspaces/72678/git/head"
28+
29+
@pytest.mark.asyncio
30+
async def test_pull_without_arguments(self, git_manager):
31+
manager, mock_client = git_manager(None)
32+
33+
await manager.pull()
34+
35+
mock_client.request.assert_awaited_once()
36+
call_args = mock_client.request.call_args
37+
assert call_args.kwargs.get("method") == "POST"
38+
assert call_args.kwargs.get("endpoint") == "/workspaces/72678/git/pull"
39+
40+
@pytest.mark.asyncio
41+
async def test_pull_with_remote(self, git_manager):
42+
manager, mock_client = git_manager(None)
43+
44+
await manager.pull(remote="origin")
45+
46+
mock_client.request.assert_awaited_once()
47+
call_args = mock_client.request.call_args
48+
assert call_args.kwargs.get("method") == "POST"
49+
assert call_args.kwargs.get("endpoint") == "/workspaces/72678/git/pull/origin"
50+
51+
@pytest.mark.asyncio
52+
async def test_pull_with_remote_and_branch(self, git_manager):
53+
manager, mock_client = git_manager(None)
54+
55+
await manager.pull(remote="origin", branch="main")
56+
57+
mock_client.request.assert_awaited_once()
58+
call_args = mock_client.request.call_args
59+
assert call_args.kwargs.get("method") == "POST"
60+
assert (
61+
call_args.kwargs.get("endpoint") == "/workspaces/72678/git/pull/origin/main"
62+
)
63+
64+
@pytest.mark.asyncio
65+
async def test_pull_with_branch_only_ignores_branch(self, git_manager):
66+
manager, mock_client = git_manager(None)
67+
68+
# Branch without remote should be ignored per the implementation
69+
await manager.pull(branch="main")
70+
71+
mock_client.request.assert_awaited_once()
72+
call_args = mock_client.request.call_args
73+
assert call_args.kwargs.get("method") == "POST"
74+
assert call_args.kwargs.get("endpoint") == "/workspaces/72678/git/pull"
75+
76+
@pytest.mark.asyncio
77+
async def test_pull_with_custom_remote(self, git_manager):
78+
"""pull should work with custom remote names."""
79+
manager, mock_client = git_manager(None)
80+
81+
await manager.pull(remote="upstream")
82+
83+
mock_client.request.assert_awaited_once()
84+
call_args = mock_client.request.call_args
85+
assert call_args.kwargs.get("endpoint") == "/workspaces/72678/git/pull/upstream"
86+
87+
@pytest.mark.asyncio
88+
async def test_pull_with_feature_branch(self, git_manager):
89+
manager, mock_client = git_manager(None)
90+
91+
await manager.pull(remote="origin", branch="feature/my-feature")
92+
93+
mock_client.request.assert_awaited_once()
94+
call_args = mock_client.request.call_args
95+
assert (
96+
call_args.kwargs.get("endpoint")
97+
== "/workspaces/72678/git/pull/origin/feature/my-feature"
98+
)
99+
100+
101+
class TestGitHeadModel:
102+
def test_create_git_head(self):
103+
git_head = GitHead(head="abc123def456")
104+
105+
assert git_head.head == "abc123def456"
106+
107+
def test_git_head_from_dict(self):
108+
git_head = GitHead.model_validate({"head": "abc123def456"})
109+
110+
assert git_head.head == "abc123def456"
111+
112+
def test_git_head_dump(self):
113+
git_head = GitHead(head="abc123def456")
114+
dumped = git_head.model_dump()
115+
116+
assert dumped == {"head": "abc123def456"}
117+
118+
def test_git_head_with_full_sha(self):
119+
full_sha = "a" * 40
120+
git_head = GitHead(head=full_sha)
121+
122+
assert git_head.head == full_sha
123+
assert len(git_head.head) == 40

0 commit comments

Comments
 (0)