feat: add gt crew add command for user-managed workspaces
Implements gt-cik.2: Create crew workspace command with: - internal/crew/types.go: CrewWorker type definition - internal/crew/manager.go: Manager for crew lifecycle - internal/crew/manager_test.go: Unit tests - internal/cmd/crew.go: CLI command with --rig and --branch flags Crew workers are user-managed persistent workspaces that: - Clone repo into <rig>/crew/<name>/ - Create optional feature branch (crew/<name>) - Set up mail directory for delivery - Initialize CLAUDE.md with crew worker prompting - Are NOT registered with witness (user-managed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
276
internal/crew/manager.go
Normal file
276
internal/crew/manager.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package crew
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrCrewExists = errors.New("crew worker already exists")
|
||||
ErrCrewNotFound = errors.New("crew worker not found")
|
||||
ErrHasChanges = errors.New("crew worker has uncommitted changes")
|
||||
)
|
||||
|
||||
// Manager handles crew worker lifecycle.
|
||||
type Manager struct {
|
||||
rig *rig.Rig
|
||||
git *git.Git
|
||||
}
|
||||
|
||||
// NewManager creates a new crew manager.
|
||||
func NewManager(r *rig.Rig, g *git.Git) *Manager {
|
||||
return &Manager{
|
||||
rig: r,
|
||||
git: g,
|
||||
}
|
||||
}
|
||||
|
||||
// crewDir returns the directory for a crew worker.
|
||||
func (m *Manager) crewDir(name string) string {
|
||||
return filepath.Join(m.rig.Path, "crew", name)
|
||||
}
|
||||
|
||||
// stateFile returns the state file path for a crew worker.
|
||||
func (m *Manager) stateFile(name string) string {
|
||||
return filepath.Join(m.crewDir(name), "state.json")
|
||||
}
|
||||
|
||||
// mailDir returns the mail directory path for a crew worker.
|
||||
func (m *Manager) mailDir(name string) string {
|
||||
return filepath.Join(m.crewDir(name), "mail")
|
||||
}
|
||||
|
||||
// exists checks if a crew worker exists.
|
||||
func (m *Manager) exists(name string) bool {
|
||||
_, err := os.Stat(m.crewDir(name))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Add creates a new crew worker with a clone of the rig.
|
||||
func (m *Manager) Add(name string, createBranch bool) (*CrewWorker, error) {
|
||||
if m.exists(name) {
|
||||
return nil, ErrCrewExists
|
||||
}
|
||||
|
||||
crewPath := m.crewDir(name)
|
||||
|
||||
// Create crew directory if needed
|
||||
crewBaseDir := filepath.Join(m.rig.Path, "crew")
|
||||
if err := os.MkdirAll(crewBaseDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("creating crew dir: %w", err)
|
||||
}
|
||||
|
||||
// Clone the rig repo
|
||||
if err := m.git.Clone(m.rig.GitURL, crewPath); err != nil {
|
||||
return nil, fmt.Errorf("cloning rig: %w", err)
|
||||
}
|
||||
|
||||
crewGit := git.NewGit(crewPath)
|
||||
branchName := "main"
|
||||
|
||||
// Optionally create a working branch
|
||||
if createBranch {
|
||||
branchName = fmt.Sprintf("crew/%s", name)
|
||||
if err := crewGit.CreateBranch(branchName); err != nil {
|
||||
os.RemoveAll(crewPath)
|
||||
return nil, fmt.Errorf("creating branch: %w", err)
|
||||
}
|
||||
if err := crewGit.Checkout(branchName); err != nil {
|
||||
os.RemoveAll(crewPath)
|
||||
return nil, fmt.Errorf("checking out branch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create mail directory for mail delivery
|
||||
mailPath := m.mailDir(name)
|
||||
if err := os.MkdirAll(mailPath, 0755); err != nil {
|
||||
os.RemoveAll(crewPath)
|
||||
return nil, fmt.Errorf("creating mail dir: %w", err)
|
||||
}
|
||||
|
||||
// Create CLAUDE.md with crew worker prompting
|
||||
if err := m.createClaudeMD(name, crewPath); err != nil {
|
||||
os.RemoveAll(crewPath)
|
||||
return nil, fmt.Errorf("creating CLAUDE.md: %w", err)
|
||||
}
|
||||
|
||||
// Create crew worker state
|
||||
now := time.Now()
|
||||
crew := &CrewWorker{
|
||||
Name: name,
|
||||
Rig: m.rig.Name,
|
||||
ClonePath: crewPath,
|
||||
Branch: branchName,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// Save state
|
||||
if err := m.saveState(crew); err != nil {
|
||||
os.RemoveAll(crewPath)
|
||||
return nil, fmt.Errorf("saving state: %w", err)
|
||||
}
|
||||
|
||||
return crew, nil
|
||||
}
|
||||
|
||||
// createClaudeMD creates the CLAUDE.md file for crew worker prompting.
|
||||
func (m *Manager) createClaudeMD(name, crewPath string) error {
|
||||
content := fmt.Sprintf(`# Claude: Crew Worker - %s
|
||||
|
||||
You are a **crew worker** in the %s rig. Crew workers are user-managed persistent workspaces.
|
||||
|
||||
## Key Differences from Polecats
|
||||
|
||||
- **User-managed**: You are NOT managed by the Witness daemon
|
||||
- **Persistent**: Your workspace is not automatically cleaned up
|
||||
- **Optional issue assignment**: You can work without a beads issue
|
||||
- **Mail enabled**: You can send and receive mail
|
||||
|
||||
## Commands
|
||||
|
||||
Check mail:
|
||||
`+"```bash"+`
|
||||
town mail inbox --as %s/%s
|
||||
`+"```"+`
|
||||
|
||||
Send mail:
|
||||
`+"```bash"+`
|
||||
town mail send <recipient> -s "Subject" -m "Message" --as %s/%s
|
||||
`+"```"+`
|
||||
|
||||
## Session Cycling (Handoff)
|
||||
|
||||
When your context fills up, use mail-to-self for handoff:
|
||||
|
||||
1. Compose a handoff note with current state
|
||||
2. Send to yourself: `+"```"+`town mail send %s/%s -s "Handoff" -m "..."--as %s/%s`+"```"+`
|
||||
3. Exit cleanly
|
||||
4. New session reads handoff from inbox
|
||||
|
||||
## Beads
|
||||
|
||||
If using beads for task tracking:
|
||||
`+"```bash"+`
|
||||
bd ready # Find available work
|
||||
bd show <id> # Review issue details
|
||||
bd update <id> --status=in_progress # Claim it
|
||||
bd close <id> # Mark complete
|
||||
`+"```"+`
|
||||
`, name, m.rig.Name,
|
||||
m.rig.Name, name,
|
||||
m.rig.Name, name,
|
||||
m.rig.Name, name, m.rig.Name, name)
|
||||
|
||||
claudePath := filepath.Join(crewPath, "CLAUDE.md")
|
||||
return os.WriteFile(claudePath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// Remove deletes a crew worker.
|
||||
func (m *Manager) Remove(name string, force bool) error {
|
||||
if !m.exists(name) {
|
||||
return ErrCrewNotFound
|
||||
}
|
||||
|
||||
crewPath := m.crewDir(name)
|
||||
|
||||
if !force {
|
||||
crewGit := git.NewGit(crewPath)
|
||||
hasChanges, err := crewGit.HasUncommittedChanges()
|
||||
if err == nil && hasChanges {
|
||||
return ErrHasChanges
|
||||
}
|
||||
}
|
||||
|
||||
// Remove directory
|
||||
if err := os.RemoveAll(crewPath); err != nil {
|
||||
return fmt.Errorf("removing crew dir: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all crew workers in the rig.
|
||||
func (m *Manager) List() ([]*CrewWorker, error) {
|
||||
crewBaseDir := filepath.Join(m.rig.Path, "crew")
|
||||
|
||||
entries, err := os.ReadDir(crewBaseDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading crew dir: %w", err)
|
||||
}
|
||||
|
||||
var workers []*CrewWorker
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
worker, err := m.Get(entry.Name())
|
||||
if err != nil {
|
||||
continue // Skip invalid workers
|
||||
}
|
||||
workers = append(workers, worker)
|
||||
}
|
||||
|
||||
return workers, nil
|
||||
}
|
||||
|
||||
// Get returns a specific crew worker by name.
|
||||
func (m *Manager) Get(name string) (*CrewWorker, error) {
|
||||
if !m.exists(name) {
|
||||
return nil, ErrCrewNotFound
|
||||
}
|
||||
|
||||
return m.loadState(name)
|
||||
}
|
||||
|
||||
// saveState persists crew worker state to disk.
|
||||
func (m *Manager) saveState(crew *CrewWorker) error {
|
||||
data, err := json.MarshalIndent(crew, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling state: %w", err)
|
||||
}
|
||||
|
||||
stateFile := m.stateFile(crew.Name)
|
||||
if err := os.WriteFile(stateFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadState reads crew worker state from disk.
|
||||
func (m *Manager) loadState(name string) (*CrewWorker, error) {
|
||||
stateFile := m.stateFile(name)
|
||||
|
||||
data, err := os.ReadFile(stateFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Return minimal crew worker if state file missing
|
||||
return &CrewWorker{
|
||||
Name: name,
|
||||
Rig: m.rig.Name,
|
||||
ClonePath: m.crewDir(name),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading state: %w", err)
|
||||
}
|
||||
|
||||
var crew CrewWorker
|
||||
if err := json.Unmarshal(data, &crew); err != nil {
|
||||
return nil, fmt.Errorf("parsing state: %w", err)
|
||||
}
|
||||
|
||||
return &crew, nil
|
||||
}
|
||||
290
internal/crew/manager_test.go
Normal file
290
internal/crew/manager_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package crew
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
)
|
||||
|
||||
func TestManagerAddAndGet(t *testing.T) {
|
||||
// Create temp directory for test
|
||||
tmpDir, err := os.MkdirTemp("", "crew-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a mock rig
|
||||
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||
t.Fatalf("failed to create rig dir: %v", err)
|
||||
}
|
||||
|
||||
// Initialize git repo for the rig
|
||||
g := git.NewGit(rigPath)
|
||||
|
||||
// For testing, we need a git URL - use a local bare repo
|
||||
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
|
||||
cmd := []string{"git", "init", "--bare", bareRepoPath}
|
||||
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
|
||||
t.Fatalf("failed to create bare repo: %v", err)
|
||||
}
|
||||
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig",
|
||||
Path: rigPath,
|
||||
GitURL: bareRepoPath,
|
||||
}
|
||||
|
||||
mgr := NewManager(r, g)
|
||||
|
||||
// Test Add
|
||||
worker, err := mgr.Add("dave", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Add failed: %v", err)
|
||||
}
|
||||
|
||||
if worker.Name != "dave" {
|
||||
t.Errorf("expected name 'dave', got '%s'", worker.Name)
|
||||
}
|
||||
if worker.Rig != "test-rig" {
|
||||
t.Errorf("expected rig 'test-rig', got '%s'", worker.Rig)
|
||||
}
|
||||
if worker.Branch != "main" {
|
||||
t.Errorf("expected branch 'main', got '%s'", worker.Branch)
|
||||
}
|
||||
|
||||
// Verify directory structure
|
||||
crewDir := filepath.Join(rigPath, "crew", "dave")
|
||||
if _, err := os.Stat(crewDir); os.IsNotExist(err) {
|
||||
t.Error("crew directory was not created")
|
||||
}
|
||||
|
||||
mailDir := filepath.Join(crewDir, "mail")
|
||||
if _, err := os.Stat(mailDir); os.IsNotExist(err) {
|
||||
t.Error("mail directory was not created")
|
||||
}
|
||||
|
||||
claudeMD := filepath.Join(crewDir, "CLAUDE.md")
|
||||
if _, err := os.Stat(claudeMD); os.IsNotExist(err) {
|
||||
t.Error("CLAUDE.md was not created")
|
||||
}
|
||||
|
||||
stateFile := filepath.Join(crewDir, "state.json")
|
||||
if _, err := os.Stat(stateFile); os.IsNotExist(err) {
|
||||
t.Error("state.json was not created")
|
||||
}
|
||||
|
||||
// Test Get
|
||||
retrieved, err := mgr.Get("dave")
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
if retrieved.Name != "dave" {
|
||||
t.Errorf("expected name 'dave', got '%s'", retrieved.Name)
|
||||
}
|
||||
|
||||
// Test duplicate Add
|
||||
_, err = mgr.Add("dave", false)
|
||||
if err != ErrCrewExists {
|
||||
t.Errorf("expected ErrCrewExists, got %v", err)
|
||||
}
|
||||
|
||||
// Test Get non-existent
|
||||
_, err = mgr.Get("nonexistent")
|
||||
if err != ErrCrewNotFound {
|
||||
t.Errorf("expected ErrCrewNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerAddWithBranch(t *testing.T) {
|
||||
// Create temp directory for test
|
||||
tmpDir, err := os.MkdirTemp("", "crew-test-branch-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a mock rig
|
||||
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||
t.Fatalf("failed to create rig dir: %v", err)
|
||||
}
|
||||
|
||||
g := git.NewGit(rigPath)
|
||||
|
||||
// Create a local repo with initial commit for branch testing
|
||||
sourceRepoPath := filepath.Join(tmpDir, "source-repo")
|
||||
if err := os.MkdirAll(sourceRepoPath, 0755); err != nil {
|
||||
t.Fatalf("failed to create source repo dir: %v", err)
|
||||
}
|
||||
|
||||
// Initialize source repo with a commit
|
||||
cmds := [][]string{
|
||||
{"git", "-C", sourceRepoPath, "init"},
|
||||
{"git", "-C", sourceRepoPath, "config", "user.email", "test@test.com"},
|
||||
{"git", "-C", sourceRepoPath, "config", "user.name", "Test"},
|
||||
}
|
||||
for _, cmd := range cmds {
|
||||
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
|
||||
t.Fatalf("failed to run %v: %v", cmd, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create initial file and commit
|
||||
testFile := filepath.Join(sourceRepoPath, "README.md")
|
||||
if err := os.WriteFile(testFile, []byte("# Test"), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
cmds = [][]string{
|
||||
{"git", "-C", sourceRepoPath, "add", "."},
|
||||
{"git", "-C", sourceRepoPath, "commit", "-m", "Initial commit"},
|
||||
}
|
||||
for _, cmd := range cmds {
|
||||
if err := runCmd(cmd[0], cmd[1:]...); err != nil {
|
||||
t.Fatalf("failed to run %v: %v", cmd, err)
|
||||
}
|
||||
}
|
||||
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig",
|
||||
Path: rigPath,
|
||||
GitURL: sourceRepoPath,
|
||||
}
|
||||
|
||||
mgr := NewManager(r, g)
|
||||
|
||||
// Test Add with branch
|
||||
worker, err := mgr.Add("emma", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Add with branch failed: %v", err)
|
||||
}
|
||||
|
||||
if worker.Branch != "crew/emma" {
|
||||
t.Errorf("expected branch 'crew/emma', got '%s'", worker.Branch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerList(t *testing.T) {
|
||||
// Create temp directory for test
|
||||
tmpDir, err := os.MkdirTemp("", "crew-test-list-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a mock rig
|
||||
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||
t.Fatalf("failed to create rig dir: %v", err)
|
||||
}
|
||||
|
||||
g := git.NewGit(rigPath)
|
||||
|
||||
// Create a bare repo for cloning
|
||||
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
|
||||
if err := runCmd("git", "init", "--bare", bareRepoPath); err != nil {
|
||||
t.Fatalf("failed to create bare repo: %v", err)
|
||||
}
|
||||
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig",
|
||||
Path: rigPath,
|
||||
GitURL: bareRepoPath,
|
||||
}
|
||||
|
||||
mgr := NewManager(r, g)
|
||||
|
||||
// Initially empty
|
||||
workers, err := mgr.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(workers) != 0 {
|
||||
t.Errorf("expected 0 workers, got %d", len(workers))
|
||||
}
|
||||
|
||||
// Add some workers
|
||||
_, err = mgr.Add("alice", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Add alice failed: %v", err)
|
||||
}
|
||||
_, err = mgr.Add("bob", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Add bob failed: %v", err)
|
||||
}
|
||||
|
||||
workers, err = mgr.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(workers) != 2 {
|
||||
t.Errorf("expected 2 workers, got %d", len(workers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerRemove(t *testing.T) {
|
||||
// Create temp directory for test
|
||||
tmpDir, err := os.MkdirTemp("", "crew-test-remove-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a mock rig
|
||||
rigPath := filepath.Join(tmpDir, "test-rig")
|
||||
if err := os.MkdirAll(rigPath, 0755); err != nil {
|
||||
t.Fatalf("failed to create rig dir: %v", err)
|
||||
}
|
||||
|
||||
g := git.NewGit(rigPath)
|
||||
|
||||
// Create a bare repo for cloning
|
||||
bareRepoPath := filepath.Join(tmpDir, "bare-repo.git")
|
||||
if err := runCmd("git", "init", "--bare", bareRepoPath); err != nil {
|
||||
t.Fatalf("failed to create bare repo: %v", err)
|
||||
}
|
||||
|
||||
r := &rig.Rig{
|
||||
Name: "test-rig",
|
||||
Path: rigPath,
|
||||
GitURL: bareRepoPath,
|
||||
}
|
||||
|
||||
mgr := NewManager(r, g)
|
||||
|
||||
// Add a worker
|
||||
_, err = mgr.Add("charlie", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Add failed: %v", err)
|
||||
}
|
||||
|
||||
// Remove it (with force since CLAUDE.md is uncommitted)
|
||||
err = mgr.Remove("charlie", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Remove failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify it's gone
|
||||
_, err = mgr.Get("charlie")
|
||||
if err != ErrCrewNotFound {
|
||||
t.Errorf("expected ErrCrewNotFound, got %v", err)
|
||||
}
|
||||
|
||||
// Remove non-existent
|
||||
err = mgr.Remove("nonexistent", false)
|
||||
if err != ErrCrewNotFound {
|
||||
t.Errorf("expected ErrCrewNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to run commands
|
||||
func runCmd(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
return cmd.Run()
|
||||
}
|
||||
39
internal/crew/types.go
Normal file
39
internal/crew/types.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Package crew provides crew workspace management for overseer workspaces.
|
||||
package crew
|
||||
|
||||
import "time"
|
||||
|
||||
// CrewWorker represents a user-managed workspace in a rig.
|
||||
type CrewWorker struct {
|
||||
// Name is the crew worker identifier.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Rig is the rig this crew worker belongs to.
|
||||
Rig string `json:"rig"`
|
||||
|
||||
// ClonePath is the path to the crew worker's clone of the rig.
|
||||
ClonePath string `json:"clone_path"`
|
||||
|
||||
// Branch is the current git branch.
|
||||
Branch string `json:"branch"`
|
||||
|
||||
// CreatedAt is when the crew worker was created.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// UpdatedAt is when the crew worker was last updated.
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Summary provides a concise view of crew worker status.
|
||||
type Summary struct {
|
||||
Name string `json:"name"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
// Summary returns a Summary for this crew worker.
|
||||
func (c *CrewWorker) Summary() Summary {
|
||||
return Summary{
|
||||
Name: c.Name,
|
||||
Branch: c.Branch,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user