You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
473 lines
15 KiB
473 lines
15 KiB
import chalk from "chalk";
|
|
import inquirer from "inquirer";
|
|
import { DatePrompt } from "./libs/date_prompt.mjs";
|
|
import Conf from "conf";
|
|
import { program } from "commander";
|
|
import { homedir, EOL } from "os";
|
|
import {
|
|
existsSync,
|
|
readFileSync,
|
|
appendFileSync,
|
|
writeFileSync,
|
|
readdirSync,
|
|
unlinkSync,
|
|
} from "fs";
|
|
import { join, sep, dirname } from "path";
|
|
import moment from "moment";
|
|
import { cwd } from "process";
|
|
import { promisify } from "util";
|
|
import { exec } from "child_process";
|
|
|
|
const execp = promisify(exec);
|
|
|
|
inquirer.registerPrompt("date", DatePrompt);
|
|
|
|
program.version(process.env.npm_package_version);
|
|
program
|
|
.option("-c, --configure", "run configuration mode")
|
|
.option("-d, --daemon", "run commit mode")
|
|
.option("-a, --apply", "run apply all pending patches mode");
|
|
program.parse(process.argv);
|
|
const program_options = program.opts();
|
|
|
|
const config = new Conf();
|
|
|
|
const home = homedir();
|
|
|
|
// this is so fucking cursed lmaooo
|
|
const processDirectory = cwd();
|
|
const pathSplit = (path) =>
|
|
path
|
|
.split(sep)
|
|
.map((dir, currentIndex, arr) => {
|
|
let result = "";
|
|
for (let index = 0; index <= currentIndex; index++) {
|
|
// could probably be recursed but brain melty rn
|
|
result = join(result, arr[index]);
|
|
}
|
|
return result;
|
|
})
|
|
.splice(1) // first value will always be drive root we dont care about it (probably ?)
|
|
.map((v) => `${sep}${v}`); // dont even worry about it
|
|
|
|
const detectGitRepository = (paths) =>
|
|
paths.reduce(
|
|
(previous, current, index) => {
|
|
if (previous.found) {
|
|
return previous;
|
|
}
|
|
const exists = existsSync(join(current, ".git"));
|
|
return { index, found: exists };
|
|
},
|
|
{ index: -1, found: false },
|
|
);
|
|
|
|
(async () => {
|
|
console.log(`welcome to ${chalk.bold.red("git-scheduler")}${EOL}`);
|
|
|
|
// TODO: make this smarter
|
|
if (
|
|
(program_options.daemon && program_options.apply) ||
|
|
(program_options.daemon && program_options.configure) ||
|
|
(program_options.configure && program_options.apply) ||
|
|
(program_options.daemon &&
|
|
program_options.apply &&
|
|
program_options.configure)
|
|
) {
|
|
console.log(
|
|
`only one mode can be active at ${chalk.bold.red("the same time")}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// TODO: add a push config option and cli toggle
|
|
// configuration
|
|
const already_ran_once = config.get("already_ran_once");
|
|
if (!already_ran_once || program_options.configure) {
|
|
console.log(
|
|
`it seems that this is ${chalk.red(
|
|
"your first",
|
|
)} time running this utility${EOL}`,
|
|
);
|
|
|
|
// gitignore
|
|
console.log(
|
|
`git-scheduler handles ${chalk.red(
|
|
".git-scheduler",
|
|
)} files to configure repositories individually`,
|
|
);
|
|
console.log(
|
|
`it is ${chalk.red(
|
|
"highly recommended",
|
|
)} that you add this file to your global .gitignore to not commit and push it`,
|
|
);
|
|
const { first_run_gitignore } = await inquirer.prompt([
|
|
{
|
|
type: "confirm",
|
|
name: "first_run_gitignore",
|
|
message: `would you like to add the ${chalk.bold.red(
|
|
".git-scheduler",
|
|
)} config file to your global ${chalk.bold.red(".gitignore")} ?`,
|
|
},
|
|
]);
|
|
const gitignoreTemplate = `${EOL}#########################################${EOL}# automatically appended by git-scheduler${EOL}.git-scheduler${EOL}#########################################${EOL}`;
|
|
if (first_run_gitignore) {
|
|
const globalGitignore = join(home, ".gitignore");
|
|
if (existsSync(globalGitignore)) {
|
|
const content = readFileSync(globalGitignore, "utf-8");
|
|
const contentArray = content.split(EOL);
|
|
const found = contentArray.indexOf(".git-scheduler");
|
|
if (found !== -1) {
|
|
console.log(".git-scheduler already ignored, skipping");
|
|
} else {
|
|
console.log("appending .git-scheduler to global .gitignore file");
|
|
const result = appendFileSync(globalGitignore, gitignoreTemplate);
|
|
}
|
|
} else {
|
|
console.log("could not find global .gitignore file, creating it");
|
|
const result = writeFileSync(globalGitignore, gitignoreTemplate);
|
|
}
|
|
}
|
|
|
|
// work hours
|
|
let shouldAskForWorkHours = true;
|
|
if (
|
|
config.get("first_run_work_hours_start") &&
|
|
config.get("first_run_work_hours_start")
|
|
) {
|
|
console.log(
|
|
`${EOL}you already have some work hours setup, starting at ${chalk.red(
|
|
moment(config.get("first_run_work_hours_start")).format("hh:mm A"),
|
|
)}, and ending at ${chalk.red(
|
|
moment(config.get("first_run_work_hours_end")).format("hh:mm A"),
|
|
)}`,
|
|
);
|
|
const { reset_hours } = await inquirer.prompt([
|
|
{
|
|
type: "confirm",
|
|
name: "reset_hours",
|
|
message: `would you like to reconfigure the ${chalk.bold.red(
|
|
"work hours",
|
|
)} ?`,
|
|
default: false,
|
|
},
|
|
]);
|
|
|
|
shouldAskForWorkHours = reset_hours;
|
|
}
|
|
if (shouldAskForWorkHours) {
|
|
console.log(
|
|
`${EOL}now lets setup your ${chalk.red(
|
|
"work hours",
|
|
)} to know when the commits should be made`,
|
|
);
|
|
console.log(
|
|
`you can use your ${chalk.red("arrow keys")} to set the times`,
|
|
);
|
|
const { first_run_work_hours_start, first_run_work_hours_end } =
|
|
await inquirer.prompt([
|
|
{
|
|
type: "date",
|
|
name: "first_run_work_hours_start",
|
|
message: `at what time does your work ${chalk.bold.red("start")} ?`,
|
|
format: { month: undefined, year: undefined, day: undefined },
|
|
},
|
|
{
|
|
type: "date",
|
|
name: "first_run_work_hours_end",
|
|
message: `at what time does your work ${chalk.bold.red("end")} ?`,
|
|
format: { month: undefined, year: undefined, day: undefined },
|
|
},
|
|
]);
|
|
|
|
config.set("first_run_work_hours_start", first_run_work_hours_start);
|
|
config.set("first_run_work_hours_end", first_run_work_hours_end);
|
|
}
|
|
console.log("work hours configured");
|
|
|
|
// value generation rate
|
|
// TODO: for now we ask how many lines we want to push in a day but thats probably not the best idea
|
|
console.log(
|
|
`${EOL}we will now configure the ${chalk.red(
|
|
"rate",
|
|
)} at which the commits should be made`,
|
|
);
|
|
console.log(
|
|
`this takes into account the ${chalk.red(
|
|
"size",
|
|
)} of each individual commit`,
|
|
);
|
|
const { first_run_generation_rate } = await inquirer.prompt([
|
|
{
|
|
type: "number",
|
|
name: "first_run_generation_rate",
|
|
message: `how fast should we be ${chalk.bold.red(
|
|
"generating value",
|
|
)} (lines of code per day) ?`,
|
|
default: config.get("first_run_generation_rate") || 100,
|
|
},
|
|
]);
|
|
config.set("first_run_generation_rate", first_run_generation_rate);
|
|
console.log("generation rate configured");
|
|
config.set("already_ran_once", true);
|
|
if (program_options.configure) process.exit(0);
|
|
}
|
|
|
|
// main logic
|
|
// check if current directory is git repo
|
|
const paths = pathSplit(processDirectory);
|
|
const detected = detectGitRepository(paths);
|
|
|
|
if (detected.found) {
|
|
const path = paths[detected.index];
|
|
console.log(`detected git repository ${chalk.bold.red(path)}`);
|
|
const { detected_git_repo } = await inquirer.prompt([
|
|
{
|
|
type: "confirm",
|
|
name: "detected_git_repo",
|
|
message: `is this ${chalk.bold.red("correct")} ?`,
|
|
},
|
|
]);
|
|
if (!detected_git_repo) {
|
|
console.log("aborting");
|
|
process.exit(1);
|
|
}
|
|
|
|
// check if it contains a gitscheduler file and parse it as options
|
|
const possibleConfigOverridePath = join(path, ".git-scheduler");
|
|
const hasGitSchedulerConfig = existsSync(possibleConfigOverridePath);
|
|
|
|
let options = {
|
|
start: config.get("first_run_work_hours_start"),
|
|
end: config.get("first_run_work_hours_end"),
|
|
rate: config.get("first_run_generation_rate"),
|
|
};
|
|
|
|
if (hasGitSchedulerConfig) {
|
|
console.log(
|
|
`${EOL}.git-scheduler configuration file ${chalk.bold.red("detected")}`,
|
|
);
|
|
const configContents = JSON.parse(
|
|
readFileSync(possibleConfigOverridePath, "utf-8"),
|
|
);
|
|
options = { ...options, ...configContents };
|
|
console.log(
|
|
`repository specific configuration ${chalk.bold.red(
|
|
"overrides",
|
|
)} applied`,
|
|
);
|
|
} else {
|
|
console.log(
|
|
`${EOL}.git-scheduler configuration file ${chalk.bold.red(
|
|
"not present",
|
|
)}, using defaults`,
|
|
);
|
|
}
|
|
|
|
console.log();
|
|
// console.log(`current ${chalk.bold.red("configuration")} is`);
|
|
// console.log(`${JSON.stringify(options, 0, 2)}${EOL}`);
|
|
|
|
async function waitUntil(time) {
|
|
return await new Promise((resolve) => {
|
|
const interval = setInterval(() => {
|
|
if (moment().isAfter(moment(time))) {
|
|
resolve("foo");
|
|
clearInterval(interval);
|
|
}
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
// daemon mode
|
|
if (program_options.daemon) {
|
|
const configDir = dirname(config.path);
|
|
const uid = config.get(path);
|
|
if (!uid) {
|
|
console.log(
|
|
`current repository ${chalk.bold.red("not found")} in configuration`,
|
|
);
|
|
console.log(
|
|
`assuming ${chalk.bold.red(
|
|
"no patches",
|
|
)} to apply or a ${chalk.bold.red("corrupt")} configuration`,
|
|
);
|
|
console.log(
|
|
`see ${chalk.bold.red(
|
|
configDir,
|
|
)} directly if this is not supposed to happen`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const files = readdirSync(configDir);
|
|
const patches = files
|
|
.filter((file) => file.includes(`patch_${uid}`))
|
|
.sort()
|
|
.map((patchFile) => join(configDir, patchFile));
|
|
|
|
if (patches.length === 0) {
|
|
console.log("no patches to apply");
|
|
process.exit(1);
|
|
}
|
|
|
|
for (const patch of patches) {
|
|
const patchContent = readFileSync(patch, "utf-8");
|
|
const lines = patchContent.split(EOL);
|
|
|
|
// TODO: improve magic math part
|
|
const ratio = lines.length / options.rate;
|
|
console.log(
|
|
`this patch represents ${chalk.bold.red(
|
|
Math.round(ratio * 100),
|
|
)}% of daily code rate`,
|
|
);
|
|
|
|
// TODO: add a value to config to say how much time was left over at end of day and add it to start of next day maths
|
|
const workingTime = moment(options.end).diff(moment(options.start));
|
|
const duration = moment
|
|
.utc(moment.duration(workingTime).as("milliseconds"))
|
|
.format("HH:mm");
|
|
console.log(`you work ${chalk.bold.red(duration)} hours in a day`);
|
|
|
|
const proportionalValueGeneration = ratio * workingTime;
|
|
const proportionalDuration = moment
|
|
.utc(moment.duration(proportionalValueGeneration).as("milliseconds"))
|
|
.format("HH:mm:ss");
|
|
console.log(
|
|
`we will be spending ${chalk.bold.red(
|
|
proportionalDuration,
|
|
)} hours on this patch`,
|
|
);
|
|
|
|
const waitDate = moment().add(proportionalValueGeneration);
|
|
await waitUntil(waitDate);
|
|
|
|
console.log("i have worked very well, applying patch");
|
|
lines[2] = `Date: ${moment().format("ddd, D MMM YYYY HH:mm:ss ZZ")}`;
|
|
writeFileSync(`${patch}`, lines.join(EOL));
|
|
await execp(`git -C ${path} am < ${patch}`);
|
|
unlinkSync(patch);
|
|
console.log("pushing");
|
|
await execp(`git -C ${path} push`);
|
|
}
|
|
|
|
// TODO: handle day start and end times
|
|
|
|
console.log("all done with the patches");
|
|
process.exit(0);
|
|
}
|
|
|
|
// apply mode
|
|
if (program_options.apply) {
|
|
const configDir = dirname(config.path);
|
|
const uid = config.get(path);
|
|
if (!uid) {
|
|
console.log(
|
|
`current repository ${chalk.bold.red("not found")} in configuration`,
|
|
);
|
|
console.log(
|
|
`assuming ${chalk.bold.red(
|
|
"no patches",
|
|
)} to apply or a ${chalk.bold.red("corrupt")} configuration`,
|
|
);
|
|
console.log(
|
|
`see ${chalk.bold.red(
|
|
configDir,
|
|
)} directly if this is not supposed to happen`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const files = readdirSync(configDir);
|
|
const patches = files
|
|
.filter((file) => file.includes(`patch_${uid}`))
|
|
.sort()
|
|
.map((patchFile) => join(configDir, patchFile));
|
|
|
|
if (patches.length === 0) {
|
|
console.log("no patches to apply");
|
|
process.exit(1);
|
|
}
|
|
|
|
for (const patch of patches) {
|
|
await execp(`git -C ${path} am < ${patch}`);
|
|
unlinkSync(patch);
|
|
}
|
|
|
|
console.log(
|
|
`${EOL}done applying ${chalk.bold.red(patches.length)} patches`,
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
// patch creation mode
|
|
const branch = await execp(`git -C ${path} rev-parse --abbrev-ref HEAD`);
|
|
const log = await execp(
|
|
`git -C ${path} log --oneline origin/${branch.stdout.trim()}..HEAD`,
|
|
);
|
|
const choices = log.stdout
|
|
.trim()
|
|
.split(EOL)
|
|
.filter((v) => v)
|
|
.map((v, i) => `${i + 1}) ${v}`);
|
|
|
|
let howManyCommits = 0;
|
|
if (choices.length > 0) {
|
|
const { how_far_back } = await inquirer.prompt([
|
|
{
|
|
type: "list",
|
|
name: "how_far_back",
|
|
message: `how many unpushed commits back do we want to ${chalk.bold.red(
|
|
"schedule",
|
|
)} ?`,
|
|
default: 0,
|
|
choices,
|
|
},
|
|
]);
|
|
howManyCommits = choices.indexOf(how_far_back) + 1;
|
|
} else {
|
|
console.log(`${EOL}no unpushed commits ${chalk.bold.red("detected")}`);
|
|
}
|
|
|
|
let uid = config.get(path);
|
|
if (howManyCommits > 0) {
|
|
let patches = [];
|
|
for (let index = 0; index < howManyCommits; index++) {
|
|
patches.push(
|
|
(
|
|
await execp(
|
|
`git -C ${path} format-patch -1 --keep-subject --stdout HEAD`,
|
|
)
|
|
).stdout,
|
|
); // add patch to array
|
|
await execp(`git -C ${path} reset --hard HEAD~1`); // hard remove this commit from head
|
|
}
|
|
|
|
patches = patches.reverse();
|
|
|
|
for (let index = 0; index < howManyCommits; index++) {
|
|
uid = Buffer.from(path).toString("base64");
|
|
config.set(path, uid);
|
|
// TODO: check if some patches are already there and resume indexing from there ?
|
|
writeFileSync(
|
|
join(dirname(config.path), `patch_${uid}_${index}.patch`),
|
|
patches[index],
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
`${EOL}done creating ${chalk.bold.red(
|
|
howManyCommits,
|
|
)} patches and removing commits from head`,
|
|
);
|
|
console.log(
|
|
`you may now run the tool in ${chalk.bold.red(
|
|
"daemon",
|
|
)} or ${chalk.bold.red("apply")} mode`,
|
|
);
|
|
}
|
|
} else {
|
|
console.log(`git repository ${chalk.bold.red("not found")}`);
|
|
}
|
|
})();
|
|
|