dotfiles/bin/§ai-commit.mjs

160 lines
5.4 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env nodejsscript
/* jshint esversion: 11,-W097, -W040, module: true, node: true, expr: true, undef: true *//* global echo, $, pipe, s, fetch, cyclicLoop */
$.is_fatal= true;
const token_file= "~/.config/openai.token";
let token;
const gitmoji_list= {
build: "building_construction",
chore: "bricks",
ci: "construction_worker",
docs: "memo",
feat: "sparkles",
fix: "bug",
perf: "zap",
refactor: "recycle",
revert: "rewind",
style: "art",
test: "white_check_mark"
};
const git3moji_list= {
build: "tv",
chore: "tv",
ci: "tv",
docs: "abc",
feat: "zap",
fix: "bug",
perf: "zap",
refactor: "cop",
style: "zap",
test: "cop"
};
const conventional_desc= [
"Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)",
"Other changes that don't modify src or test files",
"Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)",
"Documentation only changes",
"A new feature",
"A bug Fix",
"A code change that improves performance",
"A code change that neither fixes a bug nor adds a feature",
"Reverts a previous commit",
"Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
"Adding missing tests or correcting existing tests"
];
$.api("", true)
.version("2024-02-22")
.describe([
"Utility to use ChatGPT to generate a commit message from COMMIT_EDITMSG file.",
`Don't forget to set the token in ${token_file} file.`,
"",
"Conventional/gitmoji commits should follow https://www.conventionalcommits.org/en/v1.0.0/ (https://github.com/pvdlg/conventional-commit-types).",
"For the gitmoji the <type> uses emojis as follows:",
...Object.entries(gitmoji_list).map(( v, i )=> echo.format("%c"+v[0]+` (${v[1]}): ${conventional_desc[i]}`, "display: list-item")),
...Object.entries(git3moji_list).map(( v, i )=> echo.format("%c"+v[0]+` (${v[1]}): ${conventional_desc[i]}`, "display: list-item"))
])
.option("--format, -f", [ "Use one of the following formats to generate the commit message: [regular (default), conventional, gitmoji, git3moji]",
"For gitmoji see: https://gitmoji.dev/"
])
.action(async function({ format= "regular" }= {}){
const question= questionChatGPT(format);
const response= (await pipe(
()=> s.cat("./.git/COMMIT_EDITMSG"),
s=> s.slice(s.indexOf("diff --git")),
diffToChunks(3900-545), //the worst scenario of ★ +new lines
ch=> ch.map(pipe( question, requestCommitMessage )),
ch=> Promise.all(ch)
)())
.map(pipe(
j=> j.choices[0].text.trim(),
t=> t.match(/\[[^\]]*\]/) ?? convertToJSONArray(t),
JSON.parse,
format==="regular" ? i=> i : i=> gitmoji(i, format==="git3moji"),
a=> a.join("\n")
))
.join("\n\n");
echo(response);
$.exit(0);
})
.parse();
function diffToChunks(max_tokens){ return function(input){
if(input.length < max_tokens)
return [ input ];
return input.split(/(?=diff --git)/g)
.flatMap(function(input){
if(input.length < max_tokens)
return [ input ];
const [ file, ...diffs ]= input.split(/\n(?=@@)/g);
if(file.includes("new file"))
return [ file ];
return diffs
.filter(chunk=> chunk.length < max_tokens)
.reduce(function(chunks, chunk){
const i= chunks.length-1;
if(chunks[i].length + chunk.length < max_tokens-1)
chunks[i]+= "\n"+chunk;
else
chunks.push(file+"\n"+chunk);
return chunks;
}, [ file ])
.filter(chunk=> chunk.length < max_tokens);
});
}; }
function convertToJSONArray(text){
const arr= text.split("\n")
.filter(line=> line.trim().match(/^[1-3]\. /))
.map(line=> line.slice(3))
.map(line => line.startsWith('"') ? line : "\"" + ( "'`".slice("").includes(line[0]) ? line.slice(1, -1) : line ) + "\"");
return `[${arr.join(", ")}]`;
}
function questionChatGPT(format){ return function(diff){
const msg= [
[
"I would like to ask you to act like a git commit message writer.",
"I will enter a git diff, and your job is to convert it into a useful commit message and make 3 options as JSON array.",
"Do not preface the commit with anything, use a concise, precise, present-tense, complete sentence.",
"The length should be fewer than 50 characters if possible.",
].join(" ") //340chars★
];
if(format!=="regular")
msg.push(
[
"It should follow the conventional commits.",
"The format is <type in lowercase>: <description>.",
"A type can be one of the following: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, or test.",
].join(" ") //203chars★
);
msg.push("", diff);
return msg.join("\n");
}; }
function gitmoji(candidates, is_three= false){
return candidates.map(message=> message.trim().replace(/^[^:]*:/, toGitmoji));
function toGitmoji(name){
const candidate= ( is_three ? git3moji_list : gitmoji_list )[name.slice(0, -1)];
return !candidate ? name : `:${candidate}:`;
}
}
function requestCommitMessage(prompt){
if(!token) token= s.cat(token_file).stdout.trim();
const model= "gpt-3.5-turbo-instruct";
return fetch(`https://api.openai.com/v1/engines/${model}/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer "+token
},
body: JSON.stringify({
max_tokens: 1000,
temperature: 0.1,
prompt
}),
signal: AbortSignal.timeout(10000)
}).then(r=> r.json());
}
// vim: set tabstop=4 shiftwidth=4 textwidth=250 noexpandtab :
// vim>60: set foldmethod=indent foldlevel=1 foldnestmax=2: