Skip to content

Commit 2f02194

Browse files
committed
feat: add branch manipulation features
1 parent aa086be commit 2f02194

File tree

12 files changed

+383
-6
lines changed

12 files changed

+383
-6
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ $ git branch
4444
main
4545
$ popd
4646

47+
# List branch directories under the current repository
48+
$ git-replicator branch
49+
base
50+
foo
51+
52+
# Delete a branch directory under the current repository
53+
$ git-replicator delete foo
54+
Deleted branch directory: foo
55+
56+
$ git-replicator switch foo
57+
Enumerating objects: 5, done.
58+
Counting objects: 100% (5/5), done.
59+
Compressing objects: 100% (4/4), done.
60+
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
61+
cloned branch: foo to dir: /home/$USER/git-replicator/github.com/terakoya76/git-replicator-test/foo
62+
4763
# From now on, you can let the agent use this directory as it pleases.
4864
# e.g.
4965
# $ cursor ./foo/
@@ -53,6 +69,8 @@ $ popd
5369
- Clone a git repository into a structured local directory (`get <url>`)
5470
- List all managed repositories (`list`)
5571
- Clone current repo into a new branch directory (`switch <branch>`, like `git switch`)
72+
- List branch directories under the current repository (`branch`)
73+
- Delete a branch directory under the current repository (`delete <branch>`)
5674

5775
## Development
5876

cmd/branch.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/terakoya76/git-replicator/internal/handlers"
10+
"github.com/terakoya76/git-replicator/internal/utils"
11+
)
12+
13+
var branchCmd = &cobra.Command{
14+
Use: "branch",
15+
Short: "List branch directories under the current repository (like git switch)",
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
cwd, err := os.Getwd()
18+
if err != nil {
19+
return fmt.Errorf("failed to get current directory: %w", err)
20+
}
21+
rootDir, err := utils.GetGitReplicatorRoot()
22+
if err != nil {
23+
return fmt.Errorf("failed to get git-replicator root: %w", err)
24+
}
25+
repoDir, err := utils.FindRepoDir(cwd, rootDir)
26+
if err != nil {
27+
return err
28+
}
29+
branches, err := handlers.ListBranchDirs(context.Background(), repoDir)
30+
if err != nil {
31+
return err
32+
}
33+
for _, b := range branches {
34+
fmt.Println(b)
35+
}
36+
return nil
37+
},
38+
}
39+
40+
func init() {
41+
rootCmd.AddCommand(branchCmd)
42+
}

cmd/delete.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/terakoya76/git-replicator/internal/handlers"
10+
"github.com/terakoya76/git-replicator/internal/utils"
11+
)
12+
13+
var deleteCmd = &cobra.Command{
14+
Use: "delete <branch>",
15+
Short: "Delete a branch directory under the current repository",
16+
Args: cobra.ExactArgs(1),
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
branch := args[0]
19+
cwd, err := os.Getwd()
20+
if err != nil {
21+
return fmt.Errorf("failed to get current directory: %w", err)
22+
}
23+
rootDir, err := utils.GetGitReplicatorRoot()
24+
if err != nil {
25+
return fmt.Errorf("failed to get git-replicator root: %w", err)
26+
}
27+
repoDir, err := utils.FindRepoDir(cwd, rootDir)
28+
if err != nil {
29+
return err
30+
}
31+
if err := handlers.DeleteBranchDir(context.Background(), repoDir, branch); err != nil {
32+
return err
33+
}
34+
fmt.Printf("Deleted branch directory: %s\n", branch)
35+
return nil
36+
},
37+
}
38+
39+
func init() {
40+
rootCmd.AddCommand(deleteCmd)
41+
}

cmd/get.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var getCmd = &cobra.Command{
1818
url := args[0]
1919
rootDir, err := utils.GetGitReplicatorRoot()
2020
if err != nil {
21-
return fmt.Errorf("failed to get git-replicator root: %w", err)
21+
return fmt.Errorf("failed to get git-replicator root ($HOME/git-replicator): %w", err)
2222
}
2323
if err := handlers.Get(ctx, url, rootDir); err != nil {
2424
return fmt.Errorf("failed to clone repository: %w", err)

internal/config/config.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88

99
type Config struct {
1010
// Add your configuration fields here
11-
SourceRepo string `mapstructure:"source_repo"`
12-
TargetRepo string `mapstructure:"target_repo"`
1311
}
1412

1513
func Load() (*Config, error) {

internal/handlers/branch.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package handlers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
)
8+
9+
// ListBranchDirs returns a list of branch directory names under the given repoDir (including 'base').
10+
func ListBranchDirs(ctx context.Context, repoDir string) ([]string, error) {
11+
entries, err := os.ReadDir(repoDir)
12+
if err != nil {
13+
return nil, fmt.Errorf("failed to read repo directory: %w", err)
14+
}
15+
var branches []string
16+
for _, entry := range entries {
17+
if entry.IsDir() {
18+
branches = append(branches, entry.Name())
19+
}
20+
}
21+
return branches, nil
22+
}

internal/handlers/branch_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package handlers_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/terakoya76/git-replicator/internal/handlers"
10+
)
11+
12+
func TestListBranchDirs(t *testing.T) {
13+
tmpDir := t.TempDir()
14+
repoDir := filepath.Join(tmpDir, "repo")
15+
if err := os.MkdirAll(repoDir, 0o755); err != nil {
16+
t.Fatalf("failed to create repo dir: %v", err)
17+
}
18+
// Create base and branch directories
19+
if err := os.Mkdir(filepath.Join(repoDir, "base"), 0o755); err != nil {
20+
t.Fatalf("failed to create base dir: %v", err)
21+
}
22+
if err := os.Mkdir(filepath.Join(repoDir, "feature-x"), 0o755); err != nil {
23+
t.Fatalf("failed to create feature-x dir: %v", err)
24+
}
25+
if err := os.Mkdir(filepath.Join(repoDir, "bugfix-y"), 0o755); err != nil {
26+
t.Fatalf("failed to create bugfix-y dir: %v", err)
27+
}
28+
29+
t.Run("list branch dirs (including base)", func(t *testing.T) {
30+
branches, err := handlers.ListBranchDirs(context.Background(), repoDir)
31+
if err != nil {
32+
t.Fatalf("unexpected error: %v", err)
33+
}
34+
want := map[string]bool{"feature-x": true, "bugfix-y": true, "base": true}
35+
if len(branches) != 3 {
36+
t.Errorf("expected 3 branches, got %d", len(branches))
37+
}
38+
for _, b := range branches {
39+
if !want[b] {
40+
t.Errorf("unexpected branch: %s", b)
41+
}
42+
}
43+
})
44+
45+
t.Run("repo dir does not exist", func(t *testing.T) {
46+
_, err := handlers.ListBranchDirs(context.Background(), filepath.Join(tmpDir, "not-exist"))
47+
if err == nil {
48+
t.Errorf("expected error for non-existent dir, got nil")
49+
}
50+
})
51+
}

internal/handlers/delete.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package handlers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"path/filepath"
7+
8+
"github.com/terakoya76/git-replicator/internal/utils"
9+
)
10+
11+
// DeleteBranchDir deletes the branch directory under the given repo for a branch name.
12+
func DeleteBranchDir(ctx context.Context, repoDir, branchName string) error {
13+
branchDir := filepath.Join(repoDir, branchName)
14+
if err := utils.RemoveDir(branchDir); err != nil {
15+
return fmt.Errorf("failed to delete branch directory %s: %w", branchDir, err)
16+
}
17+
return nil
18+
}

internal/handlers/delete_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package handlers_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/terakoya76/git-replicator/internal/handlers"
10+
)
11+
12+
func TestDeleteBranchDir(t *testing.T) {
13+
tmpDir := t.TempDir()
14+
repoDir := filepath.Join(tmpDir, "repo")
15+
branchName := "feature-x"
16+
branchDir := filepath.Join(repoDir, branchName)
17+
18+
tests := []struct {
19+
name string
20+
branch string
21+
prepare func()
22+
wantErr bool
23+
checkAfter func(branchDir string) error
24+
}{
25+
{
26+
name: "delete existing branch dir",
27+
branch: branchName,
28+
prepare: func() {
29+
if err := os.MkdirAll(branchDir, 0o755); err != nil {
30+
t.Fatalf("failed to create branch dir: %v", err)
31+
}
32+
filePath := filepath.Join(branchDir, "dummy.txt")
33+
if err := os.WriteFile(filePath, []byte("dummy"), 0o644); err != nil {
34+
t.Fatalf("failed to create file: %v", err)
35+
}
36+
},
37+
wantErr: false,
38+
checkAfter: func(branchDir string) error {
39+
if _, err := os.Stat(branchDir); !os.IsNotExist(err) {
40+
return err
41+
}
42+
return nil
43+
},
44+
},
45+
{
46+
name: "delete already deleted branch dir",
47+
branch: branchName,
48+
prepare: func() {
49+
_ = os.RemoveAll(branchDir)
50+
},
51+
wantErr: false,
52+
checkAfter: func(branchDir string) error {
53+
if _, err := os.Stat(branchDir); !os.IsNotExist(err) {
54+
return err
55+
}
56+
return nil
57+
},
58+
},
59+
{
60+
name: "delete non-existent branch dir",
61+
branch: "nonexistent",
62+
prepare: func() {},
63+
wantErr: false,
64+
checkAfter: func(branchDir string) error {
65+
if _, err := os.Stat(branchDir); !os.IsNotExist(err) {
66+
return err
67+
}
68+
return nil
69+
},
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
tt.prepare()
76+
branchDirPath := filepath.Join(repoDir, tt.branch)
77+
err := handlers.DeleteBranchDir(context.Background(), repoDir, tt.branch)
78+
if (err != nil) != tt.wantErr {
79+
t.Errorf("DeleteBranchDir() error = %v, wantErr %v", err, tt.wantErr)
80+
}
81+
if err := tt.checkAfter(branchDirPath); err != nil {
82+
t.Errorf("post-check failed: %v", err)
83+
}
84+
})
85+
}
86+
}

internal/utils/filesystem.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,16 @@ func FindRepoDir(cwd, gitReplicatorRoot string) (string, error) {
2626
return dir, nil
2727
}
2828
if dir == gitReplicatorRoot || dir == "/" || dir == "." {
29-
return "", fmt.Errorf("could not find repo directory under git-replicator root")
29+
return "", fmt.Errorf("could not find repo directory, so move to the repo directory ($HOME/git-replicator/<host>/<owner>/<repo>)")
3030
}
3131
dir = parent
3232
}
3333
}
34+
35+
// RemoveDir deletes the specified directory and all its contents.
36+
func RemoveDir(dir string) error {
37+
if err := os.RemoveAll(dir); err != nil {
38+
return fmt.Errorf("failed to remove directory %s: %w", dir, err)
39+
}
40+
return nil
41+
}

0 commit comments

Comments
 (0)