160 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			160 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| #!/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-19")
 | ||
| 	.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= "text-davinci-003";
 | ||
| 	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:
 |