schedule git commits to pass as "normal" timezone person
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.
 
git-scheduler/index.mjs

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")}`);
}
})();