submodule-version 2.2.0 → 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/src/sv.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import path from 'path';
1
2
  import { pkgJSONManager } from "./pkgJSONManager";
2
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
9
  constructor (
@@ -24,11 +25,10 @@ export class SV {
24
25
  }
25
26
 
26
27
  const targetPkg = await pkgJSONManager.read(parentModule);
27
- let installedPkg = await pkgJSONManager.read(name);
28
+ const installedPkg = await pkgJSONManager.read(name);
28
29
 
29
30
  if (!installedPkg) {
30
31
  await git.addSumbmodule(url);
31
- installedPkg = pkgJSONManager.read(name);
32
32
  }
33
33
 
34
34
  const versions = await git.listVersions(name);
@@ -61,6 +61,76 @@ export class SV {
61
61
  }));
62
62
  };
63
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
+
64
134
  // eslint-disable-next-line class-methods-use-this
65
135
  public remove = async (submoduleName: string, parentModule?: string) => {
66
136
  await git.rm(submoduleName);
@@ -79,4 +149,138 @@ export class SV {
79
149
  await pkgJSONManager.write(json, parentModule);
80
150
  await buildGraph();
81
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);
283
+
284
+ return next;
285
+ };
82
286
  }
@@ -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
  };