From 780adf0850185738a1a811e145cf7e094bace54b Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Sat, 14 Mar 2026 04:36:12 +0000 Subject: [PATCH] refactor: extract git to separate package --- package-lock.json | 14 ++++ package.json | 3 +- packages/cli/package.json | 1 + packages/cli/src/git.ts | 156 ------------------------------------ packages/cli/src/index.ts | 2 +- packages/cli/src/server.ts | 16 ++-- packages/cli/tsconfig.json | 1 + packages/git/package.json | 23 ++++++ packages/git/src/commits.ts | 15 ++++ packages/git/src/diff.ts | 31 +++++++ packages/git/src/exec.ts | 26 ++++++ packages/git/src/index.ts | 5 ++ packages/git/src/repo.ts | 37 +++++++++ packages/git/src/status.ts | 9 +++ packages/git/src/types.ts | 12 +++ packages/git/tsconfig.json | 9 +++ tsconfig.json | 1 + 17 files changed, 195 insertions(+), 166 deletions(-) delete mode 100644 packages/cli/src/git.ts create mode 100644 packages/git/package.json create mode 100644 packages/git/src/commits.ts create mode 100644 packages/git/src/diff.ts create mode 100644 packages/git/src/exec.ts create mode 100644 packages/git/src/index.ts create mode 100644 packages/git/src/repo.ts create mode 100644 packages/git/src/status.ts create mode 100644 packages/git/src/types.ts create mode 100644 packages/git/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 3f58c85..f80b4cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,15 @@ "license": "MIT", "workspaces": [ "packages/cli", + "packages/git", "packages/parser", "packages/ui" ] }, + "node_modules/@diffity/git": { + "resolved": "packages/git", + "link": true + }, "node_modules/@diffity/parser": { "resolved": "packages/parser", "link": true @@ -3661,6 +3666,7 @@ "name": "diffity", "version": "0.1.0", "dependencies": { + "@diffity/git": "0.1.0", "@diffity/parser": "0.1.0", "commander": "latest", "open": "latest", @@ -3675,6 +3681,14 @@ "typescript": "^5.9.3" } }, + "packages/git": { + "name": "@diffity/git", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^25.5.0", + "typescript": "^5.9.3" + } + }, "packages/parser": { "name": "@diffity/parser", "version": "0.1.0", diff --git a/package.json b/package.json index 8f7ec8f..521f354 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ "description": "GitHub-style git diff viewer in the browser", "workspaces": [ "packages/cli", + "packages/git", "packages/parser", "packages/ui" ], "scripts": { - "build": "npm run build -w @diffity/parser && npm run build -w @diffity/ui && npm run build -w diffity", + "build": "npm run build -w @diffity/parser && npm run build -w @diffity/git && npm run build -w @diffity/ui && npm run build -w diffity", "test": "npm run test -w @diffity/parser && npm run test -w @diffity/ui", "dev": "npm run dev -w @diffity/ui", "start": "npm run dev -w diffity" diff --git a/packages/cli/package.json b/packages/cli/package.json index 408c43e..ddafa17 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -12,6 +12,7 @@ "dev": "tsx src/index.ts" }, "dependencies": { + "@diffity/git": "0.1.0", "@diffity/parser": "0.1.0", "commander": "latest", "open": "latest", diff --git a/packages/cli/src/git.ts b/packages/cli/src/git.ts deleted file mode 100644 index eec0107..0000000 --- a/packages/cli/src/git.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { execSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; - -export function isGitRepo(): boolean { - try { - execSync('git rev-parse --is-inside-work-tree', { - stdio: 'pipe', - }); - return true; - } catch { - return false; - } -} - -export function getRepoRoot(): string { - return execSync('git rev-parse --show-toplevel', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); -} - -export function getRepoName(): string { - const root = getRepoRoot(); - return root.split('/').pop() || root; -} - -export function getCurrentBranch(): string { - try { - return execSync('git rev-parse --abbrev-ref HEAD', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - } catch { - return 'HEAD'; - } -} - -export function getGitDiff(args: string[] = []): string { - const cmd = ['git', 'diff', ...args].join(' '); - return execSync(cmd, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - maxBuffer: 50 * 1024 * 1024, - }); -} - -export function getUntrackedFiles(): string[] { - const output = execSync('git ls-files --others --exclude-standard', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - - if (!output) { - return []; - } - - return output.split('\n'); -} - -export function getUntrackedDiff(files: string[]): string { - const diffs: string[] = []; - - for (const file of files) { - try { - execSync(`git diff --no-index -- /dev/null "${file}"`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - maxBuffer: 50 * 1024 * 1024, - }); - } catch (err: unknown) { - const error = err as { stdout?: string; status?: number }; - if (error.status === 1 && error.stdout) { - diffs.push(error.stdout); - } - } - } - - return diffs.join('\n'); -} - -export function getFileContent(path: string, ref?: string): string { - if (ref) { - return execSync(`git show ${ref}:${path}`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }); - } - - return execSync(`git show HEAD:${path}`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }); -} - -export function getStagedFiles(): string[] { - const output = execSync('git diff --staged --name-only', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - - if (!output) { - return []; - } - - return output.split('\n'); -} - -export function getUnstagedFiles(): string[] { - const output = execSync('git diff --name-only', { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - - if (!output) { - return []; - } - - return output.split('\n'); -} - -export interface Commit { - hash: string; - shortHash: string; - message: string; - relativeDate: string; -} - -export function getRecentCommits(count: number, skip = 0): Commit[] { - const output = execSync(`git log -n ${count} --skip=${skip} --format="%H|%h|%s|%cr"`, { - encoding: 'utf-8', - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - - if (!output) { - return []; - } - - return output.split('\n').map((line) => { - const [hash, shortHash, message, relativeDate] = line.split('|'); - return { hash, shortHash, message, relativeDate }; - }); -} - -export interface RepoInfo { - name: string; - branch: string; - root: string; -} - -export function getRepoInfo(): RepoInfo { - return { - name: getRepoName(), - branch: getCurrentBranch(), - root: getRepoRoot(), - }; -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 6e785ef..4c51c3e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,7 +3,7 @@ import { Command } from 'commander'; import open from 'open'; import pc from 'picocolors'; -import { isGitRepo } from './git.js'; +import { isGitRepo } from '@diffity/git'; import { startServer } from './server.js'; const program = new Command(); diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 4410163..38d6b91 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -6,7 +6,7 @@ import { dirname } from 'node:path'; import { parseDiff } from '@diffity/parser'; import type { ParsedDiff } from '@diffity/parser'; import { - getGitDiff, + getDiff, getUntrackedFiles, getUntrackedDiff, getRepoInfo, @@ -14,7 +14,7 @@ import { getStagedFiles, getUnstagedFiles, getRecentCommits, -} from './git.js'; +} from '@diffity/git'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -65,13 +65,13 @@ interface ServerResult { function resolveRef(ref: string, extraArgs: string[] = []): string { switch (ref) { case 'staged': { - return getGitDiff(['--staged', ...extraArgs]); + return getDiff(['--staged', ...extraArgs]); } case 'unstaged': { - return getGitDiff(extraArgs); + return getDiff(extraArgs); } case 'working': { - return getGitDiff(['HEAD', ...extraArgs]); + return getDiff(['HEAD', ...extraArgs]); } case 'untracked': { const files = getUntrackedFiles(); @@ -81,7 +81,7 @@ function resolveRef(ref: string, extraArgs: string[] = []): string { return getUntrackedDiff(files); } case 'all': { - let raw = getGitDiff(['HEAD', ...extraArgs]); + let raw = getDiff(['HEAD', ...extraArgs]); const untrackedFiles = getUntrackedFiles(); if (untrackedFiles.length > 0) { raw += '\n' + getUntrackedDiff(untrackedFiles); @@ -89,7 +89,7 @@ function resolveRef(ref: string, extraArgs: string[] = []): string { return raw; } default: { - return getGitDiff([ref, ...extraArgs]); + return getDiff([ref, ...extraArgs]); } } } @@ -99,7 +99,7 @@ export function startServer(options: ServerOptions): Promise { const includeUntracked = diffArgs.length === 0; function getFullDiff(args: string[]): string { - let raw = getGitDiff(args); + let raw = getDiff(args); if (includeUntracked) { const untrackedFiles = getUntrackedFiles(); if (untrackedFiles.length > 0) { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 5207d16..08a556f 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -7,6 +7,7 @@ }, "include": ["src/**/*.ts"], "references": [ + { "path": "../git" }, { "path": "../parser" } ] } diff --git a/packages/git/package.json b/packages/git/package.json new file mode 100644 index 0000000..1b2d2c2 --- /dev/null +++ b/packages/git/package.json @@ -0,0 +1,23 @@ +{ + "name": "@diffity/git", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc" + }, + "files": [ + "dist" + ], + "devDependencies": { + "@types/node": "^25.5.0", + "typescript": "^5.9.3" + } +} diff --git a/packages/git/src/commits.ts b/packages/git/src/commits.ts new file mode 100644 index 0000000..5e6397a --- /dev/null +++ b/packages/git/src/commits.ts @@ -0,0 +1,15 @@ +import { exec } from './exec.js'; +import type { Commit } from './types.js'; + +export function getRecentCommits(count: number, skip = 0): Commit[] { + const output = exec(`git log -n ${count} --skip=${skip} --format="%H|%h|%s|%cr"`); + + if (!output) { + return []; + } + + return output.split('\n').map((line) => { + const [hash, shortHash, message, relativeDate] = line.split('|'); + return { hash, shortHash, message, relativeDate }; + }); +} diff --git a/packages/git/src/diff.ts b/packages/git/src/diff.ts new file mode 100644 index 0000000..c21b198 --- /dev/null +++ b/packages/git/src/diff.ts @@ -0,0 +1,31 @@ +import { exec, execLarge, execLines } from './exec.js'; + +export function getDiff(args: string[] = []): string { + const cmd = ['git', 'diff', ...args].join(' '); + return execLarge(cmd); +} + +export function getUntrackedFiles(): string[] { + return execLines('git ls-files --others --exclude-standard'); +} + +export function getUntrackedDiff(files: string[]): string { + const diffs: string[] = []; + + for (const file of files) { + try { + execLarge(`git diff --no-index -- /dev/null "${file}"`); + } catch (err: unknown) { + const error = err as { stdout?: string; status?: number }; + if (error.status === 1 && error.stdout) { + diffs.push(error.stdout); + } + } + } + + return diffs.join('\n'); +} + +export function getFileContent(path: string, ref = 'HEAD'): string { + return exec(`git show ${ref}:${path}`); +} diff --git a/packages/git/src/exec.ts b/packages/git/src/exec.ts new file mode 100644 index 0000000..f957009 --- /dev/null +++ b/packages/git/src/exec.ts @@ -0,0 +1,26 @@ +import { execSync, type StdioOptions } from 'node:child_process'; + +const STDIO: StdioOptions = ['pipe', 'pipe', 'pipe']; + +export function exec(cmd: string): string { + return execSync(cmd, { + encoding: 'utf-8', + stdio: STDIO, + }).trim(); +} + +export function execLarge(cmd: string): string { + return execSync(cmd, { + encoding: 'utf-8', + stdio: STDIO, + maxBuffer: 50 * 1024 * 1024, + }); +} + +export function execLines(cmd: string): string[] { + const output = exec(cmd); + if (!output) { + return []; + } + return output.split('\n'); +} diff --git a/packages/git/src/index.ts b/packages/git/src/index.ts new file mode 100644 index 0000000..a8cca22 --- /dev/null +++ b/packages/git/src/index.ts @@ -0,0 +1,5 @@ +export type { Commit, RepoInfo } from './types.js'; +export { isGitRepo, getRepoRoot, getRepoName, getCurrentBranch, getRepoInfo } from './repo.js'; +export { getDiff, getUntrackedFiles, getUntrackedDiff, getFileContent } from './diff.js'; +export { getStagedFiles, getUnstagedFiles } from './status.js'; +export { getRecentCommits } from './commits.js'; diff --git a/packages/git/src/repo.ts b/packages/git/src/repo.ts new file mode 100644 index 0000000..63eb637 --- /dev/null +++ b/packages/git/src/repo.ts @@ -0,0 +1,37 @@ +import { execSync } from 'node:child_process'; +import { exec } from './exec.js'; +import type { RepoInfo } from './types.js'; + +export function isGitRepo(): boolean { + try { + execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' }); + return true; + } catch { + return false; + } +} + +export function getRepoRoot(): string { + return exec('git rev-parse --show-toplevel'); +} + +export function getRepoName(): string { + const root = getRepoRoot(); + return root.split('/').pop() || root; +} + +export function getCurrentBranch(): string { + try { + return exec('git rev-parse --abbrev-ref HEAD'); + } catch { + return 'HEAD'; + } +} + +export function getRepoInfo(): RepoInfo { + return { + name: getRepoName(), + branch: getCurrentBranch(), + root: getRepoRoot(), + }; +} diff --git a/packages/git/src/status.ts b/packages/git/src/status.ts new file mode 100644 index 0000000..f2a10f9 --- /dev/null +++ b/packages/git/src/status.ts @@ -0,0 +1,9 @@ +import { execLines } from './exec.js'; + +export function getStagedFiles(): string[] { + return execLines('git diff --staged --name-only'); +} + +export function getUnstagedFiles(): string[] { + return execLines('git diff --name-only'); +} diff --git a/packages/git/src/types.ts b/packages/git/src/types.ts new file mode 100644 index 0000000..ee0b872 --- /dev/null +++ b/packages/git/src/types.ts @@ -0,0 +1,12 @@ +export interface Commit { + hash: string; + shortHash: string; + message: string; + relativeDate: string; +} + +export interface RepoInfo { + name: string; + branch: string; + root: string; +} diff --git a/packages/git/tsconfig.json b/packages/git/tsconfig.json new file mode 100644 index 0000000..ac53a7b --- /dev/null +++ b/packages/git/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true + }, + "include": ["src/**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 983578c..6bbac1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ }, "references": [ { "path": "./packages/parser" }, + { "path": "./packages/git" }, { "path": "./packages/cli" }, { "path": "./packages/ui" } ]