Compare commits
9 Commits
f497fde8ad
...
main
Author | SHA1 | Date |
---|---|---|
Yanick Champoux | dd5b0124d0 | |
Yanick Champoux | f5af510d28 | |
Yanick Champoux | 5fc32f831a | |
Yanick Champoux | 4c1c4989b5 | |
Yanick Champoux | bba261c7c2 | |
Yanick Champoux | c3ae19e77c | |
Yanick Champoux | 0eb0840f10 | |
Yanick Champoux | 1f676d6b68 | |
Yanick Champoux | f3bfe0f905 |
|
@ -2,3 +2,4 @@ node_modules/
|
|||
pnpm-debug.log
|
||||
pnpm-lock.yaml
|
||||
dist
|
||||
git-mikado-*.tgz
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../src/git-mikado.js').run();
|
10
package.json
10
package.json
|
@ -1,8 +1,11 @@
|
|||
{
|
||||
"name": "git-mikado",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"version": "0.1.0",
|
||||
"description": "Tool using the Mikado method for git branch management",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"git-mikado": "./bin/git-mikado.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
@ -15,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",
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
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 isClean = () => git.status().then(({ files }) => !files.length);
|
||||
|
||||
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,
|
||||
isClean,
|
||||
};
|
|
@ -1,15 +1,28 @@
|
|||
const Git = require('simple-git');
|
||||
const inquirer = require('inquirer');
|
||||
const report = require('yurnalist');
|
||||
const _ = require('lodash');
|
||||
const { currentBranch } = require( '../utils');
|
||||
const Git = require("simple-git");
|
||||
const inquirer = require("inquirer");
|
||||
const report = require("yurnalist");
|
||||
const _ = require("lodash");
|
||||
const fp = require("lodash/fp");
|
||||
|
||||
const myGit = require("../branches");
|
||||
const log = require("../log");
|
||||
|
||||
module.exports = async (yargs) => {
|
||||
if (!(await myGit.isClean())) {
|
||||
log.error("workspace not clean, aborting");
|
||||
}
|
||||
|
||||
const current = await currentBranch();
|
||||
const current = await myGit.currentBranch();
|
||||
|
||||
console.log(`marking branch '${current}' as done`);
|
||||
log.success(`W00t! marking branch '${current}' as done\n`);
|
||||
|
||||
await Git().addConfig( `branch.${current}.mikado-done`, true );
|
||||
await Git().addConfig(`branch.${current}.mikado-done`, true);
|
||||
|
||||
}
|
||||
log.info(
|
||||
"upstreams to merge with:",
|
||||
await Git()
|
||||
.getConfig(`branch.${current}.mikado-upstream`)
|
||||
.then(fp.get("values"))
|
||||
.then((x) => x.join(", "))
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,103 +1,139 @@
|
|||
const Git = require('simple-git');
|
||||
const inquirer = require('inquirer');
|
||||
const report = require('yurnalist');
|
||||
const _ = require('lodash');
|
||||
const { defaults } = require('lodash/fp');
|
||||
const u = require('updeep');
|
||||
const columnify = require('columnify');
|
||||
const chalk = require('chalk');
|
||||
const Git = require("simple-git");
|
||||
const inquirer = require("inquirer");
|
||||
const report = require("yurnalist");
|
||||
const _ = require("lodash");
|
||||
const { defaults } = require("lodash/fp");
|
||||
const u = require("updeep");
|
||||
const columnify = require("columnify");
|
||||
const chalk = require("chalk");
|
||||
|
||||
module.exports = async (yargs) => {
|
||||
const config = (await Git().listConfig('local')).all;
|
||||
const myGit = require("../branches");
|
||||
const log = require("../log");
|
||||
|
||||
let tree = Object.entries(config).reduce(
|
||||
(accum, [key,value]) => u.updateIn(key,value,accum), {}
|
||||
);
|
||||
const iconFor = (branch, branches) => {
|
||||
if (branch.done) return "✅";
|
||||
|
||||
const branches = {};
|
||||
for (const dep of branch.dependencies) {
|
||||
if (!branches[dep].done) return "⛔";
|
||||
}
|
||||
|
||||
const normalizeArray = value => Array.isArray(value)?value: [value].filter(x=>x);
|
||||
return "🙋";
|
||||
};
|
||||
|
||||
const initBranch = config => defaults({
|
||||
upstream: [],
|
||||
dependencies: [],
|
||||
},config);
|
||||
const decorateCurrent = async (branch) => {
|
||||
if (branch !== (await myGit.currentBranch())) return branch;
|
||||
|
||||
const current = (await Git().raw('branch','--show-current')).replace("\n",'');
|
||||
return chalk.underline.bold(branch);
|
||||
};
|
||||
|
||||
for( const branch in tree.branch ) {
|
||||
const entry = tree.branch[branch];
|
||||
async function printNeedsRebasing() {
|
||||
const needsRebasing = await myGit.needsRebasing();
|
||||
|
||||
if( !('mikado-upstream' in entry) ) continue;
|
||||
if (!needsRebasing.length) return false;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if( branches[ current ] ) branches[current].current = true;
|
||||
|
||||
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");
|
||||
|
||||
}
|
||||
log.subtitle(":arrow_heading_up: needs rebasing");
|
||||
const b = await Promise.all(needsRebasing.map(decorateCurrent));
|
||||
log.info("\t", b.join(", "));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function printNeedsMerging() {
|
||||
const needsMerging = await myGit.needsMerging();
|
||||
|
||||
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 printNeedsMerging());
|
||||
|
||||
accumSomething(await printNeedsRebasing());
|
||||
|
||||
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));
|
||||
log.info("\t", b.join(", "));
|
||||
}
|
||||
|
||||
if (!something) {
|
||||
log.info("nothing? :shrug:");
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
|
|
|
@ -5,10 +5,12 @@ const status = require("./commands/status");
|
|||
const done = require("./commands/done");
|
||||
const upstream = require("./commands/upstream");
|
||||
|
||||
const { currentBranch } = require("./utils");
|
||||
const utils = require("./utils");
|
||||
|
||||
currentBranch().then((currentBranch) => {
|
||||
yargs
|
||||
module.exports.run = async () => {
|
||||
const currentBranch = await utils.currentBranch();
|
||||
|
||||
return yargs
|
||||
.scriptName("git-mikado")
|
||||
.showHelpOnFail(true)
|
||||
.command("status", "show status of all mikado branches", status)
|
||||
|
@ -74,4 +76,4 @@ currentBranch().then((currentBranch) => {
|
|||
.help()
|
||||
.demandCommand(1, "")
|
||||
.strict().argv;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
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)),
|
||||
error: (message) => groomLog(":skull:", chalk.red(message)),
|
||||
success: (message) => groomLog(":sparkles:", chalk.green(message)),
|
||||
};
|
Loading…
Reference in New Issue