diff --git a/package.json b/package.json index 338c6e6..2b35037 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "columnify": "^1.5.4", "inquirer": "^7.3.3", "lodash": "^4.17.21", + "node-emoji": "^1.11.0", "simple-git": "^2.47.0", "stringify-tree": "^1.1.1", "updeep": "^1.2.1", diff --git a/src/branches.js b/src/branches.js new file mode 100644 index 0000000..39630c5 --- /dev/null +++ b/src/branches.js @@ -0,0 +1,122 @@ +const Git = require("simple-git"); +const u = require("updeep"); +const _ = require("lodash"); +const fp = require("lodash/fp"); + +const log = require('./log'); + +const normalizeArray = (value) => + Array.isArray(value) ? value : [value].filter((x) => x); + +const initBranch = (config = {}) => ({ + upstream: [], + dependencies: [], + ...config, +}); + +const git = Git(); + +const currentBranch = _.once(() =>git + .raw("branch", "--show-current") + .then((r) => r.replace("\n", ""))); + +const branches = _.once(async () => { + const config = await git.listConfig("local").then(fp.get('all')); + + let tree = Object.entries(config).reduce( + (accum, [key, value]) => u.updateIn(key, value, accum), + {} + ); + + let branches = {}; + + for (const branch in tree.branch) { + const entry = tree.branch[branch]; + + if (!("mikado-upstream" in entry)) continue; + + branches[branch] = initBranch({ + name: branch, + upstream: normalizeArray(entry["mikado-upstream"]), + base: entry["mikado-base"], + done: !!entry["mikado-done"], + }); + } + + for (const branch of Object.values(branches)) { + for (const ups of branch["upstream"]) { + if (!branches[ups]) + branches[ups] = initBranch({ + name: ups, + upstream: [], + dependencies: [], + }); + + branches[ups].dependencies.push(branch.name); + } + } + + // include the merged info + for (const branch of Object.values(branches)) { + branch.contains = _.compact(await git.raw( 'branch', '--merged', branch.name ) + .then( r => r.replace( /[ *]/g, '' ) ) + .then( r => r.split("\n") )) + } + + const current = await currentBranch(); + + if (branches[current]) branches[current].current = true; + + return branches; +}); + +async function canBeDeleted() { + const allBranches = await branches(); + + return Object.values(allBranches).filter( + fp.get('done') + ).filter( ({name,upstream})=> + upstream.every( u => allBranches[u].contains.includes(name) ) + ).map(fp.get('name')); + +} + +async function canBeWorkedOn() { + const allBranches = await branches(); + + return Object.values(allBranches).filter( + ({done}) => !done + ).filter( ({name,dependencies})=> + dependencies.every( u => allBranches[u].done ) + ).map(fp.get('name')); + +} + +async function needsRebasing() { + const allBranches = await branches(); + + return Object.values(allBranches).filter( + fp.get('base') + ).filter( ({base,name})=> + !allBranches[name].contains.includes(base) + ).map(fp.get('name')); + +} + +async function needsMerging() { + const allBranches = await branches(); + + return Object.values(allBranches).filter( + fp.get('done') + ).flatMap( ({upstream,name})=> upstream.map( u => [ name, u ] ) ) + .filter( ([name,up]) => !allBranches[up].contains.includes(name) ) +} + +module.exports = { + currentBranch, + branches, + canBeDeleted, + canBeWorkedOn, + needsRebasing, + needsMerging, +}; diff --git a/src/commands/status.js b/src/commands/status.js index c228f30..6370c79 100644 --- a/src/commands/status.js +++ b/src/commands/status.js @@ -7,6 +7,9 @@ const u = require("updeep"); const columnify = require("columnify"); const chalk = require("chalk"); +const myGit = require('../branches'); +const log = require('../log'); + const iconFor = ( branch, branches ) => { if(branch.done ) return "✅"; @@ -18,60 +21,78 @@ const iconFor = ( branch, branches ) => { } -module.exports = async (yargs) => { - const config = (await Git().listConfig("local")).all; +const decorateCurrent = async branch => { + if( branch !== await myGit.currentBranch() ) return branch; - let tree = Object.entries(config).reduce( - (accum, [key, value]) => u.updateIn(key, value, accum), - {} + return chalk.underline.bold(branch); +} + +async function printNeedsRebasing() { + const needsRebasing = await myGit.needsRebasing(); + + if( !needsRebasing.length ) return false; + + log.subtitle(':arrow_heading_up: needs rebasing'); + const b = await Promise.all( + needsRebasing.map( decorateCurrent ) ); + log.info( "\t", b.join(', ') ); - const branches = {}; + return true; +} - const normalizeArray = (value) => - Array.isArray(value) ? value : [value].filter((x) => x); +async function printNeedsMerging() { + const needsMerging = await myGit.needsMerging(); - const initBranch = (config) => - defaults( - { - upstream: [], - dependencies: [], - }, - config + if(!needsMerging.length) return false; + + log.subtitle(':thumbsup: ready to be merged'); + log.info( "\t", needsMerging.map( x => x.join('->') ).join(', ') ); + + return true; +} + + +async function printActionables() { + + log.title(":point_right: What's next?"); + + let something = false; + const accumSomething = (value) => something ||= value; + + accumSomething( await printNeedsRebasing() ); + + accumSomething( await printNeedsMerging() ); + + const canBeDeleted = await myGit.canBeDeleted(); + + if( canBeDeleted.length ) { + something = true; + + log.subtitle(':recycle: branches that can be deleted'); + log.info( "\t", canBeDeleted.join(', ') ); + } + + const canBeWorkedOn = await myGit.canBeWorkedOn(); + + if( canBeWorkedOn.length ) { + something = true; + + log.subtitle(':hammer: ready to be worked on'); + const b = await Promise.all( + canBeWorkedOn.map( decorateCurrent ) ); - - const current = (await Git().raw("branch", "--show-current")).replace( - "\n", - "" - ); - - for (const branch in tree.branch) { - const entry = tree.branch[branch]; - - if (!("mikado-upstream" in entry)) continue; - - branches[branch] = initBranch({ - name: branch, - upstream: normalizeArray(entry["mikado-upstream"]), - base: entry["mikado-base"], - done: entry["mikado-done"], - }); + log.info( "\t", b.join(', ') ); } - for (const branch of Object.values(branches)) { - for (const ups of branch["upstream"]) { - if (!branches[ups]) - branches[ups] = initBranch({ - name: ups, - upstream: [], - dependencies: [], - }); - - branches[ups].dependencies.push(branch.name); - } + if(!something) { + log.info("nothing? :shrug:"); } - if (branches[current]) branches[current].current = true; +} + +module.exports = async () => { + const branches = await myGit.branches(); const depColor = (current) => async (dep) => { let color = branches[dep].done ? chalk.green : chalk.red; @@ -99,7 +120,7 @@ module.exports = async (yargs) => { return color(up); }; - console.log("\n=== Mikado status ===\n"); + log.title(":chopsticks: Mikado status\n"); const sorted = _.sortBy(Object.values(branches), ["current"]); @@ -126,4 +147,7 @@ module.exports = async (yargs) => { } } + + await printActionables(); + }; diff --git a/src/log.js b/src/log.js new file mode 100644 index 0000000..f9b8615 --- /dev/null +++ b/src/log.js @@ -0,0 +1,12 @@ +const emoji = require('node-emoji'); +const chalk = require("chalk"); + +function groomLog(...entries) { + console.log( ...(entries.map(emoji.emojify)) ); +} + +module.exports = { + info: groomLog, + title: title => groomLog("\n",chalk.blue.bold(title)), + subtitle: title => groomLog("\n",chalk.blue(title)), +}