Skip to content

Commit bedabad

Browse files
authored
Merge pull request #437 from Mng-dev-ai/feat/create-commit-action
Create Commit Action
2 parents d141d44 + 8ce527d commit bedabad

18 files changed

Lines changed: 382 additions & 26 deletions

File tree

backend/app/api/endpoints/github.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from app.models.schemas.github import (
99
CreatePullRequestRequest,
1010
CreatePullRequestResponse,
11+
GenerateCommitMessageRequest,
12+
GenerateCommitMessageResponse,
1113
GeneratePRDescriptionRequest,
1214
GeneratePRDescriptionResponse,
1315
GitHubCollaborator,
@@ -109,6 +111,32 @@ async def generate_pr_description(
109111
return GeneratePRDescriptionResponse(description=description)
110112

111113

114+
@router.post(
115+
"/generate-commit-message",
116+
response_model=GenerateCommitMessageResponse,
117+
)
118+
async def generate_commit_message(
119+
request: GenerateCommitMessageRequest,
120+
current_user: User = Depends(get_current_user),
121+
ai_service: ClaudeAgentService = Depends(get_claude_agent_service),
122+
) -> GenerateCommitMessageResponse:
123+
try:
124+
message = await ai_service.generate_commit_message(
125+
request.diff, request.model_id, current_user
126+
)
127+
except APIKeyValidationError as e:
128+
raise HTTPException(
129+
status_code=status.HTTP_400_BAD_REQUEST,
130+
detail=str(e),
131+
) from e
132+
except ClaudeAgentException as e:
133+
raise HTTPException(
134+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
135+
detail=str(e),
136+
) from e
137+
return GenerateCommitMessageResponse(message=message)
138+
139+
112140
@router.get("/collaborators")
113141
async def list_collaborators(
114142
owner: str = Query(..., min_length=1),

backend/app/api/endpoints/sandbox.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
GitBranchesResponse,
1616
GitCheckoutRequest,
1717
GitCheckoutResponse,
18+
GitCommandResponse,
19+
GitCommitRequest,
1820
GitCreateBranchRequest,
1921
GitCreateBranchResponse,
2022
GitDiffResponse,
21-
GitPushPullResponse,
2223
GitRemoteUrlResponse,
2324
IDEUrlResponse,
2425
SandboxFilesMetadataResponse,
@@ -53,7 +54,7 @@ def _git_cd_prefix(cwd: str | None = None) -> str:
5354
status_code=status.HTTP_400_BAD_REQUEST,
5455
detail="Invalid cwd path",
5556
)
56-
return f"cd '{cwd}'; "
57+
return f"cd '{cwd}' && "
5758

5859

5960
@router.get("/{sandbox_id}/preview-links", response_model=PreviewLinksResponse)
@@ -482,50 +483,65 @@ async def checkout_git_branch(
482483
)
483484

484485

485-
async def _run_git_push_pull(
486+
async def _run_git_command(
486487
sandbox_id: str,
487488
command: str,
488489
sandbox_service: SandboxService,
489490
cwd: str | None = None,
490-
) -> GitPushPullResponse:
491+
) -> GitCommandResponse:
491492
try:
492493
cd_prefix = _git_cd_prefix(cwd)
493494
result = await sandbox_service.execute_command(
494495
sandbox_id,
495496
f"{cd_prefix}{command} 2>&1",
496497
)
497498
if result.exit_code != 0:
498-
return GitPushPullResponse(
499+
return GitCommandResponse(
499500
success=False,
500501
output="",
501502
error=result.stdout.strip() or result.stderr.strip(),
502503
)
503-
return GitPushPullResponse(success=True, output=result.stdout.strip())
504+
return GitCommandResponse(success=True, output=result.stdout.strip())
504505
except SandboxException as e:
505506
raise HTTPException(
506507
status_code=status.HTTP_400_BAD_REQUEST,
507508
detail=str(e),
508509
)
509510

510511

511-
@router.post("/{sandbox_id}/git/push", response_model=GitPushPullResponse)
512+
@router.post("/{sandbox_id}/git/push", response_model=GitCommandResponse)
512513
async def git_push(
513514
sandbox_id: str = Depends(validate_sandbox_ownership),
514515
sandbox_service: SandboxService = Depends(get_sandbox_service),
515516
cwd: str | None = Query(None),
516-
) -> GitPushPullResponse:
517-
return await _run_git_push_pull(
517+
) -> GitCommandResponse:
518+
return await _run_git_command(
518519
sandbox_id, "git push -u origin HEAD", sandbox_service, cwd
519520
)
520521

521522

522-
@router.post("/{sandbox_id}/git/pull", response_model=GitPushPullResponse)
523+
@router.post("/{sandbox_id}/git/pull", response_model=GitCommandResponse)
523524
async def git_pull(
524525
sandbox_id: str = Depends(validate_sandbox_ownership),
525526
sandbox_service: SandboxService = Depends(get_sandbox_service),
526527
cwd: str | None = Query(None),
527-
) -> GitPushPullResponse:
528-
return await _run_git_push_pull(sandbox_id, "git pull", sandbox_service, cwd)
528+
) -> GitCommandResponse:
529+
return await _run_git_command(sandbox_id, "git pull", sandbox_service, cwd)
530+
531+
532+
@router.post("/{sandbox_id}/git/commit", response_model=GitCommandResponse)
533+
async def git_commit(
534+
request: GitCommitRequest,
535+
sandbox_id: str = Depends(validate_sandbox_ownership),
536+
sandbox_service: SandboxService = Depends(get_sandbox_service),
537+
) -> GitCommandResponse:
538+
msg = request.message.replace("'", "'\\''")
539+
return await _run_git_command(
540+
sandbox_id,
541+
f"git add -A && git commit -m '{msg}'",
542+
sandbox_service,
543+
request.cwd,
544+
)
529545

530546

531547
@router.post("/{sandbox_id}/git/create-branch", response_model=GitCreateBranchResponse)

backend/app/models/schemas/github.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,12 @@ class GeneratePRDescriptionRequest(BaseModel):
8080

8181
class GeneratePRDescriptionResponse(BaseModel):
8282
description: str
83+
84+
85+
class GenerateCommitMessageRequest(BaseModel):
86+
diff: str = Field(min_length=1, max_length=200_000)
87+
model_id: str = Field(max_length=128)
88+
89+
90+
class GenerateCommitMessageResponse(BaseModel):
91+
message: str

backend/app/models/schemas/sandbox.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ class GitCreateBranchResponse(BaseModel):
105105
error: str | None = None
106106

107107

108-
class GitPushPullResponse(BaseModel):
108+
class GitCommitRequest(BaseModel):
109+
message: str = Field(..., min_length=1, max_length=2000)
110+
cwd: str | None = None
111+
112+
113+
class GitCommandResponse(BaseModel):
109114
success: bool
110115
output: str
111116
error: str | None = None
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT = """You are a git commit message writer. Given a git diff, produce a clear, concise commit message following conventional commit style.
2+
3+
Output ONLY the commit message with no preamble, explanation, or meta-commentary.
4+
5+
<guidelines>
6+
- First line: a short summary (50 chars or less ideally, 72 max) in imperative mood (e.g., "Add user authentication", "Fix null pointer in parser")
7+
- If more detail is needed, add a blank line followed by a body with bullet points
8+
- Focus on the "what" and "why", not line-by-line details
9+
- Do not include generic text like "Update files" or "Make changes"
10+
- If the diff is too large or unclear, summarize at a high level based on file paths and change patterns
11+
- Do not wrap the message in quotes or backticks
12+
</guidelines>"""

backend/app/services/claude_agent.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from app.models.db_models.user import User, UserSettings
2626
from app.models.schemas.settings import ProviderType
2727
from app.prompts.enhance_prompt import ENHANCE_PROMPT
28+
from app.prompts.generate_commit_message import (
29+
GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT,
30+
)
2831
from app.prompts.generate_pr_description import (
2932
GENERATE_PR_DESCRIPTION_SYSTEM_PROMPT,
3033
GENERATE_PR_DESCRIPTION_TITLE_PREFIX,
@@ -336,8 +339,13 @@ async def generate_title(self, prompt: str, user: User) -> str | None:
336339
logger.debug("Title generation SDK call failed for user %s", user.id)
337340
return None
338341

339-
async def generate_pr_description(
340-
self, title: str, diff: str, model_id: str, user: User
342+
async def _generate_with_ai(
343+
self,
344+
system_prompt: str,
345+
user_message: str,
346+
model_id: str,
347+
user: User,
348+
empty_error: str,
341349
) -> str:
342350
user_settings = await UserService(
343351
session_factory=self.session_factory
@@ -347,29 +355,48 @@ async def generate_pr_description(
347355
env, _, actual_model_id = self._build_auth_env(model_id, user_settings)
348356

349357
options = ClaudeAgentOptions(
350-
system_prompt=GENERATE_PR_DESCRIPTION_SYSTEM_PROMPT,
358+
system_prompt=system_prompt,
351359
permission_mode="default",
352360
model=actual_model_id,
353361
max_turns=1,
354362
env=env,
355363
)
356364

357365
try:
358-
description = ""
366+
result = ""
359367
async with ClaudeSDKClient(options=options) as client:
360-
user_message = (
361-
GENERATE_PR_DESCRIPTION_TITLE_PREFIX + title + "\n\n" + diff
362-
)
363368
await client.query(user_message)
364369
async for message in client.receive_response():
365370
if isinstance(message, ResultMessage) and message.result:
366-
description = message.result
371+
result = message.result
367372

368-
if not description:
369-
raise ClaudeAgentException("AI returned an empty description")
370-
return description
373+
if not result:
374+
raise ClaudeAgentException(empty_error)
375+
return result
371376
except ClaudeSDKError as e:
372-
raise ClaudeAgentException(f"Failed to generate PR description: {e}") from e
377+
raise ClaudeAgentException(f"{empty_error}: {e}") from e
378+
379+
async def generate_pr_description(
380+
self, title: str, diff: str, model_id: str, user: User
381+
) -> str:
382+
return await self._generate_with_ai(
383+
GENERATE_PR_DESCRIPTION_SYSTEM_PROMPT,
384+
GENERATE_PR_DESCRIPTION_TITLE_PREFIX + title + "\n\n" + diff,
385+
model_id,
386+
user,
387+
"AI returned an empty description",
388+
)
389+
390+
async def generate_commit_message(
391+
self, diff: str, model_id: str, user: User
392+
) -> str:
393+
return await self._generate_with_ai(
394+
GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT,
395+
diff,
396+
model_id,
397+
user,
398+
"AI returned an empty commit message",
399+
)
373400

374401
@staticmethod
375402
def _build_permission_server(

0 commit comments

Comments
 (0)