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
95 changes: 83 additions & 12 deletions file_copier.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,23 @@ func (fc *FileCopier) copyUntrackedFiles(worktreePath string) error {

for _, file := range files {
destPath := filepath.Join(worktreePath, file)
if err := fc.copyWithCOW(file, destPath); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", yellow.Styled(fmt.Sprintf("Unable to copy file %s to %s - folder may not exist", file, destPath)))

// Check if this is a directory that should be copied as a whole
srcInfo, err := os.Stat(file)
if err != nil {
continue // Skip files that can't be accessed
}

if !srcInfo.IsDir() {
// For individual files, use the existing approach
if err := fc.copyWithCOW(file, destPath); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", yellow.Styled(fmt.Sprintf("Unable to copy file %s to %s - folder may not exist", file, destPath)))
}
} else {
// For directories, copy the entire directory at once
if err := fc.copyWithCOW(file, destPath); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", yellow.Styled(fmt.Sprintf("Unable to copy directory %s to %s", file, destPath)))
}
}
}

Expand Down Expand Up @@ -62,7 +77,12 @@ func (fc *FileCopier) findFiles(pattern string) ([]string, error) {
}

func (fc *FileCopier) findFilesWithFd(pattern string) ([]string, error) {
cmd := exec.Command("fd", "-u", pattern, "-E", "node_modules")
// Use fd to find files matching the pattern
cmd := exec.Command("fd", "-u", pattern)
if fc.config.excludeNodeModules {
// Add node_modules to exclusion list
cmd = exec.Command("fd", "-u", pattern, "-E", "node_modules")
}
output, err := cmd.Output()
if err != nil {
return nil, err
Expand All @@ -83,8 +103,17 @@ func (fc *FileCopier) findFilesWithWalk(re *regexp.Regexp) ([]string, error) {
return err
}

if strings.Contains(path, "node_modules") {
return nil
// Skip node_modules if configured to do so
if fc.config.excludeNodeModules && strings.Contains(path, "node_modules") {
return filepath.SkipDir
}

// Special handling for large directories like node_modules when not excluding them
if info.IsDir() && strings.Contains(path, "node_modules") && !fc.config.excludeNodeModules {
// Instead of walking through all files, just add the directory itself
// The copy function will handle copying the entire directory efficiently
files = append(files, path)
return filepath.SkipDir
}

if !info.IsDir() && re.MatchString(info.Name()) {
Expand All @@ -103,18 +132,60 @@ func (fc *FileCopier) copyWithCOW(src, dest string) error {
return err
}

copyStrategies := [][]string{
{"-Rc"}, // BSD/macOS copy-on-write
{"-R", "--reflink"}, // GNU copy-on-write
{"-R"}, // Regular copy
// Check if this is a directory (like node_modules) that should be copied as a whole
srcInfo, err := os.Stat(src)
if err != nil {
return err
}

for _, strategy := range copyStrategies {
args := append(strategy, src, dest)
cmd := exec.Command("cp", args...)
if srcInfo.IsDir() {
// For directories, try to copy the entire directory at once
// This is much more efficient than copying individual files

// First try rsync for optimal performance
if hasCommand("rsync") {
// Use rsync with hard links and archive mode for maximum efficiency
cmd := exec.Command("rsync", "-aH", src, dest)
if err := cmd.Run(); err == nil {
return nil
}
}

// Fallback to cp with hard links
cmd := exec.Command("cp", "-aH", src, dest)
if err := cmd.Run(); err == nil {
return nil
}

// Final fallback to existing strategies
copyStrategies := [][]string{
{"-Rc"}, // BSD/macOS copy-on-write
{"-R", "--reflink"}, // GNU copy-on-write
{"-R"}, // Regular copy
}

for _, strategy := range copyStrategies {
args := append(strategy, src, dest)
cmd := exec.Command("cp", args...)
if err := cmd.Run(); err == nil {
return nil
}
}
} else {
// For individual files, use the existing approach
copyStrategies := [][]string{
{"-c"}, // BSD/macOS copy-on-write
{"--reflink"}, // GNU copy-on-write
{}, // Regular copy (no flags)
}

for _, strategy := range copyStrategies {
args := append(strategy, src, dest)
cmd := exec.Command("cp", args...)
if err := cmd.Run(); err == nil {
return nil
}
}
}

return fmt.Errorf("failed to copy %s to %s", src, dest)
Expand Down
13 changes: 10 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ var (
)

type Config struct {
verbose bool
logger *log.Logger
verbose bool
excludeNodeModules bool
logger *log.Logger
}

type WorktreeManager struct {
Expand All @@ -38,8 +39,10 @@ type WorktreeManager struct {

func main() {
var verbose bool
var excludeNodeModules bool
flag.BoolVar(&verbose, "v", false, "verbose output")
flag.BoolVar(&verbose, "verbose", false, "verbose output")
flag.BoolVar(&excludeNodeModules, "exclude-node-modules", false, "exclude node_modules directory from file copying")
flag.Usage = usage
flag.Parse()

Expand All @@ -51,6 +54,7 @@ func main() {

config := &Config{
verbose: verbose,
excludeNodeModules: excludeNodeModules,
logger: log.New(os.Stderr, "", 0),
}

Expand All @@ -64,7 +68,7 @@ func main() {
}

func usage() {
fmt.Print(`worktree [-v] <branch name>
fmt.Print(`worktree [-v] [--exclude-node-modules] <branch name>

create a git worktree with <branch name>. Will create a worktree if one isn't
found that matches the given name.
Expand All @@ -82,6 +86,9 @@ To set a global configuration for all repositories:

If you have any custom configuration set, it will override the defaults
completely, so add all files you want copied.

Use --exclude-node-modules to skip copying node_modules directories, which can
significantly speed up worktree creation for projects with large node_modules.
`)
}

Expand Down