1- import { appendFile , mkdir , readFile } from 'node:fs/promises' ;
2- import { join } from 'node:path' ;
1+ import { appendFile , cp , mkdir , readFile } from 'node:fs/promises' ;
2+ import { dirname , join } from 'node:path' ;
33import { GitError , git } from '@/utils' ;
44
55const CODIVA_DIR = '.codiva' ;
66const WORKTREES_SUBDIR = join ( CODIVA_DIR , 'worktrees' ) ;
77const EXCLUDE_MARKER = '# codiva' ;
88
9+ /**
10+ * `git worktree add` が引き継ぐのは追跡対象ファイルだけなので、`.gitignore` された
11+ * `node_modules/` や `.env` などは新しい worktree に現れない。これらをリポジトリ
12+ * ルートから複製すると、セッションが即座にビルド/実行できる(依存や環境変数を
13+ * 手で用意し直さなくてよい)。既定で有効。
14+ */
15+ export interface WorktreeOptions {
16+ /** `.gitignore` された未追跡ファイルを新しい worktree へコピーするか。未設定は true。 */
17+ copyIgnored ?: boolean ;
18+ }
19+
920export interface Worktree {
1021 slug : string ;
1122 branch : string ;
@@ -19,14 +30,41 @@ export interface DiffStat {
1930 uncommitted : string [ ] ;
2031}
2132
33+ /**
34+ * `git ls-files --others --ignored --exclude-standard --directory` の生出力から、
35+ * 新しい worktree へコピーすべき ignore 済みエントリだけを取り出す純関数。
36+ *
37+ * `--directory` によりディレクトリ全体が ignore されている場合は末尾 `/` 付きの
38+ * 1エントリに畳まれる(`node_modules/` を数万ファイル列挙せずに済む)。codiva 自身の
39+ * 作業ディレクトリ(`.codiva/`)と `.git` は、worktree 群を再帰コピーしたり内部状態を
40+ * 壊したりするため必ず除外する。
41+ */
42+ export function ignoredCopyEntries ( raw : string ) : string [ ] {
43+ return raw
44+ . split ( '\n' )
45+ . map ( ( line ) => line . trim ( ) )
46+ . filter ( Boolean )
47+ . filter ( ( entry ) => {
48+ const normalized = entry . replace ( / \/ $ / , '' ) ;
49+ return normalized !== CODIVA_DIR && normalized !== '.git' ;
50+ } ) ;
51+ }
52+
2253/**
2354 * Creates and tears down git worktrees for sessions. Every worktree lives under
2455 * `.codiva/worktrees/<slug>` on branch `codiva/<slug>`, branched from the repo's
2556 * current HEAD. The repo's own files are never modified except a one-time
2657 * `.git/info/exclude` entry for `.codiva/`.
2758 */
2859export class WorktreeManager {
29- constructor ( private readonly repoRoot : string ) { }
60+ private readonly copyIgnored : boolean ;
61+
62+ constructor (
63+ private readonly repoRoot : string ,
64+ options : WorktreeOptions = { } ,
65+ ) {
66+ this . copyIgnored = options . copyIgnored !== false ;
67+ }
3068
3169 /** The base branch worktrees are cut from and merged back into. */
3270 async baseBranch ( ) : Promise < string > {
@@ -89,7 +127,39 @@ export class WorktreeManager {
89127 const relPath = join ( WORKTREES_SUBDIR , slug ) ;
90128 const branch = `codiva/${ slug } ` ;
91129 await git ( this . repoRoot , [ 'worktree' , 'add' , relPath , '-b' , branch ] ) ;
92- return { slug, branch, path : join ( this . repoRoot , relPath ) } ;
130+ const worktreePath = join ( this . repoRoot , relPath ) ;
131+ if ( this . copyIgnored ) {
132+ await this . copyIgnoredFiles ( worktreePath ) ;
133+ }
134+ return { slug, branch, path : worktreePath } ;
135+ }
136+
137+ /**
138+ * `.gitignore` された未追跡ファイル(`node_modules/`・`.env` など)をリポジトリ
139+ * ルートから新しい worktree へ複製する。git worktree は追跡対象しか引き継がないため、
140+ * これがないとセッション側で依存の再インストールや環境変数の再設定が必要になる。
141+ *
142+ * ベストエフォート: 個々のコピー失敗(競合・権限等)は worktree 作成を巻き込まず
143+ * スキップする(環境ファイルが1つ欠けても致命ではない)。
144+ */
145+ private async copyIgnoredFiles ( worktreePath : string ) : Promise < void > {
146+ const raw = await git ( this . repoRoot , [
147+ 'ls-files' ,
148+ '--others' ,
149+ '--ignored' ,
150+ '--exclude-standard' ,
151+ '--directory' ,
152+ ] ) . catch ( ( ) => '' ) ;
153+ for ( const entry of ignoredCopyEntries ( raw ) ) {
154+ const from = join ( this . repoRoot , entry ) ;
155+ const to = join ( worktreePath , entry ) ;
156+ try {
157+ await mkdir ( dirname ( to ) , { recursive : true } ) ;
158+ await cp ( from , to , { recursive : true , force : true , errorOnExist : false } ) ;
159+ } catch {
160+ // best-effort: 1エントリの失敗で worktree 作成全体を止めない
161+ }
162+ }
93163 }
94164
95165 /** Committed diff stat vs. the base branch plus any uncommitted paths. */
0 commit comments