From dcdd34f52860ed4836623467200c6b28a9572541 Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 22:50:38 -0400 Subject: [PATCH 01/12] Add git root directory support to Git functions Introduce getGitRootDir function to consistently determine the Git root directory for executing Git commands. This change modifies existing Git utility functions to use the -C flag with the root directory, ensuring that all operations execute in the correct repository context, which enhances reliability when scripts are run from subdirectories. Release-Note: Add support for executing Git commands from any subdirectory within a repository Signed-off-by: Ian Skelskey --- src/util/git.ts | 50 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/util/git.ts b/src/util/git.ts index bf6fa22..e972072 100644 --- a/src/util/git.ts +++ b/src/util/git.ts @@ -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' }); @@ -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.'); @@ -43,27 +52,33 @@ 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)) @@ -71,7 +86,8 @@ export function listChangedFiles(): string[] { } 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['!']; } @@ -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) { @@ -102,7 +119,8 @@ function removeDiffForFile(diff: string, filePath: string): string { export function getCurrentBranchName(): string { try { - const branchName = execSync('git rev-parse --abbrev-ref HEAD', { + const gitRoot = getGitRootDir(); + const branchName = execSync(`git -C "${gitRoot}" rev-parse --abbrev-ref HEAD`, { encoding: 'utf-8', }).trim(); @@ -113,7 +131,8 @@ export function getCurrentBranchName(): string { } 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; } @@ -126,11 +145,12 @@ export function getEmail(): string { } 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 } @@ -139,12 +159,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] : ''; } From e84c25f36472fc903cc3773ef806f10c9d1e7e0c Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 22:57:19 -0400 Subject: [PATCH 02/12] Add test for getGitRootDir utility function Introduce a new integration test for the `getGitRootDir` utility function. This test verifies the function correctly returns the root directory of the Git repository by comparing normalized directory paths. This enhancement ensures that path inconsistencies due to different operating systems do not affect test outcomes. Release-Note: add integration test for getGitRootDir utility function Signed-off-by: Ian Skelskey --- tests/util/git.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/util/git.test.ts b/tests/util/git.test.ts index 093caf1..40aacba 100644 --- a/tests/util/git.test.ts +++ b/tests/util/git.test.ts @@ -7,6 +7,7 @@ import { commitWithMessage, setUserEmail, setUserName, + getGitRootDir, } from '../../src/util/git'; import { execSync } from 'child_process'; import * as fs from 'fs'; @@ -48,6 +49,16 @@ describe('Git Utilities Integration Tests', () => { }); }); + describe('getGitRootDir', () => { + test('should return the root directory of the Git repository', () => { + const rootDir = getGitRootDir(); + // Normalize paths to use forward slashes for comparison + const normalizedRootDir = rootDir.replace(/\\/g, '/'); + const normalizedTestRepoDir = testRepoDir.replace(/\\/g, '/'); + expect(normalizedRootDir).toBe(normalizedTestRepoDir); + }); + }); + describe('addAllChanges and hasGitChanges', () => { test('should detect changes after adding a file', () => { fs.writeFileSync(testFilePath, 'Test content'); From eba596022f63401f8708c11b474f8b292098a54c Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 23:07:39 -0400 Subject: [PATCH 03/12] Add detailed integration tests for git utilities - Incorporated comprehensive tests covering git operations: staging, unstaging, listing changes, and handling remote repositories. - Introduced tests for special character handling in commit messages, remote detection, and user info retrieval. - Provided tests for buffer overflow scenarios and upstream branch setup. Release-Note: Enhance integration test coverage for git utilities Signed-off-by: Ian Skelskey --- tests/util/git.test.ts | 126 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/tests/util/git.test.ts b/tests/util/git.test.ts index 40aacba..a47344b 100644 --- a/tests/util/git.test.ts +++ b/tests/util/git.test.ts @@ -17,6 +17,8 @@ import * as os from 'os'; describe('Git Utilities Integration Tests', () => { const testFilePath = 'test-file.txt'; let testRepoDir: string; + const remoteName = 'origin'; + const remoteUrl = 'https://example.com/fake.git'; beforeAll(() => { // Create a temporary directory for the test repository @@ -26,7 +28,7 @@ describe('Git Utilities Integration Tests', () => { // Ensure a Git repository is initialized and an initial commit is made try { execSync('git init'); - // Set Git user name and email + execSync(`git remote add ${remoteName} ${remoteUrl}`); setUserName('Test User'); setUserEmail('test@example.com'); fs.writeFileSync(testFilePath, 'Initial content'); @@ -80,5 +82,127 @@ describe('Git Utilities Integration Tests', () => { stageAllFiles(); expect(getStatusForFile(testFilePath)).toBe('M'); }); + test('should return ignored for a .gitignored file', () => { + const ignoredFile = 'ignored.txt'; + fs.writeFileSync('.gitignore', 'ignored.txt\n'); + fs.writeFileSync(ignoredFile, 'ignore me'); + stageAllFiles(); + commitWithMessage('Add .gitignore and ignored.txt'); + expect(getStatusForFile(ignoredFile)).toBe('Ignored'); + }); + test('should return untracked for a new file', () => { + const untrackedFile = 'untracked.txt'; + fs.writeFileSync(untrackedFile, 'new file'); + expect(getStatusForFile(untrackedFile)).toBe('?'); + }); + }); + + describe('stageFile, unstageFile, unstageAllFiles', () => { + test('should stage and unstage a file', () => { + const file = 'stage-me.txt'; + fs.writeFileSync(file, 'stage this'); + require('../../src/util/git').stageFile(file); + expect(getStatusForFile(file)).toBe('A'); + require('../../src/util/git').unstageFile(file); + // After unstaging, should be untracked + expect(getStatusForFile(file)).toBe('?'); + }); + test('should unstage all files', () => { + const file = 'unstage-all.txt'; + fs.writeFileSync(file, 'unstage all'); + require('../../src/util/git').stageFile(file); + require('../../src/util/git').unstageAllFiles(); + expect(getStatusForFile(file)).toBe('?'); + }); + }); + + describe('listChangedFiles', () => { + test('should list changed files', () => { + const file = 'changed.txt'; + fs.writeFileSync(file, 'changed'); + require('../../src/util/git').stageFile(file); + const changed = require('../../src/util/git').listChangedFiles(); + expect(changed.map((f: string) => f.trim())).toContain(file); + }); + }); + + describe('getName and getEmail', () => { + test('should get the configured user name and email', () => { + const { getName, getEmail } = require('../../src/util/git'); + expect(getName()).toBe('Test User'); + expect(getEmail()).toBe('test@example.com'); + }); + }); + + describe('commitWithMessage', () => { + test('should commit with special characters in the message', () => { + fs.writeFileSync('special.txt', 'special'); + require('../../src/util/git').stageFile('special.txt'); + expect(() => commitWithMessage('A commit with "quotes" and special chars !@#$%^&*()')).not.toThrow(); + }); + }); + + describe('checkForRemote and getRemoteName', () => { + test('should detect the remote and get its name', () => { + const { checkForRemote, getRemoteName } = require('../../src/util/git'); + expect(checkForRemote(remoteUrl)).toBe(true); + expect(getRemoteName(remoteUrl)).toBe(remoteName); + }); + }); + + describe('getDiffForStagedFiles', () => { + test('should return a diff for staged files', () => { + const file = 'diffme.txt'; + fs.writeFileSync(file, 'diff this'); + require('../../src/util/git').stageFile(file); + const diff = require('../../src/util/git').getDiffForStagedFiles(); + expect(diff).toContain('diff --git'); + }); + test('should throw on ENOBUFS error', () => { + // Mock execSync to throw ENOBUFS only for diff command + const git = require('../../src/util/git'); + const childProcess = require('child_process'); + const originalExecSync = childProcess.execSync; + childProcess.execSync = jest.fn((cmd: string, options?: any) => { + if (typeof cmd === 'string' && cmd.includes('diff --staged')) { + const err = new Error('ENOBUFS'); + (err as any).code = 'ENOBUFS'; + throw err; + } + return originalExecSync(cmd, options); + }); + try { + expect(() => git.getDiffForStagedFiles()).toThrow('Buffer overflow'); + } finally { + childProcess.execSync = originalExecSync; + } + }); + }); + + describe('pushChanges and setupUpstreamBranch', () => { + test('should throw if no upstream branch', () => { + const git = require('../../src/util/git'); + // Create a new branch with no upstream + execSync('git checkout -b test-branch'); + expect(() => git.pushChanges()).toThrow('The current branch has no upstream branch.'); + }); + test('should not throw when setting up upstream branch', () => { + const git = require('../../src/util/git'); + const childProcess = require('child_process'); + const originalExecSync = childProcess.execSync; + execSync('git checkout -b upstream-branch'); + // Mock execSync to prevent actual push only + childProcess.execSync = jest.fn((cmd: string, options?: any) => { + if (typeof cmd === 'string' && cmd.includes('push --set-upstream')) { + return ''; + } + return originalExecSync(cmd, options); + }); + try { + expect(() => git.setupUpstreamBranch()).not.toThrow(); + } finally { + childProcess.execSync = originalExecSync; + } + }); }); }); From ca52f435cb2b53d9edda3b3966eea8d0bbdfd512 Mon Sep 17 00:00:00 2001 From: IanSkelskey Date: Thu, 26 Jun 2025 03:07:53 +0000 Subject: [PATCH 04/12] Prettified Code! --- tests/util/git.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/util/git.test.ts b/tests/util/git.test.ts index a47344b..e380798 100644 --- a/tests/util/git.test.ts +++ b/tests/util/git.test.ts @@ -165,9 +165,9 @@ describe('Git Utilities Integration Tests', () => { const originalExecSync = childProcess.execSync; childProcess.execSync = jest.fn((cmd: string, options?: any) => { if (typeof cmd === 'string' && cmd.includes('diff --staged')) { - const err = new Error('ENOBUFS'); - (err as any).code = 'ENOBUFS'; - throw err; + const err = new Error('ENOBUFS'); + (err as any).code = 'ENOBUFS'; + throw err; } return originalExecSync(cmd, options); }); From c90c5e7decd3a4bbba028e952387a268fad655f4 Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 23:19:20 -0400 Subject: [PATCH 05/12] Enhance getCurrentBranchName function to handle new repositories and detached HEAD state --- src/util/git.ts | 42 +++++++++++++++++++++++++++++++++++++----- tests/util/git.test.ts | 18 ++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/util/git.ts b/src/util/git.ts index e972072..24334c6 100644 --- a/src/util/git.ts +++ b/src/util/git.ts @@ -120,11 +120,43 @@ function removeDiffForFile(diff: string, filePath: string): string { export function getCurrentBranchName(): string { try { const gitRoot = getGitRootDir(); - const branchName = execSync(`git -C "${gitRoot}" rev-parse --abbrev-ref HEAD`, { - encoding: 'utf-8', - }).trim(); - - return branchName; + + // 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.'); } diff --git a/tests/util/git.test.ts b/tests/util/git.test.ts index a47344b..0e9400a 100644 --- a/tests/util/git.test.ts +++ b/tests/util/git.test.ts @@ -73,6 +73,24 @@ describe('Git Utilities Integration Tests', () => { const branchName = getCurrentBranchName(); expect(branchName === 'master' || branchName === 'main').toBe(true); }); + + test('should handle new repository with no commits', () => { + // Create a new test directory for this specific test + const newRepoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'new-repo-')); + const originalCwd = process.cwd(); + + try { + process.chdir(newRepoDir); + execSync('git init'); + + // Should still be able to get branch name even with no commits + const branchName = getCurrentBranchName(); + expect(branchName === 'master' || branchName === 'main').toBe(true); + } finally { + process.chdir(originalCwd); + fs.rmSync(newRepoDir, { recursive: true, force: true }); + } + }); }); describe('getStatusForFile', () => { From a9ac3951cd32190083689e04a07b39e02e177a99 Mon Sep 17 00:00:00 2001 From: IanSkelskey Date: Thu, 26 Jun 2025 03:19:38 +0000 Subject: [PATCH 06/12] Prettified Code! --- src/util/git.ts | 12 ++++++------ tests/util/git.test.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/util/git.ts b/src/util/git.ts index 24334c6..99b973c 100644 --- a/src/util/git.ts +++ b/src/util/git.ts @@ -120,26 +120,26 @@ function removeDiffForFile(diff: string, filePath: string): string { export function getCurrentBranchName(): string { try { 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 + 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' + encoding: 'utf-8', }).trim(); return branchName; } catch { @@ -147,7 +147,7 @@ export function getCurrentBranchName(): string { // Try to get the default branch name from config try { const defaultBranch = execSync(`git -C "${gitRoot}" config init.defaultBranch`, { - encoding: 'utf-8' + encoding: 'utf-8', }).trim(); return defaultBranch || 'main'; // Default to 'main' if not configured } catch { diff --git a/tests/util/git.test.ts b/tests/util/git.test.ts index 5e704cb..816f5d4 100644 --- a/tests/util/git.test.ts +++ b/tests/util/git.test.ts @@ -73,16 +73,16 @@ describe('Git Utilities Integration Tests', () => { const branchName = getCurrentBranchName(); expect(branchName === 'master' || branchName === 'main').toBe(true); }); - + test('should handle new repository with no commits', () => { // Create a new test directory for this specific test const newRepoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'new-repo-')); const originalCwd = process.cwd(); - + try { process.chdir(newRepoDir); execSync('git init'); - + // Should still be able to get branch name even with no commits const branchName = getCurrentBranchName(); expect(branchName === 'master' || branchName === 'main').toBe(true); From 598c8c9a82083b80c195f676d5cff091be61312b Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 23:21:10 -0400 Subject: [PATCH 07/12] Improve error handling in getCurrentBranchName function Enhance error handling in `getCurrentBranchName` by trailing commas for execSync calls configuration - Clean up whitespace for consistent formatting - Ensure errors are captured and a default branch is used where relevant Release-Note: improve error handling in branch name retrieval function Signed-off-by: Ian Skelskey --- src/util/git.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/util/git.ts b/src/util/git.ts index 24334c6..99b973c 100644 --- a/src/util/git.ts +++ b/src/util/git.ts @@ -120,26 +120,26 @@ function removeDiffForFile(diff: string, filePath: string): string { export function getCurrentBranchName(): string { try { 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 + 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' + encoding: 'utf-8', }).trim(); return branchName; } catch { @@ -147,7 +147,7 @@ export function getCurrentBranchName(): string { // Try to get the default branch name from config try { const defaultBranch = execSync(`git -C "${gitRoot}" config init.defaultBranch`, { - encoding: 'utf-8' + encoding: 'utf-8', }).trim(); return defaultBranch || 'main'; // Default to 'main' if not configured } catch { From a08d88d6c249aad8c8b6e3b972c9c26d31a88971 Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 23:22:12 -0400 Subject: [PATCH 08/12] Refactor Git utilities tests for style consistency Clean up whitespace inconsistencies in the Git utilities integration tests - Remove trailing spaces and ensure uniform spacing throughout the test file for better readability - Simplify setup process by ensuring clear separation between logical sections Signed-off-by: Ian Skelskey --- tests/util/git.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/util/git.test.ts b/tests/util/git.test.ts index 5e704cb..816f5d4 100644 --- a/tests/util/git.test.ts +++ b/tests/util/git.test.ts @@ -73,16 +73,16 @@ describe('Git Utilities Integration Tests', () => { const branchName = getCurrentBranchName(); expect(branchName === 'master' || branchName === 'main').toBe(true); }); - + test('should handle new repository with no commits', () => { // Create a new test directory for this specific test const newRepoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'new-repo-')); const originalCwd = process.cwd(); - + try { process.chdir(newRepoDir); execSync('git init'); - + // Should still be able to get branch name even with no commits const branchName = getCurrentBranchName(); expect(branchName === 'master' || branchName === 'main').toBe(true); From fda751484e957503734b649a1d0cf3e9ec02514c Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 23:31:30 -0400 Subject: [PATCH 09/12] Handle missing Git user config with error guidance Ensure `getUserInfo` handles cases where Git user name or email is not configured by providing detailed error messages and guidance for setting these values. Update `getName` and `getEmail` functions to properly handle errors when Git user information is not set. - Add error handling to `getName` and `getEmail` to return empty strings if Git is not configured. - Extend `getUserInfo` to suggest configuration commands when name or email is missing. - Add integration tests to verify behavior when Git user configurations are missing. Release-Note: Improve user guidance for missing Git config in commit process Signed-off-by: Ian Skelskey --- src/cmd/commit.ts | 21 +++++++++++++++++++++ src/util/git.ts | 12 ++++++++++-- tests/util/git.test.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/cmd/commit.ts b/src/cmd/commit.ts index e5e702b..9df6838 100644 --- a/src/cmd/commit.ts +++ b/src/cmd/commit.ts @@ -131,6 +131,27 @@ async function getStagedFiles(): Promise { 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 "your.email@example.com"'); + print('info', ' OR'); + print('info', ' evergit config set email "your.email@example.com"'); + } + throw new Error('Missing git configuration'); + } + return { name, email, diff --git a/src/util/git.ts b/src/util/git.ts index 99b973c..29f9772 100644 --- a/src/util/git.ts +++ b/src/util/git.ts @@ -169,11 +169,19 @@ export function hasGitChanges(): boolean { } 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 { diff --git a/tests/util/git.test.ts b/tests/util/git.test.ts index 816f5d4..c753475 100644 --- a/tests/util/git.test.ts +++ b/tests/util/git.test.ts @@ -150,6 +150,40 @@ describe('Git Utilities Integration Tests', () => { expect(getName()).toBe('Test User'); expect(getEmail()).toBe('test@example.com'); }); + + test('should return empty string when name is not configured', () => { + const { getName } = require('../../src/util/git'); + const childProcess = require('child_process'); + const originalExecSync = childProcess.execSync; + childProcess.execSync = jest.fn((cmd: string, options?: any) => { + if (typeof cmd === 'string' && cmd === 'git config user.name') { + throw new Error('No user.name set'); + } + return originalExecSync(cmd, options); + }); + try { + expect(getName()).toBe(''); + } finally { + childProcess.execSync = originalExecSync; + } + }); + + test('should return empty string when email is not configured', () => { + const { getEmail } = require('../../src/util/git'); + const childProcess = require('child_process'); + const originalExecSync = childProcess.execSync; + childProcess.execSync = jest.fn((cmd: string, options?: any) => { + if (typeof cmd === 'string' && cmd === 'git config user.email') { + throw new Error('No user.email set'); + } + return originalExecSync(cmd, options); + }); + try { + expect(getEmail()).toBe(''); + } finally { + childProcess.execSync = originalExecSync; + } + }); }); describe('commitWithMessage', () => { From 4eb4ff769a23fd316724a4f256a55cc3cba298dd Mon Sep 17 00:00:00 2001 From: IanSkelskey Date: Thu, 26 Jun 2025 03:31:44 +0000 Subject: [PATCH 10/12] Prettified Code! --- src/cmd/commit.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmd/commit.ts b/src/cmd/commit.ts index 9df6838..a3bf737 100644 --- a/src/cmd/commit.ts +++ b/src/cmd/commit.ts @@ -131,12 +131,12 @@ async function getStagedFiles(): Promise { 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) { @@ -151,7 +151,7 @@ function getUserInfo(): { name: string; email: string; diff: string } { } throw new Error('Missing git configuration'); } - + return { name, email, From 24a3f0d6a1d347dcf4f05cf549d8188466ef3f60 Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 23:38:17 -0400 Subject: [PATCH 11/12] Update Evergit to version 0.2.1 with enhanced features Version 0.2.1 introduces several key improvements: - Enhanced error handling and support for new repositories in `getCurrentBranchName`. - Added integration tests and refactored Git utilities tests for consistency. - Introduced git root directory support in Git functions. - Improved code quality with prettier formatting. Release-Note: Update includes enhanced error handling, testing, and new git directory support. Signed-off-by: Ian Skelskey --- CHANGELOG.md | 25 +++++++++++++++++++++++++ README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/cmd/commit.ts | 6 +++--- src/main.ts | 2 +- 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6890aa..c0cc630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 83c35ce..efb1009 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/package-lock.json b/package-lock.json index 2358a5c..f4cf1bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "evergit", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "evergit", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "@types/oauth": "^0.9.6", diff --git a/package.json b/package.json index e273d7f..a620502 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/cmd/commit.ts b/src/cmd/commit.ts index 9df6838..a3bf737 100644 --- a/src/cmd/commit.ts +++ b/src/cmd/commit.ts @@ -131,12 +131,12 @@ async function getStagedFiles(): Promise { 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) { @@ -151,7 +151,7 @@ function getUserInfo(): { name: string; email: string; diff: string } { } throw new Error('Missing git configuration'); } - + return { name, email, diff --git a/src/main.ts b/src/main.ts index e3adae5..9b799f0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,7 +21,7 @@ function getGitEditor(): Promise { } export async function main(args = process.argv): Promise { - 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') From b89e642fe4b7dcaea9d00deef36bf1c1c630e008 Mon Sep 17 00:00:00 2001 From: Ian Skelskey Date: Wed, 25 Jun 2025 23:41:32 -0400 Subject: [PATCH 12/12] Update GitHub Action to use upload-artifact@v4 This commit updates the GitHub Actions workflow to utilize `actions/upload-artifact@v4` instead of the previous version. This change is intended to leverage improvements and fixes available in the newer version of the action. Release-Note: update GitHub Action to upload-artifact@v4 Signed-off-by: Ian Skelskey --- .github/workflows/pre-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-publish.yml b/.github/workflows/pre-publish.yml index b0c3687..b6c714d 100644 --- a/.github/workflows/pre-publish.yml +++ b/.github/workflows/pre-publish.yml @@ -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