| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import fs from 'fs' |
| | import path from 'path' |
| | import { execFileSync } from 'child_process' |
| |
|
| | import { program } from 'commander' |
| | import chalk from 'chalk' |
| | import walk from 'walk-sync' |
| | import yaml from 'js-yaml' |
| | import escapeStringRegexp from 'escape-string-regexp' |
| |
|
| | import fm from '@/frame/lib/frontmatter' |
| | import readFrontmatter from '@/frame/lib/read-frontmatter' |
| |
|
| | |
| | interface MoveOptions { |
| | verbose: boolean |
| | undo: boolean |
| | git: boolean |
| | } |
| |
|
| | type FileTuple = [string, string, string, string] |
| |
|
| | interface PositionInfo { |
| | childrenPosition: number |
| | childGroupPositions: number[][] |
| | } |
| |
|
| | |
| | const ROOT = process.env.ROOT || '.' |
| | const CONTENT_ROOT = path.resolve(path.join(ROOT, 'content')) |
| | const DATA_ROOT = path.resolve(path.join(ROOT, 'data')) |
| |
|
| | const REDIRECT_FROM_KEY = 'redirect_from' |
| | const CHILDREN_KEY = 'children' |
| | const CHILDGROUPS_KEY = 'childGroups' |
| |
|
| | program |
| | .description('Helps you move (rename) files or folders') |
| | .option('-v, --verbose', 'Verbose outputs') |
| | .option( |
| | '--no-git', |
| | "DON'T use 'git mv' and 'git commit' to move the file. Just regular file moves.", |
| | ) |
| | .option('--undo', 'Reverse of moving. I.e. moving it back. Only applies to the last run.') |
| | .argument('old', 'old file or folder name') |
| | .argument('new', 'new file or folder name') |
| | .parse(process.argv) |
| |
|
| | main(program.opts(), program.args) |
| |
|
| | async function main(opts: MoveOptions, nameTuple: string[]) { |
| | const { verbose, undo, git } = opts |
| | if (nameTuple.length !== 2) { |
| | console.error( |
| | chalk.red(`Must be exactly 2 file paths as arguments. Not ${nameTuple.length} arguments.`), |
| | ) |
| | process.exit(1) |
| | } |
| | const [old, new_] = nameTuple |
| | if (old === new_) { |
| | throw new Error('old == new') |
| | } |
| |
|
| | const uppercases = new_.match(/[A-Z]+/g) || [] |
| | if (uppercases.length > 0) { |
| | throw new Error(`Uppercase in file name not allowed ('${uppercases}')`) |
| | } |
| |
|
| | let oldPath = old |
| | let newPath = new_ |
| | if (undo) { |
| | oldPath = new_ |
| | newPath = old |
| | } else { |
| | oldPath = old |
| | newPath = new_ |
| | } |
| |
|
| | |
| | if (!fs.existsSync(oldPath)) { |
| | console.error(chalk.red(`${oldPath} does not exist.`)) |
| | process.exit(1) |
| | } |
| |
|
| | let isFolder = fs.lstatSync(oldPath).isDirectory() |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (undo) { |
| | if (isFolder) { |
| | const wouldBe = path.join(oldPath, path.basename(newPath)) |
| | |
| | |
| | if (fs.existsSync(wouldBe) && !fs.lstatSync(wouldBe).isDirectory()) { |
| | isFolder = false |
| | oldPath = wouldBe |
| | } |
| | } |
| | } else { |
| | if (!isFolder) { |
| | if (fs.existsSync(newPath) && fs.lstatSync(newPath).isDirectory()) { |
| | newPath = path.join(newPath, path.basename(oldPath)) |
| | } |
| | } |
| | } |
| |
|
| | const currentBranchName = getCurrentBranchName(verbose) |
| | if (currentBranchName === 'main' && git) { |
| | console.error(chalk.red("Cannot proceed because you're on the 'main' branch.")) |
| | console.error("This command will execute 'git mv ...' and 'git commit ...'") |
| | console.error('Create a new dedicated branch instead, first, for this move.\n') |
| | process.exit(2) |
| | } |
| |
|
| | |
| | validateFileInputs(oldPath, newPath, isFolder) |
| |
|
| | const oldHref = makeHref(CONTENT_ROOT, undo ? newPath : oldPath) |
| | const newHref = makeHref(CONTENT_ROOT, undo ? oldPath : newPath) |
| |
|
| | if (isFolder) { |
| | |
| | const indexFilePath = path.join(oldPath, 'index.md') |
| | if (!fs.existsSync(indexFilePath)) { |
| | throw new Error(`${oldPath} does not have an index.md file`) |
| | } |
| | |
| | |
| | const files = findFilesInFolder(oldPath, newPath, opts) |
| |
|
| | |
| | if (undo) { |
| | undoFolder(oldPath, newPath, files, opts) |
| | } else { |
| | moveFolder(oldPath, newPath, files, opts) |
| | } |
| |
|
| | addToChildren(newPath, removeFromChildren(oldPath, opts), opts) |
| |
|
| | if (undo) { |
| | undoFiles(files, false, opts) |
| | } else { |
| | editFiles(files, false, opts) |
| | } |
| | } else { |
| | |
| | const files: FileTuple[] = [[oldPath, newPath, oldHref, newHref]] |
| |
|
| | |
| | moveFiles(files, opts) |
| |
|
| | if (undo) { |
| | undoFiles(files, true, opts) |
| | } else { |
| | editFiles(files, true, opts) |
| | } |
| | } |
| |
|
| | |
| | |
| | changeFeaturedLinks(oldHref, newHref) |
| |
|
| | |
| | changeHomepageLinks(oldHref, newHref, verbose) |
| |
|
| | if (!undo) { |
| | if (verbose) { |
| | console.log( |
| | chalk.yellow( |
| | 'To undo (reverse) what you just did, run the same exact command but with --undo added to the end', |
| | ), |
| | ) |
| | } |
| | } |
| | } |
| |
|
| | function validateFileInputs(oldPath: string, newPath: string, isFolder: boolean) { |
| | if (isFolder) { |
| | |
| | |
| | const [oldBase, oldName] = splitDirectory(oldPath) |
| | const [newBase] = splitDirectory(newPath) |
| | if (oldBase !== newBase && !existsAndIsDirectory(newBase)) { |
| | console.error( |
| | chalk.red( |
| | `When moving a directory, both bases need to be the same. '${oldBase}' != '${newBase}'`, |
| | ), |
| | ) |
| | console.warn(chalk.yellow(`Only the name (e.g. '${oldName}') can be different.`)) |
| | process.exit(1) |
| | } |
| | } |
| |
|
| | if (!path.resolve(newPath).startsWith(CONTENT_ROOT)) { |
| | const relativeRoot = path.relative('.', CONTENT_ROOT) |
| | console.error(chalk.red(`New path does not start with '${relativeRoot}'`)) |
| | process.exit(1) |
| | } |
| |
|
| | if (!fs.existsSync(oldPath)) { |
| | console.error(chalk.red(`${oldPath} does not resolve to an existing file or a folder`)) |
| | process.exit(1) |
| | } |
| | if (path.basename(oldPath) === 'index.md') { |
| | console.error( |
| | chalk.red(`File path can't be 'index.md'. Refer to it by its foldername instead.`), |
| | ) |
| | process.exit(1) |
| | } |
| | if (path.basename(newPath) === 'index.md') { |
| | console.error( |
| | chalk.red(`File path can't be 'index.md'. Refer to it by its foldername instead.`), |
| | ) |
| | process.exit(1) |
| | } |
| |
|
| | if (fs.existsSync(newPath)) { |
| | console.error(chalk.red(`Can't move to a ${isFolder ? 'folder' : 'file'} that already exists.`)) |
| | process.exit(1) |
| | } |
| |
|
| | if (/\s/.test(newPath)) { |
| | throw new Error(`New path (${newPath}) can't contain whitespace`) |
| | } |
| | } |
| |
|
| | function existsAndIsDirectory(directory: string) { |
| | return fs.existsSync(directory) && fs.lstatSync(directory).isDirectory() |
| | } |
| |
|
| | function splitDirectory(directory: string) { |
| | return [path.dirname(directory), path.basename(directory)] |
| | } |
| |
|
| | function findFilesInFolder(oldPath: string, newPath: string, opts: MoveOptions): FileTuple[] { |
| | const { undo, verbose } = opts |
| | const files: FileTuple[] = [] |
| | const allFiles = walk(oldPath, { includeBasePath: true, directories: false }) |
| | for (const filePath of allFiles) { |
| | const newFilePath = filePath.replace(oldPath, newPath) |
| | const oldHref = makeHref(CONTENT_ROOT, undo ? newFilePath : filePath) |
| | const newHref = makeHref(CONTENT_ROOT, undo ? filePath : newFilePath) |
| | files.push([filePath, newFilePath, oldHref, newHref]) |
| | } |
| | if (verbose) { |
| | console.log(chalk.yellow(`Found ${files.length} files within ${oldPath}`)) |
| | } |
| | return files |
| | } |
| |
|
| | function makeHref(root: string, filePath: string) { |
| | const nameSplit = path.relative(root, filePath).split(path.sep) |
| | if (nameSplit.slice(-1)[0] === 'index.md') { |
| | nameSplit.pop() |
| | } else { |
| | nameSplit.push(nameSplit.pop()!.replace(/\.md$/, '')) |
| | } |
| | return `/${nameSplit.join('/')}` |
| | } |
| |
|
| | function moveFolder(oldPath: string, newPath: string, files: FileTuple[], opts: MoveOptions) { |
| | const { verbose, git: useGit } = opts |
| | if (useGit) { |
| | let cmd = ['mv', oldPath, newPath] |
| | if (verbose) { |
| | console.log(`git mv command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | execFileSync('git', cmd) |
| |
|
| | cmd = ['commit', '-a', '-m', `renamed ${files.length} files`] |
| | if (verbose) { |
| | console.log(`git commit command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | execFileSync('git', cmd) |
| | } else { |
| | fs.renameSync(oldPath, newPath) |
| | if (verbose) { |
| | console.log(`Renamed folder ${chalk.bold(oldPath)} to ${chalk.bold(newPath)}`) |
| | } |
| | } |
| | } |
| |
|
| | function undoFolder(oldPath: string, newPath: string, files: FileTuple[], opts: MoveOptions) { |
| | const { verbose, git: useGit } = opts |
| |
|
| | if (useGit) { |
| | let cmd = ['mv', oldPath, newPath] |
| | execFileSync('git', cmd) |
| | if (verbose) { |
| | console.log(`git mv command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| |
|
| | cmd = ['commit', '-a', '-m', `renamed ${files.length} files`] |
| | execFileSync('git', cmd) |
| | if (verbose) { |
| | console.log(`git commit command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | } else { |
| | fs.renameSync(oldPath, newPath) |
| | if (verbose) { |
| | console.log(`Renamed folder ${chalk.bold(oldPath)} to ${chalk.bold(newPath)}`) |
| | } |
| | } |
| | } |
| |
|
| | function getBasename(fileOrDirectory: string) { |
| | |
| | |
| |
|
| | if (fileOrDirectory.endsWith('index.md')) { |
| | return path.basename(path.dirname(fileOrDirectory)) |
| | } |
| | if (fileOrDirectory.endsWith('.md')) { |
| | return path.basename(fileOrDirectory).replace(/\.md$/, '') |
| | } |
| | return path.basename(fileOrDirectory) |
| | } |
| |
|
| | function removeFromChildren(oldPath: string, opts: MoveOptions): PositionInfo { |
| | const { verbose } = opts |
| |
|
| | const parentFilePath = path.join(path.dirname(oldPath), 'index.md') |
| | const fileContent = fs.readFileSync(parentFilePath, 'utf-8') |
| | const { content, data } = readFrontmatter(fileContent) |
| | const oldName = getBasename(oldPath) |
| |
|
| | let childrenPosition = -1 |
| | if (data && CHILDREN_KEY in data) { |
| | data[CHILDREN_KEY] = data[CHILDREN_KEY].filter((entry: any, i: number) => { |
| | if (entry === oldName || entry === `/${oldName}`) { |
| | childrenPosition = i |
| | return false |
| | } |
| | return true |
| | }) |
| | if (data[CHILDREN_KEY].length === 0) { |
| | delete data[CHILDREN_KEY] |
| | } |
| | } |
| |
|
| | const childGroupPositions: number[][] = [] |
| |
|
| | const childGroups = (data && data[CHILDGROUPS_KEY]) || [] |
| | for (let i = 0; i < childGroups.length; i++) { |
| | const group = childGroups[i] |
| | if (group.children) { |
| | group.children = group.children.filter((entry: any, j: number) => { |
| | if (entry === oldName || entry === `/${oldName}`) { |
| | childGroupPositions.push([i, j]) |
| | return false |
| | } |
| | return true |
| | }) |
| | } |
| | } |
| |
|
| | if (data) { |
| | fs.writeFileSync( |
| | parentFilePath, |
| | readFrontmatter.stringify(content, data, { lineWidth: 10000 } as any), |
| | 'utf-8', |
| | ) |
| | } |
| | if (verbose) { |
| | console.log(`Removed 'children' (${oldName}) key in ${parentFilePath}`) |
| | } |
| |
|
| | return { childrenPosition, childGroupPositions } |
| | } |
| |
|
| | function addToChildren(newPath: string, positions: PositionInfo, opts: MoveOptions) { |
| | const { verbose } = opts |
| | const parentFilePath = path.join(path.dirname(newPath), 'index.md') |
| | const fileContent = fs.readFileSync(parentFilePath, 'utf-8') |
| | const { content, data } = readFrontmatter(fileContent) |
| | const newName = getBasename(newPath) |
| |
|
| | const { childrenPosition, childGroupPositions } = positions |
| | if (childrenPosition > -1 && data) { |
| | const children = data[CHILDREN_KEY] || [] |
| | let prefix = '' |
| | if (children.every((entry: any) => entry.startsWith('/'))) { |
| | prefix += '/' |
| | } |
| | if (childrenPosition > -1 && childrenPosition < children.length) { |
| | children.splice(childrenPosition, 0, prefix + newName) |
| | } else { |
| | children.push(prefix + newName) |
| | } |
| | data[CHILDREN_KEY] = children |
| | } |
| |
|
| | if (data && CHILDGROUPS_KEY in data) { |
| | for (const [groupIndex, groupChildPosition] of childGroupPositions) { |
| | if (groupIndex < data[CHILDGROUPS_KEY].length) { |
| | const group = data[CHILDGROUPS_KEY][groupIndex] |
| | if (groupChildPosition < group.children.length) { |
| | group.children.splice(groupChildPosition, 0, newName) |
| | } else { |
| | group.children.push(newName) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | if (data) { |
| | fs.writeFileSync( |
| | parentFilePath, |
| | readFrontmatter.stringify(content, data, { lineWidth: 10000 } as any), |
| | 'utf-8', |
| | ) |
| | } |
| | if (verbose) { |
| | console.log(`Added 'children' (${newName}) key in ${parentFilePath}`) |
| | } |
| | } |
| |
|
| | function moveFiles(files: FileTuple[], opts: MoveOptions) { |
| | const { verbose, git: useGit } = opts |
| | |
| | for (const [oldPath] of files) { |
| | const fileContent = fs.readFileSync(oldPath, 'utf-8') |
| | const { errors } = fm(fileContent, { filepath: oldPath }) |
| | for (let i = 0; i < errors.length; i++) { |
| | const error = errors[i] |
| | if (!i) console.warn(chalk.yellow(`Error parsing file (${oldPath}) frontmatter:`)) |
| | console.error(`${chalk.red(error.message)}: ${chalk.yellow(error.reason)}`) |
| | } |
| | if (errors.length > 0) throw new Error('There were more than 0 parse errors') |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | for (const [oldPath, newPath] of files) { |
| | if (verbose) { |
| | console.log(`Moving ${chalk.bold(oldPath)} to ${chalk.bold(newPath)}`) |
| | } |
| |
|
| | if (useGit) { |
| | const cmd = ['mv', oldPath, newPath] |
| | execFileSync('git', cmd) |
| | if (verbose) { |
| | console.log(`git mv command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | } else { |
| | fs.renameSync(oldPath, newPath) |
| | if (verbose) { |
| | console.log(`Renamed ${chalk.bold(oldPath)} to ${chalk.bold(newPath)}`) |
| | } |
| | } |
| | } |
| |
|
| | if (useGit) { |
| | const cmd = ['commit', '-a', '-m', `renamed ${files.length} files`] |
| | execFileSync('git', cmd) |
| | if (verbose) { |
| | console.log(`git commit command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | } |
| | } |
| |
|
| | function editFiles(files: FileTuple[], updateParent: boolean, opts: MoveOptions) { |
| | const { verbose, git: useGit } = opts |
| |
|
| | |
| | |
| | |
| | |
| | for (const [oldPath, newPath, oldHref, newHref] of files) { |
| | const fileContent = fs.readFileSync(newPath, 'utf-8') |
| | const { content, data } = readFrontmatter(fileContent) |
| | if (!data) continue |
| | if (!(REDIRECT_FROM_KEY in data)) { |
| | data[REDIRECT_FROM_KEY] = [] |
| | } |
| | data[REDIRECT_FROM_KEY].push(oldHref) |
| | fs.writeFileSync( |
| | newPath, |
| | readFrontmatter.stringify(content, data, { lineWidth: 10000 } as any), |
| | 'utf-8', |
| | ) |
| | if (verbose) { |
| | console.log(`Added ${oldHref} to 'redirects_from' in ${newPath}`) |
| | } |
| |
|
| | if (updateParent) { |
| | addToChildren(newPath, removeFromChildren(oldPath, opts), opts) |
| | } |
| |
|
| | |
| | for (const filePath of findInLearningTracks(oldHref)) { |
| | changeLearningTracks(filePath, oldHref, newHref) |
| | if (verbose) { |
| | console.log(`Updated learning tracks in ${filePath}`) |
| | } |
| | } |
| | } |
| |
|
| | |
| | if (files.length > 0) { |
| | const filePaths = files.map(([, newPath]) => newPath) |
| | try { |
| | const cmd = ['run', 'add-content-type', '--', '--paths', ...filePaths] |
| | const result = execFileSync('npm', cmd, { cwd: process.cwd(), encoding: 'utf8' }) as any |
| | if (result.trim()) { |
| | console.log(result.trim()) |
| | } |
| | } catch (error: any) { |
| | console.warn(`Warning: Failed to add contentType frontmatter: ${error.message}`) |
| | } |
| | } |
| |
|
| | if (useGit) { |
| | const cmd = [ |
| | 'commit', |
| | '-a', |
| | '-m', |
| | `set ${REDIRECT_FROM_KEY} and contentType on ${files.length} files`, |
| | ] |
| | execFileSync('git', cmd) |
| | if (verbose) { |
| | console.log(`git commit command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | } |
| | } |
| |
|
| | function undoFiles(files: FileTuple[], updateParent: boolean, opts: MoveOptions) { |
| | const { verbose, git: useGit } = opts |
| |
|
| | |
| | for (const [oldPath, newPath, oldHref, newHref] of files) { |
| | const fileContent = fs.readFileSync(newPath, 'utf-8') |
| | const { content, data } = readFrontmatter(fileContent) |
| | if (!data) continue |
| |
|
| | data[REDIRECT_FROM_KEY] = (data[REDIRECT_FROM_KEY] || []).filter( |
| | (entry: any) => entry !== oldHref, |
| | ) |
| | if (data[REDIRECT_FROM_KEY].length === 0) { |
| | delete data[REDIRECT_FROM_KEY] |
| | } |
| |
|
| | fs.writeFileSync( |
| | newPath, |
| | readFrontmatter.stringify(content, data, { lineWidth: 10000 } as any), |
| | 'utf-8', |
| | ) |
| | if (updateParent) { |
| | addToChildren(newPath, removeFromChildren(oldPath, opts), opts) |
| | } |
| |
|
| | |
| | for (const filePath of findInLearningTracks(newHref)) { |
| | changeLearningTracks(filePath, newHref, oldHref) |
| | if (verbose) { |
| | console.log(`Updated learning tracks in ${filePath}`) |
| | } |
| | } |
| | } |
| | if (useGit) { |
| | const cmd = ['commit', '-a', '-m', `unset ${REDIRECT_FROM_KEY} on ${files.length} files`] |
| | execFileSync('git', cmd) |
| | if (verbose) { |
| | console.log(`git commit command: ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | } |
| | } |
| |
|
| | function findInLearningTracks(href: string) { |
| | const allFiles: string[] = walk(path.join(DATA_ROOT, 'learning-tracks'), { |
| | globs: ['*.yml'], |
| | includeBasePath: true, |
| | directories: false, |
| | }) |
| | const found: string[] = [] |
| | for (const filePath of allFiles) { |
| | const tracks = yaml.load(fs.readFileSync(filePath, 'utf-8')) as Record< |
| | string, |
| | { guides?: string[] } |
| | > |
| |
|
| | if ( |
| | Object.values(tracks).find((track) => { |
| | const guides = track.guides || [] |
| | return guides.includes(href) |
| | }) |
| | ) { |
| | found.push(filePath) |
| | } |
| | } |
| | return found |
| | } |
| |
|
| | function changeLearningTracks(filePath: string, oldHref: string, newHref: string) { |
| | |
| | |
| | const regex = new RegExp(`- ${oldHref}$`, 'gm') |
| | const oldContent = fs.readFileSync(filePath, 'utf-8') |
| | const newContent = oldContent.replace(regex, `- ${newHref}`) |
| | fs.writeFileSync(filePath, newContent, 'utf-8') |
| | } |
| |
|
| | function changeHomepageLinks(oldHref: string, newHref: string, verbose: boolean) { |
| | |
| | |
| | |
| | const homepageOldHref = oldHref.replace('/', '') |
| | const homepageNewHref = newHref.replace('/', '') |
| | const escapedHomepageOldHref = escapeStringRegexp(homepageOldHref) |
| | const regex = new RegExp(`- ${escapedHomepageOldHref}$`, 'gm') |
| | const homepage = path.join(CONTENT_ROOT, 'index.md') |
| | const oldContent = fs.readFileSync(homepage, 'utf-8') |
| | const newContent = oldContent.replace(regex, `- ${homepageNewHref}`) |
| | if (oldContent !== newContent) { |
| | fs.writeFileSync(homepage, newContent, 'utf-8') |
| | if (verbose) console.log(`Updated homepage links`) |
| | } |
| | } |
| |
|
| | function changeFeaturedLinks(oldHref: string, newHref: string): void { |
| | const allFiles = walk(CONTENT_ROOT, { |
| | globs: ['**/*.md'], |
| | includeBasePath: true, |
| | directories: false, |
| | }).filter((file) => !file.includes('README.md')) |
| |
|
| | const regex = new RegExp(`(^|%} )${escapeStringRegexp(oldHref)}($| {%)`) |
| |
|
| | for (const file of allFiles) { |
| | let changed = false |
| | const fileContent = fs.readFileSync(file, 'utf-8') |
| | const { content, data } = readFrontmatter(fileContent) |
| | if (!data) continue |
| | const featuredLinks = data.featuredLinks || {} |
| | for (const [key, entries] of Object.entries(featuredLinks) as [string, string[]][]) { |
| | if (key === 'popularHeading') { |
| | continue |
| | } |
| | for (let i = 0; i < entries.length; i++) { |
| | const entry = entries[i] |
| | if (regex.test(entry)) { |
| | entries[i] = entry.replace(regex, `${newHref}$1`) |
| | changed = true |
| | } |
| | } |
| | } |
| |
|
| | if (changed) { |
| | fs.writeFileSync( |
| | file, |
| | readFrontmatter.stringify(content, data, { lineWidth: 10000 } as any), |
| | 'utf-8', |
| | ) |
| | } |
| | } |
| | } |
| |
|
| | function getCurrentBranchName(verbose = false) { |
| | const cmd = ['branch', '--show-current'] |
| | const o = execFileSync('git', cmd) |
| | if (verbose) { |
| | console.log(`git commit command: git ${chalk.grey(cmd.join(' '))}`) |
| | } |
| | return o.toString().trim() |
| | } |
| |
|