Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Tag naming: `0.y.z` (no `v` prefix). Align `cmd/version.go` and `config/agent.ex

## [Unreleased]

### Changed

- `gitlab.create_branch`, `gitlab.commit_files`, and `gitlab.create_mr` honor `project_path` in the skill payload (fallback to agent config when omitted).
- `gitlab.commit_files`: when an action includes `append_content`, the agent loads the file at `file_path` on the target branch, appends the block, and commits the merged body (avoids replacing the file with RAG-assembled snapshots).

## [0.1.0] - 2026-05-26

### Added
Expand Down
9 changes: 6 additions & 3 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ func (a *Agent) execute(skill string, payload map[string]interface{}, _ map[stri

switch skill {
case "gitlab.create_branch":
projectPath := stringFromPayload(payload, "project_path", a.cfg.GitLab.ProjectPath)
branch, _ := payload["branch"].(string)
ref, _ := payload["ref"].(string)
if ref == "" {
ref = a.cfg.GitLab.TargetBranch
}
if err := a.gitlab.CreateBranch(a.cfg.GitLab.ProjectPath, branch, ref); err != nil {
if err := a.gitlab.CreateBranch(projectPath, branch, ref); err != nil {
return nil, err
}
return skillresult.Success(map[string]interface{}{"branch": branch}), nil
Expand All @@ -138,7 +139,8 @@ func (a *Agent) execute(skill string, payload map[string]interface{}, _ map[stri
normalized = append(normalized, m)
}
}
if err := a.gitlab.CommitFiles(a.cfg.GitLab.ProjectPath, branch, message, normalized); err != nil {
projectPath := stringFromPayload(payload, "project_path", a.cfg.GitLab.ProjectPath)
if err := a.gitlab.CommitFiles(projectPath, branch, message, normalized); err != nil {
return nil, err
}
return skillresult.Success(map[string]interface{}{}), nil
Expand All @@ -151,7 +153,8 @@ func (a *Agent) execute(skill string, payload map[string]interface{}, _ map[stri
}
title, _ := payload["title"].(string)
description, _ := payload["description"].(string)
url, err := a.gitlab.CreateMergeRequest(a.cfg.GitLab.ProjectPath, source, target, title, description)
projectPath := stringFromPayload(payload, "project_path", a.cfg.GitLab.ProjectPath)
url, err := a.gitlab.CreateMergeRequest(projectPath, source, target, title, description)
if err != nil {
return nil, err
}
Expand Down
72 changes: 70 additions & 2 deletions internal/gitlab/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gitlab

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand All @@ -26,7 +27,14 @@ type APIError struct {
}

func (e *APIError) Error() string {
return fmt.Sprintf("gitlab API error status=%d", e.StatusCode)
body := strings.TrimSpace(e.Body)
if body == "" {
return fmt.Sprintf("gitlab API error status=%d", e.StatusCode)
}
if len(body) > 500 {
body = body[:500] + "..."
}
return fmt.Sprintf("gitlab API error status=%d: %s", e.StatusCode, body)
}

func New(baseURL, token, apiVersion string) *Client {
Expand Down Expand Up @@ -72,15 +80,75 @@ func (c *Client) CreateBranch(projectPath, branch, ref string) error {
return c.post(fmt.Sprintf("/projects/%s/repository/branches", c.projectID(projectPath)), body, nil)
}

// GetRepositoryFile returns the decoded text of a file at ref (branch, tag, or commit SHA).
func (c *Client) GetRepositoryFile(projectPath, filePath, ref string) (string, error) {
resp := map[string]interface{}{}
path := fmt.Sprintf(
"/projects/%s/repository/files/%s?ref=%s",
c.projectID(projectPath),
url.PathEscape(strings.TrimPrefix(filePath, "/")),
url.QueryEscape(ref),
)
if err := c.get(path, &resp); err != nil {
return "", err
}
encoding, _ := resp["encoding"].(string)
content, _ := resp["content"].(string)
if content == "" {
return "", fmt.Errorf("repository file %q at ref %q is empty", filePath, ref)
}
if encoding == "base64" {
raw, err := base64.StdEncoding.DecodeString(content)
if err != nil {
return "", fmt.Errorf("decode repository file %q: %w", filePath, err)
}
return string(raw), nil
}
return content, nil
}

func (c *Client) CommitFiles(projectPath, branch, message string, actions []map[string]interface{}) error {
resolved := make([]map[string]interface{}, 0, len(actions))
for _, action := range actions {
merged, err := c.resolveCommitAction(projectPath, branch, action)
if err != nil {
return err
}
resolved = append(resolved, merged)
}
body := map[string]interface{}{
"branch": branch,
"commit_message": message,
"actions": actions,
"actions": resolved,
}
return c.post(fmt.Sprintf("/projects/%s/repository/commits", c.projectID(projectPath)), body, nil)
}

func (c *Client) resolveCommitAction(projectPath, branch string, action map[string]interface{}) (map[string]interface{}, error) {
out := make(map[string]interface{}, len(action))
for k, v := range action {
out[k] = v
}
appendText, _ := out["append_content"].(string)
appendText = strings.TrimSpace(appendText)
if appendText == "" {
delete(out, "append_content")
return out, nil
}
act, _ := out["action"].(string)
filePath, _ := out["file_path"].(string)
if act != "update" || strings.TrimSpace(filePath) == "" {
return nil, fmt.Errorf("append_content requires action update and file_path")
}
existing, err := c.GetRepositoryFile(projectPath, filePath, branch)
if err != nil {
return nil, err
}
out["content"] = strings.TrimRight(existing, "\n") + "\n\n" + appendText + "\n"
delete(out, "append_content")
return out, nil
}

func (c *Client) CreateMergeRequest(projectPath, source, target, title, description string) (string, error) {
resp := map[string]interface{}{}
body := map[string]interface{}{
Expand Down
Loading