bamboo-coding 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent/__init__.py +0 -0
- agent/bamboo_git_agent/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/repo.py +189 -0
- agent/bamboo_git_agent/config.py +76 -0
- agent/bamboo_git_agent/controller_client.py +73 -0
- agent/bamboo_git_agent/journal.py +209 -0
- agent/bamboo_git_agent/main.py +164 -0
- agent/bamboo_git_agent/protocol.py +21 -0
- agent/bamboo_git_agent/status.py +59 -0
- api/__init__.py +2 -0
- api/commit.py +82 -0
- api/repository.py +592 -0
- bamboo_coding/__init__.py +3 -0
- bamboo_coding/client/__init__.py +3 -0
- bamboo_coding/client/capabilities/__init__.py +1 -0
- bamboo_coding/client/capabilities/repo.py +279 -0
- bamboo_coding/client/config.py +179 -0
- bamboo_coding/client/controller_client.py +112 -0
- bamboo_coding/client/journal.py +1 -0
- bamboo_coding/client/main.py +425 -0
- bamboo_coding/client/status.py +59 -0
- bamboo_coding/client/terminal_runtime.py +251 -0
- bamboo_coding/server/__init__.py +3 -0
- bamboo_coding/server/core/__init__.py +1 -0
- bamboo_coding/server/core/config.py +1 -0
- bamboo_coding/server/main.py +39 -0
- bamboo_coding/shared/__init__.py +39 -0
- bamboo_coding/shared/protocol.py +1 -0
- bamboo_coding-0.1.0.dist-info/METADATA +284 -0
- bamboo_coding-0.1.0.dist-info/RECORD +62 -0
- bamboo_coding-0.1.0.dist-info/WHEEL +5 -0
- bamboo_coding-0.1.0.dist-info/entry_points.txt +3 -0
- bamboo_coding-0.1.0.dist-info/top_level.txt +6 -0
- controller/__init__.py +0 -0
- controller/app/__init__.py +0 -0
- controller/app/api/__init__.py +0 -0
- controller/app/api/agents/__init__.py +3 -0
- controller/app/api/agents/ws.py +133 -0
- controller/app/api/public/__init__.py +11 -0
- controller/app/api/public/repos.py +207 -0
- controller/app/api/public/terminals.py +64 -0
- controller/app/core/__init__.py +0 -0
- controller/app/core/config.py +20 -0
- controller/app/core/errors.py +14 -0
- controller/app/db/__init__.py +0 -0
- controller/app/db/models.py +42 -0
- controller/app/db/session.py +74 -0
- controller/app/main.py +71 -0
- controller/app/schemas/__init__.py +23 -0
- controller/app/schemas/agent_messages.py +21 -0
- controller/app/schemas/public.py +3 -0
- controller/app/services/__init__.py +0 -0
- controller/app/services/agents.py +108 -0
- controller/app/services/registrations.py +17 -0
- controller/app/services/repositories.py +85 -0
- controller/app/services/router.py +140 -0
- controller/app/services/tasks.py +104 -0
- controller/app/services/terminals.py +148 -0
- git_utils.py +2238 -0
- shared/__init__.py +39 -0
- shared/protocol.py +253 -0
api/repository.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository API Endpoints
|
|
3
|
+
|
|
4
|
+
Provides endpoints for repository file tree, diff, status, and branch management operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from sanic import Blueprint, response
|
|
8
|
+
from sanic.exceptions import InvalidUsage, ServerError, NotFound, SanicException
|
|
9
|
+
|
|
10
|
+
import git_utils
|
|
11
|
+
from git import InvalidGitRepositoryError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Custom exception for conflict responses (HTTP 409)
|
|
15
|
+
class Conflict(SanicException):
|
|
16
|
+
status_code = 409
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
repository_bp = Blueprint("repository", url_prefix="/api/repo")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@repository_bp.route("/tree")
|
|
23
|
+
async def get_tree(request):
|
|
24
|
+
"""
|
|
25
|
+
Get repository file tree.
|
|
26
|
+
|
|
27
|
+
Query params:
|
|
28
|
+
all: "true" to get all files, otherwise only changed files (default: false)
|
|
29
|
+
no_cache: "true" to bypass cache and get fresh data (default: false)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
JSON with file tree structure and file statuses
|
|
33
|
+
"""
|
|
34
|
+
repo_path = request.ctx.repo_path
|
|
35
|
+
|
|
36
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
37
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
38
|
+
|
|
39
|
+
# Check if user wants all files or just changed files
|
|
40
|
+
show_all = request.args.get("all", "false").lower() == "true"
|
|
41
|
+
changed_only = not show_all
|
|
42
|
+
|
|
43
|
+
# Check if cache should be bypassed
|
|
44
|
+
no_cache = request.args.get("no_cache", "false").lower() == "true"
|
|
45
|
+
if no_cache:
|
|
46
|
+
git_utils.clear_cache()
|
|
47
|
+
|
|
48
|
+
tree_data = git_utils.get_file_tree(repo_path, changed_only=changed_only)
|
|
49
|
+
return response.json(tree_data)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@repository_bp.route("/browse")
|
|
53
|
+
async def browse_directory(request):
|
|
54
|
+
"""
|
|
55
|
+
Browse contents of a specific directory within the repository.
|
|
56
|
+
|
|
57
|
+
Query params:
|
|
58
|
+
path: Directory path relative to repository root (required)
|
|
59
|
+
Use "/" for root directory
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
JSON with directory contents including files and subdirectories
|
|
63
|
+
"""
|
|
64
|
+
repo_path = request.ctx.repo_path
|
|
65
|
+
dir_path = request.args.get("path")
|
|
66
|
+
|
|
67
|
+
if not dir_path and dir_path != "":
|
|
68
|
+
raise InvalidUsage("Missing 'path' parameter")
|
|
69
|
+
|
|
70
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
71
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
directory_data = git_utils.browse_directory(repo_path, dir_path)
|
|
75
|
+
return response.json(directory_data)
|
|
76
|
+
except ValueError as e:
|
|
77
|
+
# Path traversal or invalid path
|
|
78
|
+
raise InvalidUsage(str(e))
|
|
79
|
+
except FileNotFoundError as e:
|
|
80
|
+
raise NotFound(str(e))
|
|
81
|
+
except git_utils.GitPermissionError as e:
|
|
82
|
+
raise ServerError(str(e))
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise ServerError(f"Failed to browse directory: {str(e)}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@repository_bp.route("/diff")
|
|
88
|
+
async def get_diff(request):
|
|
89
|
+
"""
|
|
90
|
+
Get diff for a specific file.
|
|
91
|
+
|
|
92
|
+
Query params:
|
|
93
|
+
file: File path relative to repository root (required)
|
|
94
|
+
staged: "true" to get staged diff, otherwise unstaged (optional)
|
|
95
|
+
context: Number of context lines (default 3, optional)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
JSON with diff information
|
|
99
|
+
"""
|
|
100
|
+
repo_path = request.ctx.repo_path
|
|
101
|
+
file_path = request.args.get("file")
|
|
102
|
+
|
|
103
|
+
if not file_path:
|
|
104
|
+
raise InvalidUsage("Missing 'file' parameter")
|
|
105
|
+
|
|
106
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
107
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
108
|
+
|
|
109
|
+
staged = request.args.get("staged", "false").lower() == "true"
|
|
110
|
+
context = int(request.args.get("context", "3"))
|
|
111
|
+
|
|
112
|
+
diff_data = git_utils.get_file_diff(repo_path, file_path, staged, context)
|
|
113
|
+
return response.json(diff_data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@repository_bp.route("/status")
|
|
117
|
+
async def get_status(request):
|
|
118
|
+
"""
|
|
119
|
+
Get repository status.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
JSON with current branch, modified/added/deleted files, and ahead/behind counts
|
|
123
|
+
"""
|
|
124
|
+
repo_path = request.ctx.repo_path
|
|
125
|
+
|
|
126
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
127
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
128
|
+
|
|
129
|
+
status_data = git_utils.get_repo_status(repo_path)
|
|
130
|
+
return response.json(status_data)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@repository_bp.route("/branches")
|
|
134
|
+
async def get_branches(request):
|
|
135
|
+
"""
|
|
136
|
+
Get all repository branches.
|
|
137
|
+
|
|
138
|
+
Query params:
|
|
139
|
+
type: Filter by 'local' or 'remote' (optional, default: all)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
JSON with local and remote branches
|
|
143
|
+
"""
|
|
144
|
+
repo_path = request.ctx.repo_path
|
|
145
|
+
|
|
146
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
147
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
branches = git_utils.list_branches(repo_path)
|
|
151
|
+
|
|
152
|
+
# Filter by type if requested
|
|
153
|
+
branch_type = request.args.get("type")
|
|
154
|
+
if branch_type == "local":
|
|
155
|
+
branches = {'local': branches['local'], 'current': branches['current']}
|
|
156
|
+
elif branch_type == "remote":
|
|
157
|
+
branches = {'remote': branches['remote']}
|
|
158
|
+
|
|
159
|
+
return response.json(branches)
|
|
160
|
+
|
|
161
|
+
except git_utils.GitPermissionError as e:
|
|
162
|
+
raise ServerError(str(e))
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise ServerError(f"Failed to list branches: {str(e)}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@repository_bp.route("/branches", methods=["POST"])
|
|
168
|
+
async def create_branch(request):
|
|
169
|
+
"""
|
|
170
|
+
Create a new branch.
|
|
171
|
+
|
|
172
|
+
Request body:
|
|
173
|
+
{
|
|
174
|
+
"name": str (required),
|
|
175
|
+
"start_point": str (optional, default: HEAD)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
JSON with new branch details (HTTP 201)
|
|
180
|
+
"""
|
|
181
|
+
repo_path = request.ctx.repo_path
|
|
182
|
+
|
|
183
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
184
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
185
|
+
|
|
186
|
+
# Validate request body
|
|
187
|
+
if not request.json:
|
|
188
|
+
raise InvalidUsage("Request body required")
|
|
189
|
+
|
|
190
|
+
branch_name = request.json.get("name")
|
|
191
|
+
if not branch_name:
|
|
192
|
+
raise InvalidUsage("Missing 'name' in request body")
|
|
193
|
+
|
|
194
|
+
start_point = request.json.get("start_point")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
branch_data = git_utils.create_branch(repo_path, branch_name, start_point)
|
|
198
|
+
return response.json(branch_data, status=201)
|
|
199
|
+
|
|
200
|
+
except ValueError as e:
|
|
201
|
+
if "already exists" in str(e):
|
|
202
|
+
raise Conflict(str(e))
|
|
203
|
+
else:
|
|
204
|
+
raise InvalidUsage(str(e))
|
|
205
|
+
except git_utils.GitPermissionError as e:
|
|
206
|
+
raise ServerError(str(e))
|
|
207
|
+
except Exception as e:
|
|
208
|
+
raise ServerError(f"Failed to create branch: {str(e)}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@repository_bp.route("/branches/checkout", methods=["PUT"])
|
|
212
|
+
async def checkout_branch(request):
|
|
213
|
+
"""
|
|
214
|
+
Switch to a different branch.
|
|
215
|
+
|
|
216
|
+
Request body:
|
|
217
|
+
{
|
|
218
|
+
"name": str (required)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
JSON with new branch details and repository status
|
|
223
|
+
"""
|
|
224
|
+
repo_path = request.ctx.repo_path
|
|
225
|
+
|
|
226
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
227
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
228
|
+
|
|
229
|
+
# Validate request body
|
|
230
|
+
if not request.json:
|
|
231
|
+
raise InvalidUsage("Request body required")
|
|
232
|
+
|
|
233
|
+
branch_name = request.json.get("name")
|
|
234
|
+
if not branch_name:
|
|
235
|
+
raise InvalidUsage("Missing 'name' in request body")
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
status_data = git_utils.switch_branch(repo_path, branch_name)
|
|
239
|
+
return response.json(status_data)
|
|
240
|
+
|
|
241
|
+
except ValueError as e:
|
|
242
|
+
raise NotFound(str(e))
|
|
243
|
+
except git_utils.GitPermissionError as e:
|
|
244
|
+
if "Uncommitted changes" in str(e):
|
|
245
|
+
raise Conflict(str(e))
|
|
246
|
+
else:
|
|
247
|
+
raise ServerError(str(e))
|
|
248
|
+
except Exception as e:
|
|
249
|
+
raise ServerError(f"Failed to switch branch: {str(e)}")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@repository_bp.route("/branches/<branch_name:path>", methods=["DELETE"])
|
|
253
|
+
async def delete_branch(request, branch_name):
|
|
254
|
+
"""
|
|
255
|
+
Delete a branch.
|
|
256
|
+
|
|
257
|
+
Path params:
|
|
258
|
+
branch_name: Name of branch to delete
|
|
259
|
+
|
|
260
|
+
Query params:
|
|
261
|
+
force: "true" to delete even if unmerged (optional, default: false)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
HTTP 204 No Content on success
|
|
265
|
+
"""
|
|
266
|
+
repo_path = request.ctx.repo_path
|
|
267
|
+
|
|
268
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
269
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
270
|
+
|
|
271
|
+
force = request.args.get("force", "false").lower() == "true"
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
git_utils.delete_branch(repo_path, branch_name, force)
|
|
275
|
+
return response.empty(status=204)
|
|
276
|
+
|
|
277
|
+
except ValueError as e:
|
|
278
|
+
if "Cannot delete current branch" in str(e):
|
|
279
|
+
raise InvalidUsage(str(e))
|
|
280
|
+
elif "not found" in str(e).lower():
|
|
281
|
+
raise NotFound(str(e))
|
|
282
|
+
elif "unmerged commits" in str(e):
|
|
283
|
+
raise Conflict(str(e))
|
|
284
|
+
else:
|
|
285
|
+
raise InvalidUsage(str(e))
|
|
286
|
+
except git_utils.GitPermissionError as e:
|
|
287
|
+
raise ServerError(str(e))
|
|
288
|
+
except Exception as e:
|
|
289
|
+
raise ServerError(f"Failed to delete branch: {str(e)}")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@repository_bp.route("/branches/merge", methods=["POST"])
|
|
293
|
+
async def merge_branch(request):
|
|
294
|
+
"""
|
|
295
|
+
Merge a branch into the current branch.
|
|
296
|
+
|
|
297
|
+
Request body:
|
|
298
|
+
{
|
|
299
|
+
"source": str (required),
|
|
300
|
+
"message": str (optional)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
JSON with merge details
|
|
305
|
+
"""
|
|
306
|
+
repo_path = request.ctx.repo_path
|
|
307
|
+
|
|
308
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
309
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
310
|
+
|
|
311
|
+
# Validate request body
|
|
312
|
+
if not request.json:
|
|
313
|
+
raise InvalidUsage("Request body required")
|
|
314
|
+
|
|
315
|
+
source_branch = request.json.get("source")
|
|
316
|
+
if not source_branch:
|
|
317
|
+
raise InvalidUsage("Missing 'source' in request body")
|
|
318
|
+
|
|
319
|
+
merge_message = request.json.get("message")
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
merge_data = git_utils.merge_branch(repo_path, source_branch, merge_message)
|
|
323
|
+
return response.json(merge_data)
|
|
324
|
+
|
|
325
|
+
except ValueError as e:
|
|
326
|
+
raise NotFound(str(e))
|
|
327
|
+
except git_utils.GitConflictError as e:
|
|
328
|
+
raise Conflict(str(e))
|
|
329
|
+
except git_utils.GitPermissionError as e:
|
|
330
|
+
raise ServerError(str(e))
|
|
331
|
+
except Exception as e:
|
|
332
|
+
raise ServerError(f"Failed to merge branch: {str(e)}")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@repository_bp.route("/file/content", methods=["GET"])
|
|
336
|
+
async def get_file_content(request):
|
|
337
|
+
"""
|
|
338
|
+
Get file content for viewing in code editor.
|
|
339
|
+
|
|
340
|
+
Query params:
|
|
341
|
+
file_path: Path to file relative to repository root (required)
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
JSON with file content and metadata:
|
|
345
|
+
{
|
|
346
|
+
'file_path': str,
|
|
347
|
+
'content': str (if text file),
|
|
348
|
+
'encoding': str,
|
|
349
|
+
'size': int,
|
|
350
|
+
'mime_type': str,
|
|
351
|
+
'is_binary': bool,
|
|
352
|
+
'message': str (if binary or too large)
|
|
353
|
+
}
|
|
354
|
+
"""
|
|
355
|
+
repo_path = request.ctx.repo_path
|
|
356
|
+
file_path = request.args.get("file_path")
|
|
357
|
+
|
|
358
|
+
if not file_path:
|
|
359
|
+
raise InvalidUsage("Missing 'file_path' parameter")
|
|
360
|
+
|
|
361
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
362
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
file_data = git_utils.read_file_content(repo_path, file_path)
|
|
366
|
+
return response.json(file_data)
|
|
367
|
+
|
|
368
|
+
except ValueError as e:
|
|
369
|
+
# Invalid path or path traversal
|
|
370
|
+
if "traversal" in str(e).lower():
|
|
371
|
+
raise InvalidUsage(str(e))
|
|
372
|
+
elif "outside repository" in str(e):
|
|
373
|
+
raise ServerError(str(e), status_code=403)
|
|
374
|
+
else:
|
|
375
|
+
raise InvalidUsage(str(e))
|
|
376
|
+
except FileNotFoundError as e:
|
|
377
|
+
raise NotFound(str(e))
|
|
378
|
+
except git_utils.GitPermissionError as e:
|
|
379
|
+
raise ServerError(str(e), status_code=500)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
raise ServerError(f"Failed to read file content: {str(e)}")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@repository_bp.route("/file/content", methods=["PUT"])
|
|
385
|
+
async def save_file_content(request):
|
|
386
|
+
"""
|
|
387
|
+
Save file content from code editor.
|
|
388
|
+
|
|
389
|
+
Query params:
|
|
390
|
+
file_path: Path to file relative to repository root (required)
|
|
391
|
+
|
|
392
|
+
JSON body:
|
|
393
|
+
{
|
|
394
|
+
'content': str (file content to save),
|
|
395
|
+
'encoding': str (optional, default: utf-8)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
JSON with save confirmation:
|
|
400
|
+
{
|
|
401
|
+
'file_path': str,
|
|
402
|
+
'size': int,
|
|
403
|
+
'message': str
|
|
404
|
+
}
|
|
405
|
+
"""
|
|
406
|
+
repo_path = request.ctx.repo_path
|
|
407
|
+
file_path = request.args.get("file_path")
|
|
408
|
+
|
|
409
|
+
if not file_path:
|
|
410
|
+
raise InvalidUsage("Missing 'file_path' parameter")
|
|
411
|
+
|
|
412
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
413
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
414
|
+
|
|
415
|
+
# Get request body
|
|
416
|
+
try:
|
|
417
|
+
body = request.json
|
|
418
|
+
if not body:
|
|
419
|
+
raise InvalidUsage("Missing request body")
|
|
420
|
+
|
|
421
|
+
content = body.get('content')
|
|
422
|
+
if content is None:
|
|
423
|
+
raise InvalidUsage("Missing 'content' in request body")
|
|
424
|
+
|
|
425
|
+
encoding = body.get('encoding', 'utf-8')
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
raise InvalidUsage(f"Invalid request body: {str(e)}")
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
result = git_utils.write_file_content(repo_path, file_path, content, encoding)
|
|
432
|
+
return response.json(result)
|
|
433
|
+
|
|
434
|
+
except ValueError as e:
|
|
435
|
+
# Invalid path or path traversal
|
|
436
|
+
if "traversal" in str(e).lower():
|
|
437
|
+
raise InvalidUsage(str(e))
|
|
438
|
+
elif "outside repository" in str(e):
|
|
439
|
+
raise ServerError(str(e), status_code=403)
|
|
440
|
+
else:
|
|
441
|
+
raise InvalidUsage(str(e))
|
|
442
|
+
except git_utils.GitPermissionError as e:
|
|
443
|
+
raise ServerError(str(e), status_code=500)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
raise ServerError(f"Failed to save file content: {str(e)}")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@repository_bp.route("/stage", methods=["POST"])
|
|
449
|
+
async def stage_files(request):
|
|
450
|
+
"""
|
|
451
|
+
Stage files for commit.
|
|
452
|
+
|
|
453
|
+
Request body:
|
|
454
|
+
{
|
|
455
|
+
"file_path": str (optional, for single file),
|
|
456
|
+
"stage_all": bool (optional, to stage all changed files)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
JSON with staging result
|
|
461
|
+
"""
|
|
462
|
+
repo_path = request.ctx.repo_path
|
|
463
|
+
|
|
464
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
465
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
466
|
+
|
|
467
|
+
# Validate request body
|
|
468
|
+
if not request.json:
|
|
469
|
+
raise InvalidUsage("Request body required")
|
|
470
|
+
|
|
471
|
+
file_path = request.json.get("file_path")
|
|
472
|
+
stage_all = request.json.get("stage_all", False)
|
|
473
|
+
|
|
474
|
+
if not file_path and not stage_all:
|
|
475
|
+
raise InvalidUsage("Missing 'file_path' or 'stage_all' in request body")
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
if stage_all:
|
|
479
|
+
result = git_utils.stage_all_files(repo_path)
|
|
480
|
+
else:
|
|
481
|
+
result = git_utils.stage_file(repo_path, file_path)
|
|
482
|
+
|
|
483
|
+
# Clear cache to ensure fresh data on next request
|
|
484
|
+
git_utils.clear_cache()
|
|
485
|
+
|
|
486
|
+
return response.json(result)
|
|
487
|
+
|
|
488
|
+
except FileNotFoundError as e:
|
|
489
|
+
raise NotFound(str(e))
|
|
490
|
+
except ValueError as e:
|
|
491
|
+
raise InvalidUsage(str(e))
|
|
492
|
+
except git_utils.GitPermissionError as e:
|
|
493
|
+
raise ServerError(str(e))
|
|
494
|
+
except Exception as e:
|
|
495
|
+
raise ServerError(f"Failed to stage files: {str(e)}")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@repository_bp.route("/unstage", methods=["POST"])
|
|
499
|
+
async def unstage_files(request):
|
|
500
|
+
"""
|
|
501
|
+
Remove files from staging area.
|
|
502
|
+
|
|
503
|
+
Request body:
|
|
504
|
+
{
|
|
505
|
+
"file_path": str (required)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
JSON with unstaging result
|
|
510
|
+
"""
|
|
511
|
+
repo_path = request.ctx.repo_path
|
|
512
|
+
|
|
513
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
514
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
515
|
+
|
|
516
|
+
# Validate request body
|
|
517
|
+
if not request.json:
|
|
518
|
+
raise InvalidUsage("Request body required")
|
|
519
|
+
|
|
520
|
+
file_path = request.json.get("file_path")
|
|
521
|
+
if not file_path:
|
|
522
|
+
raise InvalidUsage("Missing 'file_path' in request body")
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
result = git_utils.unstage_file(repo_path, file_path)
|
|
526
|
+
|
|
527
|
+
# Clear cache to ensure fresh data on next request
|
|
528
|
+
git_utils.clear_cache()
|
|
529
|
+
|
|
530
|
+
return response.json(result)
|
|
531
|
+
|
|
532
|
+
except ValueError as e:
|
|
533
|
+
raise InvalidUsage(str(e))
|
|
534
|
+
except git_utils.GitPermissionError as e:
|
|
535
|
+
raise ServerError(str(e))
|
|
536
|
+
except Exception as e:
|
|
537
|
+
raise ServerError(f"Failed to unstage file: {str(e)}")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@repository_bp.route("/commit", methods=["POST"])
|
|
541
|
+
async def create_commit(request):
|
|
542
|
+
"""
|
|
543
|
+
Create a commit with staged changes.
|
|
544
|
+
|
|
545
|
+
Request body:
|
|
546
|
+
{
|
|
547
|
+
"message": str (required),
|
|
548
|
+
"author_name": str (optional),
|
|
549
|
+
"author_email": str (optional)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
JSON with commit details (HTTP 201)
|
|
554
|
+
"""
|
|
555
|
+
repo_path = request.ctx.repo_path
|
|
556
|
+
|
|
557
|
+
if not git_utils.is_valid_repository(repo_path):
|
|
558
|
+
raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
|
|
559
|
+
|
|
560
|
+
# Validate request body
|
|
561
|
+
if not request.json:
|
|
562
|
+
raise InvalidUsage("Request body required")
|
|
563
|
+
|
|
564
|
+
message = request.json.get("message")
|
|
565
|
+
if not message:
|
|
566
|
+
raise InvalidUsage("Missing 'message' in request body")
|
|
567
|
+
|
|
568
|
+
author_name = request.json.get("author_name")
|
|
569
|
+
author_email = request.json.get("author_email")
|
|
570
|
+
|
|
571
|
+
try:
|
|
572
|
+
result = git_utils.create_commit(repo_path, message, author_name, author_email)
|
|
573
|
+
|
|
574
|
+
# Clear cache to ensure fresh data on next request
|
|
575
|
+
git_utils.clear_cache()
|
|
576
|
+
|
|
577
|
+
return response.json(result, status=201)
|
|
578
|
+
|
|
579
|
+
except ValueError as e:
|
|
580
|
+
# Empty message, too long, no staged changes, invalid email
|
|
581
|
+
raise InvalidUsage(str(e))
|
|
582
|
+
except git_utils.GitPermissionError as e:
|
|
583
|
+
if "locked" in str(e).lower():
|
|
584
|
+
raise Conflict(str(e))
|
|
585
|
+
else:
|
|
586
|
+
raise ServerError(str(e))
|
|
587
|
+
except git_utils.GitCommandError as e:
|
|
588
|
+
# Commit hook failure
|
|
589
|
+
raise ServerError(str(e))
|
|
590
|
+
except Exception as e:
|
|
591
|
+
raise ServerError(f"Failed to create commit: {str(e)}")
|
|
592
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Client capability handlers."""
|