Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"build": "babel --out-dir=dist source",
"dev": "babel --out-dir=dist --watch source",
"clean": "npm run clean:linux",
"clean:linux": "rm -rf dist",
"clean:windows": "rmdir /s /q dist",
"clean:linux": "rm -rf dist || echo Directory not found, skipping removal",
"clean:windows": "rmdir /s /q dist || echo Directory not found, skipping removal",
"setup": "npm run setup:linux",
"setup:linux": "npm run clean:linux && npm run build && npm i -g .",
"setup:windows": "npm run clean:windows && npm run build",
Expand All @@ -47,6 +47,7 @@
"ink-select-input": "^6.0.0",
"is-git-repository": "^2.0.0",
"meow": "^11.0.0",
"ollama": "^0.5.9",
"openai": "^4.28.4",
"react": "^18.2.0",
"readline": "^1.3.0"
Expand Down
33 changes: 2 additions & 31 deletions source/app.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,7 @@
import React from 'react';
import isGit from 'is-git-repository';
import isCommitterSet from './utils/errors.js';
import askForCommitMessage from './utils/commit.js';
import {getOpenAIKey, setOpenAIKey, deleteOPenAIKey} from './utils/api.js';
import Logo from './utils/logo.js';

export default function App({flags}) {
if (flags.setopenai) {
setOpenAIKey(flags.setopenai);
}
if (flags.delopenai) {
deleteOPenAIKey();
}
if (!getOpenAIKey()) {
console.log('Please provide an OpenAI API key.');
console.log(
'You can get one from https://platform.openai.com/account/api-keys',
);
console.log(
'Run `magicc --setopenai=<api-key>` to save your API key and try again.',
);
} else {
console.log(
'You have an OpenAI API key, you can now generate a commit message.',
);
const gitCheck = isGit();
const committerCheck = isCommitterSet();
if (gitCheck && committerCheck) {
askForCommitMessage();
} else {
console.log('This is not a git repository.');
}
}
return <Logo />;

return <Logo flags={flags} />;
}
36 changes: 36 additions & 0 deletions source/models/ollama.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Ollama from 'ollama'; // Import the Ollama model
import config from '../utils/config.json';

async function ollamaModel(model, flags, diffContent) {
try {
// Use the prompt from the config file emoji and send to Ollama
const categoryResponse = await Ollama.chat({
messages: [
{role: 'system', content: config.commitConfig.emoji},
{role: 'user', content: diffContent},
],
model,
});
// Use the prompt from the config file message and send to Ollama

const messageResponse = await Ollama.chat({
messages: [
{role: 'system', content: config.commitConfig.message},
{role: 'user', content: diffContent},
],
model,
});
console.log('categoryResponse', categoryResponse);
console.log('messageResponse', messageResponse);
return {
category: categoryResponse?.message?.content,
message: messageResponse?.message?.content,
};
} catch (error) {
throw new Error(
'Failed to connect to local Ollama instance. To start Ollama, first download it at https://ollama.ai.',
);
}
}

export default ollamaModel;
54 changes: 54 additions & 0 deletions source/models/openai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import OpenAI from 'openai';
import config from '../utils/config.json';
import dotenv from 'dotenv';

import {getOpenAIKey, setOpenAIKey, deleteOPenAIKey} from '../utils/api.js';

dotenv.config();

async function openAiModel(model, flags, diffContent) {
if (flags.setopenai) {
setOpenAIKey(flags.setopenai);
}
if (flags.delopenai) {
deleteOPenAIKey();
}
if (!getOpenAIKey()) {
return {
message:
'Please provide an OpenAI API key.\n' +
'You can get one from https://platform.openai.com/account/api-keys\n' +
'Run `magicc --setopenai=<api-key>` to save your API key and try again.',
};
} else {
console.log(
'You have an OpenAI API key, you can now generate a commit message.',
);

const apiKey = await getOpenAIKey();
const openai = new OpenAI({apiKey: apiKey});

const category = await openai.chat.completions.create({
messages: [
{role: 'system', content: config.commitConfig.emoji},
{role: 'user', content: diffContent},
],
model,
});
// use the prmopt from the config file message and send to openai
const message = await openai.chat.completions.create({
messages: [
{role: 'system', content: config.commitConfig.message},
{role: 'user', content: diffContent},
],
model,
});

return {
category: category.choices[0].message.content,
message: message.choices[0].message.content,
};
}
}

export default openAiModel;
58 changes: 54 additions & 4 deletions source/utils/commit.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import generatePrompt from './openai.js';
import generateCommitMessage from './generateCommitMessage.js';
import {execa} from 'execa';
import readline from 'readline';
import React from 'react';
import {Box, render, Text, useApp} from 'ink';
import SelectInput from 'ink-select-input';
import Logo from './logo.js';

async function askForCommitMessage() {
const prompt = await generatePrompt();
async function askForCommitMessage(flags, model) {
const prompt = await generateCommitMessage(flags, model);

const rl = readline.createInterface({
input: process.stdin,
Expand All @@ -28,6 +28,7 @@ async function askForCommitMessage() {
} else {
console.log('Changes not committed.');
}
rl.close();
exit();
};

Expand All @@ -51,7 +52,6 @@ async function askForCommitMessage() {
</Logo>
);
};

if (prompt) {
render(<SelectSuggestedCommit />);
} else {
Expand All @@ -60,4 +60,54 @@ async function askForCommitMessage() {
}
}

export async function initGit() {
try {
await execa('git', ['restore', '--staged', '.']);
} catch (error) {
console.error(error);
}
}

// git status to see if there are any changes
// if there's any changes add the first file in the list of changes
let firstFilePath = '';

export async function gitStatus() {
try {
const {stdout: status} = await execa('git', ['status', '--porcelain']);
if (status) {
// get the first file path in the list of changes
const lines = status.split('\n');
const filePaths = lines
.map(line => line.split(' ').slice(2).join(' ').trim())
.filter(filePath => filePath !== '')
.concat(
lines
.filter(line => line.startsWith('??'))
.map(line => line.split(' ').slice(1).join(' ').trim()),
);
// git add the first file in the list of changes
firstFilePath = filePaths[0];
await execa('git', ['add', firstFilePath]);
console.log(`${firstFilePath} has been added to the staging area.`);
} else {
console.log('No changes to commit.');
return false;
}
} catch (error) {
console.error(error);
}
}

// get the diff of the staged changes
export async function gitDiff() {
try {
const {stdout: gitDiff} = await execa('git', ['diff', '--staged']);
return gitDiff;
} catch (error) {
console.error(error);
}
}


export default askForCommitMessage;
24 changes: 20 additions & 4 deletions source/utils/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
{
"emoji": "YYou are the author of the commit message. Your task is to select the appropriate category for the git diff based on the changes. Use the following categories (emoji category name => usage): 📦 new => for new files or new features; ✨ tweak => for enhancements or updates to the codebase; ☕ chore => for updates or changes outside the project codebase, including README.md; 🐞 fix => for fixing code bugs and errors. Please reply with the category name only.",
"message": "You are the author of the changes, you are going to provide a professional git commit message that is no longer than 25 characters in imperative present tense. Stricly no emojis are allowed and no conventional commit message as prefix is already provided. For example, instead of 'fix: fix a bug' make it 'fix a bug'. The message should be in lower case and no period at the end.",
"default_model": "gpt-4o-mini",
"maxDiffSize": 4000
"commitConfig": {
"emoji": "You are the author of the commit message. Your task is to select the appropriate category for the git diff based on the changes. Use the following categories (emoji category name => usage): 📦 new => for new files or new features; ✨ tweak => for enhancements or updates to the codebase; ☕ chore => for updates or changes outside the project codebase, including README.md; 🐞 fix => for fixing code bugs and errors. Please reply with the category name only.",
"message": "You are the author of the changes, and you will provide a professional git commit message that is no longer than 25 characters in imperative present tense. Strictly no emojis are allowed, and no conventional commit message prefix is provided. For example, instead of 'fix: fix a bug,' make it 'fix a bug.' The message should be in lowercase and should not have a period at the end."
},

"models": [
{
"title": "GPT-4o Mini",
"provider": "openai",
"model": "gpt-4o-mini",
"maxDiffSize": 4000,
"apiKey": ""
},
{
"title": "Llama 3.1 8B",
"provider": "ollama",
"model": "llama3.1:8b",
"maxDiffSize": 4000
}
]
}
61 changes: 61 additions & 0 deletions source/utils/generateCommitMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import openAiModel from '../models/openai.js';
import ollamaModel from '../models/ollama.js';
import config from './config.json';
import {gitDiff, gitStatus, initGit} from './commit.js';

// remove any staged changes in git

async function generateCommitMessage(flags, model) {
const maxDiffSize = config.maxDiffSize;

await initGit();
const status = await gitStatus();

const gitDiffContent = await gitDiff();
const {category, message} = await getModelResponse(
model,
flags,
gitDiffContent,
);

if (gitDiffContent.length > maxDiffSize) {
console.log('Diff content is too large. Skipping OpenAI request.');
return `✨ tweak (${firstFilePath}): update ${firstFilePath}`;
}

if (status !== false) {
return `${category} (${firstFilePath}): ${message}`;
} else {
return false;
}
}

async function getModelResponse(model, flags, gitDiffContent) {
let response;

try {
switch (model) {
case 'gpt-4o-mini':
response = await openAiModel(model, flags, gitDiffContent);
break;
case 'llama3.1:8b':
response = await ollamaModel(model, flags, gitDiffContent);
break;
default:
throw new Error('Unsupported model selected');
}
console.log('response', response);

if (response && response.category && response.message) {
// Destructure and return the required fields
const {category, message} = response;
return {category, message};
} else {
throw new Error(response.message);
}
} catch (error) {
console.log(error.message);
}
}

export default generateCommitMessage;
6 changes: 4 additions & 2 deletions source/utils/logo.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import info from './info.js';
import BigText from 'ink-big-text';
import Gradient from 'ink-gradient';
import {Text, Newline} from 'ink';
import ModelSelection from './modelSelection.js';

export default function Logo({children}) {
export default function Logo(children) {
return (
<>
<Gradient name="passion">
Expand All @@ -23,7 +24,8 @@ export default function Logo({children}) {
<Newline />
==================================================
</Text>
{children}
<ModelSelection {...children} />

</>
);
}
Loading