Compare commits

...

5 Commits

8 changed files with 300 additions and 92 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ node_modules/
pnpm-debug.log pnpm-debug.log
pnpm-lock.yaml pnpm-lock.yaml
dist dist
git-mikado-*.tgz

14
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,14 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-merge-conflict
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.4.1" # Use the sha / tag you want to point at
hooks:
- id: prettier
additional_dependencies:
- prettier@2.4.1

3
bin/git-mikado.js Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../src/git-mikado.js').run();

View File

@ -1,8 +1,11 @@
{ {
"name": "git-mikado", "name": "git-mikado",
"version": "1.0.0", "version": "0.1.0",
"description": "", "description": "Tool using the Mikado method for git branch management",
"main": "index.js", "main": "src/index.js",
"bin": {
"git-mikado": "./bin/git-mikado.js"
},
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
@ -15,6 +18,7 @@
"columnify": "^1.5.4", "columnify": "^1.5.4",
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"node-emoji": "^1.11.0",
"simple-git": "^2.47.0", "simple-git": "^2.47.0",
"stringify-tree": "^1.1.1", "stringify-tree": "^1.1.1",
"updeep": "^1.2.1", "updeep": "^1.2.1",

122
src/branches.js Normal file
View File

@ -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,
};

View File

@ -1,103 +1,153 @@
const Git = require('simple-git'); const Git = require("simple-git");
const inquirer = require('inquirer'); const inquirer = require("inquirer");
const report = require('yurnalist'); const report = require("yurnalist");
const _ = require('lodash'); const _ = require("lodash");
const { defaults } = require('lodash/fp'); const { defaults } = require("lodash/fp");
const u = require('updeep'); const u = require("updeep");
const columnify = require('columnify'); const columnify = require("columnify");
const chalk = require('chalk'); const chalk = require("chalk");
module.exports = async (yargs) => { const myGit = require('../branches');
const config = (await Git().listConfig('local')).all; const log = require('../log');
let tree = Object.entries(config).reduce( const iconFor = ( branch, branches ) => {
(accum, [key,value]) => u.updateIn(key,value,accum), {} if(branch.done ) return "✅";
for( const dep of branch.dependencies ) {
if( !branches[dep].done ) return "⛔";
}
return "🙋";
}
const decorateCurrent = async branch => {
if( branch !== await myGit.currentBranch() ) return branch;
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({ if(!needsMerging.length) return false;
upstream: [],
dependencies: [],
},config);
const current = (await Git().raw('branch','--show-current')).replace("\n",''); log.subtitle(':thumbsup: ready to be merged');
log.info( "\t", needsMerging.map( x => x.join('->') ).join(', ') );
for( const branch in tree.branch ) { return true;
const entry = tree.branch[branch]; }
if( !('mikado-upstream' in entry) ) continue;
branches[branch] = initBranch({ async function printActionables() {
name: branch,
upstream: normalizeArray(entry['mikado-upstream']), log.title(":point_right: What's next?");
base: entry['mikado-base'],
done: entry['mikado-done'], 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(', ') );
} }
for ( const branch of Object.values(branches) ) { const canBeWorkedOn = await myGit.canBeWorkedOn();
for( const ups of branch['upstream'] ) {
if(!branches[ups]) branches[ups] = initBranch({
name: ups,
upstream: [],
dependencies: [],
});
branches[ups].dependencies.push(branch.name); if( canBeWorkedOn.length ) {
} something = true;
log.subtitle(':hammer: ready to be worked on');
const b = await Promise.all(
canBeWorkedOn.map( decorateCurrent )
);
log.info( "\t", b.join(', ') );
} }
if( branches[ current ] ) branches[current].current = true; if(!something) {
log.info("nothing? :shrug:");
const depColor = current => async ( dep ) => {
let color = branches[dep].done ? chalk.green: chalk.red;
let contains = await Git().raw('branch', '--contains', dep);
contains = contains.split("\n").map( x => x.trim().replace('* ','') );
if( contains.includes(current) )
color = color.underline;
return color(dep);
}
const upstreamColor = current => async ( up ) => {
let color = branches[up].done ? chalk.green: chalk.red;
let contains = await Git().raw('branch', '--contains', current);
contains = contains.split("\n").map( x => x.trim().replace('* ','') );
if( contains.includes(up) )
color = color.underline;
return color(up);
}
console.log( "\n=== Mikado status ===\n" );
const sorted = _.sortBy(Object.values(branches), ['current'])
for( const branch of sorted ) {
console.log( chalk.blue(branch.name), branch.current ? "(current branch)" : "", branch.done ? chalk.green.bold('done'):"" );
console.log( `\tbase: ${chalk.magenta(branch.base ?? '<none>')}` );
if(branch.upstream.length) {
const ups = await Promise.all(branch.upstream.map(upstreamColor(branch.name)));
console.log( "\tupstream:", ups.join(" ") );
}
if(branch.dependencies.length) {
const deps = await Promise.all(branch.dependencies.map(depColor(branch.name)));
console.log( "\tdependencies:", deps.join(" ") );
}
console.log("\n\n");
} }
} }
module.exports = async () => {
const branches = await myGit.branches();
const depColor = (current) => async (dep) => {
let color = branches[dep].done ? chalk.green : chalk.red;
let contains = await Git().raw("branch", "--contains", dep);
contains = contains
.split("\n")
.map((x) => x.trim().replace("* ", ""));
if (contains.includes(current)) color = color.underline;
return color(dep);
};
const upstreamColor = (current) => async (up) => {
let color = branches[up].done ? chalk.green : chalk.red;
let contains = await Git().raw("branch", "--contains", current);
contains = contains
.split("\n")
.map((x) => x.trim().replace("* ", ""));
if (contains.includes(up)) color = color.underline;
return color(up);
};
log.title(":chopsticks: Mikado status\n");
const sorted = _.sortBy(Object.values(branches), ["current"]);
for (const branch of sorted) {
console.log(
iconFor(branch,branches),
chalk.blue(branch.name),
branch.current ? "👷" : ' '
);
console.log(`\tbase: ${chalk.magenta(branch.base ?? "<none>")}`);
if (branch.upstream.length) {
const ups = await Promise.all(
branch.upstream.map(upstreamColor(branch.name))
);
console.log("\tupstream:", ups.join(" "));
}
if (branch.dependencies.length) {
const deps = await Promise.all(
branch.dependencies.map(depColor(branch.name))
);
console.log("\tdependencies:", deps.join(" "));
}
}
await printActionables();
};

10
src/git-mikado.js Normal file → Executable file
View File

@ -5,10 +5,12 @@ const status = require("./commands/status");
const done = require("./commands/done"); const done = require("./commands/done");
const upstream = require("./commands/upstream"); const upstream = require("./commands/upstream");
const { currentBranch } = require("./utils"); const utils = require("./utils");
currentBranch().then((currentBranch) => { module.exports.run = async () => {
yargs const currentBranch = await utils.currentBranch();
return yargs
.scriptName("git-mikado") .scriptName("git-mikado")
.showHelpOnFail(true) .showHelpOnFail(true)
.command("status", "show status of all mikado branches", status) .command("status", "show status of all mikado branches", status)
@ -74,4 +76,4 @@ currentBranch().then((currentBranch) => {
.help() .help()
.demandCommand(1, "") .demandCommand(1, "")
.strict().argv; .strict().argv;
}); }

12
src/log.js Normal file
View File

@ -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)),
}