submodule-version 2.1.3 → 2.3.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/dist/runner/handleErrors/handleBuildGraphError.js +2 -2
- package/dist/runner/handleErrors/handleBuildGraphError.js.map +1 -1
- package/dist/src/__tests__/versionUtil.test.js +14 -0
- package/dist/src/__tests__/versionUtil.test.js.map +1 -1
- package/dist/src/buildGraph/GraphError.js.map +1 -0
- package/dist/src/buildGraph/buildGraph.d.ts +8 -0
- package/dist/src/buildGraph/buildGraph.js +100 -0
- package/dist/src/buildGraph/buildGraph.js.map +1 -0
- package/dist/src/{Graph → buildGraph}/index.d.ts +1 -1
- package/dist/src/buildGraph/index.js +8 -0
- package/dist/src/buildGraph/index.js.map +1 -0
- package/dist/src/git/git.d.ts +19 -0
- package/dist/src/git/git.js +215 -2
- package/dist/src/git/git.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/{PkgJSONManager.d.ts → pkgJSONManager.d.ts} +2 -4
- package/dist/src/pkgJSONManager.js +41 -0
- package/dist/src/pkgJSONManager.js.map +1 -0
- package/dist/src/sv.d.ts +16 -3
- package/dist/src/sv.js +179 -16
- package/dist/src/sv.js.map +1 -1
- package/dist/src/versionUtil.d.ts +1 -0
- package/dist/src/versionUtil.js +9 -0
- package/dist/src/versionUtil.js.map +1 -1
- package/package.json +1 -1
- package/runner/handleErrors/handleBuildGraphError.ts +1 -1
- package/src/__tests__/versionUtil.test.ts +17 -0
- package/src/buildGraph/buildGraph.ts +150 -0
- package/src/{Graph → buildGraph}/index.ts +1 -1
- package/src/git/git.ts +331 -2
- package/src/index.ts +1 -0
- package/src/{PkgJSONManager.ts → pkgJSONManager.ts} +6 -20
- package/src/sv.ts +220 -21
- package/src/versionUtil.ts +13 -0
- package/dist/src/Graph/Graph.d.ts +0 -12
- package/dist/src/Graph/Graph.js +0 -85
- package/dist/src/Graph/Graph.js.map +0 -1
- package/dist/src/Graph/GraphError.js.map +0 -1
- package/dist/src/Graph/index.js +0 -8
- package/dist/src/Graph/index.js.map +0 -1
- package/dist/src/PkgJSONManager.js +0 -53
- package/dist/src/PkgJSONManager.js.map +0 -1
- package/src/Graph/Graph.ts +0 -121
- /package/dist/src/{Graph → buildGraph}/GraphError.d.ts +0 -0
- /package/dist/src/{Graph → buildGraph}/GraphError.js +0 -0
- /package/src/{Graph → buildGraph}/GraphError.ts +0 -0
package/src/git/git.ts
CHANGED
|
@@ -40,10 +40,339 @@ export const git = {
|
|
|
40
40
|
|
|
41
41
|
listVersions: async (projectName: string): Promise<string[]> => {
|
|
42
42
|
const module = path.join(RunOptions.modulesDir, projectName);
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
return git.listTags(module);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
status: async (dir: string): Promise<string> => {
|
|
48
|
+
const res = await execAsync(
|
|
49
|
+
`git -C ${dir} status --porcelain`,
|
|
50
|
+
{ cwd: RunOptions.cwd },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return res.stdout.toString();
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
hasChanges: async (dir: string): Promise<boolean> => {
|
|
57
|
+
const status = await git.status(dir);
|
|
58
|
+
|
|
59
|
+
return status.trim().length > 0;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
hasUnpushedCommits: async (dir: string): Promise<boolean> => {
|
|
63
|
+
await git.fetch(dir);
|
|
64
|
+
|
|
65
|
+
const branch = await git.currentBranch(dir);
|
|
66
|
+
|
|
67
|
+
if (branch) {
|
|
68
|
+
let upstream: string | null = null;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const res = await execAsync(
|
|
72
|
+
`git -C ${dir} rev-parse --abbrev-ref --symbolic-full-name @{u}`,
|
|
73
|
+
{ cwd: RunOptions.cwd },
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
upstream = res.stdout.toString().trim() || null;
|
|
77
|
+
} catch {
|
|
78
|
+
upstream = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!upstream) {
|
|
82
|
+
try {
|
|
83
|
+
await execAsync(
|
|
84
|
+
`git -C ${dir} rev-parse --verify --quiet origin/${branch}`,
|
|
85
|
+
{ cwd: RunOptions.cwd },
|
|
86
|
+
);
|
|
87
|
+
upstream = `origin/${branch}`;
|
|
88
|
+
} catch {
|
|
89
|
+
upstream = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Ветки на remote нет — всё локальное не опубликовано */
|
|
94
|
+
if (!upstream) return true;
|
|
95
|
+
|
|
96
|
+
const res = await execAsync(
|
|
97
|
+
`git -C ${dir} rev-list ${upstream}..HEAD --count`,
|
|
98
|
+
{ cwd: RunOptions.cwd },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return Number(res.stdout.toString().trim()) > 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* Detached HEAD (checkout на тег): локальные теги не доказывают публикацию
|
|
106
|
+
* (могут быть не запушены) — сверяем HEAD с фактическими рефами remote.
|
|
107
|
+
*/
|
|
108
|
+
let remoteOutput = '';
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const res = await execAsync(
|
|
112
|
+
`git -C ${dir} ls-remote origin`,
|
|
113
|
+
{ cwd: RunOptions.cwd },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
remoteOutput = res.stdout.toString();
|
|
117
|
+
} catch {
|
|
118
|
+
/* Remote недоступен/не настроен — считаем всё неопубликованным */
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const remoteShas = remoteOutput.split(/\r?\n/)
|
|
123
|
+
.map(line => line.split(/\s+/)[0])
|
|
124
|
+
.filter(sha => /^[0-9a-f]{40}$/.test(sha));
|
|
125
|
+
|
|
126
|
+
for (const sha of remoteShas) {
|
|
127
|
+
try {
|
|
128
|
+
await execAsync(
|
|
129
|
+
`git -C ${dir} merge-base --is-ancestor HEAD ${sha}`,
|
|
130
|
+
{ cwd: RunOptions.cwd },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
/* HEAD достижим из remote-рефа — опубликован */
|
|
134
|
+
return false;
|
|
135
|
+
} catch {
|
|
136
|
+
// HEAD не предок этого рефа — проверяем остальные
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
isGitRepo: async (dir: string): Promise<boolean> => {
|
|
144
|
+
try {
|
|
145
|
+
await execAsync(
|
|
146
|
+
`git -C ${dir} rev-parse --is-inside-work-tree`,
|
|
147
|
+
{ cwd: RunOptions.cwd },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
getRemote: async (dir: string): Promise<string | null> => {
|
|
157
|
+
try {
|
|
158
|
+
const res = await execAsync(
|
|
159
|
+
`git -C ${dir} remote get-url origin`,
|
|
160
|
+
{ cwd: RunOptions.cwd },
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return res.stdout.toString().trim() || null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
init: async (dir: string): Promise<void> => {
|
|
170
|
+
await execAsync(
|
|
171
|
+
`git -C ${dir} init`,
|
|
172
|
+
{ cwd: RunOptions.cwd },
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
addRemote: async (dir: string, url: string): Promise<void> => {
|
|
177
|
+
const existing = await git.getRemote(dir);
|
|
178
|
+
|
|
179
|
+
const command = existing
|
|
180
|
+
? `git -C ${dir} remote set-url origin ${url}`
|
|
181
|
+
: `git -C ${dir} remote add origin ${url}`;
|
|
182
|
+
|
|
183
|
+
await execAsync(command, { cwd: RunOptions.cwd });
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
currentBranch: async (dir: string): Promise<string | null> => {
|
|
187
|
+
try {
|
|
188
|
+
const res = await execAsync(
|
|
189
|
+
`git -C ${dir} symbolic-ref --short -q HEAD`,
|
|
190
|
+
{ cwd: RunOptions.cwd },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return res.stdout.toString().trim() || null;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
defaultRemoteBranch: async (dir: string): Promise<string | null> => {
|
|
200
|
+
try {
|
|
201
|
+
const res = await execAsync(
|
|
202
|
+
`git -C ${dir} symbolic-ref --short refs/remotes/origin/HEAD`,
|
|
203
|
+
{ cwd: RunOptions.cwd },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return res.stdout.toString().trim().replace(/^origin\//, '') || null;
|
|
207
|
+
} catch {
|
|
208
|
+
// origin/HEAD not set - probe common defaults
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const candidate of ['main', 'master']) {
|
|
212
|
+
try {
|
|
213
|
+
await execAsync(
|
|
214
|
+
`git -C ${dir} rev-parse --verify --quiet origin/${candidate}`,
|
|
215
|
+
{ cwd: RunOptions.cwd },
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return candidate;
|
|
219
|
+
} catch {
|
|
220
|
+
// No such remote branch
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* Аддоны checkout'нуты на тег (detached HEAD). Перед коммитом поднимаем
|
|
229
|
+
* локальную ветку на текущем коммите и привязываем её к remote-ветке,
|
|
230
|
+
* чтобы sync/push работали по ветке, а не в оторванной голове.
|
|
231
|
+
*/
|
|
232
|
+
ensureBranch: async (dir: string): Promise<void> => {
|
|
233
|
+
const branch = await git.currentBranch(dir);
|
|
234
|
+
if (branch) return;
|
|
235
|
+
|
|
236
|
+
await git.fetch(dir);
|
|
237
|
+
const remoteBranch = await git.defaultRemoteBranch(dir);
|
|
238
|
+
const target = remoteBranch || 'main';
|
|
239
|
+
|
|
240
|
+
await execAsync(
|
|
241
|
+
`git -C ${dir} checkout -B ${target}`,
|
|
242
|
+
{ cwd: RunOptions.cwd },
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (remoteBranch) {
|
|
246
|
+
await execAsync(
|
|
247
|
+
// eslint-disable-next-line max-len
|
|
248
|
+
`git -C ${dir} branch --set-upstream-to=origin/${remoteBranch} ${target}`,
|
|
249
|
+
{ cwd: RunOptions.cwd },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
commitAll: async (dir: string, message: string): Promise<void> => {
|
|
255
|
+
await execAsync(
|
|
256
|
+
`git -C ${dir} add -A`,
|
|
257
|
+
{ cwd: RunOptions.cwd },
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
await execAsync(
|
|
261
|
+
`git -C ${dir} commit -m ${JSON.stringify(message)}`,
|
|
262
|
+
{ cwd: RunOptions.cwd },
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
addTag: async (dir: string, version: string): Promise<void> => {
|
|
267
|
+
await execAsync(
|
|
268
|
+
`git -C ${dir} tag ${version}`,
|
|
269
|
+
{ cwd: RunOptions.cwd },
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
push: async (dir: string, tag?: string): Promise<void> => {
|
|
274
|
+
await execAsync(
|
|
275
|
+
`git -C ${dir} push -u origin HEAD`,
|
|
276
|
+
{ cwd: RunOptions.cwd },
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (tag) {
|
|
280
|
+
await execAsync(
|
|
281
|
+
`git -C ${dir} push origin ${tag}`,
|
|
282
|
+
{ cwd: RunOptions.cwd },
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
fetch: async (dir: string): Promise<void> => {
|
|
288
|
+
try {
|
|
289
|
+
await execAsync(
|
|
290
|
+
`git -C ${dir} fetch origin`,
|
|
291
|
+
{ cwd: RunOptions.cwd },
|
|
292
|
+
);
|
|
293
|
+
} catch {
|
|
294
|
+
// Fresh repo without upstream yet - nothing to fetch
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
isRemoteAhead: async (dir: string): Promise<boolean> => {
|
|
299
|
+
await git.fetch(dir);
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const res = await execAsync(
|
|
303
|
+
`git -C ${dir} rev-list HEAD..@{u} --count`,
|
|
304
|
+
{ cwd: RunOptions.cwd },
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
return Number(res.stdout.toString().trim()) > 0;
|
|
308
|
+
} catch {
|
|
309
|
+
// No upstream configured - remote is not ahead
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
pullRebaseAutostash: async (dir: string): Promise<void> => {
|
|
315
|
+
try {
|
|
316
|
+
await execAsync(
|
|
317
|
+
`git -C ${dir} pull --rebase --autostash`,
|
|
318
|
+
{ cwd: RunOptions.cwd },
|
|
319
|
+
);
|
|
320
|
+
} catch {
|
|
321
|
+
try {
|
|
322
|
+
await execAsync(
|
|
323
|
+
`git -C ${dir} rebase --abort`,
|
|
324
|
+
{ cwd: RunOptions.cwd },
|
|
325
|
+
);
|
|
326
|
+
} catch {
|
|
327
|
+
// Best effort - no rebase in progress
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
await execAsync(
|
|
332
|
+
`git -C ${dir} stash pop`,
|
|
333
|
+
{ cwd: RunOptions.cwd },
|
|
334
|
+
);
|
|
335
|
+
} catch {
|
|
336
|
+
// Best effort - nothing stashed
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
throw new GitError('GIT_SYNC_CONFLICT');
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
tagsAtHead: async (dir: string): Promise<string[]> => {
|
|
344
|
+
try {
|
|
345
|
+
const res = await execAsync(
|
|
346
|
+
`git -C ${dir} tag --points-at HEAD`,
|
|
347
|
+
{ cwd: RunOptions.cwd },
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return res.stdout.toString().split(/\r?\n/)
|
|
351
|
+
.filter(tag => tag && versionUtil.validate(tag));
|
|
352
|
+
} catch {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
currentVersion: async (name: string): Promise<string | null> => {
|
|
358
|
+
const module = path.join(RunOptions.modulesDir, name);
|
|
359
|
+
const tags = await git.tagsAtHead(module);
|
|
360
|
+
|
|
361
|
+
return versionUtil.latest(tags) || null;
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
listTags: async (dir: string): Promise<string[]> => {
|
|
365
|
+
try {
|
|
366
|
+
await execAsync(
|
|
367
|
+
`git -C ${dir} fetch --tags`,
|
|
368
|
+
{ cwd: RunOptions.cwd },
|
|
369
|
+
);
|
|
370
|
+
} catch {
|
|
371
|
+
// Repo without remote - local tags only
|
|
372
|
+
}
|
|
44
373
|
|
|
45
374
|
const res = await execAsync(
|
|
46
|
-
`git -C ${
|
|
375
|
+
`git -C ${dir} tag -l`,
|
|
47
376
|
{ cwd: RunOptions.cwd },
|
|
48
377
|
);
|
|
49
378
|
|
package/src/index.ts
CHANGED
|
@@ -19,39 +19,25 @@ const getJsonPath = async (submoduleName?: string): Promise<TJSONPath> => {
|
|
|
19
19
|
return jsonPath;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
export
|
|
23
|
-
private cache: Map<string, any> = new Map();
|
|
24
|
-
|
|
22
|
+
export const pkgJSONManager = {
|
|
25
23
|
/* Reads package.json in project folder or in submodule */
|
|
26
|
-
|
|
24
|
+
read: async (submoduleName?: string) => {
|
|
27
25
|
const jsonPath = await getJsonPath(submoduleName);
|
|
28
26
|
|
|
29
27
|
if (!jsonPath) return null;
|
|
30
28
|
|
|
31
|
-
if (this.cache.has(jsonPath)) {
|
|
32
|
-
return this.cache.get(jsonPath);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
29
|
const jsonString = await readFileAsync(jsonPath, 'utf-8');
|
|
36
30
|
const parsed = JSON.parse(jsonString);
|
|
37
31
|
|
|
38
|
-
this.cache.set(jsonPath, parsed);
|
|
39
|
-
|
|
40
32
|
return parsed;
|
|
41
|
-
}
|
|
33
|
+
},
|
|
42
34
|
|
|
43
35
|
/* Writes package.json in project folder or in submodule */
|
|
44
|
-
|
|
36
|
+
write: async (data: Object, submoduleName?: string) => {
|
|
45
37
|
const jsonPath = await getJsonPath(submoduleName);
|
|
46
38
|
|
|
47
39
|
if (!jsonPath) return;
|
|
48
40
|
|
|
49
|
-
this.cache.set(jsonPath, data);
|
|
50
|
-
|
|
51
41
|
await writeFileAsync(jsonPath, JSON.stringify(data, null, 2));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
public clearCache (): void {
|
|
55
|
-
this.cache.clear();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
42
|
+
},
|
|
43
|
+
};
|
package/src/sv.ts
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { pkgJSONManager } from "./pkgJSONManager";
|
|
3
|
+
import { buildGraph } from "./buildGraph";
|
|
3
4
|
import { RunOptions } from "./runOptions";
|
|
4
5
|
import { versionUtil } from "./versionUtil";
|
|
5
|
-
import { git } from "./git";
|
|
6
|
+
import { git, GitError } from "./git";
|
|
6
7
|
|
|
7
8
|
export class SV {
|
|
8
|
-
private pkgJSONManager: PkgJSONManager;
|
|
9
|
-
private graph: Graph;
|
|
10
|
-
|
|
11
9
|
constructor (
|
|
12
10
|
projectDir: string,
|
|
13
11
|
modulesDir: string = 'addons',
|
|
14
12
|
) {
|
|
15
13
|
RunOptions.cwd = projectDir;
|
|
16
14
|
RunOptions.modulesDir = modulesDir;
|
|
17
|
-
|
|
18
|
-
this.pkgJSONManager = new PkgJSONManager();
|
|
19
|
-
this.graph = new Graph(this.pkgJSONManager);
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
public buildGraph =
|
|
17
|
+
public buildGraph = buildGraph;
|
|
23
18
|
|
|
19
|
+
// eslint-disable-next-line class-methods-use-this
|
|
24
20
|
public install = async (gitUrl: string, parentModule?: string) => {
|
|
25
21
|
const { url, version, name } = git.parseUrl(gitUrl);
|
|
26
22
|
|
|
@@ -28,12 +24,11 @@ export class SV {
|
|
|
28
24
|
throw new Error('NOT_A_GIT_URL');
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
const targetPkg = await
|
|
32
|
-
|
|
27
|
+
const targetPkg = await pkgJSONManager.read(parentModule);
|
|
28
|
+
const installedPkg = await pkgJSONManager.read(name);
|
|
33
29
|
|
|
34
30
|
if (!installedPkg) {
|
|
35
31
|
await git.addSumbmodule(url);
|
|
36
|
-
installedPkg = this.pkgJSONManager.read(name);
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
const versions = await git.listVersions(name);
|
|
@@ -49,12 +44,13 @@ export class SV {
|
|
|
49
44
|
}
|
|
50
45
|
|
|
51
46
|
targetPkg.sv[url] = requestedVersion ? `^${requestedVersion}` : '*';
|
|
52
|
-
await
|
|
53
|
-
await
|
|
47
|
+
await pkgJSONManager.write(targetPkg, parentModule);
|
|
48
|
+
await buildGraph();
|
|
54
49
|
};
|
|
55
50
|
|
|
51
|
+
// eslint-disable-next-line class-methods-use-this
|
|
56
52
|
public update = async () => {
|
|
57
|
-
const graph = await
|
|
53
|
+
const graph = await buildGraph();
|
|
58
54
|
if (!graph) return;
|
|
59
55
|
await Promise.all(Object.entries(graph).map(async ([name, data]) => {
|
|
60
56
|
const { version, used } = data;
|
|
@@ -65,13 +61,84 @@ export class SV {
|
|
|
65
61
|
}));
|
|
66
62
|
};
|
|
67
63
|
|
|
64
|
+
/*
|
|
65
|
+
* Устанавливает конкретную версию аддона: checkout на тег + запись constraint'а
|
|
66
|
+
* в package.json проекта (иначе buildGraph вернёт latest(used)).
|
|
67
|
+
* constraint по умолчанию — точный пин выбранной версии; при обновлении до
|
|
68
|
+
* последней имеет смысл передать `^<version>`, чтобы новые версии продолжали приходить.
|
|
69
|
+
*/
|
|
70
|
+
// eslint-disable-next-line class-methods-use-this
|
|
71
|
+
public setVersion = async (name: string, version: string, constraint?: string) => {
|
|
72
|
+
const versions = await git.listVersions(name);
|
|
73
|
+
|
|
74
|
+
if (!versions.includes(version)) {
|
|
75
|
+
throw new Error('REQUESTED_VERSION_NOT_EXISTS');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const dir = path.join(RunOptions.modulesDir, name);
|
|
79
|
+
const dirty = await git.hasChanges(dir);
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* checkout с локальными изменениями транзакционен: git переносит их на
|
|
83
|
+
* новую версию, а если перенести нельзя — отказывается, НЕ трогая дерево.
|
|
84
|
+
* Значит при ошибке мы гарантированно остаёмся на исходной версии с теми
|
|
85
|
+
* же изменениями — сообщаем, что перенос нужно сделать вручную.
|
|
86
|
+
*/
|
|
87
|
+
try {
|
|
88
|
+
await git.checkout(name, version);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (dirty) {
|
|
91
|
+
throw new GitError('GIT_DIRTY_SWITCH_CONFLICT');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const json = await pkgJSONManager.read();
|
|
98
|
+
if (!json?.sv) return;
|
|
99
|
+
|
|
100
|
+
const key = Object.keys(json.sv).find(k => k.endsWith(`${name}.git`));
|
|
101
|
+
const nextConstraint = constraint || version;
|
|
102
|
+
if (key && json.sv[key] !== nextConstraint) {
|
|
103
|
+
json.sv[key] = nextConstraint;
|
|
104
|
+
await pkgJSONManager.write(json);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* Constraint корневого проекта на аддон (ключ в sv ищем по имени, как в remove).
|
|
110
|
+
*/
|
|
111
|
+
// eslint-disable-next-line class-methods-use-this
|
|
112
|
+
public getConstraint = async (name: string): Promise<string | null> => {
|
|
113
|
+
const json = await pkgJSONManager.read();
|
|
114
|
+
if (!json?.sv) return null;
|
|
115
|
+
|
|
116
|
+
const key = Object.keys(json.sv).find(k => k.endsWith(`${name}.git`));
|
|
117
|
+
|
|
118
|
+
return key ? json.sv[key] : null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// eslint-disable-next-line class-methods-use-this
|
|
122
|
+
public setConstraint = async (name: string, constraint: string): Promise<void> => {
|
|
123
|
+
const json = await pkgJSONManager.read();
|
|
124
|
+
if (!json?.sv) return;
|
|
125
|
+
|
|
126
|
+
const key = Object.keys(json.sv).find(k => k.endsWith(`${name}.git`));
|
|
127
|
+
|
|
128
|
+
if (key && json.sv[key] !== constraint) {
|
|
129
|
+
json.sv[key] = constraint;
|
|
130
|
+
await pkgJSONManager.write(json);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// eslint-disable-next-line class-methods-use-this
|
|
68
135
|
public remove = async (submoduleName: string, parentModule?: string) => {
|
|
69
136
|
await git.rm(submoduleName);
|
|
70
|
-
const json = await
|
|
137
|
+
const json = await pkgJSONManager.read(parentModule);
|
|
71
138
|
|
|
72
139
|
if (!json.sv) return;
|
|
73
140
|
|
|
74
|
-
|
|
141
|
+
json.sv = Object.keys(json.sv).reduce((acc, k) => {
|
|
75
142
|
if (k.endsWith(`${submoduleName}.git`)) {
|
|
76
143
|
return acc;
|
|
77
144
|
}
|
|
@@ -79,9 +146,141 @@ export class SV {
|
|
|
79
146
|
return { ...acc, [k]: json.sv[k] };
|
|
80
147
|
}, {});
|
|
81
148
|
|
|
82
|
-
json
|
|
149
|
+
await pkgJSONManager.write(json, parentModule);
|
|
150
|
+
await buildGraph();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// eslint-disable-next-line class-methods-use-this
|
|
154
|
+
public hasChanges = async (module?: string): Promise<boolean> => {
|
|
155
|
+
const dir = module
|
|
156
|
+
? path.join(RunOptions.modulesDir, module)
|
|
157
|
+
: RunOptions.cwd;
|
|
158
|
+
|
|
159
|
+
/* Сначала дешёвая проверка рабочей копии; если чисто — смотрим,
|
|
160
|
+
* нет ли незапушенных коммитов (идёт fetch в remote) */
|
|
161
|
+
if (await git.hasChanges(dir)) return true;
|
|
162
|
+
|
|
163
|
+
return git.hasUnpushedCommits(dir);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// eslint-disable-next-line class-methods-use-this
|
|
167
|
+
public hasUncommittedChanges = async (module?: string): Promise<boolean> => {
|
|
168
|
+
const dir = module
|
|
169
|
+
? path.join(RunOptions.modulesDir, module)
|
|
170
|
+
: RunOptions.cwd;
|
|
171
|
+
|
|
172
|
+
return git.hasChanges(dir);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// eslint-disable-next-line class-methods-use-this
|
|
176
|
+
public currentVersion = async (module?: string): Promise<string | null> => {
|
|
177
|
+
const dir = module
|
|
178
|
+
? path.join(RunOptions.modulesDir, module)
|
|
179
|
+
: RunOptions.cwd;
|
|
180
|
+
|
|
181
|
+
const tags = await git.tagsAtHead(dir);
|
|
182
|
+
|
|
183
|
+
return versionUtil.latest(tags) || null;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// eslint-disable-next-line class-methods-use-this
|
|
187
|
+
public latestVersion = async (module?: string): Promise<string> => {
|
|
188
|
+
const dir = module
|
|
189
|
+
? path.join(RunOptions.modulesDir, module)
|
|
190
|
+
: RunOptions.cwd;
|
|
191
|
+
|
|
192
|
+
const versions = await git.listTags(dir);
|
|
193
|
+
|
|
194
|
+
return versionUtil.latest(versions) || '0.0.0';
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// eslint-disable-next-line class-methods-use-this
|
|
198
|
+
public getRemote = async (module?: string): Promise<string | null> => {
|
|
199
|
+
const dir = module
|
|
200
|
+
? path.join(RunOptions.modulesDir, module)
|
|
201
|
+
: RunOptions.cwd;
|
|
202
|
+
|
|
203
|
+
return git.getRemote(dir);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
public isPublished = async (module?: string): Promise<boolean> => {
|
|
207
|
+
const dir = module
|
|
208
|
+
? path.join(RunOptions.modulesDir, module)
|
|
209
|
+
: RunOptions.cwd;
|
|
210
|
+
|
|
211
|
+
if (!await git.isGitRepo(dir)) return false;
|
|
212
|
+
|
|
213
|
+
return !!(await this.getRemote(module));
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
public publish = async (opts: {
|
|
217
|
+
module?: string,
|
|
218
|
+
repoUrl?: string,
|
|
219
|
+
message?: string,
|
|
220
|
+
bump: 'release' | 'minor' | 'major',
|
|
221
|
+
}): Promise<string> => {
|
|
222
|
+
const { module, repoUrl, message, bump } = opts;
|
|
223
|
+
const dir = module
|
|
224
|
+
? path.join(RunOptions.modulesDir, module)
|
|
225
|
+
: RunOptions.cwd;
|
|
226
|
+
|
|
227
|
+
if (!await this.isPublished(module)) {
|
|
228
|
+
if (!repoUrl) {
|
|
229
|
+
throw new GitError('GIT_REPO_URL_REQUIRED');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!await git.isGitRepo(dir)) {
|
|
233
|
+
await git.init(dir);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await git.addRemote(dir, repoUrl);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Публикация не последней версии запрещена: HEAD на теге, отличном
|
|
240
|
+
* от последнего, — обновлять такую базу можно только вручную */
|
|
241
|
+
const currentTag = await this.currentVersion(module);
|
|
242
|
+
const latestKnown = await this.latestVersion(module);
|
|
243
|
+
|
|
244
|
+
if (currentTag && latestKnown && currentTag !== latestKnown) {
|
|
245
|
+
throw new GitError('GIT_NOT_LATEST_VERSION');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Аддоны checkout'нуты на тег: поднимаем ветку до sync/commit,
|
|
249
|
+
* иначе коммиты и push уйдут в detached HEAD */
|
|
250
|
+
await git.ensureBranch(dir);
|
|
251
|
+
|
|
252
|
+
if (await git.isRemoteAhead(dir)) {
|
|
253
|
+
await git.pullRebaseAutostash(dir);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const dirty = await git.hasChanges(dir);
|
|
257
|
+
const headTag = await this.currentVersion(module);
|
|
258
|
+
|
|
259
|
+
/* Дерево чистое и HEAD уже отмечен версионным тегом:
|
|
260
|
+
* коммитить и бампить нечего — просто доставляем ветку и тег на remote */
|
|
261
|
+
if (!dirty && headTag) {
|
|
262
|
+
await git.push(dir, headTag);
|
|
263
|
+
|
|
264
|
+
return headTag;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const next = versionUtil.bump(await this.latestVersion(module), bump);
|
|
268
|
+
|
|
269
|
+
/* Фиксируем версию и в package.json, чтобы она не расходилась с git-тегом */
|
|
270
|
+
const pkg = await pkgJSONManager.read(module);
|
|
271
|
+
if (pkg) {
|
|
272
|
+
pkg.version = next;
|
|
273
|
+
await pkgJSONManager.write(pkg, module);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* Перечитываем: запись версии в package.json сама по себе даёт изменения */
|
|
277
|
+
if (await git.hasChanges(dir)) {
|
|
278
|
+
await git.commitAll(dir, message || 'Update');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await git.addTag(dir, next);
|
|
282
|
+
await git.push(dir, next);
|
|
83
283
|
|
|
84
|
-
|
|
85
|
-
await this.graph.build();
|
|
284
|
+
return next;
|
|
86
285
|
};
|
|
87
286
|
}
|
package/src/versionUtil.ts
CHANGED
|
@@ -74,9 +74,22 @@ const latest = (versions: string[]) => (
|
|
|
74
74
|
})[0] || '')
|
|
75
75
|
);
|
|
76
76
|
|
|
77
|
+
const bump = (
|
|
78
|
+
version: string,
|
|
79
|
+
kind: 'release' | 'minor' | 'major',
|
|
80
|
+
): string => {
|
|
81
|
+
const [major, minor, patch] = toArray(version);
|
|
82
|
+
|
|
83
|
+
if (kind === 'major') return `${major + 1}.0.0`;
|
|
84
|
+
if (kind === 'minor') return `${major}.${minor + 1}.0`;
|
|
85
|
+
|
|
86
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
87
|
+
};
|
|
88
|
+
|
|
77
89
|
export const versionUtil = {
|
|
78
90
|
compare,
|
|
79
91
|
pick,
|
|
80
92
|
validate,
|
|
81
93
|
latest,
|
|
94
|
+
bump,
|
|
82
95
|
};
|