From dba64c546f7cc5d7c2631a55fedef795b2b5e665 Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 10 May 2026 03:23:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(v030):=20GitClient=20=E2=80=94=20fetch/reb?= =?UTF-8?q?aseOnto/rebaseAbort/hasUncommittedChanges/listConflicts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/main/services/GitClient.ts | 23 ++++++++++++++ tests/unit/GitClient.fetch.test.ts | 51 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/unit/GitClient.fetch.test.ts diff --git a/src/main/services/GitClient.ts b/src/main/services/GitClient.ts index e4db135..71ba3b0 100644 --- a/src/main/services/GitClient.ts +++ b/src/main/services/GitClient.ts @@ -89,4 +89,27 @@ export class GitClient { if (r.exitCode !== 0) throw new Error(`git rev-parse failed: ${r.stderr}`); return r.stdout.trim(); } + + async fetch(remote: string = 'origin'): Promise { + return this.run(['fetch', remote]); + } + + async rebaseOnto(ref: string): Promise { + return this.run(['rebase', ref]); + } + + async rebaseAbort(): Promise { + return this.run(['rebase', '--abort']); + } + + async hasUncommittedChanges(): Promise { + const r = await this.run(['status', '--porcelain']); + return r.stdout.trim().length > 0; + } + + /** rebase conflict 시 conflict 마킹된 파일 list. `git diff --name-only --diff-filter=U`. */ + async listConflicts(): Promise { + const r = await this.run(['diff', '--name-only', '--diff-filter=U']); + return r.stdout.split('\n').map((s) => s.trim()).filter((s) => s.length > 0); + } } diff --git a/tests/unit/GitClient.fetch.test.ts b/tests/unit/GitClient.fetch.test.ts new file mode 100644 index 0000000..4a76b4b --- /dev/null +++ b/tests/unit/GitClient.fetch.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GitClient } from '../../src/main/services/GitClient.js'; + +describe('GitClient — fetch / rebase / conflict 메서드', () => { + let client: GitClient; + let runSpy: ReturnType; + + beforeEach(() => { + client = new GitClient('/tmp/sync'); + runSpy = vi.spyOn(client, 'run'); + }); + + it('fetch — git fetch origin 호출', async () => { + runSpy.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 }); + const r = await client.fetch(); + expect(runSpy).toHaveBeenCalledWith(['fetch', 'origin']); + expect(r.exitCode).toBe(0); + }); + + it('rebaseOnto — git rebase origin/main', async () => { + runSpy.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 }); + const r = await client.rebaseOnto('origin/main'); + expect(runSpy).toHaveBeenCalledWith(['rebase', 'origin/main']); + expect(r.exitCode).toBe(0); + }); + + it('rebaseAbort — git rebase --abort', async () => { + runSpy.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 }); + await client.rebaseAbort(); + expect(runSpy).toHaveBeenCalledWith(['rebase', '--abort']); + }); + + it('hasUncommittedChanges — git status --porcelain 의 출력 있으면 true', async () => { + runSpy.mockResolvedValueOnce({ stdout: ' M notes/abc.md\n', stderr: '', exitCode: 0 }); + expect(await client.hasUncommittedChanges()).toBe(true); + + runSpy.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 }); + expect(await client.hasUncommittedChanges()).toBe(false); + }); + + it('listConflicts — git diff --name-only --diff-filter=U 결과 파싱', async () => { + runSpy.mockResolvedValueOnce({ + stdout: 'notes/aaa.md\nnotes/bbb.md\n', + stderr: '', + exitCode: 0 + }); + const r = await client.listConflicts(); + expect(runSpy).toHaveBeenCalledWith(['diff', '--name-only', '--diff-filter=U']); + expect(r).toEqual(['notes/aaa.md', 'notes/bbb.md']); + }); +});