@vibe-validate/git 0.9.0

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 ADDED
@@ -0,0 +1,136 @@
1
+ # @vibe-validate/git
2
+
3
+ Git utilities for vibe-validate - deterministic tree hash calculation, branch synchronization, and post-merge cleanup.
4
+
5
+ ## Features
6
+
7
+ - **Deterministic Git Tree Hash**: Content-based hashing using `git write-tree` (no timestamps)
8
+ - **Branch Sync Checking**: Safe branch synchronization verification without auto-merging
9
+ - **Post-Merge Cleanup**: Automated cleanup of merged branches after PR completion
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @vibe-validate/git
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Git Tree Hash (Deterministic)
20
+
21
+ Calculate a content-based hash of the working tree including staged, unstaged, and untracked files:
22
+
23
+ ```typescript
24
+ import { getGitTreeHash } from '@vibe-validate/git';
25
+
26
+ const treeHash = await getGitTreeHash();
27
+ console.log(`Tree hash: ${treeHash}`);
28
+ // Deterministic - same content = same hash (no timestamp variance)
29
+ ```
30
+
31
+ ### Branch Sync Checking
32
+
33
+ Check if the current branch is behind origin/main without auto-merging:
34
+
35
+ ```typescript
36
+ import { BranchSyncChecker } from '@vibe-validate/git';
37
+
38
+ const checker = new BranchSyncChecker();
39
+ const result = await checker.checkSync();
40
+
41
+ if (!result.isUpToDate) {
42
+ console.log(`Branch is ${result.behindBy} commits behind origin/main`);
43
+ console.log('Manual merge required');
44
+ }
45
+ ```
46
+
47
+ ### Post-Merge Cleanup
48
+
49
+ Clean up local branches after PR merge:
50
+
51
+ ```typescript
52
+ import { PostPRMergeCleanup } from '@vibe-validate/git';
53
+
54
+ const cleanup = new PostPRMergeCleanup();
55
+ const result = await cleanup.runCleanup();
56
+
57
+ console.log(`Deleted ${result.branchesDeleted.length} merged branches`);
58
+ ```
59
+
60
+ ## API Reference
61
+
62
+ ### `getGitTreeHash()`
63
+
64
+ Returns a deterministic content-based hash of the working tree.
65
+
66
+ **Implementation Details:**
67
+ - Uses `git add --intent-to-add .` to mark untracked files (without staging)
68
+ - Uses `git write-tree` for content-based hashing (no timestamps)
69
+ - Resets index after hash calculation
70
+ - Falls back to `HEAD^{tree}` if no changes exist
71
+
72
+ **Returns:** `Promise<string>` - Git tree SHA-1 hash
73
+
74
+ ### `BranchSyncChecker`
75
+
76
+ Class for checking branch synchronization status.
77
+
78
+ **Methods:**
79
+ - `checkSync()`: Check if current branch is behind origin/main
80
+ - `printStatus(result)`: Display formatted status information
81
+ - `getExitCode(result)`: Get appropriate exit code (0=ok, 1=needs merge, 2=error)
82
+
83
+ ### `PostPRMergeCleanup`
84
+
85
+ Class for post-merge cleanup operations.
86
+
87
+ **Methods:**
88
+ - `runCleanup()`: Execute complete cleanup workflow
89
+ 1. Switch to main branch
90
+ 2. Sync main with origin/main
91
+ 3. Delete merged branches
92
+ 4. Prune remote references
93
+
94
+ ## Design Decisions
95
+
96
+ ### Deterministic Git Tree Hash
97
+
98
+ **Problem**: `git stash create` includes timestamps, making hashes non-deterministic.
99
+
100
+ **Solution**: Use `git write-tree` with intent-to-add for untracked files:
101
+
102
+ ```typescript
103
+ // Old approach (non-deterministic - includes timestamps)
104
+ git stash create // Different hash on each run even with same content
105
+
106
+ // New approach (deterministic - content-based only)
107
+ git add --intent-to-add . // Mark untracked files (no staging)
108
+ git write-tree // Content-based hash (no timestamps)
109
+ git reset // Restore index to clean state
110
+ ```
111
+
112
+ **Benefits:**
113
+ - Same content always produces same hash
114
+ - Enables reliable validation state caching
115
+ - Includes all files (staged, unstaged, untracked)
116
+ - No side effects (index restored after hash)
117
+
118
+ ### Safe Branch Sync
119
+
120
+ **Philosophy**: Never auto-merge. Always require manual conflict resolution.
121
+
122
+ **Why:**
123
+ - Preserves visibility of conflicts
124
+ - Prevents accidental code overwrites
125
+ - Explicit developer control over merges
126
+
127
+ ### Post-Merge Cleanup
128
+
129
+ **Safety Features:**
130
+ - Only deletes branches confirmed merged into main
131
+ - Never deletes main branch
132
+ - Provides clear feedback on all operations
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Smart Branch Sync Checker
3
+ *
4
+ * Safely checks if the current branch is behind a remote branch without auto-merging.
5
+ * Provides clear status reporting and next-step instructions.
6
+ *
7
+ * Key safety features:
8
+ * - Never auto-merges (preserves conflict visibility)
9
+ * - Clear exit codes for CI/agent integration
10
+ * - Explicit instructions when manual intervention needed
11
+ * - Cross-platform compatibility
12
+ */
13
+ declare const GIT_OPTIONS: {
14
+ timeout: number;
15
+ encoding: "utf8";
16
+ maxBuffer: number;
17
+ };
18
+ export interface SyncCheckResult {
19
+ isUpToDate: boolean;
20
+ behindBy: number;
21
+ currentBranch: string;
22
+ hasRemote: boolean;
23
+ error?: string;
24
+ }
25
+ export type ExecAsyncFunction = (command: string, options: typeof GIT_OPTIONS) => Promise<{
26
+ stdout: string;
27
+ stderr: string;
28
+ }>;
29
+ export interface SyncCheckOptions {
30
+ remoteBranch?: string;
31
+ execAsync?: ExecAsyncFunction;
32
+ }
33
+ /**
34
+ * Branch Sync Checker
35
+ *
36
+ * Checks if current branch is behind a remote branch
37
+ */
38
+ export declare class BranchSyncChecker {
39
+ private readonly remoteBranch;
40
+ private readonly execAsync;
41
+ constructor(options?: SyncCheckOptions);
42
+ /**
43
+ * Check if the current branch is synchronized with remote branch
44
+ *
45
+ * @returns Promise resolving to sync status information
46
+ */
47
+ checkSync(): Promise<SyncCheckResult>;
48
+ private getCurrentBranch;
49
+ private hasRemoteBranch;
50
+ private fetchRemote;
51
+ private getCommitsBehind;
52
+ /**
53
+ * Get appropriate exit code based on sync result
54
+ *
55
+ * @param result - The sync check result
56
+ * @returns Exit code (0=success, 1=needs merge, 2=error)
57
+ */
58
+ getExitCode(result: SyncCheckResult): number;
59
+ }
60
+ /**
61
+ * Convenience function for quick sync checking
62
+ *
63
+ * @param options - Sync check options
64
+ * @returns Sync check result
65
+ */
66
+ export declare function checkBranchSync(options?: SyncCheckOptions): Promise<SyncCheckResult>;
67
+ export {};
68
+ //# sourceMappingURL=branch-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-sync.d.ts","sourceRoot":"","sources":["../src/branch-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,QAAA,MAAM,WAAW;;;;CAIhB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAGD,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,WAAW,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE9H,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AAED;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;gBAElC,OAAO,GAAE,gBAAqB;IAK1C;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC;YA0C7B,gBAAgB;YAUhB,eAAe;YASf,WAAW;YAUX,gBAAgB;IAW9B;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM;CAK7C;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAG9F"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Smart Branch Sync Checker
3
+ *
4
+ * Safely checks if the current branch is behind a remote branch without auto-merging.
5
+ * Provides clear status reporting and next-step instructions.
6
+ *
7
+ * Key safety features:
8
+ * - Never auto-merges (preserves conflict visibility)
9
+ * - Clear exit codes for CI/agent integration
10
+ * - Explicit instructions when manual intervention needed
11
+ * - Cross-platform compatibility
12
+ */
13
+ import { exec } from 'child_process';
14
+ import { promisify } from 'util';
15
+ const GIT_TIMEOUT = 10000; // 10 seconds timeout for git operations
16
+ const GIT_OPTIONS = {
17
+ timeout: GIT_TIMEOUT,
18
+ encoding: 'utf8',
19
+ maxBuffer: 1024 * 1024 // 1MB buffer
20
+ };
21
+ /**
22
+ * Branch Sync Checker
23
+ *
24
+ * Checks if current branch is behind a remote branch
25
+ */
26
+ export class BranchSyncChecker {
27
+ remoteBranch;
28
+ execAsync;
29
+ constructor(options = {}) {
30
+ this.remoteBranch = options.remoteBranch || 'origin/main';
31
+ this.execAsync = options.execAsync || promisify(exec);
32
+ }
33
+ /**
34
+ * Check if the current branch is synchronized with remote branch
35
+ *
36
+ * @returns Promise resolving to sync status information
37
+ */
38
+ async checkSync() {
39
+ try {
40
+ // Get current branch name
41
+ const currentBranch = await this.getCurrentBranch();
42
+ // Check if remote branch exists
43
+ const hasRemote = await this.hasRemoteBranch();
44
+ if (!hasRemote) {
45
+ return {
46
+ isUpToDate: true,
47
+ behindBy: 0,
48
+ currentBranch,
49
+ hasRemote: false,
50
+ error: `No remote branch ${this.remoteBranch} found`
51
+ };
52
+ }
53
+ // Fetch latest from remote
54
+ await this.fetchRemote();
55
+ // Check how many commits behind
56
+ const behindBy = await this.getCommitsBehind();
57
+ return {
58
+ isUpToDate: behindBy === 0,
59
+ behindBy,
60
+ currentBranch,
61
+ hasRemote: true
62
+ };
63
+ }
64
+ catch (error) {
65
+ const errorMessage = error instanceof Error ? error.message : String(error);
66
+ return {
67
+ isUpToDate: false,
68
+ behindBy: -1,
69
+ currentBranch: 'unknown',
70
+ hasRemote: false,
71
+ error: errorMessage
72
+ };
73
+ }
74
+ }
75
+ async getCurrentBranch() {
76
+ try {
77
+ const { stdout } = await this.execAsync('git rev-parse --abbrev-ref HEAD', GIT_OPTIONS);
78
+ return stdout.trim();
79
+ }
80
+ catch (error) {
81
+ const errorMessage = error instanceof Error ? error.message : String(error);
82
+ throw new Error(`Not in a git repository or unable to determine current branch: ${errorMessage}`);
83
+ }
84
+ }
85
+ async hasRemoteBranch() {
86
+ try {
87
+ await this.execAsync(`git rev-parse --verify ${this.remoteBranch}`, GIT_OPTIONS);
88
+ return true;
89
+ }
90
+ catch (error) {
91
+ return false;
92
+ }
93
+ }
94
+ async fetchRemote() {
95
+ try {
96
+ const [remote, branch] = this.remoteBranch.split('/');
97
+ await this.execAsync(`git fetch --quiet ${remote} ${branch}`, GIT_OPTIONS);
98
+ }
99
+ catch (error) {
100
+ const errorMessage = error instanceof Error ? error.message : String(error);
101
+ throw new Error(`Failed to fetch from ${this.remoteBranch}: ${errorMessage}`);
102
+ }
103
+ }
104
+ async getCommitsBehind() {
105
+ try {
106
+ const { stdout } = await this.execAsync(`git rev-list --count HEAD..${this.remoteBranch}`, GIT_OPTIONS);
107
+ const count = parseInt(stdout.trim(), 10);
108
+ return isNaN(count) ? 0 : count;
109
+ }
110
+ catch (error) {
111
+ const errorMessage = error instanceof Error ? error.message : String(error);
112
+ throw new Error(`Failed to check commits behind: ${errorMessage}`);
113
+ }
114
+ }
115
+ /**
116
+ * Get appropriate exit code based on sync result
117
+ *
118
+ * @param result - The sync check result
119
+ * @returns Exit code (0=success, 1=needs merge, 2=error)
120
+ */
121
+ getExitCode(result) {
122
+ if (result.error)
123
+ return 2; // Error condition
124
+ if (!result.hasRemote)
125
+ return 0; // No remote, consider OK
126
+ return result.isUpToDate ? 0 : 1; // 0 = up to date, 1 = needs merge
127
+ }
128
+ }
129
+ /**
130
+ * Convenience function for quick sync checking
131
+ *
132
+ * @param options - Sync check options
133
+ * @returns Sync check result
134
+ */
135
+ export async function checkBranchSync(options = {}) {
136
+ const checker = new BranchSyncChecker(options);
137
+ return checker.checkSync();
138
+ }
139
+ //# sourceMappingURL=branch-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-sync.js","sourceRoot":"","sources":["../src/branch-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @vibe-validate/git
3
+ *
4
+ * Git utilities for vibe-validate - deterministic tree hash calculation,
5
+ * branch synchronization, and post-merge cleanup.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ export { getGitTreeHash, getHeadTreeHash, hasWorkingTreeChanges } from './tree-hash.js';
10
+ export { BranchSyncChecker, checkBranchSync, type SyncCheckResult, type SyncCheckOptions } from './branch-sync.js';
11
+ export { PostPRMergeCleanup, cleanupMergedBranches, type CleanupResult, type CleanupOptions } from './post-merge-cleanup.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @vibe-validate/git
3
+ *
4
+ * Git utilities for vibe-validate - deterministic tree hash calculation,
5
+ * branch synchronization, and post-merge cleanup.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ // Tree hash calculation (deterministic, content-based)
10
+ export { getGitTreeHash, getHeadTreeHash, hasWorkingTreeChanges } from './tree-hash.js';
11
+ // Branch sync checking (safe, no auto-merge)
12
+ export { BranchSyncChecker, checkBranchSync } from './branch-sync.js';
13
+ // Post-merge cleanup (delete merged branches)
14
+ export { PostPRMergeCleanup, cleanupMergedBranches } from './post-merge-cleanup.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Post-PR Merge Cleanup Tool
3
+ *
4
+ * Comprehensive post-PR cleanup workflow that:
5
+ * 1. Switches to main branch
6
+ * 2. Syncs main branch with remote origin
7
+ * 3. Deletes local branches that have been merged
8
+ * 4. Provides clean workspace for next PR
9
+ *
10
+ * Safe operations:
11
+ * - Only deletes branches that are confirmed merged
12
+ * - Never deletes the current main branch or unmerged branches
13
+ * - Provides clear feedback on all actions taken
14
+ */
15
+ export interface CleanupResult {
16
+ success: boolean;
17
+ error?: string;
18
+ branchesDeleted: string[];
19
+ currentBranch: string;
20
+ mainSynced: boolean;
21
+ }
22
+ export interface CleanupOptions {
23
+ mainBranch?: string;
24
+ remoteName?: string;
25
+ dryRun?: boolean;
26
+ }
27
+ /**
28
+ * Post-PR Merge Cleanup
29
+ *
30
+ * Cleans up merged branches and syncs main branch after PR merge
31
+ */
32
+ export declare class PostPRMergeCleanup {
33
+ private readonly mainBranch;
34
+ private readonly remoteName;
35
+ private readonly dryRun;
36
+ constructor(options?: CleanupOptions);
37
+ /**
38
+ * Run comprehensive post-PR merge cleanup workflow
39
+ */
40
+ runCleanup(): Promise<CleanupResult>;
41
+ /**
42
+ * Get the current git branch name
43
+ */
44
+ private getCurrentBranch;
45
+ /**
46
+ * Switch to main branch
47
+ */
48
+ private switchToMain;
49
+ /**
50
+ * Sync main branch with remote origin
51
+ */
52
+ private syncMainBranch;
53
+ /**
54
+ * Fetch remote branch information
55
+ */
56
+ private fetchRemoteInfo;
57
+ /**
58
+ * Find and delete branches that have been merged
59
+ */
60
+ private deleteMergedBranches;
61
+ /**
62
+ * Check if a branch has been merged into main
63
+ */
64
+ private isBranchMerged;
65
+ /**
66
+ * Clean up remote tracking references
67
+ */
68
+ private pruneRemoteReferences;
69
+ }
70
+ /**
71
+ * Convenience function for quick cleanup
72
+ *
73
+ * @param options - Cleanup options
74
+ * @returns Cleanup result
75
+ */
76
+ export declare function cleanupMergedBranches(options?: CleanupOptions): Promise<CleanupResult>;
77
+ //# sourceMappingURL=post-merge-cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-merge-cleanup.d.ts","sourceRoot":"","sources":["../src/post-merge-cleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;gBAErB,OAAO,GAAE,cAAmB;IAMxC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC;IAqC1C;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAYpB;;OAEG;IACH,OAAO,CAAC,cAAc;IAqBtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAYvB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAkD5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAetB;;OAEG;IACH,OAAO,CAAC,qBAAqB;CAW9B;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CAGhG"}
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Post-PR Merge Cleanup Tool
3
+ *
4
+ * Comprehensive post-PR cleanup workflow that:
5
+ * 1. Switches to main branch
6
+ * 2. Syncs main branch with remote origin
7
+ * 3. Deletes local branches that have been merged
8
+ * 4. Provides clean workspace for next PR
9
+ *
10
+ * Safe operations:
11
+ * - Only deletes branches that are confirmed merged
12
+ * - Never deletes the current main branch or unmerged branches
13
+ * - Provides clear feedback on all actions taken
14
+ */
15
+ import { execSync } from 'child_process';
16
+ const TIMEOUT = 30000; // 30 seconds timeout for git operations
17
+ /**
18
+ * Post-PR Merge Cleanup
19
+ *
20
+ * Cleans up merged branches and syncs main branch after PR merge
21
+ */
22
+ export class PostPRMergeCleanup {
23
+ mainBranch;
24
+ remoteName;
25
+ dryRun;
26
+ constructor(options = {}) {
27
+ this.mainBranch = options.mainBranch || 'main';
28
+ this.remoteName = options.remoteName || 'origin';
29
+ this.dryRun = options.dryRun || false;
30
+ }
31
+ /**
32
+ * Run comprehensive post-PR merge cleanup workflow
33
+ */
34
+ async runCleanup() {
35
+ const result = {
36
+ success: false,
37
+ branchesDeleted: [],
38
+ currentBranch: '',
39
+ mainSynced: false
40
+ };
41
+ try {
42
+ // Step 1: Get current branch
43
+ result.currentBranch = this.getCurrentBranch();
44
+ // Step 2: Switch to main branch
45
+ this.switchToMain();
46
+ // Step 3: Sync main branch with remote
47
+ this.syncMainBranch();
48
+ result.mainSynced = true;
49
+ // Step 4: Fetch remote branch information
50
+ this.fetchRemoteInfo();
51
+ // Step 5: Find and delete merged branches
52
+ result.branchesDeleted = this.deleteMergedBranches();
53
+ // Step 6: Clean up remote tracking branches
54
+ this.pruneRemoteReferences();
55
+ result.success = true;
56
+ return result;
57
+ }
58
+ catch (error) {
59
+ result.error = error instanceof Error ? error.message : String(error);
60
+ return result;
61
+ }
62
+ }
63
+ /**
64
+ * Get the current git branch name
65
+ */
66
+ getCurrentBranch() {
67
+ try {
68
+ return execSync('git branch --show-current', {
69
+ encoding: 'utf8',
70
+ timeout: TIMEOUT
71
+ }).trim();
72
+ }
73
+ catch (error) {
74
+ throw new Error(`Failed to get current branch: ${error}`);
75
+ }
76
+ }
77
+ /**
78
+ * Switch to main branch
79
+ */
80
+ switchToMain() {
81
+ try {
82
+ execSync(`git checkout ${this.mainBranch}`, {
83
+ encoding: 'utf8',
84
+ timeout: TIMEOUT,
85
+ stdio: ['pipe', 'pipe', 'pipe']
86
+ });
87
+ }
88
+ catch (error) {
89
+ throw new Error(`Failed to switch to ${this.mainBranch} branch: ${error}`);
90
+ }
91
+ }
92
+ /**
93
+ * Sync main branch with remote origin
94
+ */
95
+ syncMainBranch() {
96
+ try {
97
+ // Fetch latest changes from remote
98
+ execSync(`git fetch ${this.remoteName} ${this.mainBranch}`, {
99
+ encoding: 'utf8',
100
+ timeout: TIMEOUT,
101
+ stdio: ['pipe', 'pipe', 'pipe']
102
+ });
103
+ // Fast-forward merge remote/main
104
+ execSync(`git merge ${this.remoteName}/${this.mainBranch} --ff-only`, {
105
+ encoding: 'utf8',
106
+ timeout: TIMEOUT,
107
+ stdio: ['pipe', 'pipe', 'pipe']
108
+ });
109
+ }
110
+ catch (error) {
111
+ throw new Error(`Failed to sync ${this.mainBranch} branch: ${error}`);
112
+ }
113
+ }
114
+ /**
115
+ * Fetch remote branch information
116
+ */
117
+ fetchRemoteInfo() {
118
+ try {
119
+ execSync(`git fetch ${this.remoteName} --prune`, {
120
+ encoding: 'utf8',
121
+ timeout: TIMEOUT,
122
+ stdio: ['pipe', 'pipe', 'pipe']
123
+ });
124
+ }
125
+ catch (error) {
126
+ throw new Error(`Failed to fetch remote info: ${error}`);
127
+ }
128
+ }
129
+ /**
130
+ * Find and delete branches that have been merged
131
+ */
132
+ deleteMergedBranches() {
133
+ try {
134
+ // Get list of local branches (excluding main)
135
+ const allBranches = execSync('git branch --format="%(refname:short)"', {
136
+ encoding: 'utf8',
137
+ timeout: TIMEOUT
138
+ })
139
+ .trim()
140
+ .split('\n')
141
+ .filter(branch => branch && branch !== this.mainBranch && !branch.startsWith('*'));
142
+ const deletedBranches = [];
143
+ for (const branch of allBranches) {
144
+ if (this.isBranchMerged(branch)) {
145
+ if (this.dryRun) {
146
+ deletedBranches.push(branch);
147
+ continue;
148
+ }
149
+ try {
150
+ execSync(`git branch -d "${branch}"`, {
151
+ encoding: 'utf8',
152
+ timeout: TIMEOUT,
153
+ stdio: ['pipe', 'pipe', 'pipe']
154
+ });
155
+ deletedBranches.push(branch);
156
+ }
157
+ catch (deleteError) {
158
+ // Try force delete if regular delete fails
159
+ try {
160
+ execSync(`git branch -D "${branch}"`, {
161
+ encoding: 'utf8',
162
+ timeout: TIMEOUT,
163
+ stdio: ['pipe', 'pipe', 'pipe']
164
+ });
165
+ deletedBranches.push(branch);
166
+ }
167
+ catch (forceDeleteError) {
168
+ // Couldn't delete - skip this branch
169
+ }
170
+ }
171
+ }
172
+ }
173
+ return deletedBranches;
174
+ }
175
+ catch (error) {
176
+ return [];
177
+ }
178
+ }
179
+ /**
180
+ * Check if a branch has been merged into main
181
+ */
182
+ isBranchMerged(branch) {
183
+ try {
184
+ const mergedBranches = execSync(`git branch --merged ${this.mainBranch} --format="%(refname:short)"`, {
185
+ encoding: 'utf8',
186
+ timeout: TIMEOUT
187
+ });
188
+ return mergedBranches.includes(branch);
189
+ }
190
+ catch (error) {
191
+ // If we can't determine merge status, don't delete the branch
192
+ return false;
193
+ }
194
+ }
195
+ /**
196
+ * Clean up remote tracking references
197
+ */
198
+ pruneRemoteReferences() {
199
+ try {
200
+ execSync(`git remote prune ${this.remoteName}`, {
201
+ encoding: 'utf8',
202
+ timeout: TIMEOUT,
203
+ stdio: ['pipe', 'pipe', 'pipe']
204
+ });
205
+ }
206
+ catch (error) {
207
+ // Non-critical operation - don't fail on error
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * Convenience function for quick cleanup
213
+ *
214
+ * @param options - Cleanup options
215
+ * @returns Cleanup result
216
+ */
217
+ export async function cleanupMergedBranches(options = {}) {
218
+ const cleanup = new PostPRMergeCleanup(options);
219
+ return cleanup.runCleanup();
220
+ }
221
+ //# sourceMappingURL=post-merge-cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-merge-cleanup.js","sourceRoot":"","sources":["../src/post-merge-cleanup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Deterministic Git Tree Hash Calculation
3
+ *
4
+ * Provides content-based hashing of working tree state including:
5
+ * - Staged changes (index)
6
+ * - Unstaged changes (working tree modifications)
7
+ * - Untracked files
8
+ *
9
+ * CRITICAL FIX: Uses git write-tree instead of git stash create for determinism.
10
+ * git stash create includes timestamps, making hashes non-deterministic.
11
+ * git write-tree produces content-based hashes only (no timestamps).
12
+ */
13
+ /**
14
+ * Get deterministic git tree hash representing current working tree state
15
+ *
16
+ * Implementation:
17
+ * 1. Mark untracked files with --intent-to-add (no actual staging)
18
+ * 2. Calculate tree hash with git write-tree (content-based, no timestamps)
19
+ * 3. Reset index to clean state (no side effects)
20
+ *
21
+ * Why this is better than git stash create:
22
+ * - git stash create: includes timestamps in commit → different hash each time
23
+ * - git write-tree: content-based only → same content = same hash (deterministic)
24
+ *
25
+ * @returns Git tree SHA-1 hash (40 hex characters)
26
+ * @throws Error if not in a git repository or git command fails
27
+ */
28
+ export declare function getGitTreeHash(): Promise<string>;
29
+ /**
30
+ * Get tree hash for HEAD commit (committed state only, no working tree changes)
31
+ *
32
+ * This is useful for comparing committed state vs working tree state.
33
+ *
34
+ * @returns Git tree SHA-1 hash of HEAD commit
35
+ * @throws Error if not in a git repository or HEAD doesn't exist
36
+ */
37
+ export declare function getHeadTreeHash(): Promise<string>;
38
+ /**
39
+ * Check if working tree has any changes compared to HEAD
40
+ *
41
+ * @returns true if working tree differs from HEAD, false if clean
42
+ */
43
+ export declare function hasWorkingTreeChanges(): Promise<boolean>;
44
+ //# sourceMappingURL=tree-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-hash.d.ts","sourceRoot":"","sources":["../src/tree-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAWH;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CA6CtD;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAQvD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAS9D"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Deterministic Git Tree Hash Calculation
3
+ *
4
+ * Provides content-based hashing of working tree state including:
5
+ * - Staged changes (index)
6
+ * - Unstaged changes (working tree modifications)
7
+ * - Untracked files
8
+ *
9
+ * CRITICAL FIX: Uses git write-tree instead of git stash create for determinism.
10
+ * git stash create includes timestamps, making hashes non-deterministic.
11
+ * git write-tree produces content-based hashes only (no timestamps).
12
+ */
13
+ import { execSync } from 'child_process';
14
+ const GIT_TIMEOUT = 10000; // 10 seconds
15
+ const GIT_OPTIONS = {
16
+ encoding: 'utf8',
17
+ timeout: GIT_TIMEOUT,
18
+ stdio: ['pipe', 'pipe', 'ignore'],
19
+ };
20
+ /**
21
+ * Get deterministic git tree hash representing current working tree state
22
+ *
23
+ * Implementation:
24
+ * 1. Mark untracked files with --intent-to-add (no actual staging)
25
+ * 2. Calculate tree hash with git write-tree (content-based, no timestamps)
26
+ * 3. Reset index to clean state (no side effects)
27
+ *
28
+ * Why this is better than git stash create:
29
+ * - git stash create: includes timestamps in commit → different hash each time
30
+ * - git write-tree: content-based only → same content = same hash (deterministic)
31
+ *
32
+ * @returns Git tree SHA-1 hash (40 hex characters)
33
+ * @throws Error if not in a git repository or git command fails
34
+ */
35
+ export async function getGitTreeHash() {
36
+ try {
37
+ // Check if we're in a git repository
38
+ execSync('git rev-parse --is-inside-work-tree', GIT_OPTIONS);
39
+ // Step 1: Mark all untracked files with --intent-to-add
40
+ // This adds them to the index WITHOUT staging their content
41
+ // --force: include ignored files for complete state tracking
42
+ try {
43
+ execSync('git add --intent-to-add --all --force', {
44
+ ...GIT_OPTIONS,
45
+ stdio: ['pipe', 'pipe', 'pipe'] // Capture stderr for error handling
46
+ });
47
+ }
48
+ catch (addError) {
49
+ // If no untracked files, git add fails with "nothing to add"
50
+ // This is fine - just means we only have tracked files
51
+ const errorMessage = addError instanceof Error ? addError.message : String(addError);
52
+ if (!errorMessage.includes('nothing')) {
53
+ // Real error - re-throw
54
+ throw addError;
55
+ }
56
+ }
57
+ // Step 2: Get tree hash (content-based, no timestamps)
58
+ const treeHash = execSync('git write-tree', GIT_OPTIONS).trim();
59
+ // Step 3: Reset index to clean state (remove intent-to-add marks)
60
+ // This ensures no side effects from our hash calculation
61
+ execSync('git reset', GIT_OPTIONS);
62
+ return treeHash;
63
+ }
64
+ catch (error) {
65
+ // Handle not-in-git-repo case
66
+ const errorMessage = error instanceof Error ? error.message : String(error);
67
+ if (errorMessage.includes('not a git repository')) {
68
+ // Not in git repo - fall back to timestamp-based hash
69
+ console.warn('⚠️ Not in git repository, using timestamp-based hash');
70
+ return `nogit-${Date.now()}`;
71
+ }
72
+ // Other git errors
73
+ throw new Error(`Failed to calculate git tree hash: ${errorMessage}`);
74
+ }
75
+ }
76
+ /**
77
+ * Get tree hash for HEAD commit (committed state only, no working tree changes)
78
+ *
79
+ * This is useful for comparing committed state vs working tree state.
80
+ *
81
+ * @returns Git tree SHA-1 hash of HEAD commit
82
+ * @throws Error if not in a git repository or HEAD doesn't exist
83
+ */
84
+ export async function getHeadTreeHash() {
85
+ try {
86
+ const treeHash = execSync('git rev-parse HEAD^{tree}', GIT_OPTIONS).trim();
87
+ return treeHash;
88
+ }
89
+ catch (error) {
90
+ const errorMessage = error instanceof Error ? error.message : String(error);
91
+ throw new Error(`Failed to get HEAD tree hash: ${errorMessage}`);
92
+ }
93
+ }
94
+ /**
95
+ * Check if working tree has any changes compared to HEAD
96
+ *
97
+ * @returns true if working tree differs from HEAD, false if clean
98
+ */
99
+ export async function hasWorkingTreeChanges() {
100
+ try {
101
+ const workingTreeHash = await getGitTreeHash();
102
+ const headTreeHash = await getHeadTreeHash();
103
+ return workingTreeHash !== headTreeHash;
104
+ }
105
+ catch (error) {
106
+ // If we can't determine, assume there are changes (safe default)
107
+ return true;
108
+ }
109
+ }
110
+ //# sourceMappingURL=tree-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-hash.js","sourceRoot":"","sources":["../src/tree-hash.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@vibe-validate/git",
3
+ "version": "0.9.0",
4
+ "description": "Git utilities for vibe-validate - tree hash calculation, branch sync, and post-merge cleanup",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "clean": "rm -rf dist",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest"
24
+ },
25
+ "keywords": [
26
+ "git",
27
+ "validation",
28
+ "tree-hash",
29
+ "branch-sync",
30
+ "cleanup",
31
+ "sdlc"
32
+ ],
33
+ "author": "Jeff Dutton",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/jdutton/vibe-validate.git",
38
+ "directory": "packages/git"
39
+ },
40
+ "engines": {
41
+ "node": ">=20.0.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "typescript": "^5.6.0",
49
+ "vitest": "^2.0.0"
50
+ }
51
+ }