Skip to content
Open
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
174 changes: 38 additions & 136 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -1365,160 +1365,62 @@ func PushFiles(t translations.TranslationHelperFunc) inventory.ServerTool {
return utils.NewToolResultError(err.Error()), nil, nil
}

// Parse files parameter - this should be an array of objects with path and content
filesObj, ok := args["files"].([]any)
if !ok {
return utils.NewToolResultError("files parameter must be an array of objects with path and content"), nil, nil
// Parse files parameter - accept several encodings from different MCP hosts.
fileEntries, err := parsePushFilesEntries(args["files"])
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

client, err := deps.GetClient(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

// Get the reference for the branch
var repositoryIsEmpty bool
var branchNotFound bool
ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch)
refName, err := ensurePushFilesBranchRef(ctx, client, owner, repo, branch)
if err != nil {
ghErr, isGhErr := err.(*github.ErrorResponse)
if isGhErr {
if ghErr.Response.StatusCode == http.StatusConflict && ghErr.Message == "Git Repository is empty." {
repositoryIsEmpty = true
} else if ghErr.Response.StatusCode == http.StatusNotFound {
branchNotFound = true
}
}

if !repositoryIsEmpty && !branchNotFound {
errMsg := err.Error()
switch {
case strings.Contains(errMsg, "initialize repository"):
return utils.NewToolResultError(fmt.Sprintf("failed to initialize repository: %v", err)), nil, nil
case strings.Contains(errMsg, "create branch from default"):
return utils.NewToolResultError(fmt.Sprintf("failed to create branch from default: %v", err)), nil, nil
default:
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get branch reference",
resp,
err,
), nil, nil
}
}
// Only close resp if it's not nil and not an error case where resp might be nil
if resp != nil && resp.Body != nil {
defer func() { _ = resp.Body.Close() }()
}

var baseCommit *github.Commit
if !repositoryIsEmpty {
if branchNotFound {
ref, err = createReferenceFromDefaultBranch(ctx, client, owner, repo, branch)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to create branch from default: %v", err)), nil, nil
}
}

// Get the commit object that the branch points to
baseCommit, resp, err = client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get base commit",
resp,
nil,
err,
), nil, nil
}
if resp != nil && resp.Body != nil {
defer func() { _ = resp.Body.Close() }()
}
} else {
var base *github.Commit
// Repository is empty, need to initialize it first
ref, base, err = initializeRepository(ctx, client, owner, repo)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to initialize repository: %v", err)), nil, nil
}

defaultBranch := strings.TrimPrefix(*ref.Ref, "refs/heads/")
if branch != defaultBranch {
// Create the requested branch from the default branch
ref, err = createReferenceFromDefaultBranch(ctx, client, owner, repo, branch)
if err != nil {
return utils.NewToolResultError(fmt.Sprintf("failed to create branch from default: %v", err)), nil, nil
}
}

baseCommit = base
}

// Create tree entries for all files (or remaining files if empty repo)
var entries []*github.TreeEntry

for _, file := range filesObj {
fileMap, ok := file.(map[string]any)
if !ok {
return utils.NewToolResultError("each file must be an object with path and content"), nil, nil
}

path, ok := fileMap["path"].(string)
if !ok || path == "" {
return utils.NewToolResultError("each file must have a path"), nil, nil
}

content, ok := fileMap["content"].(string)
if !ok {
return utils.NewToolResultError("each file must have content"), nil, nil
}

// Create a tree entry for the file
entries = append(entries, &github.TreeEntry{
Path: github.Ptr(path),
Mode: github.Ptr("100644"), // Regular file mode
Type: github.Ptr("blob"),
Content: github.Ptr(content),
})
}

// Create a new tree with the file entries (baseCommit is now guaranteed to exist)
newTree, resp, err := client.Git.CreateTree(ctx, owner, repo, *baseCommit.Tree.SHA, entries)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to create tree",
resp,
err,
), nil, nil
}
if resp != nil && resp.Body != nil {
defer func() { _ = resp.Body.Close() }()
}

// Create a new commit (baseCommit always has a value now)
commit := github.Commit{
Message: github.Ptr(message),
Tree: newTree,
Parents: []*github.Commit{{SHA: baseCommit.SHA}},
}
newCommit, resp, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to create commit",
resp,
err,
), nil, nil
}
if resp != nil && resp.Body != nil {
defer func() { _ = resp.Body.Close() }()
}

// Update the reference to point to the new commit
ref.Object.SHA = newCommit.SHA
updatedRef, resp, err := client.Git.UpdateRef(ctx, owner, repo, *ref.Ref, github.UpdateRef{
SHA: *newCommit.SHA,
Force: github.Ptr(false),
})
pushResult, resp, err := commitEntriesToRef(
ctx,
client,
owner,
repo,
refName,
message,
pushFileEntriesToTreeEntries(fileEntries),
)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to update reference",
resp,
err,
), nil, nil
stage := "failed to push files"
errMsg := err.Error()
switch {
case strings.Contains(errMsg, "get base commit"):
stage = "failed to get base commit"
case strings.Contains(errMsg, "create tree"):
stage = "failed to create tree"
case strings.Contains(errMsg, "create commit"):
stage = "failed to create commit"
case strings.Contains(errMsg, "update reference"):
stage = "failed to update reference"
case strings.Contains(errMsg, "get branch reference"):
stage = "failed to get branch reference"
}
return ghErrors.NewGitHubAPIErrorResponse(ctx, stage, resp, err), nil, nil
}
defer func() { _ = resp.Body.Close() }()

r, err := json.Marshal(updatedRef)
r, err := json.Marshal(pushResult.Ref)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
}
Expand Down
Loading