copyWorktreeIncludeFiles method

Future<List<String>> copyWorktreeIncludeFiles(
  1. String repoRoot,
  2. String worktreePath
)

Copy gitignored files specified in .worktreeinclude from base repo to worktree.

Only copies files that are BOTH:

  1. Matched by patterns in .worktreeinclude (uses .gitignore syntax)
  2. Gitignored (not tracked by git)

Implementation

Future<List<String>> copyWorktreeIncludeFiles(
  String repoRoot,
  String worktreePath,
) async {
  String includeContent;
  try {
    includeContent = await File('$repoRoot/.worktreeinclude').readAsString();
  } catch (_) {
    return [];
  }

  final patterns = includeContent
      .split(RegExp(r'\r?\n'))
      .map((line) => line.trim())
      .where((line) => line.isNotEmpty && !line.startsWith('#'))
      .toList();

  if (patterns.isEmpty) return [];

  // List gitignored files with --directory for performance
  final gitignored = await _execGit([
    'ls-files',
    '--others',
    '--ignored',
    '--exclude-standard',
    '--directory',
  ], cwd: repoRoot);

  if (gitignored.code != 0 || gitignored.stdout.trim().isEmpty) return [];

  final entries = gitignored.stdout
      .trim()
      .split('\n')
      .where((e) => e.isNotEmpty)
      .toList();

  final files = <String>[];
  final collapsedDirs = entries.where((e) => e.endsWith('/')).toList();

  // Simple pattern matching for gitignore-style patterns
  for (final entry in entries) {
    if (entry.endsWith('/')) continue;
    if (_matchesAnyPattern(entry, patterns)) {
      files.add(entry);
    }
  }

  // Expand collapsed directories if patterns target paths inside them
  final dirsToExpand = collapsedDirs.where((dir) {
    return patterns.any((p) {
      final normalized = p.startsWith('/') ? p.substring(1) : p;
      if (normalized.startsWith(dir)) return true;
      final globIdx = normalized.indexOf(RegExp(r'[*?[]'));
      if (globIdx > 0) {
        final literalPrefix = normalized.substring(0, globIdx);
        if (dir.startsWith(literalPrefix)) return true;
      }
      return false;
    });
  }).toList();

  if (dirsToExpand.isNotEmpty) {
    final expanded = await _execGit([
      'ls-files',
      '--others',
      '--ignored',
      '--exclude-standard',
      '--',
      ...dirsToExpand,
    ], cwd: repoRoot);
    if (expanded.code == 0 && expanded.stdout.trim().isNotEmpty) {
      for (final f
          in expanded.stdout.trim().split('\n').where((e) => e.isNotEmpty)) {
        if (_matchesAnyPattern(f, patterns)) {
          files.add(f);
        }
      }
    }
  }

  final copied = <String>[];
  for (final relativePath in files) {
    final srcPath = '$repoRoot/$relativePath';
    final destPath = '$worktreePath/$relativePath';
    try {
      final destDir = destPath.substring(0, destPath.lastIndexOf('/'));
      await Directory(destDir).create(recursive: true);
      await File(srcPath).copy(destPath);
      copied.add(relativePath);
    } catch (e) {
      _logForDebugging(
        'Failed to copy $relativePath to worktree: $e',
        level: 'warn',
      );
    }
  }

  if (copied.isNotEmpty) {
    _logForDebugging(
      'Copied ${copied.length} files from .worktreeinclude: ${copied.join(', ')}',
    );
  }

  return copied;
}