import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { simpleGit } from 'simple-git' import { readFile } from 'node:fs/promises' import { existsSync } from 'node:fs' import { classify, discard, load, stage } from '../src/main/git-service' describe('classify', () => { it('reads the index code as staged, working code as unstaged', () => { expect(classify('M', ' ')).toEqual([{ letter: 'M', staged: true }]) expect(classify(' ', 'M')).toEqual([{ letter: 'M', staged: false }]) expect(classify('A', ' ')).toEqual([{ letter: 'A', staged: true }]) expect(classify('D', ' ')).toEqual([{ letter: 'D', staged: true }]) expect(classify('R', ' ')).toEqual([{ letter: 'R', staged: true }]) }) it('treats untracked as a new (A) unstaged file', () => { expect(classify('?', '?')).toEqual([{ letter: 'A', staged: false }]) }) it('splits a staged-then-edited file into two rows', () => { expect(classify('M', 'M')).toEqual([ { letter: 'M', staged: true }, { letter: 'M', staged: false }, ]) expect(classify('A', 'M')).toEqual([ { letter: 'A', staged: true }, { letter: 'M', staged: false }, ]) expect(classify('M', 'D')).toEqual([ { letter: 'M', staged: true }, { letter: 'D', staged: false }, ]) }) it('keeps a merge conflict as one row', () => { expect(classify('U', 'U')).toEqual([{ letter: 'M', staged: true }]) expect(classify('A', 'A')).toEqual([{ letter: 'M', staged: true }]) expect(classify('D', 'D')).toEqual([{ letter: 'M', staged: true }]) }) }) describe('load (integration against a temp repo)', () => { let dir = '' afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }) }) async function repo(): Promise { const d = await mkdtemp(join(tmpdir(), 'helder-git-')) const g = simpleGit(d) await g.init() await g.addConfig('user.email', 't@example.com') await g.addConfig('user.name', 'Test') await g.addConfig('commit.gpgsign', 'false') await writeFile(join(d, 'a.txt'), '1\n2\n3\n') await g.add('.') await g.commit('init') return d } it('returns null for a non-repo directory', async () => { dir = await mkdtemp(join(tmpdir(), 'helder-nogit-')) expect(await load(dir)).toBeNull() }) it('reports a modified file with HEAD-vs-worktree text', async () => { dir = await repo() await writeFile(join(dir, 'a.txt'), '1\nX\n3\n') const res = await load(dir) expect(res).not.toBeNull() expect(res!.branch).toBeTruthy() const a = res!.changes.find((c) => c.path === 'a.txt') expect(a?.status).toBe('M') expect(a?.staged).toBe(false) expect(a?.original).toBe('1\n2\n3\n') expect(a?.updated).toBe('1\nX\n3\n') }) it('reports an untracked file as new (A), original empty', async () => { dir = await repo() await writeFile(join(dir, 'new.txt'), 'fresh\n') const res = await load(dir) const n = res!.changes.find((c) => c.path === 'new.txt') expect(n?.status).toBe('A') expect(n?.staged).toBe(false) expect(n?.original).toBe('') expect(n?.updated).toBe('fresh\n') }) it('reflects staging', async () => { dir = await repo() await writeFile(join(dir, 'a.txt'), '1\n2\n3\n4\n') await stage(dir, ['a.txt']) const res = await load(dir) expect(res!.changes.find((c) => c.path === 'a.txt')?.staged).toBe(true) }) it('shows a staged-then-edited file in both groups, each with its own pair', async () => { dir = await repo() await writeFile(join(dir, 'a.txt'), '1\nSTAGED\n3\n') await stage(dir, ['a.txt']) await writeFile(join(dir, 'a.txt'), '1\nSTAGED\n3\n4\n') const res = await load(dir) const rows = res!.changes.filter((c) => c.path === 'a.txt') expect(rows).toHaveLength(2) // Staged row: HEAD -> index. It must NOT include the newer edit. const s = rows.find((c) => c.staged)! expect(s.original).toBe('1\n2\n3\n') expect(s.updated).toBe('1\nSTAGED\n3\n') // Unstaged row: index -> disk. Only the newer edit. const w = rows.find((c) => !c.staged)! expect(w.original).toBe('1\nSTAGED\n3\n') expect(w.updated).toBe('1\nSTAGED\n3\n4\n') }) it('gives a staged-new file that was edited again both rows', async () => { dir = await repo() await writeFile(join(dir, 'fresh.txt'), 'one\n') await stage(dir, ['fresh.txt']) await writeFile(join(dir, 'fresh.txt'), 'one\ntwo\n') const res = await load(dir) const rows = res!.changes.filter((c) => c.path === 'fresh.txt') expect(rows.map((r) => [r.status, r.staged])).toEqual([['A', true], ['M', false]]) expect(rows[0].original).toBe('') expect(rows[0].updated).toBe('one\n') expect(rows[1].original).toBe('one\n') expect(rows[1].updated).toBe('one\ntwo\n') }) it('keeps one row when a file is only staged', async () => { dir = await repo() await writeFile(join(dir, 'a.txt'), '1\n2\n3\n4\n') await stage(dir, ['a.txt']) const rows = (await load(dir))!.changes.filter((c) => c.path === 'a.txt') expect(rows).toHaveLength(1) expect(rows[0].staged).toBe(true) expect(rows[0].original).toBe('1\n2\n3\n') expect(rows[0].updated).toBe('1\n2\n3\n4\n') }) it('discard reverts a modified tracked file to HEAD', async () => { dir = await repo() await writeFile(join(dir, 'a.txt'), '1\nCHANGED\n3\n') await discard(dir, ['a.txt']) expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('1\n2\n3\n') const res = await load(dir) expect(res!.changes.find((c) => c.path === 'a.txt')).toBeUndefined() }) it('discard removes a new (untracked) file from disk', async () => { dir = await repo() await writeFile(join(dir, 'new.txt'), 'fresh\n') await discard(dir, ['new.txt']) expect(existsSync(join(dir, 'new.txt'))).toBe(false) }) it('discard removes a staged-new file', async () => { dir = await repo() await writeFile(join(dir, 'staged-new.txt'), 'x\n') await stage(dir, ['staged-new.txt']) await discard(dir, ['staged-new.txt']) expect(existsSync(join(dir, 'staged-new.txt'))).toBe(false) const res = await load(dir) expect(res!.changes.find((c) => c.path === 'staged-new.txt')).toBeUndefined() }) })