Skip to content

Commit 2df6a5e

Browse files
authored
feat: Highlight commit that match staged files (#10)
The commit selection Quick Pick highlights commits that include the same files currently in the staging area.
1 parent 0202e94 commit 2df6a5e

12 files changed

+99
-18
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
- Highlight commits that match staged files
11+
812
## 1.1.0 (2025-01-05)
913

1014
- Dynamic hash length

images/commit-dark-highlighted.svg

+4
Loading
Loading

images/commit-dark-upstream.svg

+4
Loading

images/commit-dark.svg

+3
Loading

images/commit-light-highlighted.svg

+4
Loading
Loading

images/commit-light-upstream.svg

+4
Loading

images/commit-light.svg

+3
Loading

src/GitFacade.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import { BranchName, Commit, ShortCommitHash } from './types'
33

44
export const NO_UPSTREAM = 'NO_UPSTREAM'
55

6+
const toLines = (output: string): string[] => {
7+
const trimmed = output.replace(/\n$/, '')
8+
if (trimmed !== '') {
9+
return trimmed.split('\n')
10+
} else {
11+
return []
12+
}
13+
}
14+
615
export class GitFacade {
716

817
private readonly git: SimpleGit
@@ -15,11 +24,6 @@ export class GitFacade {
1524
this.git.cwd(path)
1625
}
1726

18-
async hasStagedFiles(): Promise<boolean> {
19-
const diff = await this.git.diff(['--name-only', '--cached'])
20-
return (diff.trim() !== '')
21-
}
22-
2327
async getCurrentBranch(): Promise<BranchName> {
2428
const branchSummary = await this.git.branch()
2529
return branchSummary.current
@@ -64,7 +68,7 @@ export class GitFacade {
6468
}
6569

6670
private async queryCommits(...args: string[]): Promise<Commit[]> {
67-
const lines = (await this.git.raw(['log', ...args, '--format=%h %s'])).trim().split('\n')
71+
const lines = toLines(await this.git.raw(['log', ...args, '--format=%h %s']))
6872
return lines.map((line) => {
6973
const separatorIndex = line.indexOf(' ')
7074
const hash = line.slice(0, separatorIndex)
@@ -73,7 +77,6 @@ export class GitFacade {
7377
})
7478
}
7579

76-
7780
async commitFixup(hash: ShortCommitHash): Promise<void> {
7881
await this.git.commit('', undefined, { '--fixup': hash })
7982
}
@@ -92,4 +95,12 @@ export class GitFacade {
9295
throw e
9396
}
9497
}
98+
99+
async getStagedFiles(): Promise<string[]> {
100+
return toLines(await this.git.diff(['--cached', '--name-only']))
101+
}
102+
103+
async getModifiedFiles(hash: ShortCommitHash): Promise<string[]> {
104+
return toLines(await this.git.raw(['show', '--name-only', '--pretty=format:', hash]))
105+
}
95106
}

src/extension.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as vscode from 'vscode'
22
import { GitFacade, NO_UPSTREAM } from './GitFacade'
3-
import { Commit } from './types'
43

54
export const activate = (context: vscode.ExtensionContext): void => {
65

@@ -11,6 +10,25 @@ export const activate = (context: vscode.ExtensionContext): void => {
1110
outputChannel.appendLine(`${new Date().toISOString()} ${message}`)
1211
}
1312

13+
const getCommitIcon = (isInUpstream: boolean, isHighlighted: boolean): vscode.IconPath => {
14+
const getThemeIconUri = (theme: 'light' | 'dark'): vscode.Uri => {
15+
const fileNameParts: string[] = []
16+
fileNameParts.push('commit')
17+
fileNameParts.push(theme)
18+
if (isInUpstream) {
19+
fileNameParts.push('upstream')
20+
}
21+
if (isHighlighted) {
22+
fileNameParts.push('highlighted')
23+
}
24+
return vscode.Uri.joinPath(context.extensionUri, 'images', `${fileNameParts.join('-')}.svg`)
25+
}
26+
return {
27+
light: getThemeIconUri('light'),
28+
dark: getThemeIconUri('dark')
29+
}
30+
}
31+
1432
const disposable = vscode.commands.registerCommand('git-fixup.amendStagedChanges', async () => {
1533

1634
try {
@@ -21,8 +39,8 @@ export const activate = (context: vscode.ExtensionContext): void => {
2139
}
2240
git.updateWorkingDirectory(workspaceFolder)
2341

24-
const hasStagedFiles = await git.hasStagedFiles()
25-
if (!hasStagedFiles) {
42+
const stagedFiles = await git.getStagedFiles()
43+
if (stagedFiles.length === 0) {
2644
vscode.window.showErrorMessage('No staged files')
2745
return
2846
}
@@ -39,16 +57,19 @@ export const activate = (context: vscode.ExtensionContext): void => {
3957
}
4058

4159
const commitsNotInUpstream = await git.getCommitsNotInUpstream()
42-
const commitChoices = selectableCommits.map((commit: Commit) => {
60+
const commitChoices = await Promise.all((selectableCommits.map(async (commit) => {
4361
const isInUpstream = (commitsNotInUpstream !== NO_UPSTREAM) && (commitsNotInUpstream.find((upstreamCommit) => upstreamCommit.hash === commit.hash) === undefined)
62+
const modifiedFiles = await git.getModifiedFiles(commit.hash)
63+
const isHighlighted = modifiedFiles.some((filePath) => stagedFiles.includes(filePath))
4464
const messageSubject = commit.subject
4565
return {
46-
label: `${isInUpstream ? '$(cloud)' : '$(git-commit)'} ${messageSubject}`,
66+
iconPath: getCommitIcon(isInUpstream, isHighlighted),
67+
label: ` ${messageSubject}`,
4768
hash: commit.hash,
4869
messageSubject,
49-
isInUpstream: isInUpstream
70+
isInUpstream
5071
}
51-
})
72+
})))
5273
const selectedCommit = await vscode.window.showQuickPick(commitChoices, { placeHolder: 'Select a Git commit to fix up' })
5374

5475
if (selectedCommit !== undefined) {

test/GitFacade.test.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,24 @@ describe('GitFacade', () => {
6868
expect(commits.map((c) => c.subject)).toEqual(['subject 3', 'subject 2', 'subject 1'])
6969
})
7070

71-
it('hasStagedFiles()', async () => {
72-
expect(await facade.hasStagedFiles()).toBe(false)
71+
it('getFeatureBranchCommits()', async () => {
72+
const branchName = `feature-${Date.now()}`
73+
await git.checkoutLocalBranch(branchName)
74+
expect(await facade.getFeatureBranchCommits(branchName, 'main')).toHaveLength(0)
75+
await createCommit('foo', 'bar')
76+
expect(await facade.getFeatureBranchCommits(branchName, 'main')).toHaveLength(1)
77+
})
78+
79+
it('getStagedFiles()', async () => {
80+
expect(await facade.getStagedFiles()).toEqual([])
7381
await modifyFileAndStageChanges('foobar')
74-
expect(await facade.hasStagedFiles()).toBe(true)
82+
expect(await facade.getStagedFiles()).toEqual(['file.txt'])
7583
await git.commit('-')
76-
expect(await facade.hasStagedFiles()).toBe(false)
84+
expect(await facade.getStagedFiles()).toEqual([])
85+
})
86+
87+
it('getModifiedFiles()', async () => {
88+
const hash = (await git.raw(['rev-parse', '--short', 'HEAD'])).trim()
89+
expect(await facade.getModifiedFiles(hash)).toEqual(['file.txt'])
7790
})
7891
})

0 commit comments

Comments
 (0)