tuture
Version:
Write tutorials from the future, with the power of Git and community.
201 lines (200 loc) • 6.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const micromatch_1 = tslib_1.__importDefault(require("micromatch"));
const nodes_1 = require("./nodes");
const collection_1 = require("./collection");
const constants_1 = require("../constants");
const git_1 = require("./git");
const logger_1 = tslib_1.__importDefault(require("./logger"));
/**
* Compare if two commit hashes are equal.
*/
function isCommitEqual(hash1, hash2) {
return hash1.startsWith(hash2) || hash2.startsWith(hash1);
}
exports.isCommitEqual = isCommitEqual;
/**
* Remove all Tuture-related files.
*/
async function removeTutureSuite() {
await fs_extra_1.default.remove(constants_1.TUTURE_ROOT);
}
exports.removeTutureSuite = removeTutureSuite;
/**
* Generate a random hex number.
*/
function randHex(digits = 8) {
return Math.random()
.toString(16)
.slice(2, digits + 2);
}
exports.randHex = randHex;
function getHiddenLines(diffItem) {
// Number of context normal lines to show for each diff.
const context = 3;
if (diffItem.chunks.length === 0) {
return [];
}
// An array to indicate whether a line should be shown.
const shownArr = diffItem.chunks[0].changes.map((change) => change.type !== 'normal');
let contextCounter = -1;
for (let i = 0; i < shownArr.length; i++) {
if (shownArr[i]) {
contextCounter = context;
}
else {
contextCounter--;
if (contextCounter >= 0) {
shownArr[i] = true;
}
}
}
contextCounter = -1;
for (let i = shownArr.length - 1; i >= 0; i--) {
if (shownArr[i]) {
contextCounter = context;
}
else {
contextCounter--;
if (contextCounter >= 0) {
shownArr[i] = true;
}
}
}
const hiddenLines = [];
let startNumber = null;
for (let i = 0; i < shownArr.length; i++) {
if (!shownArr[i] && startNumber === null) {
startNumber = i;
}
else if (i > 0 && !shownArr[i - 1] && shownArr[i]) {
hiddenLines.push([startNumber, i - 1]);
startNumber = null;
}
}
return hiddenLines;
}
function convertFile(commit, file, display = false) {
const diffBlock = {
type: 'diff-block',
file: file.to,
commit,
hiddenLines: getHiddenLines(file),
children: [{ text: '' }],
};
const fileObj = {
type: 'file',
file: file.to,
display,
children: [nodes_1.getEmptyExplain(), diffBlock, nodes_1.getEmptyExplain()],
};
return fileObj;
}
/**
* Store diff data of all commits and return corresponding steps.
*/
async function makeSteps(ignoredFiles) {
if (!(await git_1.git.branchLocal()).current) {
// No commits yet.
return [];
}
const logs = (await git_1.git.log({ '--no-merges': true })).all
.map(({ message, hash }) => ({ message, hash }))
.reverse()
// filter out commits whose commit message starts with 'tuture:'
.filter(({ message }) => !message.startsWith('tuture:'));
// Store all diff into .tuture/diff.json
const commits = logs.map(({ hash }) => hash);
const diffs = await git_1.storeDiff(commits);
const stepProms = logs.map(async ({ message, hash }) => {
const diff = diffs.filter((diff) => isCommitEqual(diff.commit, hash))[0];
const files = diff.diff;
return {
type: 'step',
id: randHex(8),
articleId: null,
commit: hash,
children: [
{
type: 'heading-two',
commit: hash,
id: randHex(8),
fixed: true,
children: [{ text: message }],
},
nodes_1.getEmptyExplain(),
...files.map((diffFile) => {
const display = ignoredFiles &&
!ignoredFiles.some((pattern) => micromatch_1.default.isMatch(diffFile.to, pattern));
return convertFile(hash, diffFile, display);
}),
nodes_1.getEmptyExplain(),
],
};
});
const steps = await Promise.all(stepProms);
return steps;
}
exports.makeSteps = makeSteps;
/**
* Merge previous and current steps. All previous explain will be kept.
* If any step is rebased out, it will be marked outdated and added to the end.
*/
function mergeSteps(prevSteps, currentSteps) {
const steps = currentSteps.map((currentStep) => {
const prevStep = prevSteps.filter((step) => isCommitEqual(step.commit, currentStep.commit))[0];
// If previous step with the same commit exists, copy it.
// Or just return the new step.
return prevStep || currentStep;
});
// Outdated steps are those not included in current git history.
const outdatedSteps = prevSteps
.filter((prevStep) => {
const currentStep = currentSteps.filter((step) => isCommitEqual(step.commit, prevStep.commit))[0];
return !currentStep;
})
.map((prevStep) => (Object.assign(Object.assign({}, prevStep), { outdated: true })));
return steps.concat(outdatedSteps);
}
exports.mergeSteps = mergeSteps;
/**
* Detect if tuture is initialized.
*/
async function checkInitStatus(nothrow = false) {
const isRepo = await git_1.git.checkIsRepo();
if (!isRepo) {
if (nothrow)
return false;
throw new Error(`Not in a git repository. Run ${chalk_1.default.bold('git init')} or ${chalk_1.default.bold('tuture init')} to initialize.`);
}
const { current: currentBranch } = await git_1.git.branchLocal();
if (!currentBranch) {
if (nothrow)
return false;
throw new Error('Current branch does not have any commits yet.');
}
if (fs_extra_1.default.existsSync(collection_1.collectionPath)) {
return true;
}
const branchExists = async () => {
return (await git_1.git.branch({ '-a': true })).all
.map((branch) => branch.split('/').slice(-1)[0])
.includes(constants_1.TUTURE_BRANCH);
};
if (await branchExists()) {
return true;
}
// Trying to update remote branches (time-consuming).
logger_1.default.log('info', 'Trying to update remote branches ...');
await git_1.git.remote(['update', '--prune']);
if (!(await branchExists())) {
if (nothrow)
return false;
throw new Error(`Tuture is not initialized. Run ${chalk_1.default.bold('tuture init')} to initialize.`);
}
return true;
}
exports.checkInitStatus = checkInitStatus;