Skip to content

Commit 71301c5

Browse files
committed
feat(@angular/cli): update with migrate only creates commit per migration
1 parent 252f1be commit 71301c5

File tree

5 files changed

+143
-23
lines changed

5 files changed

+143
-23
lines changed

packages/angular/cli/commands/update-impl.ts

Lines changed: 131 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,73 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
3535
return {};
3636
}
3737

38+
async executeMigrations(
39+
packageName: string,
40+
collectionPath: string,
41+
range: semver.Range,
42+
commit = false,
43+
) {
44+
const collection = this.getEngine().createCollection(collectionPath);
45+
46+
const migrations = [];
47+
for (const name of collection.listSchematicNames()) {
48+
const schematic = this.getEngine().createSchematic(name, collection);
49+
const description = schematic.description as typeof schematic.description & {
50+
version?: string;
51+
};
52+
if (!description.version) {
53+
continue;
54+
}
55+
56+
if (semver.satisfies(description.version, range, { includePrerelease: true })) {
57+
migrations.push(description as typeof schematic.description & { version: string });
58+
}
59+
}
60+
61+
if (migrations.length === 0) {
62+
return true;
63+
}
64+
65+
const startingGitSha = this.findCurrentGitSha();
66+
67+
migrations.sort((a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name));
68+
69+
for (const migration of migrations) {
70+
this.logger.info(
71+
`** Executing migrations for version ${migration.version} of package '${packageName}' **`,
72+
);
73+
74+
// TODO: run the schematic directly; most of the logic in the following is unneeded
75+
const result = await this.runSchematic({
76+
collectionName: migration.collection.name,
77+
schematicName: migration.name,
78+
force: false,
79+
showNothingDone: false,
80+
});
81+
82+
// 1 = failure
83+
if (result === 1) {
84+
if (startingGitSha !== null) {
85+
const currentGitSha = this.findCurrentGitSha();
86+
if (currentGitSha !== startingGitSha) {
87+
this.logger.warn(`git HEAD was at ${startingGitSha} before migrations.`);
88+
}
89+
}
90+
91+
return false;
92+
}
93+
94+
// Commit migration
95+
if (commit) {
96+
let message = `migrate workspace for ${packageName}@${migration.version}`;
97+
if (migration.description) {
98+
message += '\n' + migration.description;
99+
}
100+
this.createCommit([], message);
101+
}
102+
}
103+
}
104+
38105
// tslint:disable-next-line:no-big-function
39106
async run(options: UpdateCommandSchema & Arguments) {
40107
const packages: PackageIdentifier[] = [];
@@ -112,9 +179,9 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
112179
this.workspace.configFile &&
113180
oldConfigFileNames.includes(this.workspace.configFile)
114181
) {
115-
options.migrateOnly = true;
116-
options.from = '1.0.0';
117-
}
182+
options.migrateOnly = true;
183+
options.from = '1.0.0';
184+
}
118185

119186
this.logger.info('Collecting installed dependencies...');
120187

@@ -153,6 +220,13 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
153220
return 1;
154221
}
155222

223+
const from = coerceVersionNumber(options.from);
224+
if (!from) {
225+
this.logger.error(`"from" value [${options.from}] is not a valid version.`);
226+
227+
return 1;
228+
}
229+
156230
if (options.next) {
157231
this.logger.warn('"next" option has no effect when using "migrate-only" option.');
158232
}
@@ -230,20 +304,18 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
230304
}
231305
}
232306

233-
return this.runSchematic({
234-
collectionName: '@schematics/update',
235-
schematicName: 'migrate',
236-
dryRun: !!options.dryRun,
237-
force: false,
238-
showNothingDone: false,
239-
additionalOptions: {
240-
package: packageName,
241-
collection: migrations,
242-
from: options.from,
243-
verbose: options.verbose || false,
244-
to: options.to || packageNode.package.version,
245-
},
246-
});
307+
const migrationRange = new semver.Range(
308+
'>' + from + ' <=' + (options.to || packageNode.package.version),
309+
);
310+
311+
const result = await this.executeMigrations(
312+
packageName,
313+
migrations,
314+
migrationRange,
315+
!options.skipCommits,
316+
);
317+
318+
return result ? 1 : 0;
247319
}
248320

249321
const requests: {
@@ -287,7 +359,9 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
287359
try {
288360
// Metadata requests are internally cached; multiple requests for same name
289361
// does not result in additional network traffic
290-
metadata = await fetchPackageMetadata(packageName, this.logger, { verbose: options.verbose });
362+
metadata = await fetchPackageMetadata(packageName, this.logger, {
363+
verbose: options.verbose,
364+
});
291365
} catch (e) {
292366
this.logger.error(`Error fetching metadata for '${packageName}': ` + e.message);
293367

@@ -366,9 +440,46 @@ export class UpdateCommand extends SchematicCommand<UpdateCommandSchema> {
366440
return false;
367441
}
368442
}
369-
370-
} catch { }
443+
} catch {}
371444

372445
return true;
373446
}
447+
448+
createCommit(files: string[], message: string) {
449+
try {
450+
execSync('git add -A ' + files.join(' '), { encoding: 'utf8', stdio: 'pipe' });
451+
452+
execSync(`git commit --no-verify -m "${message}"`, { encoding: 'utf8', stdio: 'pipe' });
453+
} catch (error) {}
454+
}
455+
456+
findCurrentGitSha(): string | null {
457+
try {
458+
const result = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' });
459+
460+
return result.trim();
461+
} catch {
462+
return null;
463+
}
464+
}
465+
}
466+
467+
function coerceVersionNumber(version: string): string | null {
468+
if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) {
469+
const match = version.match(/^\d{1,30}(\.\d{1,30})*/);
470+
471+
if (!match) {
472+
return null;
473+
}
474+
475+
if (!match[1]) {
476+
version = version.substr(0, match[0].length) + '.0.0' + version.substr(match[0].length);
477+
} else if (!match[2]) {
478+
version = version.substr(0, match[0].length) + '.0' + version.substr(match[0].length);
479+
} else {
480+
return null;
481+
}
482+
}
483+
484+
return semver.valid(version);
374485
}

packages/angular/cli/commands/update.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@
6262
"description": "Display additional details about internal operations during execution.",
6363
"type": "boolean",
6464
"default": false
65+
},
66+
"skipCommits": {
67+
"description": "Do not create source control commits for updates and migrations.",
68+
"type": "boolean",
69+
"default": false,
70+
"aliases": ["C"]
6571
}
6672
}
6773
}

tests/legacy-cli/e2e/tests/update/update-1.0.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export default async function() {
1010

1111
await useCIChrome('.');
1212
await expectToFail(() => ng('build'));
13-
await ng('update', '@angular/cli');
13+
// Turn off git commits ('-C') per migration to avoid breaking E2E cleanup process
14+
await ng('update', '@angular/cli', '-C');
1415
await useBuiltPackages();
1516
await silentNpm('install');
1617
await ng('update', '@angular/core', ...extraUpdateArgs);

tests/legacy-cli/e2e/tests/update/update-1.7-longhand.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export default async function() {
99
await createProjectFromAsset('1.7-project');
1010

1111
await expectToFail(() => ng('build'));
12-
await ng('update', '@angular/cli', '--migrate-only', '--from=1.7.1');
12+
// Turn off git commits ('-C') per migration to avoid breaking E2E cleanup process
13+
await ng('update', '@angular/cli', '--migrate-only', '--from=1.7.1', '-C');
1314
await useBuiltPackages();
1415
await silentNpm('install');
1516
await ng('update', '@angular/core', ...extraUpdateArgs);

tests/legacy-cli/e2e/tests/update/update-1.7.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export default async function() {
1212

1313
await useCIChrome('.');
1414
await expectToFail(() => ng('build'));
15-
await ng('update', '@angular/cli');
15+
// Turn off git commits ('-C') per migration to avoid breaking E2E cleanup process
16+
await ng('update', '@angular/cli', '-C');
1617
await useBuiltPackages();
1718
await silentNpm('install');
1819
await ng('update', '@angular/core', ...extraUpdateArgs);

0 commit comments

Comments
 (0)