@vibe-validate/git 0.18.4 → 0.19.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/staging.js CHANGED
@@ -20,6 +20,49 @@
20
20
  */
21
21
  import { executeGitCommand } from './git-executor.js';
22
22
  const GIT_TIMEOUT = 30000;
23
+ const GIT_NAME_ONLY_FLAG = '--name-only';
24
+ /**
25
+ * Get list of staged files (files in git index)
26
+ *
27
+ * Returns file paths relative to repository root for all files
28
+ * currently staged (Added, Copied, Modified, or Renamed).
29
+ *
30
+ * @param cwd - Working directory (defaults to process.cwd())
31
+ * @returns Array of staged file paths, empty if none or not a git repo
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const files = getStagedFiles();
36
+ * if (files.length > 0) {
37
+ * console.log('Staged files:', files);
38
+ * }
39
+ * ```
40
+ */
41
+ export function getStagedFiles(cwd = process.cwd()) {
42
+ try {
43
+ const result = executeGitCommand([
44
+ 'diff',
45
+ '--cached',
46
+ GIT_NAME_ONLY_FLAG,
47
+ '--diff-filter=ACMR', // Added, Copied, Modified, Renamed (not Deleted)
48
+ ], {
49
+ cwd,
50
+ timeout: GIT_TIMEOUT,
51
+ ignoreErrors: true,
52
+ });
53
+ if (!result.success || !result.stdout) {
54
+ return [];
55
+ }
56
+ return result.stdout
57
+ .split('\n')
58
+ .map((f) => f.trim())
59
+ .filter((f) => f.length > 0);
60
+ }
61
+ catch {
62
+ // Not a git repository, or git command failed
63
+ return [];
64
+ }
65
+ }
23
66
  /**
24
67
  * Get list of files with partially staged changes
25
68
  *
@@ -44,7 +87,7 @@ const GIT_TIMEOUT = 30000;
44
87
  export function getPartiallyStagedFiles() {
45
88
  try {
46
89
  // Get list of files with staged changes
47
- const stagedResult = executeGitCommand(['diff', '--name-only', '--cached'], {
90
+ const stagedResult = executeGitCommand(['diff', GIT_NAME_ONLY_FLAG, '--cached'], {
48
91
  timeout: GIT_TIMEOUT,
49
92
  ignoreErrors: true
50
93
  });
@@ -60,7 +103,7 @@ export function getPartiallyStagedFiles() {
60
103
  return [];
61
104
  }
62
105
  // Get list of files with unstaged changes
63
- const unstagedResult = executeGitCommand(['diff', '--name-only'], {
106
+ const unstagedResult = executeGitCommand(['diff', GIT_NAME_ONLY_FLAG], {
64
107
  timeout: GIT_TIMEOUT,
65
108
  ignoreErrors: true
66
109
  });
@@ -1 +1 @@
1
- {"version":3,"file":"staging.js","sourceRoot":"","sources":["../src/staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,GAAG,KAAK,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB;IACrC,IAAI,CAAC;QACH,wCAAwC;QACxC,MAAM,YAAY,GAAG,iBAAiB,CAAC,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE;YAC1E,OAAO,EAAE,WAAW;YACpB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;YAC1B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM;aACpC,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC,CAAC;QAEnB,8CAA8C;QAC9C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,0CAA0C;QAC1C,MAAM,cAAc,GAAG,iBAAiB,CAAC,CAAC,MAAM,EAAE,aAAa,CAAC,EAAE;YAChE,OAAO,EAAE,WAAW;YACpB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QAEH,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,cAAc,CAAC,MAAM;aAClB,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC,CACnB,CAAC;QAEF,mEAAmE;QACnE,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACjC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CACxB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;QAC9C,4EAA4E;QAC5E,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"staging.js","sourceRoot":"","sources":["../src/staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,GAAG,KAAK,CAAC;AAC1B,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAEzC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACxD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,iBAAiB,CAC9B;YACE,MAAM;YACN,UAAU;YACV,kBAAkB;YAClB,oBAAoB,EAAE,iDAAiD;SACxE,EACD;YACE,GAAG;YACH,OAAO,EAAE,WAAW;YACpB,YAAY,EAAE,IAAI;SACnB,CACF,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACtC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO,MAAM,CAAC,MAAM;aACjB,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;QAC9C,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB;IACrC,IAAI,CAAC;QACH,wCAAwC;QACxC,MAAM,YAAY,GAAG,iBAAiB,CAAC,CAAC,MAAM,EAAE,kBAAkB,EAAE,UAAU,CAAC,EAAE;YAC/E,OAAO,EAAE,WAAW;YACpB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;YAC1B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM;aACpC,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC,CAAC;QAEnB,8CAA8C;QAC9C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,0CAA0C;QAC1C,MAAM,cAAc,GAAG,iBAAiB,CAAC,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAAE;YACrE,OAAO,EAAE,WAAW;YACpB,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QAEH,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,cAAc,CAAC,MAAM;aAClB,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC,CACnB,CAAC;QAEF,mEAAmE;QACnE,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACjC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CACxB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;QAC9C,4EAA4E;QAC5E,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Git Test Helpers
3
+ *
4
+ * Centralized git operations for test setup and verification.
5
+ * Single source of truth for git commands in tests.
6
+ *
7
+ * Security: All git command execution happens here, not scattered across tests.
8
+ * Auditability: One place to review for security vulnerabilities.
9
+ * Consistency: All tests use same patterns for git operations.
10
+ */
11
+ /**
12
+ * Initialize a git repository for testing
13
+ *
14
+ * @param repoPath - Path to initialize as git repo
15
+ *
16
+ * @example
17
+ * const testDir = mkdtempSync(join(tmpdir(), 'test-'));
18
+ * initTestRepo(testDir);
19
+ */
20
+ export declare function initTestRepo(repoPath: string): void;
21
+ /**
22
+ * Configure git user for test repository
23
+ *
24
+ * @param repoPath - Path to git repository
25
+ * @param email - User email (default: test@example.com)
26
+ * @param name - User name (default: Test User)
27
+ *
28
+ * @example
29
+ * configTestUser(testDir);
30
+ * configTestUser(testDir, 'custom@example.com', 'Custom User');
31
+ */
32
+ export declare function configTestUser(repoPath: string, email?: string, name?: string): void;
33
+ /**
34
+ * Stage files in test repository
35
+ *
36
+ * @param repoPath - Path to git repository
37
+ * @param files - Files to stage (default: all files)
38
+ *
39
+ * @example
40
+ * stageTestFiles(testDir);
41
+ * stageTestFiles(testDir, ['file1.txt', 'file2.txt']);
42
+ */
43
+ export declare function stageTestFiles(repoPath: string, files?: string[]): void;
44
+ /**
45
+ * Create a commit in test repository
46
+ *
47
+ * @param repoPath - Path to git repository
48
+ * @param message - Commit message
49
+ *
50
+ * @example
51
+ * commitTestChanges(testDir, 'Initial commit');
52
+ */
53
+ export declare function commitTestChanges(repoPath: string, message: string): void;
54
+ /**
55
+ * Get tree hash for current HEAD
56
+ *
57
+ * @param repoPath - Path to git repository
58
+ * @returns Tree hash
59
+ *
60
+ * @example
61
+ * const treeHash = getTestTreeHash(testDir);
62
+ */
63
+ export declare function getTestTreeHash(repoPath: string): string;
64
+ /**
65
+ * Read git note for object
66
+ *
67
+ * @param repoPath - Path to git repository
68
+ * @param notesRef - Notes ref (e.g., 'refs/notes/vibe-validate/validate')
69
+ * @param object - Object hash to read note for
70
+ * @returns Note content or null if not found
71
+ *
72
+ * @example
73
+ * const note = readTestNote(testDir, 'refs/notes/vibe-validate/validate', treeHash);
74
+ */
75
+ export declare function readTestNote(repoPath: string, notesRef: string, object: string): string | null;
76
+ /**
77
+ * Setup a complete test repository with initial commit
78
+ *
79
+ * Combines: init, config user, create file, stage, commit
80
+ *
81
+ * @param repoPath - Path to initialize as git repo
82
+ * @param initialFile - Initial file to create (default: README.md)
83
+ * @param initialContent - Content for initial file (default: "# Test")
84
+ *
85
+ * @example
86
+ * const testDir = mkdtempSync(join(tmpdir(), 'test-'));
87
+ * setupTestRepoWithCommit(testDir);
88
+ */
89
+ export declare function setupTestRepoWithCommit(repoPath: string, initialFile?: string, initialContent?: string): void;
90
+ /**
91
+ * Configure git submodule protocol settings for local file URLs
92
+ *
93
+ * Required for testing submodules with file:// URLs
94
+ *
95
+ * @param repoPath - Path to git repository
96
+ *
97
+ * @example
98
+ * configTestSubmoduleProtocol(testDir);
99
+ */
100
+ export declare function configTestSubmoduleProtocol(repoPath: string): void;
101
+ /**
102
+ * Get current HEAD commit hash
103
+ *
104
+ * @param repoPath - Path to git repository
105
+ * @returns Commit SHA
106
+ *
107
+ * @example
108
+ * const commitHash = getTestCommitHash(testDir);
109
+ */
110
+ export declare function getTestCommitHash(repoPath: string): string;
111
+ /**
112
+ * Register a submodule in git index
113
+ *
114
+ * Low-level operation to add submodule entry without cloning
115
+ *
116
+ * @param repoPath - Path to git repository
117
+ * @param commitHash - Submodule commit hash
118
+ * @param submodulePath - Relative path for submodule
119
+ *
120
+ * @example
121
+ * registerTestSubmodule(testDir, 'abc123...', 'libs/auth');
122
+ */
123
+ export declare function registerTestSubmodule(repoPath: string, commitHash: string, submodulePath: string): void;
124
+ /**
125
+ * Initialize git submodules (register in .git/config)
126
+ *
127
+ * @param repoPath - Path to git repository
128
+ *
129
+ * @example
130
+ * initTestSubmodules(testDir);
131
+ */
132
+ export declare function initTestSubmodules(repoPath: string): void;
133
+ /**
134
+ * Add a submodule to the repository
135
+ *
136
+ * @param repoPath - Path to git repository
137
+ * @param submoduleUrl - URL of submodule repository
138
+ * @param submodulePath - Relative path where submodule should be placed
139
+ *
140
+ * @example
141
+ * addTestSubmodule(testDir, '/tmp/sub-repo', 'libs/auth');
142
+ */
143
+ export declare function addTestSubmodule(repoPath: string, submoduleUrl: string, submodulePath: string): void;
144
+ //# sourceMappingURL=test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAqB,EAC1B,IAAI,SAAc,GACjB,IAAI,CAGN;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,EAAU,GAAG,IAAI,CAE9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY9F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAc,EACzB,cAAc,SAAa,GAC1B,IAAI,CASN;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB,IAAI,CAMN;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GACpB,IAAI,CAMN"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Git Test Helpers
3
+ *
4
+ * Centralized git operations for test setup and verification.
5
+ * Single source of truth for git commands in tests.
6
+ *
7
+ * Security: All git command execution happens here, not scattered across tests.
8
+ * Auditability: One place to review for security vulnerabilities.
9
+ * Consistency: All tests use same patterns for git operations.
10
+ */
11
+ import { writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { safeExecSync, safeExecResult } from '@vibe-validate/utils';
14
+ /**
15
+ * Initialize a git repository for testing
16
+ *
17
+ * @param repoPath - Path to initialize as git repo
18
+ *
19
+ * @example
20
+ * const testDir = mkdtempSync(join(tmpdir(), 'test-'));
21
+ * initTestRepo(testDir);
22
+ */
23
+ export function initTestRepo(repoPath) {
24
+ safeExecSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' });
25
+ }
26
+ /**
27
+ * Configure git user for test repository
28
+ *
29
+ * @param repoPath - Path to git repository
30
+ * @param email - User email (default: test@example.com)
31
+ * @param name - User name (default: Test User)
32
+ *
33
+ * @example
34
+ * configTestUser(testDir);
35
+ * configTestUser(testDir, 'custom@example.com', 'Custom User');
36
+ */
37
+ export function configTestUser(repoPath, email = 'test@example.com', name = 'Test User') {
38
+ safeExecSync('git', ['config', 'user.email', email], { cwd: repoPath, stdio: 'pipe' });
39
+ safeExecSync('git', ['config', 'user.name', name], { cwd: repoPath, stdio: 'pipe' });
40
+ }
41
+ /**
42
+ * Stage files in test repository
43
+ *
44
+ * @param repoPath - Path to git repository
45
+ * @param files - Files to stage (default: all files)
46
+ *
47
+ * @example
48
+ * stageTestFiles(testDir);
49
+ * stageTestFiles(testDir, ['file1.txt', 'file2.txt']);
50
+ */
51
+ export function stageTestFiles(repoPath, files = ['.']) {
52
+ safeExecSync('git', ['add', ...files], { cwd: repoPath, stdio: 'pipe' });
53
+ }
54
+ /**
55
+ * Create a commit in test repository
56
+ *
57
+ * @param repoPath - Path to git repository
58
+ * @param message - Commit message
59
+ *
60
+ * @example
61
+ * commitTestChanges(testDir, 'Initial commit');
62
+ */
63
+ export function commitTestChanges(repoPath, message) {
64
+ safeExecSync('git', ['commit', '-m', message], { cwd: repoPath, stdio: 'pipe' });
65
+ }
66
+ /**
67
+ * Get tree hash for current HEAD
68
+ *
69
+ * @param repoPath - Path to git repository
70
+ * @returns Tree hash
71
+ *
72
+ * @example
73
+ * const treeHash = getTestTreeHash(testDir);
74
+ */
75
+ export function getTestTreeHash(repoPath) {
76
+ const result = safeExecSync('git', ['rev-parse', 'HEAD:'], {
77
+ cwd: repoPath,
78
+ encoding: 'utf-8',
79
+ stdio: 'pipe',
80
+ });
81
+ return result.trim();
82
+ }
83
+ /**
84
+ * Read git note for object
85
+ *
86
+ * @param repoPath - Path to git repository
87
+ * @param notesRef - Notes ref (e.g., 'refs/notes/vibe-validate/validate')
88
+ * @param object - Object hash to read note for
89
+ * @returns Note content or null if not found
90
+ *
91
+ * @example
92
+ * const note = readTestNote(testDir, 'refs/notes/vibe-validate/validate', treeHash);
93
+ */
94
+ export function readTestNote(repoPath, notesRef, object) {
95
+ const result = safeExecResult('git', ['notes', '--ref', notesRef, 'show', object], {
96
+ cwd: repoPath,
97
+ encoding: 'utf-8',
98
+ stdio: 'pipe',
99
+ });
100
+ if (result.status !== 0) {
101
+ return null;
102
+ }
103
+ return result.stdout.trim();
104
+ }
105
+ /**
106
+ * Setup a complete test repository with initial commit
107
+ *
108
+ * Combines: init, config user, create file, stage, commit
109
+ *
110
+ * @param repoPath - Path to initialize as git repo
111
+ * @param initialFile - Initial file to create (default: README.md)
112
+ * @param initialContent - Content for initial file (default: "# Test")
113
+ *
114
+ * @example
115
+ * const testDir = mkdtempSync(join(tmpdir(), 'test-'));
116
+ * setupTestRepoWithCommit(testDir);
117
+ */
118
+ export function setupTestRepoWithCommit(repoPath, initialFile = 'README.md', initialContent = '# Test\n') {
119
+ initTestRepo(repoPath);
120
+ configTestUser(repoPath);
121
+ // Create initial file
122
+ writeFileSync(join(repoPath, initialFile), initialContent);
123
+ stageTestFiles(repoPath);
124
+ commitTestChanges(repoPath, 'Initial commit');
125
+ }
126
+ /**
127
+ * Configure git submodule protocol settings for local file URLs
128
+ *
129
+ * Required for testing submodules with file:// URLs
130
+ *
131
+ * @param repoPath - Path to git repository
132
+ *
133
+ * @example
134
+ * configTestSubmoduleProtocol(testDir);
135
+ */
136
+ export function configTestSubmoduleProtocol(repoPath) {
137
+ safeExecSync('git', ['config', 'protocol.file.allow', 'always'], { cwd: repoPath, stdio: 'pipe' });
138
+ }
139
+ /**
140
+ * Get current HEAD commit hash
141
+ *
142
+ * @param repoPath - Path to git repository
143
+ * @returns Commit SHA
144
+ *
145
+ * @example
146
+ * const commitHash = getTestCommitHash(testDir);
147
+ */
148
+ export function getTestCommitHash(repoPath) {
149
+ const result = safeExecSync('git', ['rev-parse', 'HEAD'], {
150
+ cwd: repoPath,
151
+ encoding: 'utf-8',
152
+ stdio: 'pipe',
153
+ });
154
+ return result.trim();
155
+ }
156
+ /**
157
+ * Register a submodule in git index
158
+ *
159
+ * Low-level operation to add submodule entry without cloning
160
+ *
161
+ * @param repoPath - Path to git repository
162
+ * @param commitHash - Submodule commit hash
163
+ * @param submodulePath - Relative path for submodule
164
+ *
165
+ * @example
166
+ * registerTestSubmodule(testDir, 'abc123...', 'libs/auth');
167
+ */
168
+ export function registerTestSubmodule(repoPath, commitHash, submodulePath) {
169
+ safeExecSync('git', ['update-index', '--add', '--cacheinfo', `160000,${commitHash},${submodulePath}`], { cwd: repoPath, stdio: 'pipe' });
170
+ }
171
+ /**
172
+ * Initialize git submodules (register in .git/config)
173
+ *
174
+ * @param repoPath - Path to git repository
175
+ *
176
+ * @example
177
+ * initTestSubmodules(testDir);
178
+ */
179
+ export function initTestSubmodules(repoPath) {
180
+ safeExecSync('git', ['submodule', 'init'], { cwd: repoPath, stdio: 'pipe' });
181
+ }
182
+ /**
183
+ * Add a submodule to the repository
184
+ *
185
+ * @param repoPath - Path to git repository
186
+ * @param submoduleUrl - URL of submodule repository
187
+ * @param submodulePath - Relative path where submodule should be placed
188
+ *
189
+ * @example
190
+ * addTestSubmodule(testDir, '/tmp/sub-repo', 'libs/auth');
191
+ */
192
+ export function addTestSubmodule(repoPath, submoduleUrl, submodulePath) {
193
+ safeExecSync('git', ['-c', 'protocol.file.allow=always', 'submodule', 'add', submoduleUrl, submodulePath], { cwd: repoPath, stdio: 'pipe' });
194
+ }
195
+ //# sourceMappingURL=test-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"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"}
@@ -10,7 +10,7 @@
10
10
  * git stash create includes timestamps, making hashes non-deterministic.
11
11
  * git write-tree produces content-based hashes only (no timestamps).
12
12
  */
13
- import type { TreeHash } from './types.js';
13
+ import type { TreeHash, TreeHashResult } from './types.js';
14
14
  /**
15
15
  * Get deterministic git tree hash representing current working tree state
16
16
  *
@@ -19,18 +19,55 @@ import type { TreeHash } from './types.js';
19
19
  * 2. Copy current index to temporary index
20
20
  * 3. Mark untracked files with --intent-to-add in temp index
21
21
  * 4. Calculate tree hash with git write-tree using temp index
22
- * 5. Clean up temp index file
22
+ * 5. Detect and process git submodules (recursive)
23
+ * 6. Return parent hash + optional submodule hashes
24
+ * 7. Clean up temp index file
23
25
  *
24
26
  * Why this is better than git stash create:
25
27
  * - git stash create: includes timestamps in commit → different hash each time
26
28
  * - git write-tree: content-based only → same content = same hash (deterministic)
27
29
  *
30
+ * Submodule Support (Issue #120):
31
+ * - Detects submodules via `git submodule status`
32
+ * - Recursively calculates tree hash for each submodule
33
+ * - Returns TreeHashResult with parent hash + submodule hashes
34
+ * - Working tree changes in submodules invalidate cache
35
+ * - Git notes store full result for state reconstruction
36
+ *
37
+ * IMPORTANT: This function returns a structured result object, NOT a composite hash.
38
+ * Git notes store the TreeHashResult as-is. The hash field is the parent repo's
39
+ * standard Git SHA-1 hash (40 hex characters). The optional submoduleHashes field
40
+ * records each submodule's tree hash separately.
41
+ *
42
+ * Cache key format in git notes (v0.19.0+):
43
+ * - Parent-only repos: Use parent hash directly (backward compatible)
44
+ * - Repos with submodules: Use parent hash + submodule metadata
45
+ * - Result structure stored in git notes for state reconstruction
46
+ *
28
47
  * CRITICAL: Uses GIT_INDEX_FILE to avoid corrupting real index during git commit hooks
29
48
  *
30
- * @returns Git tree SHA-1 hash (40 hex characters) as branded TreeHash type
49
+ * @returns TreeHashResult containing:
50
+ * - hash: Parent repository tree hash (Git SHA-1, 40 hex chars)
51
+ * - submoduleHashes: Optional record of submodule paths to tree hashes
52
+ *
53
+ * @example
54
+ * // Repository without submodules (0.18.x compatible)
55
+ * const result = await getGitTreeHash();
56
+ * // { hash: 'abc123...' }
57
+ *
58
+ * @example
59
+ * // Repository with submodules (v0.19.0+)
60
+ * const result = await getGitTreeHash();
61
+ * // {
62
+ * // hash: 'abc123...', // Parent repo hash
63
+ * // submoduleHashes: {
64
+ * // 'libs/auth': 'xyz789...'
65
+ * // }
66
+ * // }
67
+ *
31
68
  * @throws Error if not in a git repository or git command fails
32
69
  */
33
- export declare function getGitTreeHash(): Promise<TreeHash>;
70
+ export declare function getGitTreeHash(): Promise<TreeHashResult>;
34
71
  /**
35
72
  * Get tree hash for HEAD commit (committed state only, no working tree changes)
36
73
  *
@@ -46,4 +83,49 @@ export declare function getHeadTreeHash(): Promise<TreeHash>;
46
83
  * @returns true if working tree differs from HEAD, false if clean
47
84
  */
48
85
  export declare function hasWorkingTreeChanges(): Promise<boolean>;
86
+ /**
87
+ * Submodule information from git submodule status
88
+ * @internal Exported for testing
89
+ */
90
+ export interface SubmoduleInfo {
91
+ /** Submodule path relative to repo root */
92
+ path: string;
93
+ /** Status character (' '=clean, '+'=modified, '-'=uninitialized, 'U'=conflict) */
94
+ status: string;
95
+ }
96
+ /**
97
+ * Get list of git submodules in current repository
98
+ *
99
+ * Parses output of `git submodule status` to detect submodules.
100
+ * Returns empty array if no submodules or command fails.
101
+ *
102
+ * Output format: " abc123 libs/auth (heads/main)"
103
+ * ^^^^^^ ^^^^^^^^^ (description)
104
+ * hash path
105
+ *
106
+ * @returns Array of submodule information
107
+ *
108
+ * @example
109
+ * const submodules = getSubmodules();
110
+ * // [{ path: 'libs/auth', status: ' ' }, { path: 'vendor/foo', status: '+' }]
111
+ *
112
+ * @internal Exported for testing
113
+ */
114
+ export declare function getSubmodules(): SubmoduleInfo[];
115
+ /**
116
+ * Calculate tree hash for a git submodule (recursive)
117
+ *
118
+ * Changes to submodule directory, calculates tree hash, then returns to original directory.
119
+ * This is recursive - if the submodule has its own submodules, they will be included.
120
+ *
121
+ * @param submodulePath - Path to submodule relative to current directory
122
+ * @returns Tree hash result for the submodule
123
+ *
124
+ * @example
125
+ * const result = await getSubmoduleTreeHash('libs/auth');
126
+ * // Returns TreeHashResult for libs/auth submodule
127
+ *
128
+ * @internal Exported for testing
129
+ */
130
+ export declare function getSubmoduleTreeHash(submodulePath: string): Promise<TreeHashResult>;
49
131
  //# sourceMappingURL=tree-hash.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tree-hash.d.ts","sourceRoot":"","sources":["../src/tree-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA+G3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,QAAQ,CAAC,CAuFxD;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,QAAQ,CAAC,CAUzD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAS9D"}
1
+ {"version":3,"file":"tree-hash.d.ts","sourceRoot":"","sources":["../src/tree-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA+G3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,wBAAsB,cAAc,IAAI,OAAO,CAAC,cAAc,CAAC,CA2I9D;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,QAAQ,CAAC,CAUzD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAS9D;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,IAAI,aAAa,EAAE,CAgD/C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CASzF"}