gitorial-cli 1.1.0 → 2.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 +114 -59
- package/package.json +6 -4
- package/src/commands/build-gitorial.js +152 -0
- package/src/commands/build-mdbook.js +321 -0
- package/src/index.js +29 -35
- package/src/lib/fs.js +112 -0
- package/src/lib/git.js +45 -0
- package/src/lib/logger.js +22 -0
- package/src/lib/mdbook-templates.js +18 -0
- package/src/lib/monaco-assets.js +549 -0
- package/src/constants.js +0 -5
- package/src/mdbook.js +0 -482
- package/src/repack.js +0 -85
- package/src/unpack.js +0 -88
- package/src/utils.js +0 -79
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { createLogger } = require('../lib/logger');
|
|
5
|
+
const { createGit, ensureBranchExists, checkoutBranch } = require('../lib/git');
|
|
6
|
+
const {
|
|
7
|
+
copyDir,
|
|
8
|
+
ensureDir,
|
|
9
|
+
readFirstHeading,
|
|
10
|
+
removeAllExcept,
|
|
11
|
+
} = require('../lib/fs');
|
|
12
|
+
const { stepMarkdown } = require('../lib/mdbook-templates');
|
|
13
|
+
const { monacoCss, monacoSetup, monacoEmbed } = require('../lib/monaco-assets');
|
|
14
|
+
|
|
15
|
+
const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
16
|
+
|
|
17
|
+
function classifyCommit(message) {
|
|
18
|
+
const lower = message.toLowerCase();
|
|
19
|
+
if (lower.startsWith('starting-template')) {
|
|
20
|
+
return 'starting-template';
|
|
21
|
+
}
|
|
22
|
+
if (lower.startsWith('readme:')) {
|
|
23
|
+
return 'readme';
|
|
24
|
+
}
|
|
25
|
+
if (lower.startsWith('section:')) {
|
|
26
|
+
return 'section';
|
|
27
|
+
}
|
|
28
|
+
if (lower.startsWith('action:')) {
|
|
29
|
+
return 'action';
|
|
30
|
+
}
|
|
31
|
+
if (lower.startsWith('template:')) {
|
|
32
|
+
return 'template';
|
|
33
|
+
}
|
|
34
|
+
if (lower.startsWith('solution:')) {
|
|
35
|
+
return 'solution';
|
|
36
|
+
}
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseFileStatuses(diffOutput) {
|
|
41
|
+
return diffOutput
|
|
42
|
+
.split('\n')
|
|
43
|
+
.map((line) => line.trim())
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.map((line) => {
|
|
46
|
+
const [status, file] = line.split('\t');
|
|
47
|
+
return { status, file };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function filterFileEntries(entries, rootFolder) {
|
|
52
|
+
const result = [];
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.file) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (entry.status === 'D') {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (entry.file.startsWith('.')) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const filename = path.parse(entry.file).base;
|
|
64
|
+
if (filename === 'README.md') {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (filename.startsWith('.')) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (filename === 'Cargo.lock') {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
result.push({
|
|
75
|
+
label: entry.file,
|
|
76
|
+
path: `./${rootFolder}/${entry.file}`,
|
|
77
|
+
status: entry.status,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getStepTitle(folder) {
|
|
84
|
+
const markdownPath = path.join(folder, 'README.md');
|
|
85
|
+
const title = readFirstHeading(markdownPath);
|
|
86
|
+
if (title) {
|
|
87
|
+
return title;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Missing step title in ${markdownPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function copySnapshot(sourceDir, targetDir) {
|
|
93
|
+
const filter = (sourcePath, isDirectory) => {
|
|
94
|
+
const baseName = path.basename(sourcePath);
|
|
95
|
+
if (baseName === '.git') {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (!isDirectory && baseName.endsWith('.diff')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
};
|
|
103
|
+
copyDir(sourceDir, targetDir, filter);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function checkoutOrCreateBranch(git, outputBranch, baseBranch) {
|
|
107
|
+
const branches = await git.branchLocal();
|
|
108
|
+
if (branches.all.includes(outputBranch)) {
|
|
109
|
+
await checkoutBranch(git, outputBranch);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await checkoutBranch(git, baseBranch);
|
|
113
|
+
await git.checkoutLocalBranch(outputBranch);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function buildMdbook(options) {
|
|
117
|
+
const logger = createLogger(options);
|
|
118
|
+
const repoPath = path.resolve(options.repo);
|
|
119
|
+
const inputBranch = options.input;
|
|
120
|
+
const outputBranch = options.output;
|
|
121
|
+
const sourceDirName = options.source;
|
|
122
|
+
|
|
123
|
+
const git = createGit(repoPath);
|
|
124
|
+
await ensureBranchExists(git, inputBranch);
|
|
125
|
+
|
|
126
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitorial-mdbook-source-'));
|
|
127
|
+
const sourceGit = createGit(tempDir);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
logger.info(`Cloning ${inputBranch} into temporary workspace...`);
|
|
131
|
+
await sourceGit.clone(repoPath, tempDir, ['--branch', inputBranch]);
|
|
132
|
+
|
|
133
|
+
const logs = await sourceGit.log();
|
|
134
|
+
const commits = logs.all.reverse();
|
|
135
|
+
|
|
136
|
+
await checkoutOrCreateBranch(git, outputBranch, inputBranch);
|
|
137
|
+
if (options.force) {
|
|
138
|
+
logger.warn('--force is ignored for build-mdbook to preserve branch history.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const outputRoot = path.join(repoPath, sourceDirName);
|
|
142
|
+
ensureDir(outputRoot);
|
|
143
|
+
removeAllExcept(outputRoot, []);
|
|
144
|
+
const assetRoot = path.join(outputRoot, '_gitorial');
|
|
145
|
+
ensureDir(assetRoot);
|
|
146
|
+
fs.writeFileSync(path.join(assetRoot, 'monaco-setup.js'), monacoSetup);
|
|
147
|
+
fs.writeFileSync(path.join(assetRoot, 'monaco-setup.css'), monacoCss);
|
|
148
|
+
|
|
149
|
+
let stepCounter = 0;
|
|
150
|
+
let pendingTemplate = null;
|
|
151
|
+
let stepEntries = [];
|
|
152
|
+
|
|
153
|
+
let previousCommit = EMPTY_TREE;
|
|
154
|
+
|
|
155
|
+
for (const commit of commits) {
|
|
156
|
+
const commitHash = commit.hash;
|
|
157
|
+
const commitMessage = commit.message;
|
|
158
|
+
const commitType = classifyCommit(commitMessage);
|
|
159
|
+
|
|
160
|
+
if (commitType === 'starting-template') {
|
|
161
|
+
previousCommit = commitHash;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (commitType === 'unknown') {
|
|
166
|
+
throw new Error(`Unknown Gitorial commit type: ${commitMessage}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (commitType === 'template') {
|
|
170
|
+
if (pendingTemplate) {
|
|
171
|
+
throw new Error('Found a template commit before completing a solution.');
|
|
172
|
+
}
|
|
173
|
+
pendingTemplate = { ...commit, baseCommit: previousCommit };
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (commitType === 'solution') {
|
|
178
|
+
if (!pendingTemplate) {
|
|
179
|
+
throw new Error('Found a solution commit without a preceding template.');
|
|
180
|
+
}
|
|
181
|
+
const stepFolder = path.join(outputRoot, stepCounter.toString());
|
|
182
|
+
ensureDir(stepFolder);
|
|
183
|
+
|
|
184
|
+
const templateFolder = path.join(stepFolder, 'template');
|
|
185
|
+
const solutionFolder = path.join(stepFolder, 'solution');
|
|
186
|
+
ensureDir(templateFolder);
|
|
187
|
+
ensureDir(solutionFolder);
|
|
188
|
+
|
|
189
|
+
await sourceGit.checkout(pendingTemplate.hash);
|
|
190
|
+
copySnapshot(tempDir, templateFolder);
|
|
191
|
+
|
|
192
|
+
const templateDiff = await sourceGit.diff([
|
|
193
|
+
'--name-status',
|
|
194
|
+
pendingTemplate.baseCommit,
|
|
195
|
+
pendingTemplate.hash,
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
await sourceGit.checkout(commitHash);
|
|
199
|
+
copySnapshot(tempDir, solutionFolder);
|
|
200
|
+
|
|
201
|
+
const solutionDiff = await sourceGit.diff(['--name-status', pendingTemplate.hash, commitHash]);
|
|
202
|
+
|
|
203
|
+
const templateEntries = filterFileEntries(parseFileStatuses(templateDiff), 'template');
|
|
204
|
+
const solutionEntries = filterFileEntries(parseFileStatuses(solutionDiff), 'solution');
|
|
205
|
+
|
|
206
|
+
const manifestPath = path.join(stepFolder, 'files.json');
|
|
207
|
+
fs.writeFileSync(
|
|
208
|
+
manifestPath,
|
|
209
|
+
JSON.stringify(
|
|
210
|
+
{
|
|
211
|
+
template: templateEntries,
|
|
212
|
+
solution: solutionEntries,
|
|
213
|
+
},
|
|
214
|
+
null,
|
|
215
|
+
2
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
let markdown = stepMarkdown;
|
|
220
|
+
markdown = markdown.replace('<!-- insert_step_readme -->', './template/README.md');
|
|
221
|
+
markdown = markdown.replace(
|
|
222
|
+
'<!-- insert_monaco -->',
|
|
223
|
+
monacoEmbed('../_gitorial', './files.json')
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
fs.writeFileSync(path.join(stepFolder, 'README.md'), markdown);
|
|
227
|
+
|
|
228
|
+
const stepTitle = getStepTitle(templateFolder);
|
|
229
|
+
stepEntries.push({ name: stepTitle, isSection: false });
|
|
230
|
+
|
|
231
|
+
stepCounter += 1;
|
|
232
|
+
pendingTemplate = null;
|
|
233
|
+
previousCommit = commitHash;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (pendingTemplate) {
|
|
238
|
+
throw new Error('Template commit must be followed by a solution commit.');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (commitType === 'section' || commitType === 'readme') {
|
|
242
|
+
const stepFolder = path.join(outputRoot, stepCounter.toString());
|
|
243
|
+
ensureDir(stepFolder);
|
|
244
|
+
|
|
245
|
+
await sourceGit.checkout(commitHash);
|
|
246
|
+
|
|
247
|
+
const sourceReadme = path.join(tempDir, 'README.md');
|
|
248
|
+
if (!fs.existsSync(sourceReadme)) {
|
|
249
|
+
throw new Error(`Section/readme commit ${commitHash} is missing README.md`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fs.copyFileSync(sourceReadme, path.join(stepFolder, 'README.md'));
|
|
253
|
+
|
|
254
|
+
const stepTitle = getStepTitle(stepFolder);
|
|
255
|
+
stepEntries.push({ name: stepTitle, isSection: true });
|
|
256
|
+
|
|
257
|
+
stepCounter += 1;
|
|
258
|
+
previousCommit = commitHash;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const stepFolder = path.join(outputRoot, stepCounter.toString());
|
|
263
|
+
ensureDir(stepFolder);
|
|
264
|
+
const sourceFolder = path.join(stepFolder, 'source');
|
|
265
|
+
ensureDir(sourceFolder);
|
|
266
|
+
|
|
267
|
+
await sourceGit.checkout(commitHash);
|
|
268
|
+
copySnapshot(tempDir, sourceFolder);
|
|
269
|
+
|
|
270
|
+
const diffOutput = await sourceGit.diff(['--name-status', previousCommit, commitHash]);
|
|
271
|
+
const sourceEntries = filterFileEntries(parseFileStatuses(diffOutput), 'source');
|
|
272
|
+
|
|
273
|
+
const manifestPath = path.join(stepFolder, 'files.json');
|
|
274
|
+
fs.writeFileSync(
|
|
275
|
+
manifestPath,
|
|
276
|
+
JSON.stringify({ template: sourceEntries, solution: [] }, null, 2)
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
let markdown = stepMarkdown;
|
|
280
|
+
markdown = markdown.replace('<!-- insert_step_readme -->', './source/README.md');
|
|
281
|
+
markdown = markdown.replace(
|
|
282
|
+
'<!-- insert_monaco -->',
|
|
283
|
+
monacoEmbed('../_gitorial', './files.json')
|
|
284
|
+
);
|
|
285
|
+
fs.writeFileSync(path.join(stepFolder, 'README.md'), markdown);
|
|
286
|
+
|
|
287
|
+
const stepTitle = getStepTitle(sourceFolder);
|
|
288
|
+
stepEntries.push({ name: stepTitle, isSection: false });
|
|
289
|
+
|
|
290
|
+
stepCounter += 1;
|
|
291
|
+
previousCommit = commitHash;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (pendingTemplate) {
|
|
295
|
+
throw new Error('Template commit did not have a matching solution.');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const summaryPath = path.join(outputRoot, 'SUMMARY.md');
|
|
299
|
+
let summary = '# Summary\n\n';
|
|
300
|
+
stepEntries.forEach((entry, index) => {
|
|
301
|
+
if (!entry.isSection) {
|
|
302
|
+
summary += ' ';
|
|
303
|
+
}
|
|
304
|
+
summary += `- [${index}. ${entry.name}](${index}/README.md)\n`;
|
|
305
|
+
});
|
|
306
|
+
fs.writeFileSync(summaryPath, summary);
|
|
307
|
+
|
|
308
|
+
await git.raw(['add', '-A', sourceDirName]);
|
|
309
|
+
const status = await git.status();
|
|
310
|
+
if (!status.isClean()) {
|
|
311
|
+
await git.commit(`mdBook generated from ${inputBranch}`);
|
|
312
|
+
logger.info('mdBook branch generated successfully.');
|
|
313
|
+
} else {
|
|
314
|
+
logger.info('No mdBook changes detected.');
|
|
315
|
+
}
|
|
316
|
+
} finally {
|
|
317
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = { buildMdbook };
|
package/src/index.js
CHANGED
|
@@ -1,47 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
|
+
const { buildGitorial } = require('./commands/build-gitorial');
|
|
5
|
+
const { buildMdbook } = require('./commands/build-mdbook');
|
|
4
6
|
|
|
5
7
|
program
|
|
6
|
-
.
|
|
7
|
-
.description('
|
|
8
|
+
.name('gitorial-cli')
|
|
9
|
+
.description('CLI tools for building and maintaining Gitorial tutorials')
|
|
10
|
+
.version('2.0.0');
|
|
8
11
|
|
|
9
|
-
// Command to unpack a Gitorial into another branch.
|
|
10
12
|
program
|
|
11
|
-
.command('
|
|
12
|
-
.description('
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
.option('-s, --
|
|
17
|
-
.
|
|
18
|
-
|
|
13
|
+
.command('build-gitorial')
|
|
14
|
+
.description('Generate a gitorial branch from an mdBook workshop branch')
|
|
15
|
+
.option('-r, --repo <path>', 'Path to the tutorial repo', process.cwd())
|
|
16
|
+
.option('-i, --input <branch>', 'Input workshop branch', 'master')
|
|
17
|
+
.option('-o, --output <branch>', 'Output gitorial branch', 'gitorial')
|
|
18
|
+
.option('-s, --source <dir>', 'mdBook source directory', 'src')
|
|
19
|
+
.option('--force', 'Replace output branch if it exists', false)
|
|
20
|
+
.option('--verbose', 'Verbose logging', false)
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
await buildGitorial(options);
|
|
19
23
|
});
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
// Command to create a repacked Gitorial from an unpacked Gitorial.
|
|
23
|
-
program
|
|
24
|
-
.command('repack')
|
|
25
|
-
.description('Create a repacked Gitorial from an unpacked Gitorial. Must repack into a new branch.')
|
|
26
|
-
.requiredOption('-p, --path <path>', 'The local path for the git repo containing the Gitorial.')
|
|
27
|
-
.requiredOption('-i, --inputBranch <inputBranch>', 'The branch in the repo with the unpacked Gitorial.')
|
|
28
|
-
.requiredOption('-o, --outputBranch <outputBranch>', 'The branch where you want to repack the Gitorial. Branch must not exist.')
|
|
29
|
-
.option('-s, --subFolder <subFolder>', 'The subfolder (relative to the <path>) where you can find the unpacked Gitorial')
|
|
30
|
-
.option('--force', 'Force the repack, even if it would replace an existing branch. WARNING: this can delete the branch history!')
|
|
31
|
-
.action(({ path, inputBranch, outputBranch, subFolder, force }) => {
|
|
32
|
-
require('./repack')(path, inputBranch, outputBranch, subFolder, force);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Command to scaffold an mdBook source from a Gitorial.
|
|
36
25
|
program
|
|
37
|
-
.command('mdbook')
|
|
38
|
-
.description('
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.option('-s, --
|
|
43
|
-
.
|
|
44
|
-
|
|
26
|
+
.command('build-mdbook')
|
|
27
|
+
.description('Generate an mdBook workshop branch from a gitorial branch')
|
|
28
|
+
.option('-r, --repo <path>', 'Path to the tutorial repo', process.cwd())
|
|
29
|
+
.option('-i, --input <branch>', 'Input gitorial branch', 'gitorial')
|
|
30
|
+
.option('-o, --output <branch>', 'Output workshop branch', 'master')
|
|
31
|
+
.option('-s, --source <dir>', 'mdBook source directory', 'src')
|
|
32
|
+
.option('--force', 'Replace output branch if it exists', false)
|
|
33
|
+
.option('--verbose', 'Verbose logging', false)
|
|
34
|
+
.action(async (options) => {
|
|
35
|
+
await buildMdbook(options);
|
|
45
36
|
});
|
|
46
37
|
|
|
47
|
-
program.
|
|
38
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
39
|
+
console.error(error.message || error);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
});
|
package/src/lib/fs.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function ensureDir(dirPath) {
|
|
5
|
+
if (!fs.existsSync(dirPath)) {
|
|
6
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function removeAllExcept(targetDir, preserveNames = ['.git']) {
|
|
11
|
+
if (!fs.existsSync(targetDir)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const items = fs.readdirSync(targetDir);
|
|
15
|
+
items.forEach((item) => {
|
|
16
|
+
if (preserveNames.includes(item)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
fs.rmSync(path.join(targetDir, item), { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function copyDir(sourceDir, targetDir, filterFn = () => true) {
|
|
24
|
+
ensureDir(targetDir);
|
|
25
|
+
const items = fs.readdirSync(sourceDir);
|
|
26
|
+
items.forEach((item) => {
|
|
27
|
+
const sourcePath = path.join(sourceDir, item);
|
|
28
|
+
const targetPath = path.join(targetDir, item);
|
|
29
|
+
const stats = fs.statSync(sourcePath);
|
|
30
|
+
if (!filterFn(sourcePath, stats.isDirectory())) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (stats.isDirectory()) {
|
|
34
|
+
copyDir(sourcePath, targetPath, filterFn);
|
|
35
|
+
} else {
|
|
36
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listNumericDirs(dirPath) {
|
|
42
|
+
if (!fs.existsSync(dirPath)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return fs.readdirSync(dirPath)
|
|
46
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
47
|
+
.filter((entry) => fs.statSync(path.join(dirPath, entry)).isDirectory())
|
|
48
|
+
.sort((a, b) => parseInt(a) - parseInt(b));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasNonDocFiles(folderPath) {
|
|
52
|
+
if (!fs.existsSync(folderPath)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const entries = fs.readdirSync(folderPath);
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.startsWith('.')) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const fullPath = path.join(folderPath, entry);
|
|
62
|
+
const stats = fs.statSync(fullPath);
|
|
63
|
+
if (stats.isDirectory()) {
|
|
64
|
+
if (hasNonDocFiles(fullPath)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (entry === 'README.md') {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (entry.endsWith('.diff')) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readFirstHeading(markdownPath) {
|
|
81
|
+
if (!fs.existsSync(markdownPath)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const content = fs.readFileSync(markdownPath, 'utf8');
|
|
85
|
+
const match = content.match(/^#\s+(.*)/m);
|
|
86
|
+
return match ? match[1].trim() : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readGitorialType(markdownPath) {
|
|
90
|
+
if (!fs.existsSync(markdownPath)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const content = fs.readFileSync(markdownPath, 'utf8');
|
|
94
|
+
|
|
95
|
+
// Hidden HTML comment metadata, safe for included markdown.
|
|
96
|
+
// Example: <!-- gitorial: template -->
|
|
97
|
+
const commentMatch = content.match(/<!--\s*gitorial(?:_type)?\s*:\s*([a-z-]+)\s*-->/i);
|
|
98
|
+
if (commentMatch) {
|
|
99
|
+
return commentMatch[1].toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
ensureDir,
|
|
106
|
+
removeAllExcept,
|
|
107
|
+
copyDir,
|
|
108
|
+
listNumericDirs,
|
|
109
|
+
hasNonDocFiles,
|
|
110
|
+
readFirstHeading,
|
|
111
|
+
readGitorialType,
|
|
112
|
+
};
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const simpleGit = require('simple-git');
|
|
2
|
+
|
|
3
|
+
function createGit(repoPath) {
|
|
4
|
+
return simpleGit(repoPath);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function ensureBranchExists(git, branchName) {
|
|
8
|
+
const branches = await git.branchLocal();
|
|
9
|
+
if (!branches.all.includes(branchName)) {
|
|
10
|
+
throw new Error(`Branch not found: ${branchName}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function checkoutBranch(git, branchName) {
|
|
15
|
+
await git.checkout(branchName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function deleteBranchIfExists(git, branchName) {
|
|
19
|
+
const branches = await git.branchLocal();
|
|
20
|
+
if (branches.all.includes(branchName)) {
|
|
21
|
+
await git.raw(['branch', '-D', branchName]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function createOrphanBranch(git, branchName, { force, fromBranch }) {
|
|
26
|
+
if (fromBranch) {
|
|
27
|
+
await checkoutBranch(git, fromBranch);
|
|
28
|
+
}
|
|
29
|
+
if (force) {
|
|
30
|
+
await deleteBranchIfExists(git, branchName);
|
|
31
|
+
} else {
|
|
32
|
+
const branches = await git.branchLocal();
|
|
33
|
+
if (branches.all.includes(branchName)) {
|
|
34
|
+
throw new Error(`Branch already exists: ${branchName}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await git.raw(['switch', '--orphan', branchName]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
createGit,
|
|
42
|
+
ensureBranchExists,
|
|
43
|
+
checkoutBranch,
|
|
44
|
+
createOrphanBranch,
|
|
45
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function createLogger({ verbose } = {}) {
|
|
2
|
+
const isVerbose = Boolean(verbose);
|
|
3
|
+
|
|
4
|
+
return {
|
|
5
|
+
info(message) {
|
|
6
|
+
console.log(message);
|
|
7
|
+
},
|
|
8
|
+
warn(message) {
|
|
9
|
+
console.warn(message);
|
|
10
|
+
},
|
|
11
|
+
error(message) {
|
|
12
|
+
console.error(message);
|
|
13
|
+
},
|
|
14
|
+
debug(message) {
|
|
15
|
+
if (isVerbose) {
|
|
16
|
+
console.log(message);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { createLogger };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const stepMarkdown = `
|
|
2
|
+
<div class="gitorial-step">
|
|
3
|
+
<div class="gitorial-step-text">
|
|
4
|
+
|
|
5
|
+
{{#include <!-- insert_step_readme -->}}
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
<div class="gitorial-step-editor">
|
|
9
|
+
|
|
10
|
+
<!-- insert_monaco -->
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
stepMarkdown,
|
|
18
|
+
};
|