@vibe-validate/git 0.17.0-rc4 → 0.17.1-rc.1
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/README.md +53 -1
- package/dist/git-commands.d.ts +61 -0
- package/dist/git-commands.d.ts.map +1 -0
- package/dist/git-commands.js +80 -0
- package/dist/git-commands.js.map +1 -0
- package/dist/git-executor.d.ts +144 -0
- package/dist/git-executor.d.ts.map +1 -0
- package/dist/git-executor.js +192 -0
- package/dist/git-executor.js.map +1 -0
- package/dist/git-notes.d.ts +142 -0
- package/dist/git-notes.d.ts.map +1 -0
- package/dist/git-notes.js +251 -0
- package/dist/git-notes.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/staging.d.ts +43 -0
- package/dist/staging.d.ts.map +1 -0
- package/dist/staging.js +77 -0
- package/dist/staging.js.map +1 -0
- package/dist/tracking-branch.d.ts +34 -0
- package/dist/tracking-branch.d.ts.map +1 -0
- package/dist/tracking-branch.js +66 -0
- package/dist/tracking-branch.js.map +1 -0
- package/dist/yaml-detection.d.ts.map +1 -1
- package/dist/yaml-detection.js +1 -0
- package/dist/yaml-detection.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Git utilities for vibe-validate - deterministic tree hash calculation, branch sy
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Deterministic Git Tree Hash**: Content-based hashing using `git write-tree` (no timestamps)
|
|
7
|
+
- **Deterministic Git Tree Hash with Automatic Work Protection**: Content-based hashing using `git write-tree` (no timestamps) - automatically creates recoverable snapshots of all files
|
|
8
8
|
- **Branch Sync Checking**: Safe branch synchronization verification without auto-merging
|
|
9
9
|
- **Post-Merge Cleanup**: Automated cleanup of merged branches after PR completion
|
|
10
10
|
|
|
@@ -114,6 +114,8 @@ git reset // Restore index to clean state
|
|
|
114
114
|
- Enables reliable validation state caching
|
|
115
115
|
- Includes all files (staged, unstaged, untracked)
|
|
116
116
|
- No side effects (index restored after hash)
|
|
117
|
+
- Automatic work protection (all files stored as git objects)
|
|
118
|
+
- Recoverable snapshots of uncommitted work
|
|
117
119
|
|
|
118
120
|
### Safe Branch Sync
|
|
119
121
|
|
|
@@ -131,6 +133,56 @@ git reset // Restore index to clean state
|
|
|
131
133
|
- Never deletes main branch
|
|
132
134
|
- Provides clear feedback on all operations
|
|
133
135
|
|
|
136
|
+
## Automatic Work Protection
|
|
137
|
+
|
|
138
|
+
A valuable side benefit of the deterministic tree hash calculation is automatic work protection.
|
|
139
|
+
|
|
140
|
+
### Technical Implementation
|
|
141
|
+
|
|
142
|
+
When `getGitTreeHash()` runs, it:
|
|
143
|
+
1. Creates temporary index: `.git/vibe-validate-temp-index`
|
|
144
|
+
2. Copies current index to temp index
|
|
145
|
+
3. Runs `git add --all` in temp index (stages everything)
|
|
146
|
+
4. Runs `git write-tree` (creates git objects for all files)
|
|
147
|
+
5. Deletes temp index (your real index remains untouched)
|
|
148
|
+
|
|
149
|
+
**Critical insight**: Step 4 creates permanent git objects in `.git/objects/` for every file, even though the temp index is deleted. These objects remain accessible via the tree hash.
|
|
150
|
+
|
|
151
|
+
### What Gets Protected
|
|
152
|
+
|
|
153
|
+
Every file in your working directory (respecting .gitignore):
|
|
154
|
+
- ✅ Staged changes (in git index)
|
|
155
|
+
- ✅ Unstaged modifications (tracked files)
|
|
156
|
+
- ✅ Untracked files (new files not yet added)
|
|
157
|
+
|
|
158
|
+
**Not protected** (by design):
|
|
159
|
+
- ❌ Files in .gitignore (secrets, credentials, build artifacts)
|
|
160
|
+
|
|
161
|
+
### Storage Overhead
|
|
162
|
+
|
|
163
|
+
**Zero additional overhead**: Git's content-addressable storage automatically deduplicates identical file content. If a file hasn't changed between validations, no additional storage is used.
|
|
164
|
+
|
|
165
|
+
### Recovery Examples
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Scenario: Accidentally deleted file that was never committed
|
|
169
|
+
$ echo "Important work" > new-feature.ts
|
|
170
|
+
$ vv validate # Tree hash: abc123...
|
|
171
|
+
$ rm new-feature.ts # Oops!
|
|
172
|
+
|
|
173
|
+
# Recovery:
|
|
174
|
+
$ git cat-file -p abc123def:new-feature.ts > new-feature.ts
|
|
175
|
+
|
|
176
|
+
# Scenario: Want to see file content from 2 hours ago
|
|
177
|
+
$ vv history list
|
|
178
|
+
2025-12-02 14:30:15 abc123... # 2 hours ago
|
|
179
|
+
2025-12-02 16:45:22 def456... # Current
|
|
180
|
+
|
|
181
|
+
$ git cat-file -p abc123def:src/feature.ts # View old version
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
See [Work Protection Guide](../../docs/work-protection.md) for more examples.
|
|
185
|
+
|
|
134
186
|
## License
|
|
135
187
|
|
|
136
188
|
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Command Utilities
|
|
3
|
+
*
|
|
4
|
+
* High-level git operations built on top of the secure git-executor.
|
|
5
|
+
* These functions provide convenient access to common git commands.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check if the current directory is inside a git repository
|
|
9
|
+
* @returns true if inside a git repository, false otherwise
|
|
10
|
+
*/
|
|
11
|
+
export declare function isGitRepository(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Get the path to the .git directory
|
|
14
|
+
* @returns The absolute path to the .git directory
|
|
15
|
+
* @throws Error if not in a git repository
|
|
16
|
+
*/
|
|
17
|
+
export declare function getGitDir(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Get the root directory of the git repository
|
|
20
|
+
* @returns The absolute path to the repository root
|
|
21
|
+
* @throws Error if not in a git repository
|
|
22
|
+
*/
|
|
23
|
+
export declare function getRepositoryRoot(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Get the current branch name
|
|
26
|
+
* @returns The name of the current branch
|
|
27
|
+
* @throws Error if not on a branch (detached HEAD)
|
|
28
|
+
*/
|
|
29
|
+
export declare function getCurrentBranch(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get the commit SHA of HEAD
|
|
32
|
+
* @returns The full SHA of the current commit
|
|
33
|
+
* @throws Error if not in a git repository or HEAD is invalid
|
|
34
|
+
*/
|
|
35
|
+
export declare function getHeadCommitSha(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Get the tree hash of the current HEAD commit
|
|
38
|
+
* @returns The tree hash of HEAD
|
|
39
|
+
* @throws Error if not in a git repository or HEAD is invalid
|
|
40
|
+
*/
|
|
41
|
+
export declare function getHeadTreeSha(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Verify that a git reference exists
|
|
44
|
+
* @param ref - The reference to verify (branch, tag, commit SHA, etc.)
|
|
45
|
+
* @returns true if the reference exists, false otherwise
|
|
46
|
+
*/
|
|
47
|
+
export declare function verifyRef(ref: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Verify that a git reference exists (alternate form for backwards compatibility)
|
|
50
|
+
* @param ref - The reference to verify
|
|
51
|
+
* @returns The SHA of the reference if it exists
|
|
52
|
+
* @throws Error if the reference doesn't exist
|
|
53
|
+
*/
|
|
54
|
+
export declare function verifyRefOrThrow(ref: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Check if git notes exist for a specific ref
|
|
57
|
+
* @param notesRef - The notes reference to check (e.g., 'refs/notes/vibe-validate/validate')
|
|
58
|
+
* @returns true if the notes ref exists, false otherwise
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasNotesRef(notesRef: string): boolean;
|
|
61
|
+
//# sourceMappingURL=git-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-commands.d.ts","sourceRoot":"","sources":["../src/git-commands.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;GAGG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAErD"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Command Utilities
|
|
3
|
+
*
|
|
4
|
+
* High-level git operations built on top of the secure git-executor.
|
|
5
|
+
* These functions provide convenient access to common git commands.
|
|
6
|
+
*/
|
|
7
|
+
import { execGitCommand, tryGitCommand } from './git-executor.js';
|
|
8
|
+
/**
|
|
9
|
+
* Check if the current directory is inside a git repository
|
|
10
|
+
* @returns true if inside a git repository, false otherwise
|
|
11
|
+
*/
|
|
12
|
+
export function isGitRepository() {
|
|
13
|
+
return tryGitCommand(['rev-parse', '--is-inside-work-tree']);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the path to the .git directory
|
|
17
|
+
* @returns The absolute path to the .git directory
|
|
18
|
+
* @throws Error if not in a git repository
|
|
19
|
+
*/
|
|
20
|
+
export function getGitDir() {
|
|
21
|
+
return execGitCommand(['rev-parse', '--git-dir']);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the root directory of the git repository
|
|
25
|
+
* @returns The absolute path to the repository root
|
|
26
|
+
* @throws Error if not in a git repository
|
|
27
|
+
*/
|
|
28
|
+
export function getRepositoryRoot() {
|
|
29
|
+
return execGitCommand(['rev-parse', '--show-toplevel']);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get the current branch name
|
|
33
|
+
* @returns The name of the current branch
|
|
34
|
+
* @throws Error if not on a branch (detached HEAD)
|
|
35
|
+
*/
|
|
36
|
+
export function getCurrentBranch() {
|
|
37
|
+
return execGitCommand(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the commit SHA of HEAD
|
|
41
|
+
* @returns The full SHA of the current commit
|
|
42
|
+
* @throws Error if not in a git repository or HEAD is invalid
|
|
43
|
+
*/
|
|
44
|
+
export function getHeadCommitSha() {
|
|
45
|
+
return execGitCommand(['rev-parse', 'HEAD']);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the tree hash of the current HEAD commit
|
|
49
|
+
* @returns The tree hash of HEAD
|
|
50
|
+
* @throws Error if not in a git repository or HEAD is invalid
|
|
51
|
+
*/
|
|
52
|
+
export function getHeadTreeSha() {
|
|
53
|
+
return execGitCommand(['rev-parse', 'HEAD^{tree}']);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Verify that a git reference exists
|
|
57
|
+
* @param ref - The reference to verify (branch, tag, commit SHA, etc.)
|
|
58
|
+
* @returns true if the reference exists, false otherwise
|
|
59
|
+
*/
|
|
60
|
+
export function verifyRef(ref) {
|
|
61
|
+
return tryGitCommand(['rev-parse', '--verify', ref]);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Verify that a git reference exists (alternate form for backwards compatibility)
|
|
65
|
+
* @param ref - The reference to verify
|
|
66
|
+
* @returns The SHA of the reference if it exists
|
|
67
|
+
* @throws Error if the reference doesn't exist
|
|
68
|
+
*/
|
|
69
|
+
export function verifyRefOrThrow(ref) {
|
|
70
|
+
return execGitCommand(['rev-parse', '--verify', ref]);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if git notes exist for a specific ref
|
|
74
|
+
* @param notesRef - The notes reference to check (e.g., 'refs/notes/vibe-validate/validate')
|
|
75
|
+
* @returns true if the notes ref exists, false otherwise
|
|
76
|
+
*/
|
|
77
|
+
export function hasNotesRef(notesRef) {
|
|
78
|
+
return tryGitCommand(['rev-parse', '--verify', notesRef]);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=git-commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-commands.js","sourceRoot":"","sources":["../src/git-commands.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElE;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,aAAa,CAAC,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,cAAc,CAAC,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,cAAc,CAAC,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,cAAc,CAAC,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,cAAc,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,cAAc,CAAC,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,aAAa,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,cAAc,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,aAAa,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure Git Command Execution
|
|
3
|
+
*
|
|
4
|
+
* This module provides a centralized, secure way to execute git commands.
|
|
5
|
+
* ALL git command execution in vibe-validate MUST go through this module.
|
|
6
|
+
*
|
|
7
|
+
* Security principles:
|
|
8
|
+
* 1. Use spawnSync with array arguments (never string interpolation)
|
|
9
|
+
* 2. Validate all user-controlled inputs
|
|
10
|
+
* 3. No shell piping or heredocs
|
|
11
|
+
* 4. Explicit argument construction
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
export interface GitExecutionOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Maximum time to wait for git command (ms)
|
|
18
|
+
* @default 30000
|
|
19
|
+
*/
|
|
20
|
+
timeout?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Encoding for stdout/stderr
|
|
23
|
+
* @default 'utf8'
|
|
24
|
+
*/
|
|
25
|
+
encoding?: BufferEncoding;
|
|
26
|
+
/**
|
|
27
|
+
* Standard input to pass to command
|
|
28
|
+
*/
|
|
29
|
+
stdin?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether to ignore errors (return empty string instead of throwing)
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
ignoreErrors?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to suppress stderr
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
suppressStderr?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Result of a git command execution
|
|
43
|
+
*/
|
|
44
|
+
export interface GitExecutionResult {
|
|
45
|
+
/** Standard output from the command */
|
|
46
|
+
stdout: string;
|
|
47
|
+
/** Standard error from the command */
|
|
48
|
+
stderr: string;
|
|
49
|
+
/** Exit code (0 for success) */
|
|
50
|
+
exitCode: number;
|
|
51
|
+
/** Whether the command succeeded */
|
|
52
|
+
success: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Error thrown when a git command fails
|
|
56
|
+
*/
|
|
57
|
+
export interface GitCommandError extends Error {
|
|
58
|
+
/** Exit code from the git command */
|
|
59
|
+
exitCode: number;
|
|
60
|
+
/** Standard error output */
|
|
61
|
+
stderr: string;
|
|
62
|
+
/** Standard output */
|
|
63
|
+
stdout: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Execute a git command securely using spawnSync with array arguments
|
|
67
|
+
*
|
|
68
|
+
* This is the ONLY function that should execute git commands. All other
|
|
69
|
+
* git operations must go through this function or higher-level abstractions.
|
|
70
|
+
*
|
|
71
|
+
* @param args - Git command arguments (e.g., ['rev-parse', '--git-dir'])
|
|
72
|
+
* @param options - Execution options
|
|
73
|
+
* @returns Execution result
|
|
74
|
+
* @throws Error if command fails and ignoreErrors is false
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // Get git directory
|
|
79
|
+
* const result = executeGitCommand(['rev-parse', '--git-dir']);
|
|
80
|
+
* console.log(result.stdout); // ".git"
|
|
81
|
+
*
|
|
82
|
+
* // Add note with stdin
|
|
83
|
+
* executeGitCommand(
|
|
84
|
+
* ['notes', '--ref=vibe-validate/validate', 'add', '-f', '-F', '-', treeHash],
|
|
85
|
+
* { stdin: noteContent }
|
|
86
|
+
* );
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare function executeGitCommand(args: string[], options?: GitExecutionOptions): GitExecutionResult;
|
|
90
|
+
/**
|
|
91
|
+
* Execute a git command and return stdout, throwing on error
|
|
92
|
+
*
|
|
93
|
+
* Convenience wrapper for the common case of executing a git command
|
|
94
|
+
* and only caring about the stdout result.
|
|
95
|
+
*
|
|
96
|
+
* @param args - Git command arguments
|
|
97
|
+
* @param options - Execution options
|
|
98
|
+
* @returns Command stdout, trimmed
|
|
99
|
+
* @throws Error if command fails
|
|
100
|
+
*/
|
|
101
|
+
export declare function execGitCommand(args: string[], options?: GitExecutionOptions): string;
|
|
102
|
+
/**
|
|
103
|
+
* Execute a git command and return success status (no throw)
|
|
104
|
+
*
|
|
105
|
+
* Useful for checking if a git operation would succeed without
|
|
106
|
+
* handling exceptions.
|
|
107
|
+
*
|
|
108
|
+
* @param args - Git command arguments
|
|
109
|
+
* @param options - Execution options
|
|
110
|
+
* @returns true if command succeeded, false otherwise
|
|
111
|
+
*/
|
|
112
|
+
export declare function tryGitCommand(args: string[], options?: GitExecutionOptions): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Validate that a string is safe to use as a git ref
|
|
115
|
+
*
|
|
116
|
+
* Git refs must:
|
|
117
|
+
* - Not contain special shell characters
|
|
118
|
+
* - Not start with a dash (looks like an option)
|
|
119
|
+
* - Not contain path traversal sequences
|
|
120
|
+
* - Match git's ref format rules
|
|
121
|
+
*
|
|
122
|
+
* @param ref - The ref to validate
|
|
123
|
+
* @throws Error if ref is invalid
|
|
124
|
+
*/
|
|
125
|
+
export declare function validateGitRef(ref: string): void;
|
|
126
|
+
/**
|
|
127
|
+
* Validate that a string is safe to use as a git notes ref
|
|
128
|
+
*
|
|
129
|
+
* Notes refs have additional restrictions beyond normal refs.
|
|
130
|
+
*
|
|
131
|
+
* @param notesRef - The notes ref to validate
|
|
132
|
+
* @throws Error if notes ref is invalid
|
|
133
|
+
*/
|
|
134
|
+
export declare function validateNotesRef(notesRef: string): void;
|
|
135
|
+
/**
|
|
136
|
+
* Validate that a string is safe to use as a tree hash
|
|
137
|
+
*
|
|
138
|
+
* Tree hashes must be valid git object IDs (40-char hex or abbreviated).
|
|
139
|
+
*
|
|
140
|
+
* @param treeHash - The tree hash to validate
|
|
141
|
+
* @throws Error if tree hash is invalid
|
|
142
|
+
*/
|
|
143
|
+
export declare function validateTreeHash(treeHash: string): void;
|
|
144
|
+
//# sourceMappingURL=git-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-executor.d.ts","sourceRoot":"","sources":["../src/git-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;IAE1B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,KAAK;IAC5C,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAqDpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAGxF;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CA6BhD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAWvD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAcvD"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure Git Command Execution
|
|
3
|
+
*
|
|
4
|
+
* This module provides a centralized, secure way to execute git commands.
|
|
5
|
+
* ALL git command execution in vibe-validate MUST go through this module.
|
|
6
|
+
*
|
|
7
|
+
* Security principles:
|
|
8
|
+
* 1. Use spawnSync with array arguments (never string interpolation)
|
|
9
|
+
* 2. Validate all user-controlled inputs
|
|
10
|
+
* 3. No shell piping or heredocs
|
|
11
|
+
* 4. Explicit argument construction
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
/**
|
|
17
|
+
* Standard options for git command execution
|
|
18
|
+
*/
|
|
19
|
+
const GIT_TIMEOUT = 30000; // 30 seconds
|
|
20
|
+
/**
|
|
21
|
+
* Execute a git command securely using spawnSync with array arguments
|
|
22
|
+
*
|
|
23
|
+
* This is the ONLY function that should execute git commands. All other
|
|
24
|
+
* git operations must go through this function or higher-level abstractions.
|
|
25
|
+
*
|
|
26
|
+
* @param args - Git command arguments (e.g., ['rev-parse', '--git-dir'])
|
|
27
|
+
* @param options - Execution options
|
|
28
|
+
* @returns Execution result
|
|
29
|
+
* @throws Error if command fails and ignoreErrors is false
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Get git directory
|
|
34
|
+
* const result = executeGitCommand(['rev-parse', '--git-dir']);
|
|
35
|
+
* console.log(result.stdout); // ".git"
|
|
36
|
+
*
|
|
37
|
+
* // Add note with stdin
|
|
38
|
+
* executeGitCommand(
|
|
39
|
+
* ['notes', '--ref=vibe-validate/validate', 'add', '-f', '-F', '-', treeHash],
|
|
40
|
+
* { stdin: noteContent }
|
|
41
|
+
* );
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function executeGitCommand(args, options = {}) {
|
|
45
|
+
const { timeout = GIT_TIMEOUT, encoding = 'utf8', stdin, ignoreErrors = false, suppressStderr = false, } = options;
|
|
46
|
+
// Validate arguments
|
|
47
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
48
|
+
throw new Error('Git command arguments must be a non-empty array');
|
|
49
|
+
}
|
|
50
|
+
// Build spawn options
|
|
51
|
+
const spawnOptions = {
|
|
52
|
+
encoding,
|
|
53
|
+
timeout,
|
|
54
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
55
|
+
};
|
|
56
|
+
// Configure stdio
|
|
57
|
+
if (stdin !== undefined) {
|
|
58
|
+
spawnOptions.input = stdin;
|
|
59
|
+
spawnOptions.stdio = ['pipe', 'pipe', suppressStderr ? 'ignore' : 'pipe'];
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
spawnOptions.stdio = ['ignore', 'pipe', suppressStderr ? 'ignore' : 'pipe'];
|
|
63
|
+
}
|
|
64
|
+
// Execute command
|
|
65
|
+
const result = spawnSync('git', args, spawnOptions);
|
|
66
|
+
const stdout = (result.stdout?.toString() || '').trim();
|
|
67
|
+
const stderr = (result.stderr?.toString() || '').trim();
|
|
68
|
+
const exitCode = result.status ?? 1;
|
|
69
|
+
const success = exitCode === 0;
|
|
70
|
+
// Handle errors
|
|
71
|
+
if (success || ignoreErrors) {
|
|
72
|
+
return {
|
|
73
|
+
stdout,
|
|
74
|
+
stderr,
|
|
75
|
+
exitCode,
|
|
76
|
+
success,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const errorMessage = stderr || stdout || 'Git command failed';
|
|
80
|
+
const error = new Error(`Git command failed: git ${args.join(' ')}\n${errorMessage}`);
|
|
81
|
+
error.exitCode = exitCode;
|
|
82
|
+
error.stderr = stderr;
|
|
83
|
+
error.stdout = stdout;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Execute a git command and return stdout, throwing on error
|
|
88
|
+
*
|
|
89
|
+
* Convenience wrapper for the common case of executing a git command
|
|
90
|
+
* and only caring about the stdout result.
|
|
91
|
+
*
|
|
92
|
+
* @param args - Git command arguments
|
|
93
|
+
* @param options - Execution options
|
|
94
|
+
* @returns Command stdout, trimmed
|
|
95
|
+
* @throws Error if command fails
|
|
96
|
+
*/
|
|
97
|
+
export function execGitCommand(args, options = {}) {
|
|
98
|
+
const result = executeGitCommand(args, options);
|
|
99
|
+
return result.stdout;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Execute a git command and return success status (no throw)
|
|
103
|
+
*
|
|
104
|
+
* Useful for checking if a git operation would succeed without
|
|
105
|
+
* handling exceptions.
|
|
106
|
+
*
|
|
107
|
+
* @param args - Git command arguments
|
|
108
|
+
* @param options - Execution options
|
|
109
|
+
* @returns true if command succeeded, false otherwise
|
|
110
|
+
*/
|
|
111
|
+
export function tryGitCommand(args, options = {}) {
|
|
112
|
+
const result = executeGitCommand(args, { ...options, ignoreErrors: true });
|
|
113
|
+
return result.success;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Validate that a string is safe to use as a git ref
|
|
117
|
+
*
|
|
118
|
+
* Git refs must:
|
|
119
|
+
* - Not contain special shell characters
|
|
120
|
+
* - Not start with a dash (looks like an option)
|
|
121
|
+
* - Not contain path traversal sequences
|
|
122
|
+
* - Match git's ref format rules
|
|
123
|
+
*
|
|
124
|
+
* @param ref - The ref to validate
|
|
125
|
+
* @throws Error if ref is invalid
|
|
126
|
+
*/
|
|
127
|
+
export function validateGitRef(ref) {
|
|
128
|
+
if (typeof ref !== 'string' || ref.length === 0) {
|
|
129
|
+
throw new Error('Git ref must be a non-empty string');
|
|
130
|
+
}
|
|
131
|
+
// Check for shell special characters
|
|
132
|
+
if (/[;&|`$(){}[\]<>!\\"]/.test(ref)) {
|
|
133
|
+
throw new Error(`Invalid git ref: contains shell special characters: ${ref}`);
|
|
134
|
+
}
|
|
135
|
+
// Check for leading dash (looks like an option)
|
|
136
|
+
if (ref.startsWith('-')) {
|
|
137
|
+
throw new Error(`Invalid git ref: starts with dash: ${ref}`);
|
|
138
|
+
}
|
|
139
|
+
// Check for path traversal
|
|
140
|
+
if (ref.includes('..') || ref.includes('//')) {
|
|
141
|
+
throw new Error(`Invalid git ref: contains path traversal: ${ref}`);
|
|
142
|
+
}
|
|
143
|
+
// Check for null bytes
|
|
144
|
+
if (ref.includes('\0')) {
|
|
145
|
+
throw new Error('Invalid git ref: contains null byte');
|
|
146
|
+
}
|
|
147
|
+
// Check for newlines (could break command)
|
|
148
|
+
if (ref.includes('\n') || ref.includes('\r')) {
|
|
149
|
+
throw new Error('Invalid git ref: contains newline');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Validate that a string is safe to use as a git notes ref
|
|
154
|
+
*
|
|
155
|
+
* Notes refs have additional restrictions beyond normal refs.
|
|
156
|
+
*
|
|
157
|
+
* @param notesRef - The notes ref to validate
|
|
158
|
+
* @throws Error if notes ref is invalid
|
|
159
|
+
*/
|
|
160
|
+
export function validateNotesRef(notesRef) {
|
|
161
|
+
validateGitRef(notesRef);
|
|
162
|
+
// Notes refs should follow refs/notes/* pattern or short form
|
|
163
|
+
// Short form: 'vibe-validate/validate' → 'refs/notes/vibe-validate/validate'
|
|
164
|
+
if (!notesRef.startsWith('refs/notes/') && notesRef.includes('/')) {
|
|
165
|
+
// Short form is valid, but must not contain spaces
|
|
166
|
+
if (/\s/.test(notesRef)) {
|
|
167
|
+
throw new Error(`Invalid notes ref: contains whitespace: ${notesRef}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Validate that a string is safe to use as a tree hash
|
|
173
|
+
*
|
|
174
|
+
* Tree hashes must be valid git object IDs (40-char hex or abbreviated).
|
|
175
|
+
*
|
|
176
|
+
* @param treeHash - The tree hash to validate
|
|
177
|
+
* @throws Error if tree hash is invalid
|
|
178
|
+
*/
|
|
179
|
+
export function validateTreeHash(treeHash) {
|
|
180
|
+
if (typeof treeHash !== 'string' || treeHash.length === 0) {
|
|
181
|
+
throw new Error('Tree hash must be a non-empty string');
|
|
182
|
+
}
|
|
183
|
+
// Must be hex characters only
|
|
184
|
+
if (!/^[0-9a-f]+$/.test(treeHash)) {
|
|
185
|
+
throw new Error(`Invalid tree hash: must be hexadecimal: ${treeHash}`);
|
|
186
|
+
}
|
|
187
|
+
// Must be reasonable length (4-40 chars for abbreviated or full hash)
|
|
188
|
+
if (treeHash.length < 4 || treeHash.length > 40) {
|
|
189
|
+
throw new Error(`Invalid tree hash: invalid length: ${treeHash}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=git-executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-executor.js","sourceRoot":"","sources":["../src/git-executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Notes Operations
|
|
3
|
+
*
|
|
4
|
+
* High-level abstraction for git notes operations. All notes-related
|
|
5
|
+
* commands in vibe-validate must use these functions.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Add or update a git note
|
|
11
|
+
*
|
|
12
|
+
* @param notesRef - The notes reference (e.g., 'vibe-validate/validate')
|
|
13
|
+
* @param object - The git object to attach the note to (tree hash, commit SHA, etc.)
|
|
14
|
+
* @param content - The note content
|
|
15
|
+
* @param force - Whether to overwrite existing note
|
|
16
|
+
* @returns true if note was added successfully
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* addNote('vibe-validate/validate', treeHash, noteContent, true);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function addNote(notesRef: string, object: string, content: string, force?: boolean): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Read a git note
|
|
26
|
+
*
|
|
27
|
+
* @param notesRef - The notes reference
|
|
28
|
+
* @param object - The git object to read the note from
|
|
29
|
+
* @returns The note content, or null if no note exists
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const note = readNote('vibe-validate/validate', treeHash);
|
|
34
|
+
* if (note) {
|
|
35
|
+
* console.log('Note content:', note);
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function readNote(notesRef: string, object: string): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Remove a git note
|
|
42
|
+
*
|
|
43
|
+
* @param notesRef - The notes reference
|
|
44
|
+
* @param object - The git object to remove the note from
|
|
45
|
+
* @returns true if note was removed, false if it didn't exist
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const removed = removeNote('vibe-validate/validate', treeHash);
|
|
50
|
+
* console.log(removed ? 'Removed' : 'Did not exist');
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function removeNote(notesRef: string, object: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* List all notes in a notes reference
|
|
56
|
+
*
|
|
57
|
+
* @param notesRef - The notes reference
|
|
58
|
+
* @returns Array of [object, content] pairs, or empty array if no notes
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const notes = listNotes('vibe-validate/validate');
|
|
63
|
+
* for (const [treeHash, content] of notes) {
|
|
64
|
+
* console.log(`${treeHash}: ${content}`);
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function listNotes(notesRef: string): Array<[string, string]>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if a note exists
|
|
71
|
+
*
|
|
72
|
+
* @param notesRef - The notes reference
|
|
73
|
+
* @param object - The git object to check
|
|
74
|
+
* @returns true if note exists, false otherwise
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* if (hasNote('vibe-validate/validate', treeHash)) {
|
|
79
|
+
* console.log('Note exists');
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function hasNote(notesRef: string, object: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* List all refs under a notes namespace
|
|
86
|
+
*
|
|
87
|
+
* This is useful for finding all notes under a particular path,
|
|
88
|
+
* such as all run cache entries under refs/notes/vibe-validate/run/
|
|
89
|
+
*
|
|
90
|
+
* @param notesPath - The notes path (e.g., 'refs/notes/vibe-validate/run')
|
|
91
|
+
* @returns Array of full ref names
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const refs = listNotesRefs('refs/notes/vibe-validate/run');
|
|
96
|
+
* for (const ref of refs) {
|
|
97
|
+
* console.log('Found note ref:', ref);
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function listNotesRefs(notesPath: string): string[];
|
|
102
|
+
/**
|
|
103
|
+
* Remove all notes refs under a namespace
|
|
104
|
+
*
|
|
105
|
+
* This is used for bulk cleanup operations, such as pruning all
|
|
106
|
+
* run cache entries under a tree hash.
|
|
107
|
+
*
|
|
108
|
+
* SECURITY: This function validates each ref before deletion to prevent
|
|
109
|
+
* accidental deletion of non-notes refs.
|
|
110
|
+
*
|
|
111
|
+
* @param notesPath - The notes path (e.g., 'refs/notes/vibe-validate/run/abc123')
|
|
112
|
+
* @returns Number of refs deleted
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const deleted = removeNotesRefs('refs/notes/vibe-validate/run/abc123');
|
|
117
|
+
* console.log(`Deleted ${deleted} refs`);
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export declare function removeNotesRefs(notesPath: string): number;
|
|
121
|
+
/**
|
|
122
|
+
* Check if a notes ref exists
|
|
123
|
+
*
|
|
124
|
+
* @param notesRef - The notes reference
|
|
125
|
+
* @returns true if the notes ref exists, false otherwise
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* if (hasNotesRef('vibe-validate/validate')) {
|
|
130
|
+
* console.log('Notes ref exists');
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export declare function hasNotesRef(notesRef: string): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Get the commit SHA that a notes ref points to
|
|
137
|
+
*
|
|
138
|
+
* @param notesRef - The notes reference
|
|
139
|
+
* @returns The commit SHA, or null if ref doesn't exist
|
|
140
|
+
*/
|
|
141
|
+
export declare function getNotesRefSha(notesRef: string): string | null;
|
|
142
|
+
//# sourceMappingURL=git-notes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-notes.d.ts","sourceRoot":"","sources":["../src/git-notes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,OAAe,GACrB,OAAO,CAiBT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUxE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOpE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA4BnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAOjE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAczD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAgCzD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAWrD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa9D"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Notes Operations
|
|
3
|
+
*
|
|
4
|
+
* High-level abstraction for git notes operations. All notes-related
|
|
5
|
+
* commands in vibe-validate must use these functions.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { executeGitCommand, tryGitCommand, validateNotesRef, validateTreeHash, } from './git-executor.js';
|
|
10
|
+
/**
|
|
11
|
+
* Add or update a git note
|
|
12
|
+
*
|
|
13
|
+
* @param notesRef - The notes reference (e.g., 'vibe-validate/validate')
|
|
14
|
+
* @param object - The git object to attach the note to (tree hash, commit SHA, etc.)
|
|
15
|
+
* @param content - The note content
|
|
16
|
+
* @param force - Whether to overwrite existing note
|
|
17
|
+
* @returns true if note was added successfully
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* addNote('vibe-validate/validate', treeHash, noteContent, true);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function addNote(notesRef, object, content, force = false) {
|
|
25
|
+
validateNotesRef(notesRef);
|
|
26
|
+
validateTreeHash(object);
|
|
27
|
+
const args = ['notes', `--ref=${notesRef}`, 'add'];
|
|
28
|
+
if (force) {
|
|
29
|
+
args.push('-f');
|
|
30
|
+
}
|
|
31
|
+
args.push('-F', '-', object);
|
|
32
|
+
const result = executeGitCommand(args, {
|
|
33
|
+
stdin: content,
|
|
34
|
+
ignoreErrors: true,
|
|
35
|
+
suppressStderr: true,
|
|
36
|
+
});
|
|
37
|
+
return result.success;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read a git note
|
|
41
|
+
*
|
|
42
|
+
* @param notesRef - The notes reference
|
|
43
|
+
* @param object - The git object to read the note from
|
|
44
|
+
* @returns The note content, or null if no note exists
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const note = readNote('vibe-validate/validate', treeHash);
|
|
49
|
+
* if (note) {
|
|
50
|
+
* console.log('Note content:', note);
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function readNote(notesRef, object) {
|
|
55
|
+
validateNotesRef(notesRef);
|
|
56
|
+
validateTreeHash(object);
|
|
57
|
+
const result = executeGitCommand(['notes', `--ref=${notesRef}`, 'show', object], {
|
|
58
|
+
ignoreErrors: true,
|
|
59
|
+
suppressStderr: true,
|
|
60
|
+
});
|
|
61
|
+
return result.success ? result.stdout : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Remove a git note
|
|
65
|
+
*
|
|
66
|
+
* @param notesRef - The notes reference
|
|
67
|
+
* @param object - The git object to remove the note from
|
|
68
|
+
* @returns true if note was removed, false if it didn't exist
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const removed = removeNote('vibe-validate/validate', treeHash);
|
|
73
|
+
* console.log(removed ? 'Removed' : 'Did not exist');
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function removeNote(notesRef, object) {
|
|
77
|
+
validateNotesRef(notesRef);
|
|
78
|
+
validateTreeHash(object);
|
|
79
|
+
return tryGitCommand(['notes', `--ref=${notesRef}`, 'remove', object], {
|
|
80
|
+
suppressStderr: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* List all notes in a notes reference
|
|
85
|
+
*
|
|
86
|
+
* @param notesRef - The notes reference
|
|
87
|
+
* @returns Array of [object, content] pairs, or empty array if no notes
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const notes = listNotes('vibe-validate/validate');
|
|
92
|
+
* for (const [treeHash, content] of notes) {
|
|
93
|
+
* console.log(`${treeHash}: ${content}`);
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function listNotes(notesRef) {
|
|
98
|
+
validateNotesRef(notesRef);
|
|
99
|
+
// Get list of objects that have notes
|
|
100
|
+
const objectsResult = executeGitCommand(['notes', `--ref=${notesRef}`, 'list'], {
|
|
101
|
+
ignoreErrors: true,
|
|
102
|
+
suppressStderr: true,
|
|
103
|
+
});
|
|
104
|
+
if (!objectsResult.success || !objectsResult.stdout) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const notes = [];
|
|
108
|
+
// Parse "note_sha object_sha" pairs
|
|
109
|
+
for (const line of objectsResult.stdout.split('\n')) {
|
|
110
|
+
const [, objectSha] = line.split(/\s+/);
|
|
111
|
+
if (!objectSha)
|
|
112
|
+
continue;
|
|
113
|
+
// Read the note content
|
|
114
|
+
const content = readNote(notesRef, objectSha);
|
|
115
|
+
if (content !== null) {
|
|
116
|
+
notes.push([objectSha, content]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return notes;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Check if a note exists
|
|
123
|
+
*
|
|
124
|
+
* @param notesRef - The notes reference
|
|
125
|
+
* @param object - The git object to check
|
|
126
|
+
* @returns true if note exists, false otherwise
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* if (hasNote('vibe-validate/validate', treeHash)) {
|
|
131
|
+
* console.log('Note exists');
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function hasNote(notesRef, object) {
|
|
136
|
+
validateNotesRef(notesRef);
|
|
137
|
+
validateTreeHash(object);
|
|
138
|
+
return tryGitCommand(['notes', `--ref=${notesRef}`, 'show', object], {
|
|
139
|
+
suppressStderr: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* List all refs under a notes namespace
|
|
144
|
+
*
|
|
145
|
+
* This is useful for finding all notes under a particular path,
|
|
146
|
+
* such as all run cache entries under refs/notes/vibe-validate/run/
|
|
147
|
+
*
|
|
148
|
+
* @param notesPath - The notes path (e.g., 'refs/notes/vibe-validate/run')
|
|
149
|
+
* @returns Array of full ref names
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const refs = listNotesRefs('refs/notes/vibe-validate/run');
|
|
154
|
+
* for (const ref of refs) {
|
|
155
|
+
* console.log('Found note ref:', ref);
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export function listNotesRefs(notesPath) {
|
|
160
|
+
// Normalize path
|
|
161
|
+
const fullPath = notesPath.startsWith('refs/') ? notesPath : `refs/notes/${notesPath}`;
|
|
162
|
+
const result = executeGitCommand(['for-each-ref', '--format=%(refname)', fullPath], { ignoreErrors: true, suppressStderr: true });
|
|
163
|
+
if (!result.success || !result.stdout) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return result.stdout.split('\n').filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Remove all notes refs under a namespace
|
|
170
|
+
*
|
|
171
|
+
* This is used for bulk cleanup operations, such as pruning all
|
|
172
|
+
* run cache entries under a tree hash.
|
|
173
|
+
*
|
|
174
|
+
* SECURITY: This function validates each ref before deletion to prevent
|
|
175
|
+
* accidental deletion of non-notes refs.
|
|
176
|
+
*
|
|
177
|
+
* @param notesPath - The notes path (e.g., 'refs/notes/vibe-validate/run/abc123')
|
|
178
|
+
* @returns Number of refs deleted
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const deleted = removeNotesRefs('refs/notes/vibe-validate/run/abc123');
|
|
183
|
+
* console.log(`Deleted ${deleted} refs`);
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function removeNotesRefs(notesPath) {
|
|
187
|
+
// Normalize path
|
|
188
|
+
const fullPath = notesPath.startsWith('refs/') ? notesPath : `refs/notes/${notesPath}`;
|
|
189
|
+
// Security: Only allow deletion under refs/notes/vibe-validate/
|
|
190
|
+
if (!fullPath.startsWith('refs/notes/vibe-validate/')) {
|
|
191
|
+
throw new Error(`Refusing to delete refs outside vibe-validate namespace: ${fullPath}`);
|
|
192
|
+
}
|
|
193
|
+
// Get list of refs
|
|
194
|
+
const refs = listNotesRefs(fullPath);
|
|
195
|
+
let deleted = 0;
|
|
196
|
+
for (const ref of refs) {
|
|
197
|
+
// Double-check each ref is under the expected namespace
|
|
198
|
+
if (!ref.startsWith('refs/notes/vibe-validate/')) {
|
|
199
|
+
console.warn(`Skipping ref outside vibe-validate namespace: ${ref}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const success = tryGitCommand(['update-ref', '-d', ref], {
|
|
203
|
+
suppressStderr: true,
|
|
204
|
+
});
|
|
205
|
+
if (success) {
|
|
206
|
+
deleted++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return deleted;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Check if a notes ref exists
|
|
213
|
+
*
|
|
214
|
+
* @param notesRef - The notes reference
|
|
215
|
+
* @returns true if the notes ref exists, false otherwise
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* if (hasNotesRef('vibe-validate/validate')) {
|
|
220
|
+
* console.log('Notes ref exists');
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export function hasNotesRef(notesRef) {
|
|
225
|
+
validateNotesRef(notesRef);
|
|
226
|
+
// Convert short form to full form if needed
|
|
227
|
+
const fullRef = notesRef.startsWith('refs/')
|
|
228
|
+
? notesRef
|
|
229
|
+
: `refs/notes/${notesRef}`;
|
|
230
|
+
return tryGitCommand(['rev-parse', '--verify', fullRef], {
|
|
231
|
+
suppressStderr: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get the commit SHA that a notes ref points to
|
|
236
|
+
*
|
|
237
|
+
* @param notesRef - The notes reference
|
|
238
|
+
* @returns The commit SHA, or null if ref doesn't exist
|
|
239
|
+
*/
|
|
240
|
+
export function getNotesRefSha(notesRef) {
|
|
241
|
+
validateNotesRef(notesRef);
|
|
242
|
+
const fullRef = notesRef.startsWith('refs/')
|
|
243
|
+
? notesRef
|
|
244
|
+
: `refs/notes/${notesRef}`;
|
|
245
|
+
const result = executeGitCommand(['rev-parse', '--verify', fullRef], {
|
|
246
|
+
ignoreErrors: true,
|
|
247
|
+
suppressStderr: true,
|
|
248
|
+
});
|
|
249
|
+
return result.success ? result.stdout : null;
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=git-notes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-notes.js","sourceRoot":"","sources":["../src/git-notes.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,9 @@ export { BranchSyncChecker, checkBranchSync, type SyncCheckResult, type SyncChec
|
|
|
11
11
|
export { PostPRMergeCleanup, cleanupMergedBranches, type CleanupResult, type CleanupOptions } from './post-merge-cleanup.js';
|
|
12
12
|
export { encodeRunCacheKey } from './cache-key.js';
|
|
13
13
|
export { extractYamlContent, extractYamlWithPreamble } from './yaml-detection.js';
|
|
14
|
+
export { isGitRepository, getGitDir, getRepositoryRoot, getCurrentBranch, getHeadCommitSha, getHeadTreeSha, verifyRef, verifyRefOrThrow, hasNotesRef } from './git-commands.js';
|
|
15
|
+
export { executeGitCommand, execGitCommand, tryGitCommand, validateGitRef, validateNotesRef, validateTreeHash, type GitExecutionOptions, type GitExecutionResult } from './git-executor.js';
|
|
16
|
+
export { addNote, readNote, removeNote, listNotes, hasNote, listNotesRefs, removeNotesRefs, getNotesRefSha } from './git-notes.js';
|
|
17
|
+
export { getPartiallyStagedFiles } from './staging.js';
|
|
18
|
+
export { isCurrentBranchBehindTracking } from './tracking-branch.js';
|
|
14
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACtB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACtB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,OAAO,EACP,QAAQ,EACR,UAAU,EACV,SAAS,EACT,OAAO,EACP,aAAa,EACb,eAAe,EACf,cAAc,EACf,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,uBAAuB,EACxB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,6BAA6B,EAC9B,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -16,4 +16,14 @@ export { PostPRMergeCleanup, cleanupMergedBranches } from './post-merge-cleanup.
|
|
|
16
16
|
export { encodeRunCacheKey } from './cache-key.js';
|
|
17
17
|
// YAML output detection
|
|
18
18
|
export { extractYamlContent, extractYamlWithPreamble } from './yaml-detection.js';
|
|
19
|
+
// Git command utilities (standardized rev-parse operations)
|
|
20
|
+
export { isGitRepository, getGitDir, getRepositoryRoot, getCurrentBranch, getHeadCommitSha, getHeadTreeSha, verifyRef, verifyRefOrThrow, hasNotesRef } from './git-commands.js';
|
|
21
|
+
// Secure git command execution (low-level - use high-level APIs when possible)
|
|
22
|
+
export { executeGitCommand, execGitCommand, tryGitCommand, validateGitRef, validateNotesRef, validateTreeHash } from './git-executor.js';
|
|
23
|
+
// Git notes operations (high-level abstraction)
|
|
24
|
+
export { addNote, readNote, removeNote, listNotes, hasNote, listNotesRefs, removeNotesRefs, getNotesRefSha } from './git-notes.js';
|
|
25
|
+
// Git staging detection (prevent partially staged files in pre-commit)
|
|
26
|
+
export { getPartiallyStagedFiles } from './staging.js';
|
|
27
|
+
// Git tracking branch detection (check if current branch is behind remote)
|
|
28
|
+
export { isCurrentBranchBehindTracking } from './tracking-branch.js';
|
|
19
29
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uDAAuD;AACvD,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACtB,MAAM,gBAAgB,CAAC;AAExB,6CAA6C;AAC7C,OAAO,EACL,iBAAiB,EACjB,eAAe,EAGhB,MAAM,kBAAkB,CAAC;AAE1B,8CAA8C;AAC9C,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EAGtB,MAAM,yBAAyB,CAAC;AAEjC,qCAAqC;AACrC,OAAO,EACL,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAExB,wBAAwB;AACxB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uDAAuD;AACvD,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACtB,MAAM,gBAAgB,CAAC;AAExB,6CAA6C;AAC7C,OAAO,EACL,iBAAiB,EACjB,eAAe,EAGhB,MAAM,kBAAkB,CAAC;AAE1B,8CAA8C;AAC9C,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EAGtB,MAAM,yBAAyB,CAAC;AAEjC,qCAAqC;AACrC,OAAO,EACL,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAExB,wBAAwB;AACxB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAE7B,4DAA4D;AAC5D,OAAO,EACL,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAGjB,MAAM,mBAAmB,CAAC;AAE3B,gDAAgD;AAChD,OAAO,EACL,OAAO,EACP,QAAQ,EACR,UAAU,EACV,SAAS,EACT,OAAO,EACP,aAAa,EACb,eAAe,EACf,cAAc,EACf,MAAM,gBAAgB,CAAC;AAExB,uEAAuE;AACvE,OAAO,EACL,uBAAuB,EACxB,MAAM,cAAc,CAAC;AAEtB,2EAA2E;AAC3E,OAAO,EACL,6BAA6B,EAC9B,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Staging Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects partially staged files to prevent validation mismatches.
|
|
5
|
+
*
|
|
6
|
+
* ## The Problem
|
|
7
|
+
*
|
|
8
|
+
* If a file has BOTH staged and unstaged changes:
|
|
9
|
+
* 1. vibe-validate validates the FULL working tree state (staged + unstaged)
|
|
10
|
+
* 2. git commit only commits the STAGED portion
|
|
11
|
+
* 3. Result: Validated code != committed code
|
|
12
|
+
*
|
|
13
|
+
* ## Solution
|
|
14
|
+
*
|
|
15
|
+
* Pre-commit hook must detect and block partially staged files.
|
|
16
|
+
* Users can:
|
|
17
|
+
* - Stage all changes: `git add <file>`
|
|
18
|
+
* - Unstage all changes: `git restore --staged <file>`
|
|
19
|
+
* - Skip validation: `git commit --no-verify` (not recommended)
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Get list of files with partially staged changes
|
|
23
|
+
*
|
|
24
|
+
* A file is "partially staged" if it has BOTH:
|
|
25
|
+
* - Changes in the staging area (git diff --cached)
|
|
26
|
+
* - Changes in the working tree (git diff)
|
|
27
|
+
*
|
|
28
|
+
* This indicates the user staged some changes but not others,
|
|
29
|
+
* which is incompatible with validation.
|
|
30
|
+
*
|
|
31
|
+
* @returns Array of file paths with partially staged changes, empty if none
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const files = getPartiallyStagedFiles();
|
|
36
|
+
* if (files.length > 0) {
|
|
37
|
+
* console.error('Partially staged files detected:', files);
|
|
38
|
+
* console.error('Stage all changes with: git add ' + files.join(' '));
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function getPartiallyStagedFiles(): string[];
|
|
43
|
+
//# sourceMappingURL=staging.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../src/staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAWH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAkClD"}
|
package/dist/staging.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Staging Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects partially staged files to prevent validation mismatches.
|
|
5
|
+
*
|
|
6
|
+
* ## The Problem
|
|
7
|
+
*
|
|
8
|
+
* If a file has BOTH staged and unstaged changes:
|
|
9
|
+
* 1. vibe-validate validates the FULL working tree state (staged + unstaged)
|
|
10
|
+
* 2. git commit only commits the STAGED portion
|
|
11
|
+
* 3. Result: Validated code != committed code
|
|
12
|
+
*
|
|
13
|
+
* ## Solution
|
|
14
|
+
*
|
|
15
|
+
* Pre-commit hook must detect and block partially staged files.
|
|
16
|
+
* Users can:
|
|
17
|
+
* - Stage all changes: `git add <file>`
|
|
18
|
+
* - Unstage all changes: `git restore --staged <file>`
|
|
19
|
+
* - Skip validation: `git commit --no-verify` (not recommended)
|
|
20
|
+
*/
|
|
21
|
+
import { execSync } from 'node:child_process';
|
|
22
|
+
const GIT_TIMEOUT = 30000;
|
|
23
|
+
const GIT_OPTIONS = {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
timeout: GIT_TIMEOUT,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Get list of files with partially staged changes
|
|
30
|
+
*
|
|
31
|
+
* A file is "partially staged" if it has BOTH:
|
|
32
|
+
* - Changes in the staging area (git diff --cached)
|
|
33
|
+
* - Changes in the working tree (git diff)
|
|
34
|
+
*
|
|
35
|
+
* This indicates the user staged some changes but not others,
|
|
36
|
+
* which is incompatible with validation.
|
|
37
|
+
*
|
|
38
|
+
* @returns Array of file paths with partially staged changes, empty if none
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const files = getPartiallyStagedFiles();
|
|
43
|
+
* if (files.length > 0) {
|
|
44
|
+
* console.error('Partially staged files detected:', files);
|
|
45
|
+
* console.error('Stage all changes with: git add ' + files.join(' '));
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function getPartiallyStagedFiles() {
|
|
50
|
+
try {
|
|
51
|
+
// Get list of files with staged changes
|
|
52
|
+
const stagedOutput = execSync('git diff --name-only --cached', GIT_OPTIONS);
|
|
53
|
+
const stagedFiles = stagedOutput
|
|
54
|
+
.trim()
|
|
55
|
+
.split('\n')
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
// No staged files = no partially staged files
|
|
58
|
+
if (stagedFiles.length === 0) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
// Get list of files with unstaged changes
|
|
62
|
+
const unstagedOutput = execSync('git diff --name-only', GIT_OPTIONS);
|
|
63
|
+
const unstagedFiles = new Set(unstagedOutput
|
|
64
|
+
.trim()
|
|
65
|
+
.split('\n')
|
|
66
|
+
.filter(Boolean));
|
|
67
|
+
// Find intersection: files that appear in BOTH staged and unstaged
|
|
68
|
+
const partiallyStagedFiles = stagedFiles.filter((file) => unstagedFiles.has(file));
|
|
69
|
+
return partiallyStagedFiles;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Not a git repository, or git command failed
|
|
73
|
+
// Return empty array - let pre-commit continue and fail elsewhere if needed
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=staging.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staging.js","sourceRoot":"","sources":["../src/staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,WAAW,GAAG,KAAK,CAAC;AAC1B,MAAM,WAAW,GAAG;IAClB,QAAQ,EAAE,MAAe;IACzB,OAAO,EAAE,WAAW;IACpB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAA+B;CAChE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB;IACrC,IAAI,CAAC;QACH,wCAAwC;QACxC,MAAM,YAAY,GAAG,QAAQ,CAAC,+BAA+B,EAAE,WAAW,CAAC,CAAC;QAC5E,MAAM,WAAW,GAAG,YAAY;aAC7B,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,QAAQ,CAAC,sBAAsB,EAAE,WAAW,CAAC,CAAC;QACrE,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,cAAc;aACX,IAAI,EAAE;aACN,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC,CACnB,CAAC;QAEF,mEAAmE;QACnE,MAAM,oBAAoB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACvD,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CACxB,CAAC;QAEF,OAAO,oBAAoB,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;QAC9C,4EAA4E;QAC5E,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Tracking Branch Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects if current branch is behind its remote tracking branch.
|
|
5
|
+
*
|
|
6
|
+
* ## The Problem
|
|
7
|
+
*
|
|
8
|
+
* If you're working on local `fix-issue-X` and someone else pushes to `origin/fix-issue-X`:
|
|
9
|
+
* 1. Your local branch is now behind the remote tracking branch
|
|
10
|
+
* 2. If you commit and push, you may create conflicts or lose their changes
|
|
11
|
+
* 3. You should pull/merge before committing
|
|
12
|
+
*
|
|
13
|
+
* ## Solution
|
|
14
|
+
*
|
|
15
|
+
* Pre-commit hook should detect and warn when current branch is behind its tracking branch.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Check if current branch is behind its remote tracking branch
|
|
19
|
+
*
|
|
20
|
+
* @returns Number of commits behind (0 = up to date), or null if no tracking branch
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const behindBy = isCurrentBranchBehindTracking();
|
|
25
|
+
* if (behindBy === null) {
|
|
26
|
+
* console.log('No remote tracking branch');
|
|
27
|
+
* } else if (behindBy > 0) {
|
|
28
|
+
* console.error(`Behind by ${behindBy} commit(s)`);
|
|
29
|
+
* console.error('Pull changes with: git pull');
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function isCurrentBranchBehindTracking(): number | null;
|
|
34
|
+
//# sourceMappingURL=tracking-branch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracking-branch.d.ts","sourceRoot":"","sources":["../src/tracking-branch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,6BAA6B,IAAI,MAAM,GAAG,IAAI,CAmC7D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Tracking Branch Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects if current branch is behind its remote tracking branch.
|
|
5
|
+
*
|
|
6
|
+
* ## The Problem
|
|
7
|
+
*
|
|
8
|
+
* If you're working on local `fix-issue-X` and someone else pushes to `origin/fix-issue-X`:
|
|
9
|
+
* 1. Your local branch is now behind the remote tracking branch
|
|
10
|
+
* 2. If you commit and push, you may create conflicts or lose their changes
|
|
11
|
+
* 3. You should pull/merge before committing
|
|
12
|
+
*
|
|
13
|
+
* ## Solution
|
|
14
|
+
*
|
|
15
|
+
* Pre-commit hook should detect and warn when current branch is behind its tracking branch.
|
|
16
|
+
*/
|
|
17
|
+
import { execSync } from 'node:child_process';
|
|
18
|
+
const GIT_TIMEOUT = 30000;
|
|
19
|
+
const GIT_OPTIONS = {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
timeout: GIT_TIMEOUT,
|
|
22
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Check if current branch is behind its remote tracking branch
|
|
26
|
+
*
|
|
27
|
+
* @returns Number of commits behind (0 = up to date), or null if no tracking branch
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const behindBy = isCurrentBranchBehindTracking();
|
|
32
|
+
* if (behindBy === null) {
|
|
33
|
+
* console.log('No remote tracking branch');
|
|
34
|
+
* } else if (behindBy > 0) {
|
|
35
|
+
* console.error(`Behind by ${behindBy} commit(s)`);
|
|
36
|
+
* console.error('Pull changes with: git pull');
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function isCurrentBranchBehindTracking() {
|
|
41
|
+
try {
|
|
42
|
+
// Get the upstream tracking branch for current branch
|
|
43
|
+
// Example output: "origin/fix-issue-X"
|
|
44
|
+
// Throws error if no upstream configured
|
|
45
|
+
const trackingBranch = execSync('git rev-parse --abbrev-ref --symbolic-full-name @{u}', GIT_OPTIONS).trim();
|
|
46
|
+
if (!trackingBranch) {
|
|
47
|
+
// No tracking branch
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// Count commits behind: HEAD..@{u}
|
|
51
|
+
// This shows commits in tracking branch that are not in HEAD
|
|
52
|
+
const behindOutput = execSync('git rev-list --count HEAD..@{u}', GIT_OPTIONS).trim();
|
|
53
|
+
const behindCount = Number.parseInt(behindOutput, 10);
|
|
54
|
+
// Return 0 if parsing failed (defensive)
|
|
55
|
+
return Number.isNaN(behindCount) ? 0 : behindCount;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// Check if error is "no upstream configured" (expected for new branches)
|
|
59
|
+
if (error instanceof Error && error.message.includes('no upstream')) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
// Other errors (not in git repo, etc.) - return null
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=tracking-branch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracking-branch.js","sourceRoot":"","sources":["../src/tracking-branch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,WAAW,GAAG,KAAK,CAAC;AAC1B,MAAM,WAAW,GAAG;IAClB,QAAQ,EAAE,MAAe;IACzB,OAAO,EAAE,WAAW;IACpB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAA+B;CAChE,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,6BAA6B;IAC3C,IAAI,CAAC;QACH,sDAAsD;QACtD,uCAAuC;QACvC,yCAAyC;QACzC,MAAM,cAAc,GAAG,QAAQ,CAC7B,sDAAsD,EACtD,WAAW,CACZ,CAAC,IAAI,EAAE,CAAC;QAET,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,qBAAqB;YACrB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,mCAAmC;QACnC,6DAA6D;QAC7D,MAAM,YAAY,GAAG,QAAQ,CAC3B,iCAAiC,EACjC,WAAW,CACZ,CAAC,IAAI,EAAE,CAAC;QAET,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QAEtD,yCAAyC;QACzC,OAAO,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,yEAAyE;QACzE,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YACpE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,qDAAqD;QACrD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yaml-detection.d.ts","sourceRoot":"","sources":["../src/yaml-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"yaml-detection.d.ts","sourceRoot":"","sources":["../src/yaml-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgCH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAWjG"}
|
package/dist/yaml-detection.js
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
* - `"---\ntitle: Post\n---\nContent"` → matches, captures `"---\ntitle: Post\n"` (stops at trailing `---`)
|
|
44
44
|
* - `"no yaml"` → no match
|
|
45
45
|
*/
|
|
46
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- Safe: Parses controlled YAML frontmatter from command output (not user input), no ReDoS risk
|
|
46
47
|
const YAML_OUTPUT_PATTERN = /(?:^|\r?\n)(---\r?\n(?:(?!---(?:\r?\n|$))[\s\S])*)/;
|
|
47
48
|
/**
|
|
48
49
|
* Extract YAML output content from command output
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yaml-detection.js","sourceRoot":"","sources":["../src/yaml-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,mBAAmB,GAAG,oDAAoD,CAAC;AAEjF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,cAAc,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC5E,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,IAAI,EAAE,CAAC;IAE5D,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AACzC,CAAC"}
|
|
1
|
+
{"version":3,"file":"yaml-detection.js","sourceRoot":"","sources":["../src/yaml-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wJAAwJ;AACxJ,MAAM,mBAAmB,GAAG,oDAAoD,CAAC;AAEjF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,cAAc,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC5E,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,IAAI,EAAE,CAAC;IAE5D,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AACzC,CAAC"}
|
package/package.json
CHANGED