Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.1.0 #2

Merged
merged 13 commits into from
Nov 15, 2024
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## [0.1.0] - 2024-11-15

## Changelog Summary

- Enhanced version bump and changelog handling.
- Improved commit flow with Launchpad bug integration.
- Enhanced commit message handling by improved sanitization logic, specifically replacing newline characters with escape sequences for better multi-line support.
- Refactored code for improved readability and error handling.
- Utilized temporary files for git commit message construction.
- General code prettification and refactoring for better code organization and functionality.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `evergit`

![Version](https://img.shields.io/badge/version-0.0.1-blue)
![Version](https://img.shields.io/badge/version-0.1.0-blue)

![TypeScript](https://img.shields.io/badge/typescript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![OpenAI](https://img.shields.io/badge/OpenAI-00A79D?style=for-the-badge&logo=openai&logoColor=white)
Expand Down
3,027 changes: 1,618 additions & 1,409 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "evergit",
"version": "0.0.1",
"version": "0.1.0",
"description": "A CLI tool for generating commit messages that adhere to the Evergreen ILS project's commit policy.",
"main": "src/main.ts",
"bin": {
Expand All @@ -24,14 +24,17 @@
"open": "^10.1.0",
"openai": "^4.68.4",
"readline-sync": "^1.4.10",
"semver": "^7.6.3",
"url": "^0.11.4"
},
"devDependencies": {
"@types/axios": "^0.9.36",
"@types/inquirer": "^9.0.7",
"@types/open": "^6.1.0",
"@types/readline-sync": "^1.4.8",
"@types/semver": "^7.5.8",
"prettier": "^3.3.3",
"ts-node": "^10.9.2",
"typescript": "^4.6.2"
},
"author": "Ian Skelskey <ianskelskey@gmail.com>",
Expand Down
59 changes: 35 additions & 24 deletions src/cmd/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import {
commitWithMessage,
} from '../util/git';
import COMMIT_POLICY from '../util/commit_policy';
import { selectFilesToStage, confirmCommitMessage, print } from '../util/prompt';
import inquirer from 'inquirer';
import { selectFilesToStage, confirmCommitMessage, print, requestLaunchpadBugNumber } from '../util/prompt';
import { authenticateLaunchpad, loadCredentials, getBugInfo, BugMessage } from '../util/launchpad';

async function commit(model: string): Promise<void> {
Expand All @@ -31,8 +30,34 @@ async function commit(model: string): Promise<void> {
print('info', `Committing changes to branch ${branch}`);

const userInfo = getUserInfo();
const bugNumber = await promptForBugNumber();
const bugNumber = await requestLaunchpadBugNumber();

if (bugNumber !== '') {
await commitWithBugInfo(userInfo, bugNumber);
} else {
commitWithoutBugInfo(userInfo);
}
}

async function generateAndProcessCommit(systemPrompt: string, userPrompt: string): Promise<void> {
const commitMessage = await createTextGeneration(systemPrompt, userPrompt);
if (commitMessage) {
await processCommitMessage(commitMessage);
} else {
print('error', 'Failed to generate commit message.');
}
}

async function commitWithoutBugInfo(userInfo: { name: string; email: string; diff: string }): Promise<void> {
const systemPrompt = COMMIT_POLICY;
const userPrompt = buildUserPrompt(userInfo);
generateAndProcessCommit(systemPrompt, userPrompt);
}

async function commitWithBugInfo(
userInfo: { name: string; email: string; diff: string },
bugNumber: string,
): Promise<void> {
const credentials = await getLaunchPadCredentials();
if (!credentials) {
print('error', 'Failed to authenticate with Launchpad.');
Expand All @@ -43,18 +68,12 @@ async function commit(model: string): Promise<void> {

const systemPrompt = COMMIT_POLICY;
const userPrompt = buildUserPrompt(userInfo, bugNumber, bugMessages);

const commitMessage = await createTextGeneration(systemPrompt, userPrompt);
if (commitMessage) {
await processCommitMessage(commitMessage);
} else {
print('error', 'Failed to generate commit message.');
}
generateAndProcessCommit(systemPrompt, userPrompt);
}

// Helper function to authenticate with Launchpad
async function getLaunchPadCredentials(): Promise<{ accessToken: string; accessTokenSecret: string } | null> {
authenticateLaunchpad('evergit');
await authenticateLaunchpad('evergit');
return loadCredentials();
}

Expand Down Expand Up @@ -82,24 +101,16 @@ function getUserInfo(): { name: string; email: string; diff: string } {
};
}

// Helper function to prompt for a Launchpad bug number
async function promptForBugNumber(): Promise<string> {
const answer = await inquirer.prompt({
type: 'input',
name: 'bugNumber',
message: 'Enter the Launchpad bug number (if applicable):',
});
return answer.bugNumber;
}

// Helper function to build the user prompt string for text generation
function buildUserPrompt(
userInfo: { name: string; email: string; diff: string },
bugNumber: string,
bugMessages: BugMessage[],
bugNumber: string = '',
bugMessages: BugMessage[] = [],
): string {
const messages = bugMessages.map((message) => message.toString()).join('\n');
return `${userInfo.name} <${userInfo.email}>\n\n${userInfo.diff}\n\n${messages}`;
return bugNumber !== ''
? `User: ${userInfo.name} <${userInfo.email}>\n\nBug: ${bugNumber}\n\n${messages}\n\n${userInfo.diff}`
: `User: ${userInfo.name} <${userInfo.email}>\n\n${userInfo.diff}`;
}

// Helper function to process and confirm the commit message with the user
Expand Down
1 change: 1 addition & 0 deletions src/util/commit_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Signed-off-by: your name <your_email@example.com>
- Use **present tense** for the Release-Note entry.
- Ensure **testing steps** are clear if included.
- Keep messages **brief and descriptive**, avoiding restating the code diff.
- If a launchpad bug number is not provided, **omit the LP# prefix** from the subject line.
`;

export default COMMIT_POLICY;
25 changes: 19 additions & 6 deletions src/util/git.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

export enum GitFileStatus {
'A' = 'Added',
Expand Down Expand Up @@ -72,12 +75,13 @@ export function getDiffForAll(): string {
}

export function getDiffForStagedFiles(): string {
return execSync('git diff --staged').toString();
}

export function commitWithMessage(message: string): void {
const sanitizedMessage = sanitizeCommitMessage(message);
execSync(`git commit -m "${sanitizedMessage}"`);
var diff: string = execSync('git diff --staged').toString();
// if the file is package-lock.json, remove that file from the diff
if (diff.includes('diff --git a/package-lock.json b/package-lock.json')) {
const regex = /diff --git a\/package-lock.json b\/package-lock.json[\s\S]*?(?=diff --git|$)/g;
diff = diff.replace(regex, '');
}
return diff;
}

export function getCurrentBranchName(): string {
Expand Down Expand Up @@ -105,6 +109,15 @@ export function getEmail(): string {
return execSync('git config user.email').toString().trim();
}

export function commitWithMessage(message: string): void {
const sanitizedMessage = sanitizeCommitMessage(message);
const tempFilePath = path.join(os.tmpdir(), 'commit-message.txt');

fs.writeFileSync(tempFilePath, sanitizedMessage);
execSync(`git commit -F "${tempFilePath}"`);
fs.unlinkSync(tempFilePath); // Clean up the temporary file
}

export function sanitizeCommitMessage(message: string): string {
return message.replace(/"/g, '\\"');
}
Expand Down
16 changes: 7 additions & 9 deletions src/util/launchpad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { URLSearchParams } from 'url';
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import os from 'os';
import { print, waitForAuthorization } from './prompt';

// Define constants for Launchpad OAuth endpoints
const requestTokenPage = '+request-token';
Expand All @@ -26,7 +27,7 @@ async function getBugInfo(bugId: string, accessToken: string, accessTokenSecret:
const data = response.data as { id: string; title: string; description: string };
return new Bug(data.id, data.title, data.description);
} catch (error) {
console.error('Error fetching bug information:', error);
print('error', 'Error fetching bug information.');
throw error;
}
}
Expand All @@ -52,9 +53,7 @@ async function authenticateLaunchpad(consumerKey: string) {
const storedCredentials = loadCredentials();

if (storedCredentials) {
console.log('Using stored credentials.');
console.log('Access Token:', storedCredentials.accessToken);
console.log('Access Token Secret:', storedCredentials.accessTokenSecret);
print('success', 'Already authorized with Launchpad.');
return;
}

Expand All @@ -65,13 +64,13 @@ async function authenticateLaunchpad(consumerKey: string) {
await authEngine.authorize(credentials);

if (credentials.accessToken) {
console.log('Authorization successful!');
print('success', 'Authorization successful.');
saveCredentials(credentials.accessToken.key, credentials.accessToken.secret);
} else {
console.log('Authorization failed or was declined by the user.');
print('error', 'Authorization failed.');
}
} catch (error) {
console.error('An error occurred during the Launchpad authentication process.');
print('error', 'Error authorizing with Launchpad.');
console.error(error);
}
}
Expand Down Expand Up @@ -195,8 +194,7 @@ class RequestTokenAuthorizationEngine {

async authorize(credentials: Credentials) {
const authUrl = await credentials.getRequestToken(this.serviceRoot);
console.log(`Please authorize the app by visiting this URL: ${authUrl}`);
readlineSync.question('Press Enter once you have completed the authorization in the browser.');
await waitForAuthorization(authUrl as string);
await credentials.exchangeRequestTokenForAccessToken(this.serviceRoot);
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/util/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ const colors: Record<string, (message: string) => string> = {
content: chalk.grey,
};

export async function waitForAuthorization(authUrl: string): Promise<void> {
print('info', `Please authorize the app by visiting this URL: ${authUrl}`);
await inquirer.prompt({
type: 'input',
name: 'authorization',
message: 'Press Enter once you have completed the authorization in the browser.',
});
}

export async function confirmCommitMessage(commitMessage: string): Promise<boolean> {
print('info', 'Commit message:');
print('content', commitMessage);
Expand All @@ -21,6 +30,14 @@ export async function confirmCommitMessage(commitMessage: string): Promise<boole
}

export async function requestLaunchpadBugNumber(): Promise<string> {
const confirm = await inquirer.prompt({
type: 'confirm',
name: 'hasBug',
message: 'Is this commit related to a Launchpad bug?',
});
if (!confirm.hasBug) {
return '';
}
const answer = await inquirer.prompt({
type: 'input',
name: 'bugNumber',
Expand Down
76 changes: 76 additions & 0 deletions version-bump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import fs from 'fs';
import { execSync } from 'child_process';
import semver from 'semver';
import { createTextGeneration } from './src/util/ai';

// Function to generate changelog from commits using createTextGeneration
async function generateChangelog(commitMessages: string[]): Promise<string> {
const systemPrompt = 'You are a helpful assistant generating a changelog based on commit messages.';
const userPrompt = `Create a concise changelog summary for the following commits:\n\n${commitMessages.join('\n')}`;
const changelog = await createTextGeneration(systemPrompt, userPrompt);
return changelog || 'Error generating changelog';
}

// Function to bump the version in package.json and update README.md badge
function bumpVersion(increment: semver.ReleaseType): string {
const packageJsonPath = './package.json';
const readmePath = './README.md';
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
const newVersion = semver.inc(currentVersion, increment) || currentVersion;
packageJson.version = newVersion;

fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(`Version bumped from ${currentVersion} to ${newVersion}`);

// Update version badge in README.md
const readmeContent = fs.readFileSync(readmePath, 'utf8');
const updatedReadmeContent = readmeContent.replace(
/!\[Version\]\(https:\/\/img.shields.io\/badge\/version-[\d.]+-blue\)/,
`![Version](https://img.shields.io/badge/version-${newVersion}-blue)`,
);
fs.writeFileSync(readmePath, updatedReadmeContent);
console.log(`README.md updated with new version badge ${newVersion}`);

return newVersion;
}

// Function to append changelog to CHANGELOG.md
function updateChangelog(version: string, changelog: string): void {
const date = new Date().toISOString().split('T')[0];
const changelogContent = `## [${version}] - ${date}\n\n${changelog}\n\n`;

fs.appendFileSync('CHANGELOG.md', changelogContent);
console.log(`Changelog updated for version ${version}`);
}

// Function to get commits unique to the current branch
function getBranchCommits(): string[] {
try {
// Fetch the latest changes from the main branch
execSync('git fetch origin main');
const commits = execSync('git log origin/main..HEAD --pretty=format:"%s"').toString().split('\n');
return commits;
} catch (error) {
console.error('Error fetching branch-specific commits:', error);
return [];
}
}

// Main function to run the bump and changelog
async function main() {
const increment: semver.ReleaseType = (process.argv[2] as semver.ReleaseType) || 'patch';
const newVersion = bumpVersion(increment);

const commitMessages = getBranchCommits();
const changelog = await generateChangelog(commitMessages);

updateChangelog(newVersion, changelog);

// Run npm install
execSync('npm install'); // Update dependencies

console.log(changelog); // Output changelog for CI use
}

main();
Loading