Skip to content

Commit 1bc2f1a

Browse files
committed
feat: initialize source code
0 parents  commit 1bc2f1a

28 files changed

Lines changed: 5148 additions & 0 deletions

.gitignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
node_modules
2+
.DS_Store
3+
dist
4+
dist-ssr
5+
*.local
6+
*.tsbuildinfo
7+
.env
8+
.env.production
9+
.nitro
10+
.tanstack
11+
.wrangler
12+
.output
13+
.vinxi
14+
__unconfig*
15+
todos.json
16+
17+
# air (Go hot-reload) build artifacts
18+
services/api/tmp/
19+
20+
# Playwright
21+
test-results
22+
playwright-report
23+
blob-report
24+
playwright
25+
26+
# VS Code settings
27+
.vscode/
28+
29+
# Kilo settings
30+
.kilo/
31+
32+
# WebAssembly build artifacts
33+
*.wasm

backend/branches.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
plugin "github.com/Paca-AI/plugin-sdk-go"
9+
"github.com/google/uuid"
10+
)
11+
12+
// ─── DTOs ────────────────────────────────────────────────────────────────────
13+
14+
type taskBranchResponse struct {
15+
ID string `json:"id"`
16+
TaskID string `json:"task_id"`
17+
RepoID string `json:"repo_id"`
18+
BranchName string `json:"branch_name"`
19+
CreatedAt string `json:"created_at"`
20+
}
21+
22+
type createBranchResponse struct {
23+
BranchName string `json:"branch_name"`
24+
}
25+
26+
// ─── POST /tasks/:taskId/github/branches ──────────────────────────────────────
27+
28+
func (p *githubPlugin) createBranch(req *plugin.Request, res *plugin.Response) {
29+
projectID := req.Caller.ProjectID
30+
taskID := req.PathParam("taskId")
31+
32+
type bodyT struct {
33+
RepoID string `json:"repo_id"`
34+
BranchName string `json:"branch_name"`
35+
SourceBranch string `json:"source_branch"`
36+
}
37+
b, err := plugin.JSONBody[bodyT](req)
38+
if err != nil || b.RepoID == "" || b.BranchName == "" {
39+
apiError(res, 400, "BAD_REQUEST", "repo_id and branch_name are required")
40+
return
41+
}
42+
43+
token, err := p.decryptToken(projectID)
44+
if err != nil {
45+
writeAppError(res, err)
46+
return
47+
}
48+
49+
// Get repository details.
50+
repoResult, rErr := p.db.Query(
51+
`SELECT owner, repo_name, default_branch FROM github_repositories WHERE id = $1 AND project_id = $2`,
52+
b.RepoID, projectID,
53+
)
54+
if rErr != nil {
55+
apiError(res, 500, "INTERNAL_ERROR", rErr.Error())
56+
return
57+
}
58+
if len(repoResult.Rows) == 0 {
59+
apiError(res, 404, "GITHUB_REPOSITORY_NOT_FOUND", "Repository not found")
60+
return
61+
}
62+
rSc := newRowScanner(repoResult.Columns, repoResult.Rows[0])
63+
owner := rSc.str("owner")
64+
repoName := rSc.str("repo_name")
65+
defaultBranch := rSc.str("default_branch")
66+
67+
sourceBranch := b.SourceBranch
68+
if sourceBranch == "" {
69+
sourceBranch = defaultBranch
70+
}
71+
72+
ghc := newGHClient(token)
73+
if err := ghc.createBranch(context.Background(), owner, repoName, b.BranchName, sourceBranch); err != nil {
74+
var apiErr *ghAPIError
75+
if errors.As(err, &apiErr) {
76+
if apiErr.StatusCode == 403 {
77+
apiError(res, 403, "GITHUB_TOKEN_INSUFFICIENT_PERMISSIONS", "Token does not have permission to create branches")
78+
return
79+
}
80+
apiError(res, 400, "BAD_REQUEST", fmt.Sprintf("GitHub API error: %s", apiErr.Message))
81+
return
82+
}
83+
apiError(res, 502, "INTERNAL_ERROR", fmt.Sprintf("failed to create branch: %s", err))
84+
return
85+
}
86+
87+
// Link the branch to the task.
88+
now := nowStr()
89+
branchID := uuid.New().String()
90+
rowsAffected, dbErr := p.db.Exec(`
91+
INSERT INTO github_task_branches (id, task_id, repo_id, branch_name, created_at)
92+
VALUES ($1,$2,$3,$4,$5)
93+
ON CONFLICT (task_id, repo_id, branch_name) DO NOTHING
94+
`, branchID, taskID, b.RepoID, b.BranchName, now)
95+
if dbErr != nil {
96+
p.log.Error("failed to link branch to task: " + dbErr.Error())
97+
} else if rowsAffected == 0 {
98+
apiError(res, 409, "GITHUB_BRANCH_ALREADY_LINKED", "Branch is already linked to this task")
99+
return
100+
}
101+
102+
plugin.EmitEvent("github.branch_linked", map[string]any{
103+
"project_id": projectID,
104+
"task_id": taskID,
105+
"repo_id": b.RepoID,
106+
"branch_name": b.BranchName,
107+
})
108+
109+
created(res, createBranchResponse{BranchName: b.BranchName})
110+
}
111+
112+
// ─── GET /tasks/:taskId/github/branches ───────────────────────────────────────
113+
114+
func (p *githubPlugin) listTaskBranches(req *plugin.Request, res *plugin.Response) {
115+
taskID := req.PathParam("taskId")
116+
117+
result, err := p.db.Query(`
118+
SELECT id, task_id, repo_id, branch_name, created_at
119+
FROM github_task_branches WHERE task_id = $1 ORDER BY created_at ASC
120+
`, taskID)
121+
if err != nil {
122+
apiError(res, 500, "INTERNAL_ERROR", err.Error())
123+
return
124+
}
125+
126+
items := make([]taskBranchResponse, 0, len(result.Rows))
127+
for _, row := range result.Rows {
128+
sc := newRowScanner(result.Columns, row)
129+
items = append(items, taskBranchResponse{
130+
ID: sc.str("id"),
131+
TaskID: sc.str("task_id"),
132+
RepoID: sc.str("repo_id"),
133+
BranchName: sc.str("branch_name"),
134+
CreatedAt: sc.str("created_at"),
135+
})
136+
}
137+
ok(res, items)
138+
}

0 commit comments

Comments
 (0)