semver-ratchet 1.0.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 +284 -0
- package/bin/semver-ratchet +4 -0
- package/index.js +29 -0
- package/package.json +50 -0
- package/src/cli.js +431 -0
- package/src/config.js +74 -0
- package/src/git_adapter.js +112 -0
- package/src/git_mock.js +348 -0
- package/src/git_real.js +210 -0
- package/src/version.js +340 -0
package/src/git_mock.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Git implementation for testing semver-ratchet.
|
|
3
|
+
*
|
|
4
|
+
* This module provides an in-memory mock implementation of GitAdapter
|
|
5
|
+
* that simulates git operations without requiring a real repository.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GitAdapter } from './git_adapter.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* In-memory mock implementation of GitAdapter for testing.
|
|
12
|
+
*
|
|
13
|
+
* This mock simulates git operations without requiring a real repository,
|
|
14
|
+
* allowing for deterministic and isolated tests.
|
|
15
|
+
*
|
|
16
|
+
* Example usage:
|
|
17
|
+
* ```javascript
|
|
18
|
+
* const mock = new MockGitAdapter();
|
|
19
|
+
* mock.setBranch('feature/test');
|
|
20
|
+
* mock.addCommit('Fix bug');
|
|
21
|
+
* mock.createTag('1.0.0');
|
|
22
|
+
* assert.equal(mock.getLatestTag(), '1.0.0');
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class MockGitAdapter extends GitAdapter {
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the mock git adapter with default state.
|
|
28
|
+
*/
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
this._currentBranch = 'main';
|
|
32
|
+
this._branches = {
|
|
33
|
+
main: [],
|
|
34
|
+
master: [],
|
|
35
|
+
};
|
|
36
|
+
this._tags = {}; // tag_name -> commit_hash
|
|
37
|
+
this._defaultBranch = 'main';
|
|
38
|
+
this._historyVerified = true;
|
|
39
|
+
this._fetchDepth = 50;
|
|
40
|
+
this._tagCreationOrder = [];
|
|
41
|
+
this._pushedTags = [];
|
|
42
|
+
this._remoteConfigured = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reset all state to defaults.
|
|
47
|
+
*/
|
|
48
|
+
reset() {
|
|
49
|
+
this._currentBranch = 'main';
|
|
50
|
+
this._branches = { main: [], master: [] };
|
|
51
|
+
this._tags = {};
|
|
52
|
+
this._defaultBranch = 'main';
|
|
53
|
+
this._historyVerified = true;
|
|
54
|
+
this._fetchDepth = 50;
|
|
55
|
+
this._tagCreationOrder = [];
|
|
56
|
+
this._pushedTags = [];
|
|
57
|
+
this._remoteConfigured = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set the current branch.
|
|
62
|
+
* @param {string} branchName - Name of the branch to switch to
|
|
63
|
+
*/
|
|
64
|
+
setBranch(branchName) {
|
|
65
|
+
this._currentBranch = branchName;
|
|
66
|
+
if (!(branchName in this._branches)) {
|
|
67
|
+
this._branches[branchName] = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set the default/main branch name.
|
|
73
|
+
* @param {string} branchName - Name of the main branch (e.g., "main" or "master")
|
|
74
|
+
*/
|
|
75
|
+
setDefaultBranch(branchName) {
|
|
76
|
+
this._defaultBranch = branchName;
|
|
77
|
+
if (!(branchName in this._branches)) {
|
|
78
|
+
this._branches[branchName] = [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add a commit to the current (or specified) branch.
|
|
84
|
+
* @param {string} message - Commit message
|
|
85
|
+
* @param {string} [branch] - Optional branch name (defaults to current branch)
|
|
86
|
+
*/
|
|
87
|
+
addCommit(message, branch = null) {
|
|
88
|
+
const targetBranch = branch || this._currentBranch;
|
|
89
|
+
if (!(targetBranch in this._branches)) {
|
|
90
|
+
this._branches[targetBranch] = [];
|
|
91
|
+
}
|
|
92
|
+
this._branches[targetBranch].push(message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a tag internally (without printing).
|
|
97
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
_createTagInternal(version) {
|
|
101
|
+
const tagName = `v${version}`;
|
|
102
|
+
|
|
103
|
+
// Tag points to the latest commit on the branch
|
|
104
|
+
const commits = this._branches[this._currentBranch];
|
|
105
|
+
if (commits && commits.length > 0) {
|
|
106
|
+
this._tags[tagName] = commits[commits.length - 1];
|
|
107
|
+
} else {
|
|
108
|
+
// Create a synthetic commit hash for empty branches
|
|
109
|
+
this._tags[tagName] = `tag_${version}_no_commits`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this._tagCreationOrder.push(tagName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a tag on the current branch.
|
|
117
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
118
|
+
* @param {boolean} [force=false] - If true, overwrite existing tag
|
|
119
|
+
* @returns {boolean} True if tag was created successfully
|
|
120
|
+
*/
|
|
121
|
+
createTag(version, force = false) {
|
|
122
|
+
const tagName = `v${version}`;
|
|
123
|
+
|
|
124
|
+
if (tagName in this._tags) {
|
|
125
|
+
if (force) {
|
|
126
|
+
// Overwrite existing tag
|
|
127
|
+
this._createTagInternal(version);
|
|
128
|
+
console.log(`Created tag v${version} (forced)`);
|
|
129
|
+
return true;
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`Tag v${version} already exists. Use --force to overwrite.`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Create new tag
|
|
136
|
+
this._createTagInternal(version);
|
|
137
|
+
console.log(`Created tag v${version}`);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Delete a tag.
|
|
144
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
145
|
+
*/
|
|
146
|
+
deleteTag(version) {
|
|
147
|
+
const tagName = `v${version}`;
|
|
148
|
+
if (tagName in this._tags) {
|
|
149
|
+
delete this._tags[tagName];
|
|
150
|
+
this._tagCreationOrder = this._tagCreationOrder.filter(t => t !== tagName);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Set multiple tags at once.
|
|
156
|
+
* @param {string[]} tags - List of version strings (without 'v' prefix)
|
|
157
|
+
*/
|
|
158
|
+
setTags(tags) {
|
|
159
|
+
for (const version of tags) {
|
|
160
|
+
this._createTagInternal(version);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Set commit messages for a branch.
|
|
166
|
+
* @param {string[]} messages - List of commit messages
|
|
167
|
+
* @param {string} [branch] - Optional branch name (defaults to current branch)
|
|
168
|
+
*/
|
|
169
|
+
setCommitMessages(messages, branch = null) {
|
|
170
|
+
const targetBranch = branch || this._currentBranch;
|
|
171
|
+
this._branches[targetBranch] = [...messages];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Set whether git history verification should succeed.
|
|
176
|
+
* @param {boolean} verified - True if history is sufficient
|
|
177
|
+
*/
|
|
178
|
+
setHistoryVerified(verified) {
|
|
179
|
+
this._historyVerified = verified;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set the required fetch depth for history verification.
|
|
184
|
+
* @param {number} depth - Minimum number of commits required
|
|
185
|
+
*/
|
|
186
|
+
setFetchDepth(depth) {
|
|
187
|
+
this._fetchDepth = depth;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set whether a remote is configured.
|
|
192
|
+
* @param {boolean} configured - True if remote is configured
|
|
193
|
+
*/
|
|
194
|
+
setRemoteConfigured(configured) {
|
|
195
|
+
this._remoteConfigured = configured;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get the current branch name.
|
|
200
|
+
* @returns {string} The branch name
|
|
201
|
+
*/
|
|
202
|
+
getCurrentBranch() {
|
|
203
|
+
return this._currentBranch;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if current branch is main or master.
|
|
208
|
+
* @returns {boolean} True if on main/master branch
|
|
209
|
+
*/
|
|
210
|
+
isMainBranch() {
|
|
211
|
+
return this._currentBranch.toLowerCase() === 'main' || this._currentBranch.toLowerCase() === 'master';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Calculate the number of commits between HEAD and main branch.
|
|
216
|
+
* @returns {number} Number of commits
|
|
217
|
+
*/
|
|
218
|
+
getGitDistance() {
|
|
219
|
+
if (this.isMainBranch()) {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const currentBranch = this._currentBranch;
|
|
224
|
+
const currentCommits = this._branches[currentBranch] || [];
|
|
225
|
+
|
|
226
|
+
// Simple distance calculation: commits on current branch
|
|
227
|
+
// In a real scenario, this would be more complex with merge tracking
|
|
228
|
+
return currentCommits.length;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the latest SemVer tag on the current branch.
|
|
233
|
+
* @returns {string|null} Tag version without 'v' prefix, or null if no tags
|
|
234
|
+
*/
|
|
235
|
+
getLatestTag() {
|
|
236
|
+
if (this._tagCreationOrder.length === 0) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Return the last tag created (simple implementation)
|
|
241
|
+
const lastTag = this._tagCreationOrder[this._tagCreationOrder.length - 1];
|
|
242
|
+
return lastTag.substring(1); // Strip 'v' prefix
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get commit messages since a specific tag or all commits.
|
|
247
|
+
* @param {string} [sinceTag] - Optional tag to start from (without 'v' prefix)
|
|
248
|
+
* @returns {string[]} List of commit messages
|
|
249
|
+
*/
|
|
250
|
+
getCommitMessages(sinceTag = null) {
|
|
251
|
+
const currentBranch = this._currentBranch;
|
|
252
|
+
const messages = this._branches[currentBranch] || [];
|
|
253
|
+
|
|
254
|
+
if (!sinceTag) {
|
|
255
|
+
return [...messages];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Find commits after the tag
|
|
259
|
+
const tagName = `v${sinceTag}`;
|
|
260
|
+
if (!(tagName in this._tags)) {
|
|
261
|
+
return [...messages];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Simple implementation: return all messages if tag exists
|
|
265
|
+
// A more sophisticated mock would track commit positions
|
|
266
|
+
return [...messages];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Verify that enough git history is available.
|
|
271
|
+
* @param {number} [fetchDepth=50] - Minimum number of commits required
|
|
272
|
+
* @returns {boolean} True if sufficient history exists
|
|
273
|
+
* @throws {Error} If history is insufficient
|
|
274
|
+
*/
|
|
275
|
+
verifyGitHistory(fetchDepth = 50) {
|
|
276
|
+
if (!this._historyVerified) {
|
|
277
|
+
throw new Error('Insufficient git history: mock history verification disabled');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check if we have enough commits
|
|
281
|
+
const currentBranch = this._currentBranch;
|
|
282
|
+
const commits = this._branches[currentBranch] || [];
|
|
283
|
+
|
|
284
|
+
if (commits.length < fetchDepth) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Insufficient git history: ${commits.length} commits found, ` +
|
|
287
|
+
`need at least ${fetchDepth} for accurate version calculation`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if a tag exists for the given version.
|
|
296
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
297
|
+
* @returns {boolean} True if tag exists
|
|
298
|
+
*/
|
|
299
|
+
tagExists(version) {
|
|
300
|
+
const tagName = `v${version}`;
|
|
301
|
+
return tagName in this._tags;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Push a tag to the remote repository.
|
|
306
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
307
|
+
* @param {string} [remote='origin'] - Remote name to push to
|
|
308
|
+
* @throws {Error} If tag doesn't exist or remote not configured
|
|
309
|
+
*/
|
|
310
|
+
pushTag(version, remote = 'origin') {
|
|
311
|
+
const tagName = `v${version}`;
|
|
312
|
+
if (!(tagName in this._tags)) {
|
|
313
|
+
throw new Error(`Tag v${version} does not exist, cannot push`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!this._remoteConfigured) {
|
|
317
|
+
throw new Error('No remote configured, cannot push');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this._pushedTags.push(tagName);
|
|
321
|
+
console.log(`Pushed tag v${version} to ${remote}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the list of pushed tags (for testing).
|
|
326
|
+
* @returns {string[]} List of pushed tag names
|
|
327
|
+
*/
|
|
328
|
+
getPushedTags() {
|
|
329
|
+
return [...this._pushedTags];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get the number of commits on a branch (for testing).
|
|
334
|
+
* @param {string} branch - Branch name
|
|
335
|
+
* @returns {number} Number of commits
|
|
336
|
+
*/
|
|
337
|
+
getCommitCount(branch) {
|
|
338
|
+
return this._branches[branch]?.length || 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get all branches (for testing).
|
|
343
|
+
* @returns {string[]} List of branch names
|
|
344
|
+
*/
|
|
345
|
+
getBranches() {
|
|
346
|
+
return Object.keys(this._branches);
|
|
347
|
+
}
|
|
348
|
+
}
|
package/src/git_real.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Git implementation for semver-ratchet.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a production implementation of GitAdapter
|
|
5
|
+
* that executes actual git commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { GitAdapter } from './git_adapter.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper function to execute git commands.
|
|
13
|
+
* @param {string} command - Git command to execute (without 'git' prefix)
|
|
14
|
+
* @returns {string} Command output
|
|
15
|
+
* @throws {Error} If command fails
|
|
16
|
+
*/
|
|
17
|
+
function execGit(command) {
|
|
18
|
+
return execSync(`git ${command}`, { encoding: 'utf-8' }).trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Real git implementation that executes actual git commands.
|
|
23
|
+
*
|
|
24
|
+
* This is the production implementation used when not testing.
|
|
25
|
+
*/
|
|
26
|
+
export class RealGitAdapter extends GitAdapter {
|
|
27
|
+
/**
|
|
28
|
+
* Get the current branch name.
|
|
29
|
+
* @returns {string} The branch name
|
|
30
|
+
* @throws {Error} If git operation fails
|
|
31
|
+
*/
|
|
32
|
+
getCurrentBranch() {
|
|
33
|
+
try {
|
|
34
|
+
return execGit('branch --show-current');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(`Failed to get current branch: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if current branch is main or master.
|
|
42
|
+
* @returns {boolean} True if on main/master branch
|
|
43
|
+
* @throws {Error} If git operation fails
|
|
44
|
+
*/
|
|
45
|
+
isMainBranch() {
|
|
46
|
+
const branch = this.getCurrentBranch();
|
|
47
|
+
return branch.toLowerCase() === 'main' || branch.toLowerCase() === 'master';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Calculate the number of commits between HEAD and main branch.
|
|
52
|
+
* @returns {number} Number of commits
|
|
53
|
+
* @throws {Error} If git operation fails
|
|
54
|
+
*/
|
|
55
|
+
getGitDistance() {
|
|
56
|
+
if (this.isMainBranch()) {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Count commits on current branch not on main
|
|
62
|
+
this.getCurrentBranch();
|
|
63
|
+
const output = execGit('rev-list --count HEAD ^origin/main 2>/dev/null || rev-list --count HEAD');
|
|
64
|
+
return parseInt(output, 10) || 0;
|
|
65
|
+
} catch {
|
|
66
|
+
// Fallback: try a simpler approach
|
|
67
|
+
try {
|
|
68
|
+
const output = execGit('rev-list --count HEAD');
|
|
69
|
+
return parseInt(output, 10) || 0;
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the latest SemVer tag on the current branch.
|
|
78
|
+
* @returns {string|null} Tag version without 'v' prefix, or null if no tags
|
|
79
|
+
* @throws {Error} If git operation fails
|
|
80
|
+
*/
|
|
81
|
+
getLatestTag() {
|
|
82
|
+
try {
|
|
83
|
+
const tag = execGit('describe --tags --abbrev=0');
|
|
84
|
+
// Strip 'v' prefix if present (e.g., "v1.0.0" -> "1.0.0")
|
|
85
|
+
let version = tag.replace(/^v/, '');
|
|
86
|
+
|
|
87
|
+
// Strip pre-release and build metadata if present
|
|
88
|
+
// e.g., "1.0.0-alpha" -> "1.0.0", "1.0.0+build123" -> "1.0.0"
|
|
89
|
+
version = version.split(/[-+]/)[0];
|
|
90
|
+
|
|
91
|
+
return version;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get commit messages since a specific tag or all commits.
|
|
99
|
+
* @param {string} [sinceTag] - Optional tag to start from (without 'v' prefix)
|
|
100
|
+
* @returns {string[]} List of commit messages
|
|
101
|
+
* @throws {Error} If git operation fails
|
|
102
|
+
*/
|
|
103
|
+
getCommitMessages(sinceTag = null) {
|
|
104
|
+
try {
|
|
105
|
+
let command;
|
|
106
|
+
if (sinceTag) {
|
|
107
|
+
command = `log ${sinceTag}..HEAD --pretty=format:%s`;
|
|
108
|
+
} else {
|
|
109
|
+
command = 'log --pretty=format:%s -100'; // Get last 100 commits
|
|
110
|
+
}
|
|
111
|
+
const output = execGit(command);
|
|
112
|
+
return output ? output.split('\n').filter(m => m.trim()) : [];
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Verify that enough git history is available.
|
|
120
|
+
* @param {number} [fetchDepth=50] - Minimum number of commits required
|
|
121
|
+
* @returns {boolean} True if sufficient history exists
|
|
122
|
+
* @throws {Error} If history is insufficient
|
|
123
|
+
*/
|
|
124
|
+
verifyGitHistory(fetchDepth = 50) {
|
|
125
|
+
try {
|
|
126
|
+
// Check if we have enough commits
|
|
127
|
+
const output = execGit('rev-list --count HEAD');
|
|
128
|
+
const commitCount = parseInt(output, 10) || 0;
|
|
129
|
+
|
|
130
|
+
if (commitCount < fetchDepth) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Insufficient git history: ${commitCount} commits found, ` +
|
|
133
|
+
`need at least ${fetchDepth} for accurate version calculation`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error.message.includes('Insufficient git history')) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Failed to verify git history: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if a tag exists for the given version.
|
|
148
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
149
|
+
* @returns {boolean} True if tag exists
|
|
150
|
+
* @throws {Error} If git operation fails
|
|
151
|
+
*/
|
|
152
|
+
tagExists(version) {
|
|
153
|
+
try {
|
|
154
|
+
const tagName = `v${version}`;
|
|
155
|
+
execGit(`rev-parse --verify refs/tags/${tagName} >/dev/null 2>&1`);
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a git tag for the specified version.
|
|
164
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
165
|
+
* @param {boolean} [force=false] - If true, overwrite existing tag
|
|
166
|
+
* @returns {boolean} True if tag was created successfully
|
|
167
|
+
* @throws {Error} If git operation fails
|
|
168
|
+
*/
|
|
169
|
+
createTag(version, force = false) {
|
|
170
|
+
try {
|
|
171
|
+
const tagName = `v${version}`;
|
|
172
|
+
|
|
173
|
+
if (this.tagExists(version)) {
|
|
174
|
+
if (force) {
|
|
175
|
+
// Delete existing tag and create new one
|
|
176
|
+
execGit(`tag -d ${tagName}`);
|
|
177
|
+
execGit(`tag -a ${tagName} -m "Release ${version}"`);
|
|
178
|
+
console.log(`Created tag v${version} (forced)`);
|
|
179
|
+
return true;
|
|
180
|
+
} else {
|
|
181
|
+
console.log(`Tag v${version} already exists. Use --force to overwrite.`);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// Create new tag
|
|
186
|
+
execGit(`tag -a ${tagName} -m "Release ${version}"`);
|
|
187
|
+
console.log(`Created tag v${version}`);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new Error(`Failed to create tag: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Push a tag to the remote repository.
|
|
197
|
+
* @param {string} version - Version string (without 'v' prefix)
|
|
198
|
+
* @param {string} [remote='origin'] - Remote name to push to
|
|
199
|
+
* @throws {Error} If git operation fails
|
|
200
|
+
*/
|
|
201
|
+
pushTag(version, remote = 'origin') {
|
|
202
|
+
try {
|
|
203
|
+
const tagName = `v${version}`;
|
|
204
|
+
execSync(`git push ${remote} ${tagName}`, { encoding: 'utf-8' });
|
|
205
|
+
console.log(`Pushed tag v${version} to ${remote}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw new Error(`Failed to push tag: ${error.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|