@pgpmjs/core 7.0.0 → 7.2.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/bundle/apply.d.ts +93 -0
- package/bundle/apply.js +137 -0
- package/bundle/index.d.ts +12 -0
- package/bundle/index.js +31 -0
- package/core/class/pgpm.d.ts +2 -2
- package/core/class/pgpm.js +6 -6
- package/esm/bundle/apply.js +131 -0
- package/esm/bundle/index.js +15 -0
- package/esm/core/class/pgpm.js +6 -6
- package/esm/index.js +7 -3
- package/esm/migrate/client.js +1 -1
- package/esm/migrate/utils/hash.js +3 -6
- package/esm/modules/modules.js +1 -1
- package/esm/packaging/package.js +40 -24
- package/esm/rebundle/index.js +4 -0
- package/esm/rebundle/module.js +111 -0
- package/esm/rebundle/rebundle.js +170 -0
- package/esm/rebundle/workspace.js +170 -0
- package/esm/refactor/rename.js +5 -27
- package/esm/resolution/deps.js +2 -2
- package/esm/resolution/resolve.js +2 -2
- package/esm/slice/slice.js +1 -1
- package/esm/workspace/minimal.js +73 -0
- package/index.d.ts +6 -2
- package/index.js +8 -4
- package/migrate/client.js +1 -1
- package/migrate/utils/hash.d.ts +2 -4
- package/migrate/utils/hash.js +4 -8
- package/modules/modules.d.ts +1 -1
- package/modules/modules.js +1 -1
- package/package.json +4 -2
- package/packaging/package.d.ts +23 -1
- package/packaging/package.js +42 -25
- package/rebundle/index.d.ts +4 -0
- package/{files/sql-scripts → rebundle}/index.js +4 -2
- package/rebundle/module.d.ts +23 -0
- package/rebundle/module.js +116 -0
- package/rebundle/rebundle.d.ts +26 -0
- package/rebundle/rebundle.js +175 -0
- package/rebundle/types.d.ts +185 -0
- package/rebundle/workspace.d.ts +15 -0
- package/rebundle/workspace.js +173 -0
- package/refactor/rename.d.ts +2 -8
- package/refactor/rename.js +6 -29
- package/resolution/deps.d.ts +1 -1
- package/resolution/deps.js +2 -2
- package/resolution/resolve.js +2 -2
- package/slice/slice.d.ts +1 -1
- package/slice/slice.js +1 -1
- package/slice/types.d.ts +1 -1
- package/workspace/minimal.d.ts +64 -0
- package/workspace/minimal.js +82 -0
- package/esm/files/extension/index.js +0 -3
- package/esm/files/extension/reader.js +0 -79
- package/esm/files/extension/writer.js +0 -99
- package/esm/files/index.js +0 -6
- package/esm/files/plan/generator.js +0 -82
- package/esm/files/plan/index.js +0 -5
- package/esm/files/plan/parser.js +0 -297
- package/esm/files/plan/validators.js +0 -181
- package/esm/files/plan/writer.js +0 -142
- package/esm/files/sql/header.js +0 -259
- package/esm/files/sql/index.js +0 -2
- package/esm/files/sql/writer.js +0 -107
- package/esm/files/sql-scripts/index.js +0 -2
- package/esm/files/sql-scripts/reader.js +0 -19
- package/esm/files/types/index.js +0 -1
- package/esm/utils/author.js +0 -11
- package/files/extension/index.d.ts +0 -2
- package/files/extension/index.js +0 -19
- package/files/extension/reader.d.ts +0 -24
- package/files/extension/reader.js +0 -86
- package/files/extension/writer.d.ts +0 -48
- package/files/extension/writer.js +0 -107
- package/files/index.d.ts +0 -5
- package/files/index.js +0 -22
- package/files/plan/generator.d.ts +0 -23
- package/files/plan/generator.js +0 -90
- package/files/plan/index.d.ts +0 -4
- package/files/plan/index.js +0 -21
- package/files/plan/parser.d.ts +0 -28
- package/files/plan/parser.js +0 -304
- package/files/plan/validators.d.ts +0 -52
- package/files/plan/validators.js +0 -187
- package/files/plan/writer.d.ts +0 -27
- package/files/plan/writer.js +0 -152
- package/files/sql/header.d.ts +0 -114
- package/files/sql/header.js +0 -266
- package/files/sql/index.d.ts +0 -2
- package/files/sql/index.js +0 -18
- package/files/sql/writer.d.ts +0 -12
- package/files/sql/writer.js +0 -114
- package/files/sql-scripts/index.d.ts +0 -1
- package/files/sql-scripts/reader.d.ts +0 -8
- package/files/sql-scripts/reader.js +0 -23
- package/files/types/index.d.ts +0 -46
- package/files/types/index.js +0 -17
- package/files/types/package.d.ts +0 -20
- package/utils/author.d.ts +0 -5
- package/utils/author.js +0 -14
- /package/esm/{files/types/package.js → rebundle/types.js} +0 -0
- /package/{files/types/package.js → rebundle/types.js} +0 -0
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { basename, dirname, relative } from 'path';
|
|
3
|
-
import { parsePlanFileSimple } from '../plan';
|
|
4
|
-
/**
|
|
5
|
-
* Parse a .control file and extract its metadata.
|
|
6
|
-
* https://www.postgresql.org/docs/current/extend-extensions.html
|
|
7
|
-
*/
|
|
8
|
-
export function parseControlFile(filePath, basePath) {
|
|
9
|
-
const contents = readFileSync(filePath, 'utf-8');
|
|
10
|
-
const key = basename(filePath).split('.control')[0];
|
|
11
|
-
const requires = contents
|
|
12
|
-
.split('\n')
|
|
13
|
-
.find((line) => /^requires/.test(line))
|
|
14
|
-
?.split('=')[1]
|
|
15
|
-
.split(',')
|
|
16
|
-
.map((req) => req.replace(/[\'\s]*/g, '').trim()) || [];
|
|
17
|
-
const version = contents
|
|
18
|
-
.split('\n')
|
|
19
|
-
.find((line) => /^default_version/.test(line))
|
|
20
|
-
?.split('=')[1]
|
|
21
|
-
.replace(/[\']*/g, '')
|
|
22
|
-
.trim() || '';
|
|
23
|
-
return {
|
|
24
|
-
path: dirname(relative(basePath, filePath)),
|
|
25
|
-
requires,
|
|
26
|
-
version,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Parse the pgpm.plan file to get the extension name.
|
|
31
|
-
*/
|
|
32
|
-
export const getExtensionName = (packageDir) => {
|
|
33
|
-
const planPath = `${packageDir}/pgpm.plan`;
|
|
34
|
-
const plan = parsePlanFileSimple(planPath);
|
|
35
|
-
if (!plan.package) {
|
|
36
|
-
throw new Error('No package name found in pgpm.plan!');
|
|
37
|
-
}
|
|
38
|
-
return plan.package;
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Get detailed information about an extension in the specified directory.
|
|
42
|
-
*/
|
|
43
|
-
export const getExtensionInfo = (packageDir) => {
|
|
44
|
-
const pkgPath = `${packageDir}/package.json`;
|
|
45
|
-
const pkg = require(pkgPath);
|
|
46
|
-
const extname = getExtensionName(packageDir);
|
|
47
|
-
const version = pkg.version;
|
|
48
|
-
const Makefile = `${packageDir}/Makefile`;
|
|
49
|
-
const controlFile = `${packageDir}/${extname}.control`;
|
|
50
|
-
const sqlFile = `sql/${extname}--${version}.sql`;
|
|
51
|
-
return { extname, packageDir, version, Makefile, controlFile, sqlFile };
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Get a list of extensions required by an extension from its control file.
|
|
55
|
-
* Returns an empty array if the file doesn't exist or has no requires line.
|
|
56
|
-
*/
|
|
57
|
-
export const getInstalledExtensions = (controlFilePath) => {
|
|
58
|
-
try {
|
|
59
|
-
const contents = readFileSync(controlFilePath, 'utf-8');
|
|
60
|
-
const match = contents.match(/^\s*requires\s*=\s*'([^']*)'/m);
|
|
61
|
-
if (!match) {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
const requiresValue = match[1];
|
|
65
|
-
if (!requiresValue || requiresValue.trim() === '') {
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
return requiresValue
|
|
69
|
-
.split(',')
|
|
70
|
-
.map((ext) => ext.trim())
|
|
71
|
-
.filter((ext) => ext.length > 0);
|
|
72
|
-
}
|
|
73
|
-
catch (e) {
|
|
74
|
-
if (e.code === 'ENOENT') {
|
|
75
|
-
return [];
|
|
76
|
-
}
|
|
77
|
-
throw new Error(`Error reading control file at ${controlFilePath}: ${e.message}`);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
-
import { getExtensionInfo } from './reader';
|
|
3
|
-
/**
|
|
4
|
-
* Write the Makefile for the extension.
|
|
5
|
-
*/
|
|
6
|
-
export const writeExtensionMakefile = (outputPath, extname, version) => {
|
|
7
|
-
const content = `EXTENSION = ${extname}
|
|
8
|
-
DATA = sql/${extname}--${version}.sql
|
|
9
|
-
|
|
10
|
-
PG_CONFIG = pg_config
|
|
11
|
-
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
|
12
|
-
include $(PGXS)
|
|
13
|
-
`;
|
|
14
|
-
writeFileSync(outputPath, content);
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Generate content for a .control file
|
|
18
|
-
* https://www.postgresql.org/docs/current/extend-extensions.html
|
|
19
|
-
*/
|
|
20
|
-
export function generateControlFileContent(options) {
|
|
21
|
-
const { name, version, comment = `${name} extension`, requires = [], default_version = version, relocatable = false, superuser = false, schema, module_pathname } = options;
|
|
22
|
-
let content = `# ${name} extension
|
|
23
|
-
comment = '${comment}'
|
|
24
|
-
default_version = '${default_version}'
|
|
25
|
-
`;
|
|
26
|
-
if (module_pathname) {
|
|
27
|
-
content += `module_pathname = '${module_pathname}'\n`;
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
content += `module_pathname = '$libdir/${name}'\n`;
|
|
31
|
-
}
|
|
32
|
-
if (requires.length > 0) {
|
|
33
|
-
content += `requires = '${requires.join(',')}'\n`;
|
|
34
|
-
}
|
|
35
|
-
content += `relocatable = ${relocatable}
|
|
36
|
-
superuser = ${superuser}\n`;
|
|
37
|
-
if (schema) {
|
|
38
|
-
content += `schema = ${schema}\n`;
|
|
39
|
-
}
|
|
40
|
-
return content;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Update (or insert/remove) the `requires =` line of an existing .control file
|
|
44
|
-
* in place, preserving every other field and the file's formatting. Used when
|
|
45
|
-
* changing a module's dependencies after init (e.g. via `pgpm extension`) so
|
|
46
|
-
* hand-tuned fields (comment, schema, relocatable, module_pathname) survive.
|
|
47
|
-
*/
|
|
48
|
-
export function upsertRequiresLine(content, requires) {
|
|
49
|
-
const lines = content.split('\n');
|
|
50
|
-
const idx = lines.findIndex((line) => /^\s*requires\s*=/.test(line));
|
|
51
|
-
if (requires.length === 0) {
|
|
52
|
-
if (idx !== -1)
|
|
53
|
-
lines.splice(idx, 1);
|
|
54
|
-
return lines.join('\n');
|
|
55
|
-
}
|
|
56
|
-
const requiresLine = `requires = '${requires.join(',')}'`;
|
|
57
|
-
if (idx !== -1) {
|
|
58
|
-
lines[idx] = requiresLine;
|
|
59
|
-
return lines.join('\n');
|
|
60
|
-
}
|
|
61
|
-
// Insert to match generated ordering (before relocatable/superuser/schema).
|
|
62
|
-
const insertAt = lines.findIndex((line) => /^\s*(relocatable|superuser|schema)\s*=/.test(line));
|
|
63
|
-
if (insertAt !== -1) {
|
|
64
|
-
lines.splice(insertAt, 0, requiresLine);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
lines.push(requiresLine);
|
|
68
|
-
}
|
|
69
|
-
return lines.join('\n');
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Write the control file for the extension.
|
|
73
|
-
* If the file already exists, only its `requires` line is updated so that any
|
|
74
|
-
* custom fields are preserved; otherwise a fresh file is generated.
|
|
75
|
-
*/
|
|
76
|
-
export const writeExtensionControlFile = (outputPath, extname, extensions, version) => {
|
|
77
|
-
if (existsSync(outputPath)) {
|
|
78
|
-
const existing = readFileSync(outputPath, 'utf-8');
|
|
79
|
-
writeFileSync(outputPath, upsertRequiresLine(existing, extensions));
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const content = generateControlFileContent({
|
|
83
|
-
name: extname,
|
|
84
|
-
version,
|
|
85
|
-
requires: extensions
|
|
86
|
-
});
|
|
87
|
-
writeFileSync(outputPath, content);
|
|
88
|
-
};
|
|
89
|
-
/**
|
|
90
|
-
* Write control and Makefile for the extension with given data.
|
|
91
|
-
* If extInfo is not provided, it will be generated from packageDir.
|
|
92
|
-
*/
|
|
93
|
-
export const writeExtensions = (packageDir, extensions, extInfo) => {
|
|
94
|
-
// If extInfo is not provided, get it from packageDir
|
|
95
|
-
const info = extInfo || getExtensionInfo(packageDir);
|
|
96
|
-
const { controlFile, Makefile, extname, version } = info;
|
|
97
|
-
writeExtensionControlFile(controlFile, extname, extensions, version);
|
|
98
|
-
writeExtensionMakefile(Makefile, extname, version);
|
|
99
|
-
};
|
package/esm/files/index.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
/**
|
|
3
|
-
* Fixed epoch timestamp used for deterministic plan generation.
|
|
4
|
-
* Can be overridden with the PGPM_FROZEN_TIME environment variable.
|
|
5
|
-
*/
|
|
6
|
-
const DEFAULT_FROZEN_TIME = '2017-08-11T08:11:51Z';
|
|
7
|
-
/**
|
|
8
|
-
* Get a UTC timestamp string from a Date object.
|
|
9
|
-
*/
|
|
10
|
-
function getUTCTimestamp(d) {
|
|
11
|
-
return (d.getUTCFullYear() +
|
|
12
|
-
'-' + String(d.getUTCMonth() + 1).padStart(2, '0') +
|
|
13
|
-
'-' + String(d.getUTCDate()).padStart(2, '0') +
|
|
14
|
-
'T' + String(d.getUTCHours()).padStart(2, '0') +
|
|
15
|
-
':' + String(d.getUTCMinutes()).padStart(2, '0') +
|
|
16
|
-
':' + String(d.getUTCSeconds()).padStart(2, '0') +
|
|
17
|
-
'Z');
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Get a timestamp for the plan file.
|
|
21
|
-
* Uses the PGPM_FROZEN_TIME env var if present, otherwise the fixed epoch.
|
|
22
|
-
*/
|
|
23
|
-
export function getNow() {
|
|
24
|
-
const frozen = process.env.PGPM_FROZEN_TIME;
|
|
25
|
-
if (frozen) {
|
|
26
|
-
return getUTCTimestamp(new Date(frozen));
|
|
27
|
-
}
|
|
28
|
-
return DEFAULT_FROZEN_TIME;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Sort dependencies by their order in the supplied plan, so prerequisites
|
|
32
|
-
* (parent migrations) appear before their dependents in the bracket.
|
|
33
|
-
* Internal dependencies are sorted by their plan index, and external
|
|
34
|
-
* dependencies (not in the plan) preserve their original order.
|
|
35
|
-
*/
|
|
36
|
-
const sortDependencies = (deps, order) => {
|
|
37
|
-
const originalOrder = new Map(deps.map((dep, index) => [dep, index]));
|
|
38
|
-
return [...deps].sort((a, b) => {
|
|
39
|
-
const aIndex = order.get(a);
|
|
40
|
-
const bIndex = order.get(b);
|
|
41
|
-
const aOriginal = originalOrder.get(a);
|
|
42
|
-
const bOriginal = originalOrder.get(b);
|
|
43
|
-
if (aIndex !== undefined && bIndex !== undefined)
|
|
44
|
-
return aIndex - bIndex || aOriginal - bOriginal;
|
|
45
|
-
if (aIndex !== undefined)
|
|
46
|
-
return -1;
|
|
47
|
-
if (bIndex !== undefined)
|
|
48
|
-
return 1;
|
|
49
|
-
return aOriginal - bOriginal;
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* Generate a plan file content from structured data
|
|
54
|
-
*/
|
|
55
|
-
export function generatePlan(options) {
|
|
56
|
-
const { moduleName, uri, entries } = options;
|
|
57
|
-
const now = getNow();
|
|
58
|
-
const planfile = [
|
|
59
|
-
`%syntax-version=1.0.0`,
|
|
60
|
-
`%project=${moduleName}`,
|
|
61
|
-
`%uri=${uri || moduleName}`
|
|
62
|
-
];
|
|
63
|
-
// Build an order map from the entry list so brackets can be sorted by plan order
|
|
64
|
-
const entryOrder = new Map(entries.map((entry, index) => [entry.change, index]));
|
|
65
|
-
// Generate the plan entries
|
|
66
|
-
entries.forEach(entry => {
|
|
67
|
-
const deps = sortDependencies(entry.dependencies || [], entryOrder);
|
|
68
|
-
if (deps.length > 0) {
|
|
69
|
-
planfile.push(`${entry.change} [${deps.join(' ')}] ${now} constructive <constructive@5b0c196eeb62>${entry.comment ? ` # ${entry.comment}` : ''}`);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
planfile.push(`${entry.change} ${now} constructive <constructive@5b0c196eeb62>${entry.comment ? ` # ${entry.comment}` : ''}`);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
return planfile.join('\n');
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Write a generated plan file to disk
|
|
79
|
-
*/
|
|
80
|
-
export function writePlan(planPath, plan) {
|
|
81
|
-
fs.writeFileSync(planPath, plan);
|
|
82
|
-
}
|
package/esm/files/plan/index.js
DELETED
package/esm/files/plan/parser.js
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { isValidChangeName, isValidDependency, isValidTagName, parseReference } from './validators';
|
|
3
|
-
import { errors } from '@pgpmjs/types';
|
|
4
|
-
/**
|
|
5
|
-
* Parse a Sqitch plan file with full validation
|
|
6
|
-
* Supports both changes and tags
|
|
7
|
-
*/
|
|
8
|
-
export function parsePlanFile(planPath) {
|
|
9
|
-
const content = readFileSync(planPath, 'utf-8');
|
|
10
|
-
const lines = content.split('\n');
|
|
11
|
-
let project = '';
|
|
12
|
-
let uri = '';
|
|
13
|
-
const changes = [];
|
|
14
|
-
const tags = [];
|
|
15
|
-
const errors = [];
|
|
16
|
-
for (let i = 0; i < lines.length; i++) {
|
|
17
|
-
const line = lines[i];
|
|
18
|
-
const lineNum = i + 1;
|
|
19
|
-
const trimmed = line.trim();
|
|
20
|
-
// Skip empty lines
|
|
21
|
-
if (!trimmed)
|
|
22
|
-
continue;
|
|
23
|
-
// Skip comments
|
|
24
|
-
if (trimmed.startsWith('#'))
|
|
25
|
-
continue;
|
|
26
|
-
// Parse package metadata
|
|
27
|
-
if (trimmed.startsWith('%project=')) {
|
|
28
|
-
project = trimmed.substring('%project='.length);
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (trimmed.startsWith('%uri=')) {
|
|
32
|
-
uri = trimmed.substring('%uri='.length);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
// Skip other metadata lines
|
|
36
|
-
if (trimmed.startsWith('%')) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
// Parse tag lines
|
|
40
|
-
if (trimmed.startsWith('@')) {
|
|
41
|
-
const lastChangeName = changes.length > 0 ? changes[changes.length - 1].name : null;
|
|
42
|
-
const tag = parseTagLine(trimmed, lastChangeName);
|
|
43
|
-
if (tag) {
|
|
44
|
-
if (isValidTagName(tag.name)) {
|
|
45
|
-
tags.push(tag);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
errors.push({
|
|
49
|
-
line: lineNum,
|
|
50
|
-
message: `Invalid tag name: ${tag.name}`
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
errors.push({
|
|
56
|
-
line: lineNum,
|
|
57
|
-
message: 'Invalid tag line format'
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
// Parse change lines
|
|
63
|
-
const change = parseChangeLine(trimmed);
|
|
64
|
-
if (change) {
|
|
65
|
-
// Validate change name
|
|
66
|
-
if (!isValidChangeName(change.name)) {
|
|
67
|
-
errors.push({
|
|
68
|
-
line: lineNum,
|
|
69
|
-
message: `Invalid change name: ${change.name}`
|
|
70
|
-
});
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
// Validate dependencies
|
|
74
|
-
for (const dep of change.dependencies) {
|
|
75
|
-
if (!isValidDependency(dep)) {
|
|
76
|
-
errors.push({
|
|
77
|
-
line: lineNum,
|
|
78
|
-
message: `Invalid dependency reference: ${dep}`
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
changes.push(change);
|
|
83
|
-
}
|
|
84
|
-
else if (trimmed) {
|
|
85
|
-
// Non-empty line that couldn't be parsed
|
|
86
|
-
errors.push({
|
|
87
|
-
line: lineNum,
|
|
88
|
-
message: 'Invalid line format'
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (errors.length > 0) {
|
|
93
|
-
return { errors };
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
data: { package: project, uri, changes, tags },
|
|
97
|
-
errors: []
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Parse a single change line from a plan file
|
|
102
|
-
* Format: change_name [deps] timestamp planner <email> # comment
|
|
103
|
-
*/
|
|
104
|
-
function parseChangeLine(line) {
|
|
105
|
-
// More flexible regex that handles various formats, including planner names with spaces
|
|
106
|
-
// Format: change_name [deps] timestamp planner <email> # comment
|
|
107
|
-
// The timestamp is required if planner/email/comment are present
|
|
108
|
-
const regex = /^(\S+)(?:\s+\[([^\]]*)\])?(?:\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)(?:\s+([^<]+?))?(?:\s+<([^>]+)>)?(?:\s+#\s+(.*))?)?$/;
|
|
109
|
-
const match = line.match(regex);
|
|
110
|
-
if (!match) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
const [, name, depsStr, timestamp, planner, email, comment] = match;
|
|
114
|
-
// Parse dependencies
|
|
115
|
-
const dependencies = depsStr
|
|
116
|
-
? depsStr.split(/\s+/).filter(dep => dep.length > 0)
|
|
117
|
-
: [];
|
|
118
|
-
return {
|
|
119
|
-
name,
|
|
120
|
-
dependencies,
|
|
121
|
-
timestamp,
|
|
122
|
-
planner,
|
|
123
|
-
email,
|
|
124
|
-
comment
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Parse a tag line from a plan file
|
|
129
|
-
* Format: @tag_name change_name timestamp planner <email> # comment
|
|
130
|
-
*/
|
|
131
|
-
function parseTagLine(line, lastChangeName) {
|
|
132
|
-
// Tag lines start with @
|
|
133
|
-
if (!line.startsWith('@')) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
// Remove the @ and parse
|
|
137
|
-
const tagContent = line.substring(1);
|
|
138
|
-
// Two possible formats:
|
|
139
|
-
// 1. tagname timestamp planner <email> # comment (tag for last change)
|
|
140
|
-
// 2. tagname changename timestamp planner <email> # comment (tag for specific change)
|
|
141
|
-
// First try to match with change name (format 2)
|
|
142
|
-
const regexWithChange = /^(\S+)\s+(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
|
|
143
|
-
let match = tagContent.match(regexWithChange);
|
|
144
|
-
if (match) {
|
|
145
|
-
const [, name, secondToken, timestamp, planner, email, comment] = match;
|
|
146
|
-
// Check if the second token is a timestamp (format 1) or change name (format 2)
|
|
147
|
-
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(secondToken)) {
|
|
148
|
-
// Format 1: no change name specified
|
|
149
|
-
return {
|
|
150
|
-
name,
|
|
151
|
-
change: lastChangeName || '', // Tag is associated with the last change
|
|
152
|
-
timestamp: secondToken,
|
|
153
|
-
planner: timestamp, // What we thought was timestamp is actually planner
|
|
154
|
-
email: planner, // Shift everything
|
|
155
|
-
comment: email || comment
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
// Format 2: explicit change name
|
|
160
|
-
return {
|
|
161
|
-
name,
|
|
162
|
-
change: secondToken,
|
|
163
|
-
timestamp,
|
|
164
|
-
planner,
|
|
165
|
-
email,
|
|
166
|
-
comment
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// Try simple format without change name
|
|
171
|
-
const regexSimple = /^(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
|
|
172
|
-
match = tagContent.match(regexSimple);
|
|
173
|
-
if (!match) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
const [, name, timestamp, planner, email, comment] = match;
|
|
177
|
-
return {
|
|
178
|
-
name,
|
|
179
|
-
change: lastChangeName || '', // Tag is associated with the last change
|
|
180
|
-
timestamp,
|
|
181
|
-
planner,
|
|
182
|
-
email,
|
|
183
|
-
comment
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Resolve a reference within a plan file context
|
|
188
|
-
* Handles symbolic references (HEAD, ROOT) and relative references
|
|
189
|
-
*/
|
|
190
|
-
export function resolveReference(ref, plan, currentPackage) {
|
|
191
|
-
const parsed = parseReference(ref);
|
|
192
|
-
if (!parsed) {
|
|
193
|
-
return { error: `Invalid reference: ${ref}` };
|
|
194
|
-
}
|
|
195
|
-
// Handle package qualifier
|
|
196
|
-
const planPackage = currentPackage || plan.package;
|
|
197
|
-
if (parsed.package && parsed.package !== planPackage) {
|
|
198
|
-
// Cross-package reference - return as-is
|
|
199
|
-
return { change: ref };
|
|
200
|
-
}
|
|
201
|
-
// Handle SHA1 references
|
|
202
|
-
if (parsed.sha1) {
|
|
203
|
-
// Would need to look up in database
|
|
204
|
-
return { change: ref };
|
|
205
|
-
}
|
|
206
|
-
// Handle symbolic references
|
|
207
|
-
if (parsed.symbolic === 'HEAD' && plan.changes.length > 0) {
|
|
208
|
-
return { change: plan.changes[plan.changes.length - 1].name };
|
|
209
|
-
}
|
|
210
|
-
if (parsed.symbolic === 'ROOT' && plan.changes.length > 0) {
|
|
211
|
-
return { change: plan.changes[0].name };
|
|
212
|
-
}
|
|
213
|
-
// Handle relative references
|
|
214
|
-
if (parsed.relative) {
|
|
215
|
-
const baseResolved = resolveReference(parsed.relative.base, plan, currentPackage);
|
|
216
|
-
if (baseResolved.error) {
|
|
217
|
-
return baseResolved;
|
|
218
|
-
}
|
|
219
|
-
// Get the change name from the resolved reference
|
|
220
|
-
const baseChange = baseResolved.change;
|
|
221
|
-
if (!baseChange) {
|
|
222
|
-
return { error: `Cannot resolve base reference: ${parsed.relative.base}` };
|
|
223
|
-
}
|
|
224
|
-
const changeIndex = plan.changes.findIndex(c => c.name === baseChange);
|
|
225
|
-
if (changeIndex === -1) {
|
|
226
|
-
return { error: `Change not found: ${baseChange}` };
|
|
227
|
-
}
|
|
228
|
-
let targetIndex;
|
|
229
|
-
if (parsed.relative.direction === '^') {
|
|
230
|
-
// Go backwards
|
|
231
|
-
targetIndex = changeIndex - parsed.relative.count;
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
// Go forwards
|
|
235
|
-
targetIndex = changeIndex + parsed.relative.count;
|
|
236
|
-
}
|
|
237
|
-
if (targetIndex < 0 || targetIndex >= plan.changes.length) {
|
|
238
|
-
return { error: `Relative reference out of bounds: ${ref}` };
|
|
239
|
-
}
|
|
240
|
-
return { change: plan.changes[targetIndex].name };
|
|
241
|
-
}
|
|
242
|
-
// Handle tag references
|
|
243
|
-
if (parsed.tag) {
|
|
244
|
-
const tag = plan.tags.find(t => t.name === parsed.tag);
|
|
245
|
-
if (!tag) {
|
|
246
|
-
return { error: `Tag not found: ${parsed.tag}` };
|
|
247
|
-
}
|
|
248
|
-
return { tag: parsed.tag, change: tag.change };
|
|
249
|
-
}
|
|
250
|
-
// Handle change@tag format
|
|
251
|
-
if (parsed.change && parsed.tag) {
|
|
252
|
-
return { change: parsed.change, tag: parsed.tag };
|
|
253
|
-
}
|
|
254
|
-
// Plain change reference
|
|
255
|
-
if (parsed.change) {
|
|
256
|
-
// Validate that the change exists in the plan
|
|
257
|
-
const changeExists = plan.changes.some(c => c.name === parsed.change);
|
|
258
|
-
if (!changeExists) {
|
|
259
|
-
return { error: `Change not found: ${parsed.change}` };
|
|
260
|
-
}
|
|
261
|
-
return { change: parsed.change };
|
|
262
|
-
}
|
|
263
|
-
return { error: `Cannot resolve reference: ${ref}` };
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Simple plan file parser without validation.
|
|
267
|
-
* Returns a PlanFile (without tags) for callers that don't need tag data.
|
|
268
|
-
*/
|
|
269
|
-
export function parsePlanFileSimple(planPath) {
|
|
270
|
-
const result = parsePlanFile(planPath);
|
|
271
|
-
if (result.data) {
|
|
272
|
-
// Return without tags for simple format
|
|
273
|
-
const { tags, ...planWithoutTags } = result.data;
|
|
274
|
-
return planWithoutTags;
|
|
275
|
-
}
|
|
276
|
-
// If there are errors, throw with details
|
|
277
|
-
if (result.errors && result.errors.length > 0) {
|
|
278
|
-
const errorMessages = result.errors.map(e => `Line ${e.line}: ${e.message}`).join('\n');
|
|
279
|
-
throw errors.PLAN_PARSE_ERROR({ planPath, errors: errorMessages });
|
|
280
|
-
}
|
|
281
|
-
// Return empty plan if no data and no errors
|
|
282
|
-
return { package: '', uri: '', changes: [] };
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Get all change names from a plan file
|
|
286
|
-
*/
|
|
287
|
-
export function getChanges(planPath) {
|
|
288
|
-
const plan = parsePlanFileSimple(planPath);
|
|
289
|
-
return plan.changes.map(change => change.name);
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Get the latest (last) change from a plan file
|
|
293
|
-
*/
|
|
294
|
-
export function getLatestChange(planPath) {
|
|
295
|
-
const changes = getChanges(planPath);
|
|
296
|
-
return changes[changes.length - 1] || '';
|
|
297
|
-
}
|