UNPKG

tuture

Version:

Write tutorials from the future, with the power of Git and community.

201 lines (200 loc) 6.93 kB
"use strict"; 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;