@eldrforge/git-tools 0.1.13 โ 0.1.14
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 +686 -117
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# @eldrforge/git-tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A comprehensive TypeScript library providing secure Git operations, process execution utilities, and NPM link management for automation workflows.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
7
|
+
`@eldrforge/git-tools` is a production-ready library designed for building Git automation tools. It provides secure command execution primitives and high-level Git operations with a focus on safety, reliability, and ease of use.
|
|
8
|
+
|
|
9
|
+
**Key Features:**
|
|
10
|
+
|
|
11
|
+
- ๐ **Secure Process Execution** - Shell injection prevention with validated arguments
|
|
12
|
+
- ๐ณ **Comprehensive Git Operations** - 20+ Git utilities for branch management, versioning, and status queries
|
|
13
|
+
- ๐ท๏ธ **Semantic Version Support** - Intelligent tag finding and version comparison for release automation
|
|
14
|
+
- ๐ **Branch Management** - Sync checking, safe syncing, and detailed status queries
|
|
15
|
+
- ๐ **NPM Link Management** - Link detection, compatibility checking, and problem diagnosis for monorepo workflows
|
|
16
|
+
- ๐ **Flexible Logging** - Bring your own logger (Winston, Pino, etc.) or use the built-in console logger
|
|
17
|
+
- โ
**Runtime Validation** - Type-safe JSON parsing and validation utilities
|
|
18
|
+
- ๐งช **Well-Tested** - Comprehensive test coverage for reliability
|
|
13
19
|
|
|
14
20
|
## Installation
|
|
15
21
|
|
|
@@ -17,10 +23,28 @@ Git utilities for automation - secure process execution and comprehensive Git op
|
|
|
17
23
|
npm install @eldrforge/git-tools
|
|
18
24
|
```
|
|
19
25
|
|
|
26
|
+
### Requirements
|
|
27
|
+
|
|
28
|
+
- Node.js 14 or higher
|
|
29
|
+
- Git 2.0 or higher
|
|
30
|
+
- TypeScript 4.5+ (for TypeScript projects)
|
|
31
|
+
|
|
32
|
+
### Optional Dependencies
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# If you want to use Winston for logging
|
|
36
|
+
npm install winston
|
|
37
|
+
```
|
|
38
|
+
|
|
20
39
|
## Quick Start
|
|
21
40
|
|
|
22
41
|
```typescript
|
|
23
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
getCurrentBranch,
|
|
44
|
+
getGitStatusSummary,
|
|
45
|
+
findPreviousReleaseTag,
|
|
46
|
+
runSecure
|
|
47
|
+
} from '@eldrforge/git-tools';
|
|
24
48
|
|
|
25
49
|
// Get current branch
|
|
26
50
|
const branch = await getCurrentBranch();
|
|
@@ -29,15 +53,40 @@ console.log(`Current branch: ${branch}`);
|
|
|
29
53
|
// Get comprehensive status
|
|
30
54
|
const status = await getGitStatusSummary();
|
|
31
55
|
console.log(`Status: ${status.status}`);
|
|
56
|
+
console.log(`Unstaged files: ${status.unstagedCount}`);
|
|
57
|
+
console.log(`Uncommitted changes: ${status.uncommittedCount}`);
|
|
58
|
+
console.log(`Unpushed commits: ${status.unpushedCount}`);
|
|
32
59
|
|
|
33
60
|
// Find previous release tag
|
|
34
61
|
const previousTag = await findPreviousReleaseTag('1.2.3', 'v*');
|
|
35
62
|
console.log(`Previous release: ${previousTag}`);
|
|
63
|
+
|
|
64
|
+
// Execute Git commands securely
|
|
65
|
+
const { stdout } = await runSecure('git', ['log', '--oneline', '-n', '5']);
|
|
66
|
+
console.log('Recent commits:', stdout);
|
|
36
67
|
```
|
|
37
68
|
|
|
38
|
-
##
|
|
69
|
+
## Core Concepts
|
|
70
|
+
|
|
71
|
+
### 1. Secure Process Execution
|
|
39
72
|
|
|
40
|
-
|
|
73
|
+
All process execution functions prioritize security by preventing shell injection attacks:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { runSecure, run } from '@eldrforge/git-tools';
|
|
77
|
+
|
|
78
|
+
// โ
SECURE: Uses argument array, no shell interpretation
|
|
79
|
+
const { stdout } = await runSecure('git', ['log', '--format=%s', userInput]);
|
|
80
|
+
|
|
81
|
+
// โ ๏ธ LESS SECURE: Uses shell command string
|
|
82
|
+
const result = await run(`git log --format=%s ${userInput}`);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Best Practice**: Always use `runSecure` or `runSecureWithDryRunSupport` for user input.
|
|
86
|
+
|
|
87
|
+
### 2. Custom Logger Integration
|
|
88
|
+
|
|
89
|
+
By default, git-tools uses a console-based logger. You can integrate your own logger:
|
|
41
90
|
|
|
42
91
|
```typescript
|
|
43
92
|
import { setLogger } from '@eldrforge/git-tools';
|
|
@@ -45,169 +94,647 @@ import winston from 'winston';
|
|
|
45
94
|
|
|
46
95
|
// Create Winston logger
|
|
47
96
|
const logger = winston.createLogger({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
level: 'info',
|
|
98
|
+
format: winston.format.combine(
|
|
99
|
+
winston.format.timestamp(),
|
|
100
|
+
winston.format.json()
|
|
101
|
+
),
|
|
102
|
+
transports: [
|
|
103
|
+
new winston.transports.Console(),
|
|
104
|
+
new winston.transports.File({ filename: 'git-tools.log' })
|
|
105
|
+
]
|
|
51
106
|
});
|
|
52
107
|
|
|
53
|
-
//
|
|
108
|
+
// Set global logger for git-tools
|
|
54
109
|
setLogger(logger);
|
|
55
|
-
```
|
|
56
110
|
|
|
57
|
-
|
|
111
|
+
// Now all git-tools operations will use your logger
|
|
112
|
+
const branch = await getCurrentBranch(); // Logs via Winston
|
|
113
|
+
```
|
|
58
114
|
|
|
59
|
-
###
|
|
115
|
+
### 3. Dry-Run Support
|
|
60
116
|
|
|
61
|
-
|
|
117
|
+
Many automation workflows need dry-run capability:
|
|
62
118
|
|
|
63
119
|
```typescript
|
|
64
|
-
import {
|
|
120
|
+
import { runSecureWithDryRunSupport } from '@eldrforge/git-tools';
|
|
65
121
|
|
|
66
|
-
|
|
67
|
-
const { stdout } = await runSecure('git', ['status', '--porcelain']);
|
|
122
|
+
const isDryRun = process.env.DRY_RUN === 'true';
|
|
68
123
|
|
|
69
|
-
//
|
|
70
|
-
const result = await runSecureWithDryRunSupport(
|
|
124
|
+
// This will only log what would happen if isDryRun is true
|
|
125
|
+
const result = await runSecureWithDryRunSupport(
|
|
126
|
+
'git',
|
|
127
|
+
['push', 'origin', 'main'],
|
|
128
|
+
isDryRun
|
|
129
|
+
);
|
|
71
130
|
```
|
|
72
131
|
|
|
73
|
-
|
|
132
|
+
## Usage Guide
|
|
133
|
+
|
|
134
|
+
### Branch Operations
|
|
74
135
|
|
|
75
|
-
|
|
136
|
+
#### Check Branch Status
|
|
76
137
|
|
|
77
|
-
**Branch Operations:**
|
|
78
138
|
```typescript
|
|
79
139
|
import {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
safeSyncBranchWithRemote
|
|
140
|
+
getCurrentBranch,
|
|
141
|
+
localBranchExists,
|
|
142
|
+
remoteBranchExists,
|
|
143
|
+
isBranchInSyncWithRemote
|
|
85
144
|
} from '@eldrforge/git-tools';
|
|
86
145
|
|
|
87
|
-
|
|
88
|
-
const
|
|
146
|
+
// Get current branch
|
|
147
|
+
const currentBranch = await getCurrentBranch();
|
|
148
|
+
console.log(`On branch: ${currentBranch}`);
|
|
149
|
+
|
|
150
|
+
// Check if branches exist
|
|
151
|
+
const hasMain = await localBranchExists('main');
|
|
152
|
+
const hasRemoteMain = await remoteBranchExists('main', 'origin');
|
|
153
|
+
|
|
154
|
+
console.log(`Local main exists: ${hasMain}`);
|
|
155
|
+
console.log(`Remote main exists: ${hasRemoteMain}`);
|
|
156
|
+
|
|
157
|
+
// Check if local and remote are in sync
|
|
89
158
|
const syncStatus = await isBranchInSyncWithRemote('main');
|
|
159
|
+
console.log(`In sync: ${syncStatus.inSync}`);
|
|
160
|
+
console.log(`Local SHA: ${syncStatus.localSha}`);
|
|
161
|
+
console.log(`Remote SHA: ${syncStatus.remoteSha}`);
|
|
90
162
|
```
|
|
91
163
|
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
import {
|
|
95
|
-
findPreviousReleaseTag,
|
|
96
|
-
getCurrentVersion,
|
|
97
|
-
getDefaultFromRef
|
|
98
|
-
} from '@eldrforge/git-tools';
|
|
164
|
+
#### Safe Branch Synchronization
|
|
99
165
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
166
|
+
```typescript
|
|
167
|
+
import { safeSyncBranchWithRemote } from '@eldrforge/git-tools';
|
|
168
|
+
|
|
169
|
+
// Safely sync branch with remote (handles conflicts gracefully)
|
|
170
|
+
const result = await safeSyncBranchWithRemote('main', 'origin');
|
|
171
|
+
|
|
172
|
+
if (result.success) {
|
|
173
|
+
console.log('Branch successfully synced with remote');
|
|
174
|
+
} else if (result.conflictResolutionRequired) {
|
|
175
|
+
console.error('Conflict resolution required:', result.error);
|
|
176
|
+
// Handle conflicts manually
|
|
177
|
+
} else {
|
|
178
|
+
console.error('Sync failed:', result.error);
|
|
179
|
+
}
|
|
103
180
|
```
|
|
104
181
|
|
|
105
|
-
|
|
182
|
+
### Repository Status
|
|
183
|
+
|
|
184
|
+
#### Get Comprehensive Status
|
|
185
|
+
|
|
106
186
|
```typescript
|
|
107
187
|
import { getGitStatusSummary } from '@eldrforge/git-tools';
|
|
108
188
|
|
|
109
189
|
const status = await getGitStatusSummary();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
190
|
+
|
|
191
|
+
console.log(`Branch: ${status.branch}`);
|
|
192
|
+
console.log(`Status: ${status.status}`); // e.g., "2 unstaged, 1 uncommitted, 3 unpushed"
|
|
193
|
+
|
|
194
|
+
// Individual status flags
|
|
195
|
+
if (status.hasUnstagedFiles) {
|
|
196
|
+
console.log(`โ ๏ธ ${status.unstagedCount} unstaged files`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (status.hasUncommittedChanges) {
|
|
200
|
+
console.log(`๐ ${status.uncommittedCount} uncommitted changes`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (status.hasUnpushedCommits) {
|
|
204
|
+
console.log(`โฌ๏ธ ${status.unpushedCount} unpushed commits`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (status.status === 'clean') {
|
|
208
|
+
console.log('โ
Working directory clean');
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### Check if Directory is a Git Repository
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { isGitRepository } from '@eldrforge/git-tools';
|
|
216
|
+
|
|
217
|
+
const isRepo = await isGitRepository('/path/to/directory');
|
|
218
|
+
if (isRepo) {
|
|
219
|
+
console.log('This is a Git repository');
|
|
220
|
+
} else {
|
|
221
|
+
console.log('Not a Git repository');
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Version and Tag Operations
|
|
226
|
+
|
|
227
|
+
#### Find Previous Release Tag
|
|
228
|
+
|
|
229
|
+
Useful for generating release notes or comparing versions:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { findPreviousReleaseTag, getCurrentVersion } from '@eldrforge/git-tools';
|
|
233
|
+
|
|
234
|
+
// Get current version from package.json
|
|
235
|
+
const currentVersion = await getCurrentVersion();
|
|
236
|
+
console.log(`Current version: ${currentVersion}`);
|
|
237
|
+
|
|
238
|
+
// Find previous release tag
|
|
239
|
+
// Looks for tags matching "v*" pattern that are < current version
|
|
240
|
+
const previousTag = await findPreviousReleaseTag(currentVersion, 'v*');
|
|
241
|
+
|
|
242
|
+
if (previousTag) {
|
|
243
|
+
console.log(`Previous release: ${previousTag}`);
|
|
244
|
+
// Now you can generate release notes from previousTag..HEAD
|
|
245
|
+
} else {
|
|
246
|
+
console.log('No previous release found (possibly first release)');
|
|
247
|
+
}
|
|
120
248
|
```
|
|
121
249
|
|
|
122
|
-
|
|
250
|
+
#### Working with Tag Patterns
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { findPreviousReleaseTag } from '@eldrforge/git-tools';
|
|
254
|
+
|
|
255
|
+
// Standard version tags (v1.0.0, v1.2.3)
|
|
256
|
+
const prevRelease = await findPreviousReleaseTag('1.2.3', 'v*');
|
|
257
|
+
|
|
258
|
+
// Working branch tags (working/v1.0.0)
|
|
259
|
+
const prevWorking = await findPreviousReleaseTag('1.2.3', 'working/v*');
|
|
260
|
+
|
|
261
|
+
// Custom prefix tags (release/v1.0.0)
|
|
262
|
+
const prevCustom = await findPreviousReleaseTag('1.2.3', 'release/v*');
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### Get Default Reference for Comparisons
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { getDefaultFromRef } from '@eldrforge/git-tools';
|
|
269
|
+
|
|
270
|
+
// Intelligently determines the best reference for release comparisons
|
|
271
|
+
// Tries: previous tag -> main -> master -> origin/main -> origin/master
|
|
272
|
+
const fromRef = await getDefaultFromRef(false, 'working');
|
|
273
|
+
console.log(`Compare from: ${fromRef}`);
|
|
274
|
+
|
|
275
|
+
// Force main branch (skip tag detection)
|
|
276
|
+
const mainRef = await getDefaultFromRef(true);
|
|
277
|
+
console.log(`Compare from main: ${mainRef}`);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### NPM Link Management
|
|
281
|
+
|
|
282
|
+
Perfect for monorepo development and local package testing:
|
|
283
|
+
|
|
284
|
+
#### Check Link Status
|
|
285
|
+
|
|
123
286
|
```typescript
|
|
124
287
|
import {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
isNpmLinked
|
|
288
|
+
isNpmLinked,
|
|
289
|
+
getGloballyLinkedPackages,
|
|
290
|
+
getLinkedDependencies
|
|
129
291
|
} from '@eldrforge/git-tools';
|
|
130
292
|
|
|
293
|
+
// Check if a package is globally linked
|
|
294
|
+
const isLinked = await isNpmLinked('/path/to/my-package');
|
|
295
|
+
console.log(`Package is linked: ${isLinked}`);
|
|
296
|
+
|
|
297
|
+
// Get all globally linked packages
|
|
131
298
|
const globalPackages = await getGloballyLinkedPackages();
|
|
132
|
-
|
|
299
|
+
console.log('Globally linked packages:', Array.from(globalPackages));
|
|
300
|
+
|
|
301
|
+
// Get packages that this project is linked to (consuming)
|
|
302
|
+
const linkedDeps = await getLinkedDependencies('/path/to/consumer');
|
|
303
|
+
console.log('Consuming linked packages:', Array.from(linkedDeps));
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Detect Link Compatibility Problems
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { getLinkCompatibilityProblems } from '@eldrforge/git-tools';
|
|
310
|
+
|
|
311
|
+
// Check for version compatibility issues with linked dependencies
|
|
133
312
|
const problems = await getLinkCompatibilityProblems('/path/to/package');
|
|
134
|
-
|
|
313
|
+
|
|
314
|
+
if (problems.size > 0) {
|
|
315
|
+
console.error('โ ๏ธ Link compatibility problems detected:');
|
|
316
|
+
for (const packageName of problems) {
|
|
317
|
+
console.error(` - ${packageName}`);
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
console.log('โ
All linked dependencies are compatible');
|
|
321
|
+
}
|
|
135
322
|
```
|
|
136
323
|
|
|
137
|
-
|
|
324
|
+
**Note**: `getLinkCompatibilityProblems` intelligently handles prerelease versions (e.g., `4.4.53-dev.0` is compatible with `^4.4`).
|
|
325
|
+
|
|
326
|
+
### Process Execution
|
|
138
327
|
|
|
139
|
-
|
|
328
|
+
#### Secure Command Execution
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { runSecure, runSecureWithInheritedStdio } from '@eldrforge/git-tools';
|
|
332
|
+
|
|
333
|
+
// Execute and capture output
|
|
334
|
+
const { stdout, stderr } = await runSecure('git', ['status', '--porcelain']);
|
|
335
|
+
console.log(stdout);
|
|
336
|
+
|
|
337
|
+
// Execute with inherited stdio (output goes directly to terminal)
|
|
338
|
+
await runSecureWithInheritedStdio('git', ['push', 'origin', 'main']);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### Suppress Error Logging
|
|
342
|
+
|
|
343
|
+
Some commands are expected to fail in certain scenarios:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { runSecure } from '@eldrforge/git-tools';
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
// Check if a branch exists without logging errors
|
|
350
|
+
await runSecure('git', ['rev-parse', '--verify', 'feature-branch'], {
|
|
351
|
+
suppressErrorLogging: true
|
|
352
|
+
});
|
|
353
|
+
console.log('Branch exists');
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.log('Branch does not exist');
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### Input Validation
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { validateGitRef, validateFilePath } from '@eldrforge/git-tools';
|
|
363
|
+
|
|
364
|
+
const userBranch = getUserInput();
|
|
365
|
+
|
|
366
|
+
// Validate before using in commands
|
|
367
|
+
if (validateGitRef(userBranch)) {
|
|
368
|
+
await runSecure('git', ['checkout', userBranch]);
|
|
369
|
+
} else {
|
|
370
|
+
console.error('Invalid branch name');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const userFile = getUserInput();
|
|
374
|
+
if (validateFilePath(userFile)) {
|
|
375
|
+
await runSecure('git', ['add', userFile]);
|
|
376
|
+
} else {
|
|
377
|
+
console.error('Invalid file path');
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Validation Utilities
|
|
382
|
+
|
|
383
|
+
#### Safe JSON Parsing
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import { safeJsonParse, validatePackageJson } from '@eldrforge/git-tools';
|
|
387
|
+
|
|
388
|
+
// Parse JSON with automatic error handling
|
|
389
|
+
try {
|
|
390
|
+
const data = safeJsonParse(jsonString, 'config.json');
|
|
391
|
+
console.log(data);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error('Failed to parse JSON:', error.message);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Validate package.json structure
|
|
397
|
+
try {
|
|
398
|
+
const packageJson = safeJsonParse(fileContents, 'package.json');
|
|
399
|
+
const validated = validatePackageJson(packageJson, 'package.json');
|
|
400
|
+
|
|
401
|
+
console.log(`Package: ${validated.name}`);
|
|
402
|
+
console.log(`Version: ${validated.version}`);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error('Invalid package.json:', error.message);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### String Validation
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { validateString, validateHasProperty } from '@eldrforge/git-tools';
|
|
412
|
+
|
|
413
|
+
// Validate non-empty string
|
|
414
|
+
try {
|
|
415
|
+
const username = validateString(userInput, 'username');
|
|
416
|
+
console.log(`Valid username: ${username}`);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('Invalid username:', error.message);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Validate object has required property
|
|
422
|
+
try {
|
|
423
|
+
validateHasProperty(config, 'apiKey', 'config.json');
|
|
424
|
+
console.log('Config has required apiKey');
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error('Missing required property:', error.message);
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Practical Examples
|
|
431
|
+
|
|
432
|
+
### Example 1: Release Note Generator
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import {
|
|
436
|
+
getCurrentVersion,
|
|
437
|
+
findPreviousReleaseTag,
|
|
438
|
+
runSecure
|
|
439
|
+
} from '@eldrforge/git-tools';
|
|
440
|
+
|
|
441
|
+
async function generateReleaseNotes() {
|
|
442
|
+
// Get version range
|
|
443
|
+
const currentVersion = await getCurrentVersion();
|
|
444
|
+
const previousTag = await findPreviousReleaseTag(currentVersion, 'v*');
|
|
445
|
+
|
|
446
|
+
if (!previousTag) {
|
|
447
|
+
console.log('No previous release found');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Get commits between tags
|
|
452
|
+
const { stdout } = await runSecure('git', [
|
|
453
|
+
'log',
|
|
454
|
+
`${previousTag}..HEAD`,
|
|
455
|
+
'--pretty=format:%s',
|
|
456
|
+
'--no-merges'
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
const commits = stdout.trim().split('\n');
|
|
460
|
+
|
|
461
|
+
console.log(`Release Notes for ${currentVersion}`);
|
|
462
|
+
console.log(`Changes since ${previousTag}:`);
|
|
463
|
+
console.log('');
|
|
464
|
+
commits.forEach(commit => console.log(`- ${commit}`));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
generateReleaseNotes().catch(console.error);
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Example 2: Pre-Push Validation
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import {
|
|
474
|
+
getGitStatusSummary,
|
|
475
|
+
isBranchInSyncWithRemote
|
|
476
|
+
} from '@eldrforge/git-tools';
|
|
477
|
+
|
|
478
|
+
async function validateBeforePush() {
|
|
479
|
+
const status = await getGitStatusSummary();
|
|
480
|
+
|
|
481
|
+
// Check for uncommitted changes
|
|
482
|
+
if (status.hasUnstagedFiles || status.hasUncommittedChanges) {
|
|
483
|
+
console.error('โ Cannot push with uncommitted changes');
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Check if in sync with remote
|
|
488
|
+
const syncStatus = await isBranchInSyncWithRemote(status.branch);
|
|
489
|
+
|
|
490
|
+
if (!syncStatus.inSync) {
|
|
491
|
+
console.error('โ Branch not in sync with remote');
|
|
492
|
+
console.error(`Local: ${syncStatus.localSha}`);
|
|
493
|
+
console.error(`Remote: ${syncStatus.remoteSha}`);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.log('โ
Ready to push');
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
validateBeforePush().catch(console.error);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Example 3: Monorepo Link Checker
|
|
140
505
|
|
|
141
506
|
```typescript
|
|
142
507
|
import {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
validatePackageJson
|
|
508
|
+
getLinkedDependencies,
|
|
509
|
+
getLinkCompatibilityProblems
|
|
146
510
|
} from '@eldrforge/git-tools';
|
|
147
511
|
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
512
|
+
async function checkMonorepoLinks(packageDirs: string[]) {
|
|
513
|
+
for (const packageDir of packageDirs) {
|
|
514
|
+
console.log(`\nChecking: ${packageDir}`);
|
|
515
|
+
|
|
516
|
+
const linked = await getLinkedDependencies(packageDir);
|
|
517
|
+
console.log(`Linked dependencies: ${Array.from(linked).join(', ') || 'none'}`);
|
|
518
|
+
|
|
519
|
+
const problems = await getLinkCompatibilityProblems(packageDir);
|
|
520
|
+
|
|
521
|
+
if (problems.size > 0) {
|
|
522
|
+
console.error('โ ๏ธ Compatibility issues:');
|
|
523
|
+
for (const pkg of problems) {
|
|
524
|
+
console.error(` - ${pkg}`);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
console.log('โ
All links compatible');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
checkMonorepoLinks([
|
|
533
|
+
'./packages/core',
|
|
534
|
+
'./packages/cli',
|
|
535
|
+
'./packages/utils'
|
|
536
|
+
]).catch(console.error);
|
|
151
537
|
```
|
|
152
538
|
|
|
153
|
-
|
|
539
|
+
### Example 4: Branch Sync Script
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import {
|
|
543
|
+
getCurrentBranch,
|
|
544
|
+
localBranchExists,
|
|
545
|
+
safeSyncBranchWithRemote
|
|
546
|
+
} from '@eldrforge/git-tools';
|
|
547
|
+
|
|
548
|
+
async function syncMainBranch() {
|
|
549
|
+
const currentBranch = await getCurrentBranch();
|
|
550
|
+
const hasMain = await localBranchExists('main');
|
|
551
|
+
|
|
552
|
+
if (!hasMain) {
|
|
553
|
+
console.error('โ Main branch does not exist locally');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
console.log(`Current branch: ${currentBranch}`);
|
|
558
|
+
console.log('Syncing main branch with remote...');
|
|
559
|
+
|
|
560
|
+
const result = await safeSyncBranchWithRemote('main');
|
|
561
|
+
|
|
562
|
+
if (result.success) {
|
|
563
|
+
console.log('โ
Main branch synced successfully');
|
|
564
|
+
} else if (result.conflictResolutionRequired) {
|
|
565
|
+
console.error('โ Conflict resolution required');
|
|
566
|
+
console.error(result.error);
|
|
567
|
+
} else {
|
|
568
|
+
console.error('โ Sync failed:', result.error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
syncMainBranch().catch(console.error);
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## API Reference
|
|
154
576
|
|
|
155
577
|
### Git Functions
|
|
156
578
|
|
|
157
|
-
| Function | Description |
|
|
158
|
-
|
|
159
|
-
| `isValidGitRef(ref)` | Tests if a git reference exists and is valid |
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
579
|
+
| Function | Parameters | Returns | Description |
|
|
580
|
+
|----------|------------|---------|-------------|
|
|
581
|
+
| `isValidGitRef(ref)` | `ref: string` | `Promise<boolean>` | Tests if a git reference exists and is valid |
|
|
582
|
+
| `isGitRepository(cwd?)` | `cwd?: string` | `Promise<boolean>` | Checks if directory is a git repository |
|
|
583
|
+
| `findPreviousReleaseTag(version, pattern?)` | `version: string, pattern?: string` | `Promise<string \| null>` | Finds highest tag less than current version |
|
|
584
|
+
| `getCurrentVersion()` | - | `Promise<string \| null>` | Gets current version from package.json |
|
|
585
|
+
| `getCurrentBranch()` | - | `Promise<string>` | Gets current branch name |
|
|
586
|
+
| `getDefaultFromRef(forceMain?, branch?)` | `forceMain?: boolean, branch?: string` | `Promise<string>` | Gets reliable default for release comparison |
|
|
587
|
+
| `getRemoteDefaultBranch(cwd?)` | `cwd?: string` | `Promise<string \| null>` | Gets default branch name from remote |
|
|
588
|
+
| `localBranchExists(branch)` | `branch: string` | `Promise<boolean>` | Checks if local branch exists |
|
|
589
|
+
| `remoteBranchExists(branch, remote?)` | `branch: string, remote?: string` | `Promise<boolean>` | Checks if remote branch exists |
|
|
590
|
+
| `getBranchCommitSha(ref)` | `ref: string` | `Promise<string>` | Gets commit SHA for a branch |
|
|
591
|
+
| `isBranchInSyncWithRemote(branch, remote?)` | `branch: string, remote?: string` | `Promise<SyncStatus>` | Checks if local/remote branches match |
|
|
592
|
+
| `safeSyncBranchWithRemote(branch, remote?)` | `branch: string, remote?: string` | `Promise<SyncResult>` | Safely syncs branch with remote |
|
|
593
|
+
| `getGitStatusSummary(workingDir?)` | `workingDir?: string` | `Promise<GitStatus>` | Gets comprehensive git status |
|
|
594
|
+
| `getGloballyLinkedPackages()` | - | `Promise<Set<string>>` | Gets globally linked npm packages |
|
|
595
|
+
| `getLinkedDependencies(packageDir)` | `packageDir: string` | `Promise<Set<string>>` | Gets linked dependencies for package |
|
|
596
|
+
| `getLinkCompatibilityProblems(packageDir)` | `packageDir: string` | `Promise<Set<string>>` | Finds version compatibility issues |
|
|
597
|
+
| `isNpmLinked(packageDir)` | `packageDir: string` | `Promise<boolean>` | Checks if package is globally linked |
|
|
176
598
|
|
|
177
599
|
### Process Execution Functions
|
|
178
600
|
|
|
179
|
-
| Function | Description |
|
|
180
|
-
|
|
181
|
-
| `runSecure(cmd, args, opts?)` | Securely executes command with argument array |
|
|
182
|
-
| `runSecureWithInheritedStdio(cmd, args, opts?)` | Secure execution with inherited stdio |
|
|
183
|
-
| `run(command, opts?)` | Executes command string (less secure) |
|
|
184
|
-
| `runWithDryRunSupport(cmd, dryRun, opts?)` | Run with dry-run support |
|
|
185
|
-
| `runSecureWithDryRunSupport(cmd, args, dryRun, opts?)` | Secure run with dry-run support |
|
|
186
|
-
| `validateGitRef(ref)` | Validates git reference for injection |
|
|
187
|
-
| `validateFilePath(path)` | Validates file path for injection |
|
|
188
|
-
| `escapeShellArg(arg)` | Escapes shell arguments |
|
|
601
|
+
| Function | Parameters | Returns | Description |
|
|
602
|
+
|----------|------------|---------|-------------|
|
|
603
|
+
| `runSecure(cmd, args, opts?)` | `cmd: string, args: string[], opts?: RunSecureOptions` | `Promise<{stdout, stderr}>` | Securely executes command with argument array |
|
|
604
|
+
| `runSecureWithInheritedStdio(cmd, args, opts?)` | `cmd: string, args: string[], opts?: SpawnOptions` | `Promise<void>` | Secure execution with inherited stdio |
|
|
605
|
+
| `run(command, opts?)` | `command: string, opts?: RunOptions` | `Promise<{stdout, stderr}>` | Executes command string (less secure) |
|
|
606
|
+
| `runWithDryRunSupport(cmd, dryRun, opts?)` | `cmd: string, dryRun: boolean, opts?: ExecOptions` | `Promise<{stdout, stderr}>` | Run with dry-run support |
|
|
607
|
+
| `runSecureWithDryRunSupport(cmd, args, dryRun, opts?)` | `cmd: string, args: string[], dryRun: boolean, opts?: SpawnOptions` | `Promise<{stdout, stderr}>` | Secure run with dry-run support |
|
|
608
|
+
| `validateGitRef(ref)` | `ref: string` | `boolean` | Validates git reference for injection |
|
|
609
|
+
| `validateFilePath(path)` | `path: string` | `boolean` | Validates file path for injection |
|
|
610
|
+
| `escapeShellArg(arg)` | `arg: string` | `string` | Escapes shell arguments |
|
|
611
|
+
|
|
612
|
+
### Logger Functions
|
|
613
|
+
|
|
614
|
+
| Function | Parameters | Returns | Description |
|
|
615
|
+
|----------|------------|---------|-------------|
|
|
616
|
+
| `setLogger(logger)` | `logger: Logger` | `void` | Sets the global logger instance |
|
|
617
|
+
| `getLogger()` | - | `Logger` | Gets the global logger instance |
|
|
189
618
|
|
|
190
619
|
### Validation Functions
|
|
191
620
|
|
|
192
|
-
| Function | Description |
|
|
193
|
-
|
|
194
|
-
| `safeJsonParse<T>(json, context?)` | Safely parses JSON with error handling |
|
|
195
|
-
| `validateString(value, fieldName)` | Validates non-empty string |
|
|
196
|
-
| `validateHasProperty(obj, property, context?)` | Validates object has property |
|
|
197
|
-
| `validatePackageJson(data, context?, requireName?)` | Validates package.json structure |
|
|
621
|
+
| Function | Parameters | Returns | Description |
|
|
622
|
+
|----------|------------|---------|-------------|
|
|
623
|
+
| `safeJsonParse<T>(json, context?)` | `json: string, context?: string` | `T` | Safely parses JSON with error handling |
|
|
624
|
+
| `validateString(value, fieldName)` | `value: any, fieldName: string` | `string` | Validates non-empty string |
|
|
625
|
+
| `validateHasProperty(obj, property, context?)` | `obj: any, property: string, context?: string` | `void` | Validates object has property |
|
|
626
|
+
| `validatePackageJson(data, context?, requireName?)` | `data: any, context?: string, requireName?: boolean` | `any` | Validates package.json structure |
|
|
198
627
|
|
|
199
|
-
|
|
628
|
+
### Type Definitions
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
interface GitStatus {
|
|
632
|
+
branch: string;
|
|
633
|
+
hasUnstagedFiles: boolean;
|
|
634
|
+
hasUncommittedChanges: boolean;
|
|
635
|
+
hasUnpushedCommits: boolean;
|
|
636
|
+
unstagedCount: number;
|
|
637
|
+
uncommittedCount: number;
|
|
638
|
+
unpushedCount: number;
|
|
639
|
+
status: string;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
interface SyncStatus {
|
|
643
|
+
inSync: boolean;
|
|
644
|
+
localSha?: string;
|
|
645
|
+
remoteSha?: string;
|
|
646
|
+
localExists: boolean;
|
|
647
|
+
remoteExists: boolean;
|
|
648
|
+
error?: string;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
interface SyncResult {
|
|
652
|
+
success: boolean;
|
|
653
|
+
error?: string;
|
|
654
|
+
conflictResolutionRequired?: boolean;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
interface Logger {
|
|
658
|
+
error(message: string, ...meta: any[]): void;
|
|
659
|
+
warn(message: string, ...meta: any[]): void;
|
|
660
|
+
info(message: string, ...meta: any[]): void;
|
|
661
|
+
verbose(message: string, ...meta: any[]): void;
|
|
662
|
+
debug(message: string, ...meta: any[]): void;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
interface RunSecureOptions extends SpawnOptions {
|
|
666
|
+
suppressErrorLogging?: boolean;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
interface RunOptions extends ExecOptions {
|
|
670
|
+
suppressErrorLogging?: boolean;
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
## Security Considerations
|
|
200
675
|
|
|
201
676
|
This library prioritizes security in command execution:
|
|
202
677
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
678
|
+
### Shell Injection Prevention
|
|
679
|
+
|
|
680
|
+
All `runSecure*` functions use argument arrays without shell execution:
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
// โ
SAFE: No shell interpretation
|
|
684
|
+
await runSecure('git', ['log', userInput]);
|
|
685
|
+
|
|
686
|
+
// โ ๏ธ UNSAFE: Shell interprets special characters
|
|
687
|
+
await run(`git log ${userInput}`);
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Input Validation
|
|
691
|
+
|
|
692
|
+
Git references and file paths are validated before use:
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
// Validates against: .., leading -, shell metacharacters
|
|
696
|
+
if (!validateGitRef(userRef)) {
|
|
697
|
+
throw new Error('Invalid git reference');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Validates against: shell metacharacters
|
|
701
|
+
if (!validateFilePath(userPath)) {
|
|
702
|
+
throw new Error('Invalid file path');
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Best Practices
|
|
707
|
+
|
|
708
|
+
1. **Always use `runSecure` for user input**
|
|
709
|
+
2. **Validate all git references with `validateGitRef`**
|
|
710
|
+
3. **Validate all file paths with `validateFilePath`**
|
|
711
|
+
4. **Use `suppressErrorLogging` to avoid leaking sensitive info**
|
|
712
|
+
5. **Set custom logger for production environments**
|
|
713
|
+
|
|
714
|
+
## Testing
|
|
715
|
+
|
|
716
|
+
The library includes comprehensive test coverage:
|
|
717
|
+
|
|
718
|
+
```bash
|
|
719
|
+
# Run tests
|
|
720
|
+
npm test
|
|
721
|
+
|
|
722
|
+
# Run tests with coverage
|
|
723
|
+
npm run test
|
|
724
|
+
|
|
725
|
+
# Watch mode
|
|
726
|
+
npm run watch
|
|
727
|
+
```
|
|
207
728
|
|
|
208
729
|
## Development
|
|
209
730
|
|
|
731
|
+
### Building from Source
|
|
732
|
+
|
|
210
733
|
```bash
|
|
734
|
+
# Clone the repository
|
|
735
|
+
git clone https://github.com/calenvarek/git-tools.git
|
|
736
|
+
cd git-tools
|
|
737
|
+
|
|
211
738
|
# Install dependencies
|
|
212
739
|
npm install
|
|
213
740
|
|
|
@@ -219,21 +746,63 @@ npm run test
|
|
|
219
746
|
|
|
220
747
|
# Lint
|
|
221
748
|
npm run lint
|
|
222
|
-
|
|
223
|
-
# Watch mode
|
|
224
|
-
npm run watch
|
|
225
749
|
```
|
|
226
750
|
|
|
751
|
+
### Contributing
|
|
752
|
+
|
|
753
|
+
Contributions are welcome! Please ensure:
|
|
754
|
+
|
|
755
|
+
1. All tests pass: `npm test`
|
|
756
|
+
2. Code is linted: `npm run lint`
|
|
757
|
+
3. Add tests for new features
|
|
758
|
+
4. Update documentation for API changes
|
|
759
|
+
|
|
760
|
+
## Troubleshooting
|
|
761
|
+
|
|
762
|
+
### Common Issues
|
|
763
|
+
|
|
764
|
+
**"Command failed with exit code 128"**
|
|
765
|
+
- Check if the directory is a git repository
|
|
766
|
+
- Verify git is installed and accessible
|
|
767
|
+
- Check git configuration
|
|
768
|
+
|
|
769
|
+
**"Invalid git reference"**
|
|
770
|
+
- Ensure branch/tag names don't contain special characters
|
|
771
|
+
- Verify the reference exists: `git rev-parse --verify <ref>`
|
|
772
|
+
|
|
773
|
+
**"Branch not in sync"**
|
|
774
|
+
- Run `git fetch` to update remote refs
|
|
775
|
+
- Use `safeSyncBranchWithRemote` to sync automatically
|
|
776
|
+
|
|
777
|
+
**NPM link detection not working**
|
|
778
|
+
- Verify package is globally linked: `npm ls -g <package-name>`
|
|
779
|
+
- Check symlinks in global node_modules: `npm prefix -g`
|
|
780
|
+
|
|
227
781
|
## License
|
|
228
782
|
|
|
229
783
|
Apache-2.0 - see [LICENSE](LICENSE) file for details.
|
|
230
784
|
|
|
231
785
|
## Author
|
|
232
786
|
|
|
233
|
-
Calen Varek
|
|
787
|
+
**Calen Varek**
|
|
788
|
+
Email: calenvarek@gmail.com
|
|
789
|
+
GitHub: [@calenvarek](https://github.com/calenvarek)
|
|
234
790
|
|
|
235
791
|
## Related Projects
|
|
236
792
|
|
|
237
|
-
This library was extracted from [kodrdriv](https://github.com/calenvarek/kodrdriv), an AI-powered Git workflow automation tool
|
|
793
|
+
This library was extracted from [kodrdriv](https://github.com/calenvarek/kodrdriv), an AI-powered Git workflow automation tool that uses these utilities for:
|
|
794
|
+
|
|
795
|
+
- Automated commit message generation
|
|
796
|
+
- Release note creation
|
|
797
|
+
- Branch management
|
|
798
|
+
- Monorepo publishing workflows
|
|
799
|
+
|
|
800
|
+
## Changelog
|
|
801
|
+
|
|
802
|
+
See [RELEASE_NOTES.md](RELEASE_NOTES.md) for version history and changes.
|
|
803
|
+
|
|
804
|
+
## Support
|
|
238
805
|
|
|
239
|
-
|
|
806
|
+
- ๐ **Bug Reports**: [GitHub Issues](https://github.com/calenvarek/git-tools/issues)
|
|
807
|
+
- ๐ฌ **Questions**: [GitHub Discussions](https://github.com/calenvarek/git-tools/discussions)
|
|
808
|
+
- ๐ง **Email**: calenvarek@gmail.com
|