|
|
|
@@ -0,0 +1,192 @@
|
|
|
|
|
package action
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
|
|
"github.com/rockliang/poimen/workflows/internal/lock"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CloneRepoInput is input to CloneRepoActivity.
|
|
|
|
|
type CloneRepoInput struct {
|
|
|
|
|
RemoteURL string
|
|
|
|
|
TargetRepoPath string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CloneRepoActivity clones a repo if it doesn't exist, or fetches if it does.
|
|
|
|
|
func CloneRepoActivity(ctx context.Context, in CloneRepoInput) error {
|
|
|
|
|
// Check if repo already exists
|
|
|
|
|
gitDir := filepath.Join(in.TargetRepoPath, ".git")
|
|
|
|
|
if _, err := os.Stat(gitDir); err == nil {
|
|
|
|
|
// Repo exists, fetch latest
|
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "-C", in.TargetRepoPath, "fetch", "origin")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git fetch failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Repo doesn't exist, clone it
|
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "clone", in.RemoteURL, in.TargetRepoPath)
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git clone failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitWorktreeAddInput is input to GitWorktreeAddActivity.
|
|
|
|
|
type GitWorktreeAddInput struct {
|
|
|
|
|
RepoPath string
|
|
|
|
|
TaskID string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitWorktreeAddActivity creates a new git worktree for a task.
|
|
|
|
|
func GitWorktreeAddActivity(ctx context.Context, in GitWorktreeAddInput) (string, error) {
|
|
|
|
|
// Acquire lock to synchronize worktree creation
|
|
|
|
|
lockPath := filepath.Join(in.RepoPath, "orchestrator.lock")
|
|
|
|
|
if err := lock.Acquire(lockPath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to acquire lock: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer lock.Release(lockPath)
|
|
|
|
|
|
|
|
|
|
// Create worktrees directory if it doesn't exist
|
|
|
|
|
worktreesDir := filepath.Join(in.RepoPath, "worktrees")
|
|
|
|
|
if err := os.MkdirAll(worktreesDir, 0755); err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to create worktrees dir: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
worktreePath := filepath.Join(worktreesDir, in.TaskID)
|
|
|
|
|
branch := "task/" + in.TaskID
|
|
|
|
|
|
|
|
|
|
// Create worktree
|
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "-C", in.RepoPath, "worktree", "add", "-b", branch, worktreePath, "origin/main")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return "", fmt.Errorf("git worktree add failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return worktreePath, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitCommitInput is input to GitCommitActivity.
|
|
|
|
|
type GitCommitInput struct {
|
|
|
|
|
WorktreePath string
|
|
|
|
|
Message string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitCommitActivity commits changes in a worktree.
|
|
|
|
|
func GitCommitActivity(ctx context.Context, in GitCommitInput) error {
|
|
|
|
|
// Stage all changes
|
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "-C", in.WorktreePath, "add", "-A")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git add failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Commit
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.WorktreePath, "commit", "-m", in.Message)
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git commit failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitPushInput is input to GitPushActivity.
|
|
|
|
|
type GitPushInput struct {
|
|
|
|
|
RepoPath string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitPushActivity pushes changes to origin.
|
|
|
|
|
func GitPushActivity(ctx context.Context, in GitPushInput) error {
|
|
|
|
|
// Acquire lock to synchronize push
|
|
|
|
|
lockPath := filepath.Join(in.RepoPath, "orchestrator.lock")
|
|
|
|
|
if err := lock.Acquire(lockPath); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to acquire lock: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer lock.Release(lockPath)
|
|
|
|
|
|
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "-C", in.RepoPath, "push", "origin", "main")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git push failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitSquashMergeInput is input to GitSquashMergeActivity.
|
|
|
|
|
type GitSquashMergeInput struct {
|
|
|
|
|
RepoPath string
|
|
|
|
|
Branches []string
|
|
|
|
|
Message string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GitSquashMergeActivity performs a squash merge of multiple branches into main.
|
|
|
|
|
func GitSquashMergeActivity(ctx context.Context, in GitSquashMergeInput) error {
|
|
|
|
|
// Acquire lock to synchronize merge
|
|
|
|
|
lockPath := filepath.Join(in.RepoPath, "orchestrator.lock")
|
|
|
|
|
if err := lock.Acquire(lockPath); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to acquire lock: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer lock.Release(lockPath)
|
|
|
|
|
|
|
|
|
|
// Fetch origin main
|
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "-C", in.RepoPath, "fetch", "origin", "main")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git fetch failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checkout main and pull with ff-only
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "checkout", "main")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git checkout main failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "pull", "--ff-only", "origin", "main")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git pull failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Squash merge each branch
|
|
|
|
|
for _, branch := range in.Branches {
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "merge", "--squash", branch)
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git merge --squash %s failed: %w", branch, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Commit squashed changes
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "commit", "-m", in.Message)
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git commit failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Push to origin
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "push", "origin", "main")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("git push failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean up worktrees and branches
|
|
|
|
|
for _, branch := range in.Branches {
|
|
|
|
|
taskID := branch[len("task/"):]
|
|
|
|
|
worktreePath := filepath.Join(in.RepoPath, "worktrees", taskID)
|
|
|
|
|
|
|
|
|
|
// Remove worktree
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "worktree", "remove", worktreePath, "--force")
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
// Log error but continue cleanup
|
|
|
|
|
fmt.Printf("warning: failed to remove worktree %s: %v\n", worktreePath, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete branch
|
|
|
|
|
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "branch", "-D", branch)
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
// Log error but continue cleanup
|
|
|
|
|
fmt.Printf("warning: failed to delete branch %s: %v\n", branch, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|