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
2 changes: 1 addition & 1 deletion .github/workflows/pre-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:

- name: Upload coverage report
if: success()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/lcov-report
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,28 @@ This file is written automatically by the [version bump script](version-bump.ts)
- Enhanced the configuration management system within Evergit.
- Removed the `open` package and its type definitions from dependencies.
- Applied code formatting improvements for better readability.

## [0.2.1] - 2025-06-26

![Increment](https://img.shields.io/badge/patch-purple)

# Changelog

- **Error Handling:**

- Improved error handling in the `getCurrentBranchName` function.
- Enhanced the `getCurrentBranchName` function to support new repositories and the detached HEAD state.
- Provided error guidance for missing Git user configuration.

- **Testing:**

- Refactored Git utilities tests for style consistency.
- Added detailed integration tests for Git utilities.
- Added test for `getGitRootDir` utility function.

- **Features:**

- Added git root directory support to Git functions.

- **Code Quality:**
- Prettified code for better readability and consistency.
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.2.0-blue)
![Version](https://img.shields.io/badge/version-0.2.1-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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "evergit",
"version": "0.2.0",
"version": "0.2.1",
"description": "A CLI tool for generating commit messages that adhere to the Evergreen ILS project's commit policy.",
"main": "src/main.ts",
"bin": {
Expand Down
21 changes: 21 additions & 0 deletions src/cmd/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ async function getStagedFiles(): Promise<string[]> {
function getUserInfo(): { name: string; email: string; diff: string } {
const name = getConfig('name') || getGitName();
const email = getConfig('email') || getGitEmail();

if (!name || !email) {
const missingFields = [];
if (!name) missingFields.push('name');
if (!email) missingFields.push('email');

print('error', `Git user ${missingFields.join(' and ')} not configured.`);
print('info', 'Please configure git or evergit:');
if (!name) {
print('info', ' git config --global user.name "Your Name"');
print('info', ' OR');
print('info', ' evergit config set name "Your Name"');
}
if (!email) {
print('info', ' git config --global user.email "[email protected]"');
print('info', ' OR');
print('info', ' evergit config set email "[email protected]"');
}
throw new Error('Missing git configuration');
}

return {
name,
email,
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function getGitEditor(): Promise<string> {
}

export async function main(args = process.argv): Promise<void> {
program.name('evergit').description('Automate your Evergreen ILS git workflow').version('0.2.0');
program.name('evergit').description('Automate your Evergreen ILS git workflow').version('0.2.1');

program
.command('commit')
Expand Down
102 changes: 82 additions & 20 deletions src/util/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export enum GitFileStatus {
'!' = 'Ignored',
}

export function getGitRootDir(): string {
try {
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
} catch (error) {
throw new Error('Unable to determine git repository root directory.');
}
}

export function isInGitRepo(): boolean {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
Expand All @@ -25,7 +33,8 @@ export function isInGitRepo(): boolean {

export function pushChanges(): void {
try {
execSync('git push');
const gitRoot = getGitRootDir();
execSync(`git -C "${gitRoot}" push`);
} catch (error: any) {
if (error.message.includes('fatal: The current branch')) {
throw new Error('The current branch has no upstream branch.');
Expand All @@ -43,35 +52,42 @@ export function setUserEmail(email: string): void {

export function setupUpstreamBranch(): void {
const branchName = getCurrentBranchName();
execSync(`git push --set-upstream origin ${branchName}`);
const gitRoot = getGitRootDir();
execSync(`git -C "${gitRoot}" push --set-upstream origin ${branchName}`);
}

export function stageAllFiles(): void {
execSync('git add .');
const gitRoot = getGitRootDir();
execSync(`git -C "${gitRoot}" add .`);
}

export function stageFile(filePath: string): void {
execSync(`git add ${filePath}`);
const gitRoot = getGitRootDir();
execSync(`git -C "${gitRoot}" add "${filePath}"`);
}

export function unstageFile(filePath: string): void {
execSync(`git restore --staged ${filePath}`);
const gitRoot = getGitRootDir();
execSync(`git -C "${gitRoot}" restore --staged "${filePath}"`);
}

export function unstageAllFiles(): void {
execSync('git restore --staged .');
const gitRoot = getGitRootDir();
execSync(`git -C "${gitRoot}" restore --staged .`);
}

export function listChangedFiles(): string[] {
const statusOutput = execSync('git status --porcelain').toString().trim();
const gitRoot = getGitRootDir();
const statusOutput = execSync(`git -C "${gitRoot}" status --porcelain`).toString().trim();
return statusOutput
.split('\n')
.map((line) => line.trim().slice(2))
.filter(Boolean);
}

export function getStatusForFile(filePath: string): GitFileStatus {
const status = execSync(`git status --porcelain "${filePath}"`).toString().trim();
const gitRoot = getGitRootDir();
const status = execSync(`git -C "${gitRoot}" status --porcelain "${filePath}"`).toString().trim();
if (!status) {
return GitFileStatus['!'];
}
Expand All @@ -81,7 +97,8 @@ export function getStatusForFile(filePath: string): GitFileStatus {
export function getDiffForStagedFiles(): string {
const maxBufferSize = 10 * 1024 * 1024; // 10 MB buffer
try {
let diff: string = execSync('git diff --staged', { maxBuffer: maxBufferSize }).toString();
const gitRoot = getGitRootDir();
let diff: string = execSync(`git -C "${gitRoot}" diff --staged`, { maxBuffer: maxBufferSize }).toString();
diff = removeDiffForFile(diff, 'package-lock.json');
return diff;
} catch (error: any) {
Expand All @@ -102,35 +119,78 @@ function removeDiffForFile(diff: string, filePath: string): string {

export function getCurrentBranchName(): string {
try {
const branchName = execSync('git rev-parse --abbrev-ref HEAD', {
encoding: 'utf-8',
}).trim();

return branchName;
const gitRoot = getGitRootDir();

// First try the standard approach
try {
const branchName = execSync(`git -C "${gitRoot}" rev-parse --abbrev-ref HEAD`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'], // Capture stderr
}).trim();

if (branchName === 'HEAD') {
// We're in a detached HEAD state
throw new Error('Repository is in detached HEAD state. Please checkout a branch.');
}

return branchName;
} catch (error: any) {
// If rev-parse fails, try symbolic-ref for new repos
if (error.message?.includes('ambiguous argument') || error.message?.includes('unknown revision')) {
try {
const branchName = execSync(`git -C "${gitRoot}" symbolic-ref --short HEAD`, {
encoding: 'utf-8',
}).trim();
return branchName;
} catch {
// If symbolic-ref also fails, we might be in a completely new repo
// Try to get the default branch name from config
try {
const defaultBranch = execSync(`git -C "${gitRoot}" config init.defaultBranch`, {
encoding: 'utf-8',
}).trim();
return defaultBranch || 'main'; // Default to 'main' if not configured
} catch {
return 'main'; // Final fallback
}
}
}
throw error;
}
} catch (error) {
throw new Error('Unable to get current branch name.');
}
}

export function hasGitChanges(): boolean {
const status = execSync('git status --porcelain').toString().trim();
const gitRoot = getGitRootDir();
const status = execSync(`git -C "${gitRoot}" status --porcelain`).toString().trim();
return status.length > 0;
}

export function getName(): string {
return execSync('git config user.name').toString().trim();
try {
return execSync('git config user.name', { encoding: 'utf-8' }).trim();
} catch {
return '';
}
}

export function getEmail(): string {
return execSync('git config user.email').toString().trim();
try {
return execSync('git config user.email', { encoding: 'utf-8' }).trim();
} catch {
return '';
}
}

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

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

Expand All @@ -139,12 +199,14 @@ function sanitizeCommitMessage(message: string): string {
}

export function checkForRemote(remoteUrl: string): boolean {
const remoteUrls = execSync('git remote -v').toString();
const gitRoot = getGitRootDir();
const remoteUrls = execSync(`git -C "${gitRoot}" remote -v`).toString();
return remoteUrls.includes(remoteUrl);
}

export function getRemoteName(remoteUrl: string): string {
const remoteUrls = execSync('git remote -v').toString();
const gitRoot = getGitRootDir();
const remoteUrls = execSync(`git -C "${gitRoot}" remote -v`).toString();
const remoteName = remoteUrls.split('\n').find((line: string) => line.includes(remoteUrl));
return remoteName ? remoteName.split('\t')[0] : '';
}
Loading