Skip to content

Commit ee5a711

Browse files
feat(source-control): add Graph section with commit history and hover popover
Adds a full Git Graph section below the Source Control file list, with commit history, Sync Changes flow, More Actions dropdown, and a rich hover popover per commit. - Add git:log IPC handler: parses commit subject, body, author, date, branch refs, and per-commit --stat (insertions/deletions/files changed) - Add git:pull IPC handler (pull --rebase) - Expose gitLog and gitPull in preload/index.ts - Graph section in SourceControlPanel: - Collapsible header with Fetch, Pull, Push action buttons - Commit rows: graph dot + line, message, branch badge, author avatar - Hover popover: avatar, author (indigo), date + full date, commit subject, bullet-point body, files changed stat with green/red insertion/deletion counts, hash copy button, Open on GitHub link - Popover position clamped to panel bounds using measured height - Sync Changes button replaces Commit after a successful commit - More Actions dropdown (opens downward): Commit, Commit & Push, Commit & Sync
1 parent 88001e7 commit ee5a711

3 files changed

Lines changed: 335 additions & 158 deletions

File tree

src/main/index.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,3 +1308,54 @@ ipcMain.handle('git:pull', async (_event, cwd: string) => {
13081308
}
13091309
})
13101310

1311+
ipcMain.handle('git:log', async (_event, cwd: string) => {
1312+
try {
1313+
const branch = await gitExec(cwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
1314+
1315+
// Get commit list with separator to handle multi-line bodies
1316+
const stdout = await gitExec(cwd, [
1317+
'log', '--max-count=50',
1318+
'--pretty=format:COMMIT_START%n%H%n%h%n%s%n%b%nCOMMIT_META%n%an%n%ar%n%ai%n%D%nCOMMIT_END'
1319+
])
1320+
1321+
const rawCommits = stdout.split('COMMIT_START\n').filter(Boolean)
1322+
1323+
const commits = await Promise.all(rawCommits.map(async raw => {
1324+
const metaIdx = raw.indexOf('\nCOMMIT_META\n')
1325+
const endIdx = raw.indexOf('\nCOMMIT_END')
1326+
const header = raw.slice(0, metaIdx).split('\n')
1327+
const meta = raw.slice(metaIdx + 13, endIdx).split('\n')
1328+
1329+
const hash = (header[0] || '').trim()
1330+
const shortHash = (header[1] || '').trim()
1331+
const subject = (header[2] || '').trim()
1332+
const body = header.slice(3).join('\n').trim()
1333+
const author = (meta[0] || '').trim()
1334+
const date = (meta[1] || '').trim()
1335+
const fullDate = (meta[2] || '').trim()
1336+
const refs = (meta[3] || '').trim()
1337+
1338+
const branches = refs.split(',').map((r: string) => r.trim()).filter((r: string) => r && !r.startsWith('HEAD') && !r.startsWith('origin/'))
1339+
1340+
// Get stat for this commit
1341+
let insertions = 0, deletions = 0, filesChanged = 0
1342+
try {
1343+
const stat = await gitExec(cwd, ['show', '--stat', '--format=', hash])
1344+
const statLine = stat.trim().split('\n').pop() || ''
1345+
const ins = statLine.match(/(\d+) insertion/)
1346+
const del = statLine.match(/(\d+) deletion/)
1347+
const fil = statLine.match(/(\d+) file/)
1348+
if (ins) insertions = parseInt(ins[1])
1349+
if (del) deletions = parseInt(del[1])
1350+
if (fil) filesChanged = parseInt(fil[1])
1351+
} catch {}
1352+
1353+
return { hash, shortHash, message: subject, body, author, date, fullDate, branch: branches[0] || null, insertions, deletions, filesChanged }
1354+
}))
1355+
1356+
return { success: true, commits, currentBranch: branch.trim() }
1357+
} catch (err: any) {
1358+
return { success: false, error: err.message, commits: [], currentBranch: '' }
1359+
}
1360+
})
1361+

src/preload/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ contextBridge.exposeInMainWorld('koda', {
7777
gitCommit: (cwd: string, message: string) => ipcRenderer.invoke('git:commit', cwd, message),
7878
gitPush: (cwd: string) => ipcRenderer.invoke('git:push', cwd),
7979
gitPull: (cwd: string) => ipcRenderer.invoke('git:pull', cwd),
80+
gitLog: (cwd: string) => ipcRenderer.invoke('git:log', cwd),
8081
// Window controls
8182
minimize: () => ipcRenderer.invoke('window:minimize'),
8283
maximize: () => ipcRenderer.invoke('window:maximize'),

0 commit comments

Comments
 (0)