From 3ae82153d99b89f7dc189631f3416c30bb55a83e Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Mon, 29 Dec 2025 01:13:10 +1030 Subject: [PATCH] feat(commits): implement commit history retrieval and display in the database view --- src/lib/server/utils/git/Git.ts | 3 +- src/lib/server/utils/git/status.ts | 45 +++- src/lib/server/utils/git/types.ts | 10 + .../api/databases/[id]/changes/+server.ts | 35 ++++ .../api/databases/[id]/commits/+server.ts | 27 +++ src/routes/databases/+page.svelte | 48 +++-- src/routes/databases/[id]/+layout.svelte | 24 ++- src/routes/databases/[id]/+page.server.ts | 11 +- .../databases/[id]/changes/+page.server.ts | 20 +- .../databases/[id]/changes/+page.svelte | 194 ++++++++++-------- .../databases/[id]/commits/+page.server.ts | 5 + .../databases/[id]/commits/+page.svelte | 171 +++++++++++++++ 12 files changed, 459 insertions(+), 134 deletions(-) create mode 100644 src/routes/api/databases/[id]/changes/+server.ts create mode 100644 src/routes/api/databases/[id]/commits/+server.ts create mode 100644 src/routes/databases/[id]/commits/+page.server.ts create mode 100644 src/routes/databases/[id]/commits/+page.svelte diff --git a/src/lib/server/utils/git/Git.ts b/src/lib/server/utils/git/Git.ts index 071b95f..a62dd12 100644 --- a/src/lib/server/utils/git/Git.ts +++ b/src/lib/server/utils/git/Git.ts @@ -2,7 +2,7 @@ * Git class - wraps git operations for a repository */ -import type { GitStatus, OperationFile, CommitResult, UpdateInfo, RepoInfo } from './types.ts'; +import type { GitStatus, OperationFile, CommitResult, UpdateInfo, Commit } from './types.ts'; import * as repo from './repo.ts'; import * as status from './status.ts'; import * as ops from './ops.ts'; @@ -23,6 +23,7 @@ export class Git { status = (options?: status.GetStatusOptions): Promise => status.getStatus(this.repoPath, options); checkForUpdates = (): Promise => status.checkForUpdates(this.repoPath); getLastPushed = () => status.getLastPushed(this.repoPath); + getCommits = (limit?: number): Promise => status.getCommits(this.repoPath, limit); // Operation file methods getUncommittedOps = (): Promise => ops.getUncommittedOps(this.repoPath); diff --git a/src/lib/server/utils/git/status.ts b/src/lib/server/utils/git/status.ts index 0f0b8ae..2005d0a 100644 --- a/src/lib/server/utils/git/status.ts +++ b/src/lib/server/utils/git/status.ts @@ -4,7 +4,7 @@ import { execGit, execGitSafe } from './exec.ts'; import { fetch } from './repo.ts'; -import type { GitStatus, UpdateInfo } from './types.ts'; +import type { GitStatus, UpdateInfo, Commit } from './types.ts'; /** * Get current branch name @@ -132,3 +132,46 @@ export async function getBranches(repoPath: string): Promise { return branches; } + +/** + * Get commit history + */ +export async function getCommits(repoPath: string, limit: number = 50): Promise { + // Format: hash|shortHash|message|author|email|date + const format = '%H|%h|%s|%an|%ae|%cI'; + const output = await execGit( + ['log', `--format=${format}`, `-${limit}`], + repoPath + ); + + if (!output.trim()) { + return []; + } + + const commits: Commit[] = []; + + for (const line of output.split('\n')) { + if (!line.trim()) continue; + + const [hash, shortHash, message, author, authorEmail, date] = line.split('|'); + + // Get files changed for this commit + const statOutput = await execGitSafe( + ['diff-tree', '--no-commit-id', '--name-only', '-r', hash], + repoPath + ); + const files = statOutput ? statOutput.split('\n').filter(f => f.trim()) : []; + + commits.push({ + hash, + shortHash, + message, + author, + authorEmail, + date, + files + }); + } + + return commits; +} diff --git a/src/lib/server/utils/git/types.ts b/src/lib/server/utils/git/types.ts index 1bcaf45..bba2f07 100644 --- a/src/lib/server/utils/git/types.ts +++ b/src/lib/server/utils/git/types.ts @@ -45,3 +45,13 @@ export interface RepoInfo { ownerType: 'User' | 'Organization'; htmlUrl: string; } + +export interface Commit { + hash: string; + shortHash: string; + message: string; + author: string; + authorEmail: string; + date: string; + files: string[]; +} diff --git a/src/routes/api/databases/[id]/changes/+server.ts b/src/routes/api/databases/[id]/changes/+server.ts new file mode 100644 index 0000000..2872b46 --- /dev/null +++ b/src/routes/api/databases/[id]/changes/+server.ts @@ -0,0 +1,35 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from '@sveltejs/kit'; +import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts'; +import { Git, getRepoInfo } from '$utils/git/index.ts'; + +export const GET: RequestHandler = async ({ params }) => { + const id = parseInt(params.id || '', 10); + const database = databaseInstancesQueries.getById(id); + + if (!database) { + error(404, 'Database not found'); + } + + if (!database.personal_access_token) { + error(403, 'Changes page requires a personal access token'); + } + + const git = new Git(database.local_path); + + const [status, uncommittedOps, lastPushed, branches, repoInfo] = await Promise.all([ + git.status(), + git.getUncommittedOps(), + git.getLastPushed(), + git.getBranches(), + getRepoInfo(database.repository_url, database.personal_access_token) + ]); + + return json({ + status, + uncommittedOps, + lastPushed, + branches, + repoInfo + }); +}; diff --git a/src/routes/api/databases/[id]/commits/+server.ts b/src/routes/api/databases/[id]/commits/+server.ts new file mode 100644 index 0000000..c7f8cb6 --- /dev/null +++ b/src/routes/api/databases/[id]/commits/+server.ts @@ -0,0 +1,27 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from '@sveltejs/kit'; +import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts'; +import { Git } from '$utils/git/index.ts'; + +export const GET: RequestHandler = async ({ params, url }) => { + const id = parseInt(params.id || '', 10); + const database = databaseInstancesQueries.getById(id); + + if (!database) { + error(404, 'Database not found'); + } + + const limit = parseInt(url.searchParams.get('limit') || '50', 10); + const git = new Git(database.local_path); + + const [commits, branch] = await Promise.all([ + git.getCommits(limit), + git.getBranch() + ]); + + return json({ + commits, + branch, + repositoryUrl: database.repository_url + }); +}; diff --git a/src/routes/databases/+page.svelte b/src/routes/databases/+page.svelte index 27640ff..4d28124 100644 --- a/src/routes/databases/+page.svelte +++ b/src/routes/databases/+page.svelte @@ -1,5 +1,5 @@ @@ -147,33 +130,80 @@
- + + {#if loading || !status} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {:else} + + {/if} - + {#if loading} +
+
+
+
+
+
+
+ {:else} + + {/if} - {#if data.uncommittedOps.length === 0} -
+ {#if loading} +
+ + + + + + + + + + + + {#each Array(5) as _} + + + + + + + + {/each} + +
OperationEntityNameFile
+
+
+
+ {:else if uncommittedOps.length === 0} +

No uncommitted changes

{:else}
- + - - - - - - {#each data.uncommittedOps as op} + + {#each uncommittedOps as op} toggleRow(op.filepath)} @@ -230,11 +250,7 @@ @@ -244,7 +260,7 @@
+ Operation + Entity + Name + File
- + {formatOperation(op.operation)} {#if op.previousName && op.previousName !== op.name} {op.previousName} - → + → {op.name || '-'} {:else} {op.name || '-'} diff --git a/src/routes/databases/[id]/commits/+page.server.ts b/src/routes/databases/[id]/commits/+page.server.ts new file mode 100644 index 0000000..45fd039 --- /dev/null +++ b/src/routes/databases/[id]/commits/+page.server.ts @@ -0,0 +1,5 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + return {}; +}; diff --git a/src/routes/databases/[id]/commits/+page.svelte b/src/routes/databases/[id]/commits/+page.svelte new file mode 100644 index 0000000..b55c77b --- /dev/null +++ b/src/routes/databases/[id]/commits/+page.svelte @@ -0,0 +1,171 @@ + + + + Commits - {data.database.name} - Profilarr + + +
+ {#if loading} + +
+ + + + + + + + + + + + {#each Array(10) as _} + + + + + + + + {/each} + +
CommitMessageAuthorDate
+
+ {:else} + row.hash} + emptyMessage="No commits found" + defaultSort={{ key: 'date', direction: 'desc' }} + > + + {#if column.key === 'shortHash'} + + {row.shortHash} + + + {:else if column.key === 'message'} + + {row.message} + + {:else if column.key === 'author'} + + {row.author} + + {:else if column.key === 'date'} + + {formatDate(row.date)} + + {/if} + + + +
+
+ + {row.files.length} file{row.files.length !== 1 ? 's' : ''} changed +
+ {#if row.files.length > 0} +
+ {#each row.files as file} + + {file} + + {/each} +
+ {/if} +
+
+
+ {/if} +