diff --git a/CHANGELOG.md b/CHANGELOG.md index a60ea9f..5c106f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +- Highlight commits that match staged files + ## 1.1.0 (2025-01-05) - Dynamic hash length diff --git a/images/commit-dark-highlighted.svg b/images/commit-dark-highlighted.svg new file mode 100644 index 0000000..94f0dd2 --- /dev/null +++ b/images/commit-dark-highlighted.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/commit-dark-upstream-highlighted.svg b/images/commit-dark-upstream-highlighted.svg new file mode 100644 index 0000000..91b19a8 --- /dev/null +++ b/images/commit-dark-upstream-highlighted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/commit-dark-upstream.svg b/images/commit-dark-upstream.svg new file mode 100644 index 0000000..d9ba8d9 --- /dev/null +++ b/images/commit-dark-upstream.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/commit-dark.svg b/images/commit-dark.svg new file mode 100644 index 0000000..e53384a --- /dev/null +++ b/images/commit-dark.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/images/commit-light-highlighted.svg b/images/commit-light-highlighted.svg new file mode 100644 index 0000000..03471c3 --- /dev/null +++ b/images/commit-light-highlighted.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/commit-light-upstream-highlighted.svg b/images/commit-light-upstream-highlighted.svg new file mode 100644 index 0000000..4abda1d --- /dev/null +++ b/images/commit-light-upstream-highlighted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/commit-light-upstream.svg b/images/commit-light-upstream.svg new file mode 100644 index 0000000..685d698 --- /dev/null +++ b/images/commit-light-upstream.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/commit-light.svg b/images/commit-light.svg new file mode 100644 index 0000000..b3bf4b6 --- /dev/null +++ b/images/commit-light.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/GitFacade.ts b/src/GitFacade.ts index cee36fc..46d9c14 100644 --- a/src/GitFacade.ts +++ b/src/GitFacade.ts @@ -3,6 +3,15 @@ import { BranchName, Commit, ShortCommitHash } from './types' export const NO_UPSTREAM = 'NO_UPSTREAM' +const toLines = (output: string): string[] => { + const trimmed = output.replace(/\n$/, '') + if (trimmed !== '') { + return trimmed.split('\n') + } else { + return [] + } +} + export class GitFacade { private readonly git: SimpleGit @@ -15,11 +24,6 @@ export class GitFacade { this.git.cwd(path) } - async hasStagedFiles(): Promise { - const diff = await this.git.diff(['--name-only', '--cached']) - return (diff.trim() !== '') - } - async getCurrentBranch(): Promise { const branchSummary = await this.git.branch() return branchSummary.current @@ -64,7 +68,7 @@ export class GitFacade { } private async queryCommits(...args: string[]): Promise { - const lines = (await this.git.raw(['log', ...args, '--format=%h %s'])).trim().split('\n') + const lines = toLines(await this.git.raw(['log', ...args, '--format=%h %s'])) return lines.map((line) => { const separatorIndex = line.indexOf(' ') const hash = line.slice(0, separatorIndex) @@ -73,7 +77,6 @@ export class GitFacade { }) } - async commitFixup(hash: ShortCommitHash): Promise { await this.git.commit('', undefined, { '--fixup': hash }) } @@ -92,4 +95,12 @@ export class GitFacade { throw e } } + + async getStagedFiles(): Promise { + return toLines(await this.git.diff(['--cached', '--name-only'])) + } + + async getModifiedFiles(hash: ShortCommitHash): Promise { + return toLines(await this.git.raw(['show', '--name-only', '--pretty=format:', hash])) + } } diff --git a/src/extension.ts b/src/extension.ts index 4b05fc0..ba30968 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode' import { GitFacade, NO_UPSTREAM } from './GitFacade' -import { Commit } from './types' export const activate = (context: vscode.ExtensionContext): void => { @@ -11,6 +10,25 @@ export const activate = (context: vscode.ExtensionContext): void => { outputChannel.appendLine(`${new Date().toISOString()} ${message}`) } + const getCommitIcon = (isInUpstream: boolean, isHighlighted: boolean): vscode.IconPath => { + const getThemeIconUri = (theme: 'light' | 'dark'): vscode.Uri => { + const fileNameParts: string[] = [] + fileNameParts.push('commit') + fileNameParts.push(theme) + if (isInUpstream) { + fileNameParts.push('upstream') + } + if (isHighlighted) { + fileNameParts.push('highlighted') + } + return vscode.Uri.joinPath(context.extensionUri, 'images', `${fileNameParts.join('-')}.svg`) + } + return { + light: getThemeIconUri('light'), + dark: getThemeIconUri('dark') + } + } + const disposable = vscode.commands.registerCommand('git-fixup.amendStagedChanges', async () => { try { @@ -21,8 +39,8 @@ export const activate = (context: vscode.ExtensionContext): void => { } git.updateWorkingDirectory(workspaceFolder) - const hasStagedFiles = await git.hasStagedFiles() - if (!hasStagedFiles) { + const stagedFiles = await git.getStagedFiles() + if (stagedFiles.length === 0) { vscode.window.showErrorMessage('No staged files') return } @@ -39,16 +57,19 @@ export const activate = (context: vscode.ExtensionContext): void => { } const commitsNotInUpstream = await git.getCommitsNotInUpstream() - const commitChoices = selectableCommits.map((commit: Commit) => { + const commitChoices = await Promise.all((selectableCommits.map(async (commit) => { const isInUpstream = (commitsNotInUpstream !== NO_UPSTREAM) && (commitsNotInUpstream.find((upstreamCommit) => upstreamCommit.hash === commit.hash) === undefined) + const modifiedFiles = await git.getModifiedFiles(commit.hash) + const isHighlighted = modifiedFiles.some((filePath) => stagedFiles.includes(filePath)) const messageSubject = commit.subject return { - label: `${isInUpstream ? '$(cloud)' : '$(git-commit)'} ${messageSubject}`, + iconPath: getCommitIcon(isInUpstream, isHighlighted), + label: ` ${messageSubject}`, hash: commit.hash, messageSubject, - isInUpstream: isInUpstream + isInUpstream } - }) + }))) const selectedCommit = await vscode.window.showQuickPick(commitChoices, { placeHolder: 'Select a Git commit to fix up' }) if (selectedCommit !== undefined) { diff --git a/test/GitFacade.test.ts b/test/GitFacade.test.ts index 4236434..8363fab 100644 --- a/test/GitFacade.test.ts +++ b/test/GitFacade.test.ts @@ -68,11 +68,24 @@ describe('GitFacade', () => { expect(commits.map((c) => c.subject)).toEqual(['subject 3', 'subject 2', 'subject 1']) }) - it('hasStagedFiles()', async () => { - expect(await facade.hasStagedFiles()).toBe(false) + it('getFeatureBranchCommits()', async () => { + const branchName = `feature-${Date.now()}` + await git.checkoutLocalBranch(branchName) + expect(await facade.getFeatureBranchCommits(branchName, 'main')).toHaveLength(0) + await createCommit('foo', 'bar') + expect(await facade.getFeatureBranchCommits(branchName, 'main')).toHaveLength(1) + }) + + it('getStagedFiles()', async () => { + expect(await facade.getStagedFiles()).toEqual([]) await modifyFileAndStageChanges('foobar') - expect(await facade.hasStagedFiles()).toBe(true) + expect(await facade.getStagedFiles()).toEqual(['file.txt']) await git.commit('-') - expect(await facade.hasStagedFiles()).toBe(false) + expect(await facade.getStagedFiles()).toEqual([]) + }) + + it('getModifiedFiles()', async () => { + const hash = (await git.raw(['rev-parse', '--short', 'HEAD'])).trim() + expect(await facade.getModifiedFiles(hash)).toEqual(['file.txt']) }) })