gitorial-cli 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/package.json +24 -0
- package/src/constants.js +5 -0
- package/src/index.js +46 -0
- package/src/mdbook.js +453 -0
- package/src/repack.js +62 -0
- package/src/unpack.js +88 -0
- package/src/utils.js +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 gitorial-sdk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# cli
|
|
2
|
+
|
|
3
|
+
The gitorial-cli is a CLI tool for helping manage and work with a Git repo following the [Gitorial format](https://github.com/gitorial-sdk).
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
The following commands are available in this CLI:
|
|
8
|
+
|
|
9
|
+
### unpack
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
yarn start unpack <gitorialPath> <gitorialBranch> <unpackedBranch>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The `unpack` command will take a branch in the Gitorial format, and unpack the steps of the gitorial into numbered folders in a separate branch.
|
|
16
|
+
|
|
17
|
+
`unpackedBranch` can be an existing branch, and a new commit will be added on top of the existing history.
|
|
18
|
+
|
|
19
|
+
A `gitorial_metadata.json` file will be created in each folder, allowing you to update the `commitMessage` for that step when the Gitorial is repacked.
|
|
20
|
+
|
|
21
|
+
### repack
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
yarn start repack <gitorialPath> <unpackedBranch> <repackBranch>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `repack` command will take a branch which is in the `unpacked` format (numbered folders with the content of each step), and create a fresh git commit history with those steps as commits in the Gitorial.
|
|
28
|
+
|
|
29
|
+
`repackBranch` cannot be an existing branch name, as it risks deleting your Git history by mistake.
|
|
30
|
+
|
|
31
|
+
The `gitorial_metadata.json` file will not be included in your repacked Gitorial.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
The workflow for managing a gitorial is still in development, but assumes the following:
|
|
36
|
+
|
|
37
|
+
- You have a Git repo with a branch in the Gitorial format.
|
|
38
|
+
- You can `unpack` that branch into a branch named `unpacked`.
|
|
39
|
+
- You can keep this branch around forever, and always unpack into it. It can act as a history of changes to your Gitorial, since your raw Gitorial always resets its history.
|
|
40
|
+
- You can then make changes to files in the `unpacked` branch. For example:
|
|
41
|
+
- Creating new step folders.
|
|
42
|
+
- Editing README or other documentation.
|
|
43
|
+
- Editing code.
|
|
44
|
+
- Although it is suggested to make code changes on the Gitorial branch, allowing you to use Git merge to ensure all changes are propagated through the tutorial.
|
|
45
|
+
- You can commit changes or merge pull requests into the `unpacked` branch like normal.
|
|
46
|
+
- When you are happy with the `unpacked` branch, you can `repack` it into a new branch `repacked`.
|
|
47
|
+
- You can audit your changes in Git history (making sure step by step changes have a clean diff).
|
|
48
|
+
- Finally, you can use `git reset repacked && git push --force` on your Gitorial branch to update your Gitorial.
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gitorial-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI Tools for Creating and Managing a Gitorial",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/gitorial-sdk/cli.git"
|
|
13
|
+
},
|
|
14
|
+
"author": "Shawn Tabrizi",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/gitorial-sdk/cli/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/gitorial-sdk/cli#readme",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.0.0",
|
|
22
|
+
"simple-git": "^3.24.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/constants.js
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
|
|
5
|
+
program
|
|
6
|
+
.version('1.0.0')
|
|
7
|
+
.description('Node.js CLI application to manage Git repositories');
|
|
8
|
+
|
|
9
|
+
// Command to unpack a Gitorial into another branch.
|
|
10
|
+
program
|
|
11
|
+
.command('unpack')
|
|
12
|
+
.description('Unpack a Gitorial into another branch.')
|
|
13
|
+
.requiredOption('-p, --path <path>', 'The local path for the git repo containing the Gitorial.')
|
|
14
|
+
.requiredOption('-i, --inputBranch <inputBranch>', 'The branch in the repo with the Gitorial.')
|
|
15
|
+
.requiredOption('-o, --outputBranch <outputBranch>', 'The branch where you want to unpack the Gitorial.')
|
|
16
|
+
.option('-s, --subFolder <subFolder>', 'The subfolder (relative to the <path>) where you want the unpacked Gitorial to be placed.')
|
|
17
|
+
.action(({ path, inputBranch, outputBranch, subFolder }) => {
|
|
18
|
+
require('./unpack')(path, inputBranch, outputBranch, subFolder);
|
|
19
|
+
});
|
|
20
|
+
|
|
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
|
+
.action(({ path, inputBranch, outputBranch, subFolder }) => {
|
|
31
|
+
require('./repack')(path, inputBranch, outputBranch, subFolder);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Command to scaffold an mdBook source from a Gitorial.
|
|
35
|
+
program
|
|
36
|
+
.command('mdbook')
|
|
37
|
+
.description('Scaffold the contents of a Gitorial in a new branch in the mdBook source format. You need to initialize an mdBook yourself ')
|
|
38
|
+
.requiredOption('-p, --path <path>', 'The local path for the git repo containing the Gitorial.')
|
|
39
|
+
.requiredOption('-i, --inputBranch <inputBranch>', 'The branch in the repo with the Gitorial.')
|
|
40
|
+
.requiredOption('-o, --outputBranch <outputBranch>', 'The branch where you want your mdBook to live')
|
|
41
|
+
.option('-s, --subFolder <subFolder>', 'The subfolder (relative to the <path>) where you want the mdBook source material to be placed.', 'src')
|
|
42
|
+
.action(({ path, inputBranch, outputBranch, subFolder }) => {
|
|
43
|
+
require('./mdbook')(path, inputBranch, outputBranch, subFolder);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
program.parse(process.argv);
|
package/src/mdbook.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
const simpleGit = require("simple-git");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { copyAllContentsAndReplace, copyFilesAndDirectories, doesBranchExist } = require("./utils");
|
|
6
|
+
|
|
7
|
+
async function mdbook(repoPath, inputBranch, outputBranch, subFolder) {
|
|
8
|
+
try {
|
|
9
|
+
// Create a new temporary folder
|
|
10
|
+
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitorial-source-'));
|
|
11
|
+
const mdbookDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitorial-mdbook-'));
|
|
12
|
+
|
|
13
|
+
// Clone the repo into the source folder.
|
|
14
|
+
const tempGit = simpleGit(sourceDir);
|
|
15
|
+
|
|
16
|
+
// Resolve the full path to the local repository
|
|
17
|
+
const resolvedRepoPath = path.resolve(repoPath);
|
|
18
|
+
await tempGit.clone(resolvedRepoPath, '.', ['--branch', inputBranch]);
|
|
19
|
+
|
|
20
|
+
await processGitorial(sourceDir, mdbookDir);
|
|
21
|
+
|
|
22
|
+
let sourceGit = simpleGit(repoPath);
|
|
23
|
+
// Check if the branch exists in the list of local branches
|
|
24
|
+
const branchExists = await doesBranchExist(sourceGit, outputBranch)
|
|
25
|
+
|
|
26
|
+
if (!branchExists) {
|
|
27
|
+
// Create a fresh branch if it does not exist.
|
|
28
|
+
await sourceGit.raw(['switch', '--orphan', outputBranch]);
|
|
29
|
+
} else {
|
|
30
|
+
// Checkout the current branch if it does.
|
|
31
|
+
await sourceGit.checkout(outputBranch);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let outputFolder = repoPath;
|
|
35
|
+
if (subFolder) {
|
|
36
|
+
outputFolder = path.join(outputFolder, subFolder);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
copyAllContentsAndReplace(mdbookDir, outputFolder);
|
|
40
|
+
|
|
41
|
+
// Stage all files
|
|
42
|
+
await sourceGit.add('*');
|
|
43
|
+
|
|
44
|
+
// Create commit with commit message
|
|
45
|
+
await sourceGit.commit(`mdBook generated from ${inputBranch}`);
|
|
46
|
+
|
|
47
|
+
// Clean up source folder
|
|
48
|
+
fs.rmSync(sourceDir, { recursive: true });
|
|
49
|
+
fs.rmSync(mdbookDir, { recursive: true });
|
|
50
|
+
console.log("Temporary files removed.");
|
|
51
|
+
|
|
52
|
+
console.log("mdBook completed.");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Error:', error.message || error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function processGitorial(sourceDir, mdbookDir) {
|
|
59
|
+
const sourceGit = simpleGit(sourceDir);
|
|
60
|
+
|
|
61
|
+
// Retrieve commit log
|
|
62
|
+
const logs = await sourceGit.log();
|
|
63
|
+
|
|
64
|
+
let stepCounter = 0;
|
|
65
|
+
let templateFound = false;
|
|
66
|
+
let solutionFound = false;
|
|
67
|
+
let templateFiles = [];
|
|
68
|
+
let solutionFiles = [];
|
|
69
|
+
let sourceFiles = [];
|
|
70
|
+
let stepNames = [];
|
|
71
|
+
|
|
72
|
+
// Create a folder for each commit
|
|
73
|
+
// Reverse to make the oldest commit first
|
|
74
|
+
for ([index, log] of logs.all.reverse().entries()) {
|
|
75
|
+
const commitHash = log.hash;
|
|
76
|
+
const commitMessage = log.message;
|
|
77
|
+
|
|
78
|
+
const isReadme = commitMessage.toLowerCase().startsWith("readme: ");
|
|
79
|
+
const isTemplate = commitMessage.toLowerCase().startsWith("template: ");
|
|
80
|
+
const isSolution = commitMessage.toLowerCase().startsWith("solution: ");
|
|
81
|
+
const isSection = commitMessage.toLowerCase().startsWith("section: ");
|
|
82
|
+
const isAction = commitMessage.toLowerCase().startsWith("action: ");
|
|
83
|
+
|
|
84
|
+
let stepFolder = path.join(mdbookDir, stepCounter.toString());
|
|
85
|
+
if (!fs.existsSync(stepFolder)) {
|
|
86
|
+
fs.mkdirSync(stepFolder);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let sourceFolder = path.join(stepFolder, "source");
|
|
90
|
+
let templateFolder = path.join(stepFolder, "template");
|
|
91
|
+
let solutionFolder = path.join(stepFolder, "solution");
|
|
92
|
+
|
|
93
|
+
// Default assumption is output is not a template or solution
|
|
94
|
+
let outputFolder = sourceFolder;
|
|
95
|
+
|
|
96
|
+
if (isTemplate) {
|
|
97
|
+
// Check there isn't a template already in queue
|
|
98
|
+
if (templateFound) {
|
|
99
|
+
console.error("A second template was found before a solution.");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
templateFound = true;
|
|
104
|
+
|
|
105
|
+
// make step folder
|
|
106
|
+
outputFolder = templateFolder;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isSolution) {
|
|
110
|
+
// Check that there is a template in queue
|
|
111
|
+
if (!templateFound) {
|
|
112
|
+
console.error("No template was found for this solution.");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check that a solution is not already found.
|
|
117
|
+
if (solutionFound) {
|
|
118
|
+
console.error("A second solution was found before a template.");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
solutionFound = true;
|
|
123
|
+
outputFolder = solutionFolder;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fs.mkdirSync(outputFolder);
|
|
127
|
+
|
|
128
|
+
// Checkout the commit
|
|
129
|
+
console.log(`Checking out commit: ${commitHash}`);
|
|
130
|
+
await sourceGit.checkout(commitHash)
|
|
131
|
+
|
|
132
|
+
// Copy the contents to the commit folder
|
|
133
|
+
copyFilesAndDirectories(sourceDir, outputFolder);
|
|
134
|
+
console.log(`Contents of commit ${index} copied to ${outputFolder}`);
|
|
135
|
+
|
|
136
|
+
let previousCommit = "HEAD~1";
|
|
137
|
+
// This is the commit hash for an empty git project.
|
|
138
|
+
let emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
139
|
+
|
|
140
|
+
if (index == 0) {
|
|
141
|
+
previousCommit = emptyTree;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get the list of modified or created files in the commit
|
|
145
|
+
const diffOutput = await sourceGit.diff(['--name-status', `${previousCommit}`, `HEAD`])
|
|
146
|
+
// Get the raw diff between the previous commit and HEAD, excluding README.md
|
|
147
|
+
const diffRaw = await sourceGit.diff([`${previousCommit}`, `HEAD`, ':(exclude)README.md']);
|
|
148
|
+
|
|
149
|
+
// Create a raw output
|
|
150
|
+
let diff_name = "changes.diff";
|
|
151
|
+
if (isSolution) {
|
|
152
|
+
diff_name = "solution.diff";
|
|
153
|
+
} else if (isTemplate) {
|
|
154
|
+
diff_name = "template.diff";
|
|
155
|
+
}
|
|
156
|
+
const diffFilePath = path.join(outputFolder, diff_name);
|
|
157
|
+
fs.writeFileSync(diffFilePath, diffRaw);
|
|
158
|
+
|
|
159
|
+
let fileStatus = diffOutput.split("\n").map((line) => {
|
|
160
|
+
const [status, file] = line.split("\t");
|
|
161
|
+
return { status, file };
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (isTemplate) {
|
|
165
|
+
templateFiles = fileStatus;
|
|
166
|
+
} else if (isSolution) {
|
|
167
|
+
solutionFiles = fileStatus;
|
|
168
|
+
} else {
|
|
169
|
+
sourceFiles = fileStatus;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Reset sanity check and increment step
|
|
173
|
+
// Handle when both template and solution is found,
|
|
174
|
+
// or when there is a step that is neither a template or solution
|
|
175
|
+
if (
|
|
176
|
+
(templateFound && solutionFound) ||
|
|
177
|
+
(!templateFound && !solutionFound)
|
|
178
|
+
) {
|
|
179
|
+
if (isReadme) {
|
|
180
|
+
markdownContent = sectionMarkdown;
|
|
181
|
+
} else if (isSection) {
|
|
182
|
+
markdownContent = sectionMarkdown;
|
|
183
|
+
stepNames.push({
|
|
184
|
+
name: getStepName(sourceFolder),
|
|
185
|
+
is_section: true,
|
|
186
|
+
});
|
|
187
|
+
} else if (templateFound) {
|
|
188
|
+
markdownContent = templateMarkdown;
|
|
189
|
+
let templateFileText = generateFileMarkdown("template", templateFiles);
|
|
190
|
+
let solutionFileText = generateFileMarkdown("solution", solutionFiles);
|
|
191
|
+
markdownContent = markdownContent.replace(
|
|
192
|
+
"<!-- insert_template_files -->",
|
|
193
|
+
templateFileText
|
|
194
|
+
);
|
|
195
|
+
markdownContent = markdownContent.replace(
|
|
196
|
+
"<!-- insert_solution_files -->",
|
|
197
|
+
solutionFileText
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
let diffText = generateDiffMarkdown("template");
|
|
201
|
+
markdownContent = markdownContent.replace(
|
|
202
|
+
"<!-- insert_diff_files -->",
|
|
203
|
+
diffText
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
stepNames.push({
|
|
207
|
+
name: getStepName(templateFolder),
|
|
208
|
+
is_section: false,
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
markdownContent = sourceMarkdown;
|
|
212
|
+
let sourceFileText = generateFileMarkdown("source", sourceFiles);
|
|
213
|
+
markdownContent = markdownContent.replace(
|
|
214
|
+
"<!-- insert_source_files -->",
|
|
215
|
+
sourceFileText
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
let diffText = generateDiffMarkdown("source");
|
|
219
|
+
markdownContent = markdownContent.replace(
|
|
220
|
+
"<!-- insert_diff_files -->",
|
|
221
|
+
diffText
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
stepNames.push({
|
|
225
|
+
name: getStepName(sourceFolder),
|
|
226
|
+
is_section: false,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Create a Markdown file in the commit folder
|
|
230
|
+
const markdownFilePath = path.join(stepFolder, "README.md");
|
|
231
|
+
fs.writeFileSync(markdownFilePath, markdownContent);
|
|
232
|
+
stepCounter += 1;
|
|
233
|
+
templateFound = false;
|
|
234
|
+
solutionFound = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
generateSidebar(mdbookDir, stepNames);
|
|
239
|
+
|
|
240
|
+
console.log("Finished Parsing.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Generate the markdown text for files.
|
|
244
|
+
function generateFileMarkdown(type, files) {
|
|
245
|
+
// type is expected to be one of "source", "solution", or "template"
|
|
246
|
+
if (type != "solution" && type != "source" && type != "template") {
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let output = "";
|
|
251
|
+
|
|
252
|
+
let parsedFiles = [];
|
|
253
|
+
for (file of files) {
|
|
254
|
+
if (!file.file) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let filepath = `./${type}/${file.file}`;
|
|
259
|
+
let filename = path.parse(filepath).base;
|
|
260
|
+
|
|
261
|
+
// Skip README
|
|
262
|
+
if (filename == "README.md") {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Skip hidden files
|
|
266
|
+
if (filename.startsWith(".")) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// Skip Cargo.lock
|
|
270
|
+
if (filename == "Cargo.lock") {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let classStyle = `file-${type}`;
|
|
275
|
+
if (file.status == "M") {
|
|
276
|
+
classStyle += " file-modified";
|
|
277
|
+
} else if (file.status == "A") {
|
|
278
|
+
classStyle += " file-added";
|
|
279
|
+
} else if (file.status == "D") {
|
|
280
|
+
classStyle += " file-deleted";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let codeStyle = "text";
|
|
284
|
+
let extname = path.extname(filepath);
|
|
285
|
+
if (extname == ".rs") {
|
|
286
|
+
codeStyle = "rust";
|
|
287
|
+
} else if (extname == ".toml") {
|
|
288
|
+
codeStyle = "toml";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
parsedFiles.push({ filename: file.file, classStyle, codeStyle, filepath })
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (parsedFiles.length > 0) {
|
|
295
|
+
output += `<div class="tab">\n`;
|
|
296
|
+
|
|
297
|
+
for ([i, file] of parsedFiles.entries()) {
|
|
298
|
+
output += `<button class="subtab tablinks ${file.classStyle}${i == 0 ? " active" : ""}" onclick="switchSubTab(event, '${file.filename}')" data-id="${file.filename}">${file.filename}</button>\n`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
output += `</div>\n`
|
|
302
|
+
|
|
303
|
+
for ([i, file] of parsedFiles.entries()) {
|
|
304
|
+
output += `<div id="${type}/${file.filename}" class="subtab tabcontent${i == 0 ? " active" : ""}" data-id="${file.filename}">\n\n`;
|
|
305
|
+
output += `\`\`\`${file.codeStyle}\n{{#include ${file.filepath}}}\n\`\`\`\n\n`;
|
|
306
|
+
output += `</div>\n\n`;
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
output = "No files edited in this step.";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return output;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function generateDiffMarkdown(type) {
|
|
316
|
+
let output = "";
|
|
317
|
+
|
|
318
|
+
if (type == "template" || type == "solution") {
|
|
319
|
+
output += solutionDiffMarkdown;
|
|
320
|
+
} else {
|
|
321
|
+
output += changesDiffMarkdown;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return output;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let solutionDiffMarkdown = `
|
|
328
|
+
<div class="tab">
|
|
329
|
+
<button class="difftab tablinks active" onclick="switchDiff(event, 'template.diff')" data-id="template.diff">template.diff</button>
|
|
330
|
+
<button class="difftab tablinks" onclick="switchDiff(event, 'solution.diff')" data-id="solution.diff">solution.diff</button>
|
|
331
|
+
</div>
|
|
332
|
+
<div id="template.diff" class="difftab tabcontent active" data-id="template.diff">
|
|
333
|
+
|
|
334
|
+
\`\`\`diff\n{{#include ./template/template.diff}}\n\`\`\`
|
|
335
|
+
|
|
336
|
+
</div>
|
|
337
|
+
<div id="solution.diff" class="difftab tabcontent" data-id="solution.diff">
|
|
338
|
+
|
|
339
|
+
\`\`\`diff\n{{#include ./solution/solution.diff}}\n\`\`\`
|
|
340
|
+
|
|
341
|
+
</div>`;
|
|
342
|
+
|
|
343
|
+
let changesDiffMarkdown = `
|
|
344
|
+
<div class="tab">
|
|
345
|
+
<button class="difftab tablinks active" onclick="switchDiff(event, 'changes.diff')" data-id="changes.diff">changes.diff</button>
|
|
346
|
+
</div>
|
|
347
|
+
<div id="changes.diff" class="difftab tabcontent active" data-id="changes.diff">
|
|
348
|
+
|
|
349
|
+
\`\`\`diff\n{{#include ./source/changes.diff}}\n\`\`\`
|
|
350
|
+
|
|
351
|
+
</div>`;
|
|
352
|
+
|
|
353
|
+
let templateMarkdown = `
|
|
354
|
+
<div class="content-row">
|
|
355
|
+
<div class="content-col">
|
|
356
|
+
|
|
357
|
+
{{#include ./template/README.md}}
|
|
358
|
+
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div class="content-col">
|
|
362
|
+
|
|
363
|
+
<div class="tab">
|
|
364
|
+
<button class="maintab tablinks active" onclick="switchMainTab(event, 'Template')">Template</button>
|
|
365
|
+
<button class="maintab tablinks" onclick="switchMainTab(event, 'Solution')">Solution</button>
|
|
366
|
+
<button class="maintab tablinks" onclick="switchMainTab(event, 'Diff')">Diff</button>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<div id="Template" class="maintab tabcontent active">
|
|
370
|
+
|
|
371
|
+
<!-- insert_template_files -->
|
|
372
|
+
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div id="Solution" class="maintab tabcontent">
|
|
376
|
+
|
|
377
|
+
<!-- insert_solution_files -->
|
|
378
|
+
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div id="Diff" class="maintab tabcontent">
|
|
382
|
+
|
|
383
|
+
<!-- insert_diff_files -->
|
|
384
|
+
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
let sourceMarkdown = `
|
|
392
|
+
<div class="content-row">
|
|
393
|
+
<div class="content-col">
|
|
394
|
+
|
|
395
|
+
{{#include ./source/README.md}}
|
|
396
|
+
|
|
397
|
+
</div>
|
|
398
|
+
<div class="content-col">
|
|
399
|
+
|
|
400
|
+
<div class="tab">
|
|
401
|
+
<button class="maintab tablinks active" onclick="switchMainTab(event, 'Source')">Source</button>
|
|
402
|
+
<button class="maintab tablinks" onclick="switchMainTab(event, 'Diff')">Diff</button>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div id="Source" class="maintab tabcontent active">
|
|
406
|
+
|
|
407
|
+
<!-- insert_source_files -->
|
|
408
|
+
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div id="Diff" class="maintab tabcontent">
|
|
412
|
+
|
|
413
|
+
<!-- insert_diff_files -->
|
|
414
|
+
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
let sectionMarkdown = `
|
|
422
|
+
<div class="content-section">
|
|
423
|
+
|
|
424
|
+
{{#include ./source/README.md}}
|
|
425
|
+
|
|
426
|
+
</div>
|
|
427
|
+
`;
|
|
428
|
+
|
|
429
|
+
function getStepName(folder) {
|
|
430
|
+
const filePath = path.join(folder, "README.md");
|
|
431
|
+
const markdownContent = fs.readFileSync(filePath, "utf8");
|
|
432
|
+
const titleMatch = markdownContent.match(/^#\s+(.*)/m);
|
|
433
|
+
if (titleMatch) {
|
|
434
|
+
return titleMatch[1];
|
|
435
|
+
} else {
|
|
436
|
+
console.error(`Error getting markdown title.`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function generateSidebar(mdbookDir, steps) {
|
|
442
|
+
const sidebarFilePath = path.join(mdbookDir, "SUMMARY.md");
|
|
443
|
+
let output = "";
|
|
444
|
+
steps.forEach(({ name, is_section }, index) => {
|
|
445
|
+
if (!is_section) {
|
|
446
|
+
output += ` `;
|
|
447
|
+
}
|
|
448
|
+
output += `- [${index}. ${name}](${index}/README.md)\n`;
|
|
449
|
+
});
|
|
450
|
+
fs.writeFileSync(sidebarFilePath, output);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
module.exports = mdbook;
|
package/src/repack.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const simpleGit = require('simple-git');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { GITORIAL_METADATA } = require('./constants');
|
|
6
|
+
const { copyAllContentsAndReplace } = require('./utils')
|
|
7
|
+
|
|
8
|
+
async function repack(repoPath, inputBranch, outputBranch, subFolder) {
|
|
9
|
+
try {
|
|
10
|
+
const git = simpleGit(repoPath);
|
|
11
|
+
await git.raw(['switch', '--orphan', outputBranch]);
|
|
12
|
+
|
|
13
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitorial-repack-'));
|
|
14
|
+
await git.clone(repoPath, tempDir, ['--branch', inputBranch]);
|
|
15
|
+
|
|
16
|
+
let unpackedDir = tempDir;
|
|
17
|
+
if (subFolder) {
|
|
18
|
+
unpackedDir = path.join(unpackedDir, subFolder)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get list of steps
|
|
22
|
+
const steps = fs.readdirSync(unpackedDir)
|
|
23
|
+
.filter(item => fs.statSync(path.join(unpackedDir, item)).isDirectory())
|
|
24
|
+
.filter(item => item != '.git') // skip the git directory
|
|
25
|
+
.sort((a, b) => parseInt(a) - parseInt(b)); // Sort folders numerically
|
|
26
|
+
|
|
27
|
+
for (const step of steps) {
|
|
28
|
+
const stepFolderPath = path.join(unpackedDir, step);
|
|
29
|
+
|
|
30
|
+
// Read commit message from GITORIAL_METADATA
|
|
31
|
+
const commitInfoPath = path.join(stepFolderPath, GITORIAL_METADATA);
|
|
32
|
+
const commitInfo = JSON.parse(fs.readFileSync(commitInfoPath, 'utf-8'));
|
|
33
|
+
const commitMessage = commitInfo.commitMessage;
|
|
34
|
+
|
|
35
|
+
// Copy files from numbered folder to repo path
|
|
36
|
+
copyAllContentsAndReplace(stepFolderPath, repoPath);
|
|
37
|
+
|
|
38
|
+
// Stage all files
|
|
39
|
+
await git.add('*');
|
|
40
|
+
|
|
41
|
+
// Remove GITORIAL_METADATA
|
|
42
|
+
await git.reset(GITORIAL_METADATA);
|
|
43
|
+
await git.rm(GITORIAL_METADATA);
|
|
44
|
+
|
|
45
|
+
// Create commit with commit message
|
|
46
|
+
await git.commit(commitMessage);
|
|
47
|
+
|
|
48
|
+
console.log(`Commit created for step ${step} with message: ${commitMessage}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Clean up temp folder
|
|
52
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
53
|
+
console.log("Temporary files removed.");
|
|
54
|
+
|
|
55
|
+
console.log('Commits created successfully.')
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Error:', error.message || error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = repack;
|
package/src/unpack.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const simpleGit = require('simple-git');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { GITORIAL_METADATA } = require('./constants');
|
|
6
|
+
const { copyAllContentsAndReplace, doesBranchExist, copyFilesAndDirectories } = require('./utils')
|
|
7
|
+
|
|
8
|
+
async function unpack(repoPath, inputBranch, outputBranch, outputSubFolder) {
|
|
9
|
+
try {
|
|
10
|
+
// Create a new temporary folder
|
|
11
|
+
const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitorial-source-'));
|
|
12
|
+
const unpackedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitorial-unpacked-'));
|
|
13
|
+
|
|
14
|
+
// Clone the repo into the source folder.
|
|
15
|
+
const tempGit = simpleGit(sourceDir);
|
|
16
|
+
|
|
17
|
+
// Resolve the full path to the local repository
|
|
18
|
+
const resolvedRepoPath = path.resolve(repoPath);
|
|
19
|
+
await tempGit.clone(resolvedRepoPath, '.', ['--branch', inputBranch]);
|
|
20
|
+
|
|
21
|
+
// Retrieve commit log
|
|
22
|
+
const logs = await tempGit.log();
|
|
23
|
+
|
|
24
|
+
// Create a folder for each commit
|
|
25
|
+
// Reverse to make the oldest commit first
|
|
26
|
+
for ([index, log] of logs.all.reverse().entries()) {
|
|
27
|
+
const commitHash = log.hash;
|
|
28
|
+
const commitMessage = log.message;
|
|
29
|
+
|
|
30
|
+
let stepFolder = path.join(unpackedDir, index.toString());
|
|
31
|
+
|
|
32
|
+
// Checkout the commit
|
|
33
|
+
console.log(`Checking out commit: ${commitHash}`);
|
|
34
|
+
await tempGit.checkout(commitHash);
|
|
35
|
+
|
|
36
|
+
// Copy the contents to the commit folder
|
|
37
|
+
copyFilesAndDirectories(sourceDir, stepFolder);
|
|
38
|
+
console.log(`Contents copied from ${sourceDir} to ${stepFolder}`);
|
|
39
|
+
|
|
40
|
+
// Create a JSON file in the commit folder
|
|
41
|
+
const jsonFilePath = path.join(stepFolder, GITORIAL_METADATA);
|
|
42
|
+
const commitInfoObject = {
|
|
43
|
+
"_Note": "This file will not be included in your final gitorial.",
|
|
44
|
+
commitMessage,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(jsonFilePath, JSON.stringify(commitInfoObject, null, 2));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let sourceGit = simpleGit(repoPath);
|
|
51
|
+
|
|
52
|
+
// Check if the branch exists in the list of local branches
|
|
53
|
+
const branchExists = await doesBranchExist(sourceGit, outputBranch)
|
|
54
|
+
|
|
55
|
+
if (!branchExists) {
|
|
56
|
+
// Create a fresh branch if it does not exist.
|
|
57
|
+
await sourceGit.raw(['switch', '--orphan', outputBranch]);
|
|
58
|
+
} else {
|
|
59
|
+
// Checkout the current branch if it does.
|
|
60
|
+
await sourceGit.checkout(outputBranch)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let outputFolder = repoPath;
|
|
64
|
+
if (outputSubFolder) {
|
|
65
|
+
outputFolder = path.join(repoPath, outputSubFolder)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
copyAllContentsAndReplace(unpackedDir, outputFolder);
|
|
69
|
+
|
|
70
|
+
// Stage all files
|
|
71
|
+
await sourceGit.add('*');
|
|
72
|
+
|
|
73
|
+
// Create commit with commit message
|
|
74
|
+
await sourceGit.commit(`Unpacked from ${inputBranch}`);
|
|
75
|
+
|
|
76
|
+
// Clean up source folder
|
|
77
|
+
fs.rmSync(sourceDir, { recursive: true });
|
|
78
|
+
fs.rmSync(unpackedDir, { recursive: true });
|
|
79
|
+
console.log("Temporary files removed.");
|
|
80
|
+
|
|
81
|
+
console.log("Process completed.");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error:', error.message || error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = unpack;
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function copyAllContentsAndReplace(sourceDir, targetDir) {
|
|
5
|
+
// Create target directory if it doesn't exist
|
|
6
|
+
if (!fs.existsSync(targetDir)) {
|
|
7
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Get list of items in source directory
|
|
11
|
+
const items = fs.readdirSync(sourceDir);
|
|
12
|
+
|
|
13
|
+
// Copy each item to target directory and replace existing items
|
|
14
|
+
items.forEach(item => {
|
|
15
|
+
const sourcePath = path.join(sourceDir, item);
|
|
16
|
+
const targetPath = path.join(targetDir, item);
|
|
17
|
+
if (fs.statSync(sourcePath).isDirectory()) {
|
|
18
|
+
if (fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
|
|
19
|
+
fs.rmSync(targetPath, { recursive: true }); // Remove existing directory
|
|
20
|
+
}
|
|
21
|
+
fs.mkdirSync(targetPath, { recursive: true }); // Create directory in target
|
|
22
|
+
copyAllContentsAndReplace(sourcePath, targetPath); // Recursively copy contents of directory
|
|
23
|
+
} else {
|
|
24
|
+
if (fs.existsSync(targetPath)) {
|
|
25
|
+
fs.unlinkSync(targetPath); // Delete existing file
|
|
26
|
+
}
|
|
27
|
+
fs.copyFileSync(sourcePath, targetPath); // Copy file
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function copyFilesAndDirectories(source, target) {
|
|
33
|
+
// Check if source exists
|
|
34
|
+
if (!fs.existsSync(source)) {
|
|
35
|
+
console.error(`Source directory ${source} does not exist.`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create target directory if it doesn't exist
|
|
40
|
+
if (!fs.existsSync(target)) {
|
|
41
|
+
fs.mkdirSync(target, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get list of items in source directory
|
|
45
|
+
const items = fs.readdirSync(source);
|
|
46
|
+
|
|
47
|
+
// Copy each item to target directory
|
|
48
|
+
items.forEach(item => {
|
|
49
|
+
// Skip .git folder
|
|
50
|
+
if (item === '.git') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sourcePath = path.join(source, item);
|
|
55
|
+
const targetPath = path.join(target, item);
|
|
56
|
+
if (fs.statSync(sourcePath).isDirectory()) {
|
|
57
|
+
// Recursively copy directories
|
|
58
|
+
copyFilesAndDirectories(sourcePath, targetPath);
|
|
59
|
+
} else {
|
|
60
|
+
// Copy files
|
|
61
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function doesBranchExist(git, branchName) {
|
|
67
|
+
try {
|
|
68
|
+
// Get a list of all local branches
|
|
69
|
+
const branches = await git.branchLocal();
|
|
70
|
+
// Check if the branch exists in the list of local branches
|
|
71
|
+
const branchExists = branches.all.includes(branchName);
|
|
72
|
+
return branchExists;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Error checking if branch exists:', error);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { copyAllContentsAndReplace, doesBranchExist, copyFilesAndDirectories };
|