* takt: abstract-git-provider * takt: abstract-git-provider * takt: abstract-git-provider * fix: pushBranch のインポートパスを infra/task に修正 Git provider 抽象化により pushBranch が infra/github から infra/task に 移動したため、taskSyncAction とテストのインポートパスを更新。
This commit is contained in:
parent
f6334b8e75
commit
6d0bac9d07
@ -39,6 +39,12 @@ vi.mock('../infra/task/index.js', async (importOriginal) => ({
|
|||||||
summarizeTaskName: vi.fn().mockResolvedValue('test-task'),
|
summarizeTaskName: vi.fn().mockResolvedValue('test-task'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/git/index.js', () => ({
|
||||||
|
getGitProvider: () => ({
|
||||||
|
createIssue: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/issue.js', () => ({
|
vi.mock('../infra/github/issue.js', () => ({
|
||||||
isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)),
|
isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)),
|
||||||
resolveIssueTask: vi.fn(),
|
resolveIssueTask: vi.fn(),
|
||||||
@ -52,7 +58,6 @@ vi.mock('../infra/github/issue.js', () => ({
|
|||||||
}
|
}
|
||||||
return numbers;
|
return numbers;
|
||||||
}),
|
}),
|
||||||
createIssue: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { interactiveMode } from '../features/interactive/index.js';
|
import { interactiveMode } from '../features/interactive/index.js';
|
||||||
|
|||||||
@ -26,14 +26,23 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { mockCheckCliStatus, mockFetchIssue } = vi.hoisted(() => ({
|
||||||
|
mockCheckCliStatus: vi.fn(),
|
||||||
|
mockFetchIssue: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/git/index.js', () => ({
|
||||||
|
getGitProvider: () => ({
|
||||||
|
checkCliStatus: (...args: unknown[]) => mockCheckCliStatus(...args),
|
||||||
|
fetchIssue: (...args: unknown[]) => mockFetchIssue(...args),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/issue.js', () => ({
|
vi.mock('../infra/github/issue.js', () => ({
|
||||||
parseIssueNumbers: vi.fn(() => []),
|
parseIssueNumbers: vi.fn(() => []),
|
||||||
checkGhCli: vi.fn(),
|
|
||||||
fetchIssue: vi.fn(),
|
|
||||||
formatIssueAsTask: vi.fn(),
|
formatIssueAsTask: vi.fn(),
|
||||||
isIssueReference: vi.fn(),
|
isIssueReference: vi.fn(),
|
||||||
resolveIssueTask: vi.fn(),
|
resolveIssueTask: vi.fn(),
|
||||||
createIssue: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/tasks/index.js', () => ({
|
vi.mock('../features/tasks/index.js', () => ({
|
||||||
@ -105,17 +114,15 @@ vi.mock('../app/cli/helpers.js', () => ({
|
|||||||
isDirectTask: vi.fn(() => false),
|
isDirectTask: vi.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
import { formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
||||||
import { selectAndExecuteTask, determinePiece, createIssueAndSaveTask } from '../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, createIssueAndSaveTask } from '../features/tasks/index.js';
|
||||||
import { interactiveMode } from '../features/interactive/index.js';
|
import { interactiveMode } from '../features/interactive/index.js';
|
||||||
import { resolveConfigValues, loadPersonaSessions } from '../infra/config/index.js';
|
import { resolveConfigValues, loadPersonaSessions } from '../infra/config/index.js';
|
||||||
import { isDirectTask } from '../app/cli/helpers.js';
|
import { isDirectTask } from '../app/cli/helpers.js';
|
||||||
import { executeDefaultAction } from '../app/cli/routing.js';
|
import { executeDefaultAction } from '../app/cli/routing.js';
|
||||||
import { info } from '../shared/ui/index.js';
|
import { info } from '../shared/ui/index.js';
|
||||||
import type { GitHubIssue } from '../infra/github/types.js';
|
import type { Issue } from '../infra/git/index.js';
|
||||||
|
|
||||||
const mockCheckGhCli = vi.mocked(checkGhCli);
|
|
||||||
const mockFetchIssue = vi.mocked(fetchIssue);
|
|
||||||
const mockFormatIssueAsTask = vi.mocked(formatIssueAsTask);
|
const mockFormatIssueAsTask = vi.mocked(formatIssueAsTask);
|
||||||
const mockParseIssueNumbers = vi.mocked(parseIssueNumbers);
|
const mockParseIssueNumbers = vi.mocked(parseIssueNumbers);
|
||||||
const mockSelectAndExecuteTask = vi.mocked(selectAndExecuteTask);
|
const mockSelectAndExecuteTask = vi.mocked(selectAndExecuteTask);
|
||||||
@ -128,7 +135,7 @@ const mockIsDirectTask = vi.mocked(isDirectTask);
|
|||||||
const mockInfo = vi.mocked(info);
|
const mockInfo = vi.mocked(info);
|
||||||
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
|
||||||
|
|
||||||
function createMockIssue(number: number): GitHubIssue {
|
function createMockIssue(number: number): Issue {
|
||||||
return {
|
return {
|
||||||
number,
|
number,
|
||||||
title: `Issue #${number}`,
|
title: `Issue #${number}`,
|
||||||
@ -159,7 +166,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
// Given
|
// Given
|
||||||
mockOpts.issue = 131;
|
mockOpts.issue = 131;
|
||||||
const issue131 = createMockIssue(131);
|
const issue131 = createMockIssue(131);
|
||||||
mockCheckGhCli.mockReturnValue({ available: true });
|
mockCheckCliStatus.mockReturnValue({ available: true });
|
||||||
mockFetchIssue.mockReturnValue(issue131);
|
mockFetchIssue.mockReturnValue(issue131);
|
||||||
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');
|
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');
|
||||||
|
|
||||||
@ -191,7 +198,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
it('should exit with error when gh CLI is unavailable for --issue', async () => {
|
it('should exit with error when gh CLI is unavailable for --issue', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockOpts.issue = 131;
|
mockOpts.issue = 131;
|
||||||
mockCheckGhCli.mockReturnValue({
|
mockCheckCliStatus.mockReturnValue({
|
||||||
available: false,
|
available: false,
|
||||||
error: 'gh CLI is not installed',
|
error: 'gh CLI is not installed',
|
||||||
});
|
});
|
||||||
@ -214,7 +221,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
// Given
|
// Given
|
||||||
const issue131 = createMockIssue(131);
|
const issue131 = createMockIssue(131);
|
||||||
mockIsDirectTask.mockReturnValue(true);
|
mockIsDirectTask.mockReturnValue(true);
|
||||||
mockCheckGhCli.mockReturnValue({ available: true });
|
mockCheckCliStatus.mockReturnValue({ available: true });
|
||||||
mockFetchIssue.mockReturnValue(issue131);
|
mockFetchIssue.mockReturnValue(issue131);
|
||||||
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');
|
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');
|
||||||
mockParseIssueNumbers.mockReturnValue([131]);
|
mockParseIssueNumbers.mockReturnValue([131]);
|
||||||
@ -421,7 +428,7 @@ describe('Issue resolution in routing', () => {
|
|||||||
// Given
|
// Given
|
||||||
mockOpts.issue = 131;
|
mockOpts.issue = 131;
|
||||||
const issue131 = createMockIssue(131);
|
const issue131 = createMockIssue(131);
|
||||||
mockCheckGhCli.mockReturnValue({ available: true });
|
mockCheckCliStatus.mockReturnValue({ available: true });
|
||||||
mockFetchIssue.mockReturnValue(issue131);
|
mockFetchIssue.mockReturnValue(issue131);
|
||||||
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131');
|
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131');
|
||||||
mockInteractiveMode.mockResolvedValue({ action: 'cancel', task: '' });
|
mockInteractiveMode.mockResolvedValue({ action: 'cancel', task: '' });
|
||||||
|
|||||||
@ -7,8 +7,14 @@
|
|||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
vi.mock('../infra/github/issue.js', () => ({
|
const { mockCreateIssue } = vi.hoisted(() => ({
|
||||||
createIssue: vi.fn(),
|
mockCreateIssue: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/git/index.js', () => ({
|
||||||
|
getGitProvider: () => ({
|
||||||
|
createIssue: (...args: unknown[]) => mockCreateIssue(...args),
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
@ -26,11 +32,9 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { createIssue } from '../infra/github/issue.js';
|
|
||||||
import { success, error } from '../shared/ui/index.js';
|
import { success, error } from '../shared/ui/index.js';
|
||||||
import { createIssueFromTask } from '../features/tasks/index.js';
|
import { createIssueFromTask } from '../features/tasks/index.js';
|
||||||
|
|
||||||
const mockCreateIssue = vi.mocked(createIssue);
|
|
||||||
const mockSuccess = vi.mocked(success);
|
const mockSuccess = vi.mocked(success);
|
||||||
const mockError = vi.mocked(error);
|
const mockError = vi.mocked(error);
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Tests for github/pr module
|
* Tests for github/pr module
|
||||||
*
|
*
|
||||||
* Tests buildPrBody formatting and findExistingPr logic.
|
* Tests buildPrBody formatting and findExistingPr logic.
|
||||||
* createPullRequest/pushBranch/commentOnPr call `gh`/`git` CLI, not unit-tested here.
|
* createPullRequest/commentOnPr call `gh` CLI, not unit-tested here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
@ -174,4 +174,5 @@ describe('buildPrBody', () => {
|
|||||||
expect(result).toContain('Closes #1');
|
expect(result).toContain('Closes #1');
|
||||||
expect(result).toContain('Closes #2');
|
expect(result).toContain('Closes #2');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
233
src/__tests__/github-provider.test.ts
Normal file
233
src/__tests__/github-provider.test.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Tests for GitHubProvider and getGitProvider factory.
|
||||||
|
*
|
||||||
|
* GitHubProvider should delegate each method to the corresponding function
|
||||||
|
* in github/issue.ts and github/pr.ts.
|
||||||
|
* getGitProvider() should return a singleton GitProvider instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockCheckGhCli,
|
||||||
|
mockFetchIssue,
|
||||||
|
mockCreateIssue,
|
||||||
|
mockFindExistingPr,
|
||||||
|
mockCommentOnPr,
|
||||||
|
mockCreatePullRequest,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
mockCheckGhCli: vi.fn(),
|
||||||
|
mockFetchIssue: vi.fn(),
|
||||||
|
mockCreateIssue: vi.fn(),
|
||||||
|
mockFindExistingPr: vi.fn(),
|
||||||
|
mockCommentOnPr: vi.fn(),
|
||||||
|
mockCreatePullRequest: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/github/issue.js', () => ({
|
||||||
|
checkGhCli: (...args: unknown[]) => mockCheckGhCli(...args),
|
||||||
|
fetchIssue: (...args: unknown[]) => mockFetchIssue(...args),
|
||||||
|
createIssue: (...args: unknown[]) => mockCreateIssue(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/github/pr.js', () => ({
|
||||||
|
findExistingPr: (...args: unknown[]) => mockFindExistingPr(...args),
|
||||||
|
commentOnPr: (...args: unknown[]) => mockCommentOnPr(...args),
|
||||||
|
createPullRequest: (...args: unknown[]) => mockCreatePullRequest(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { GitHubProvider } from '../infra/github/GitHubProvider.js';
|
||||||
|
import { getGitProvider } from '../infra/git/index.js';
|
||||||
|
import type { CommentResult } from '../infra/git/index.js';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GitHubProvider', () => {
|
||||||
|
describe('checkCliStatus', () => {
|
||||||
|
it('checkGhCli() の結果をそのまま返す', () => {
|
||||||
|
// Given
|
||||||
|
const status = { available: true };
|
||||||
|
mockCheckGhCli.mockReturnValue(status);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.checkCliStatus();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCheckGhCli).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toBe(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gh CLI が利用不可の場合は available: false を返す', () => {
|
||||||
|
// Given
|
||||||
|
mockCheckGhCli.mockReturnValue({ available: false, error: 'gh is not installed' });
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.checkCliStatus();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.available).toBe(false);
|
||||||
|
expect(result.error).toBe('gh is not installed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchIssue', () => {
|
||||||
|
it('fetchIssue(n) に委譲し結果を返す', () => {
|
||||||
|
// Given
|
||||||
|
const issue = { number: 42, title: 'Test issue', body: 'Body', labels: [], comments: [] };
|
||||||
|
mockFetchIssue.mockReturnValue(issue);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.fetchIssue(42);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockFetchIssue).toHaveBeenCalledWith(42);
|
||||||
|
expect(result).toBe(issue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createIssue', () => {
|
||||||
|
it('createIssue(opts) に委譲し結果を返す', () => {
|
||||||
|
// Given
|
||||||
|
const opts = { title: 'New issue', body: 'Description' };
|
||||||
|
const issueResult = { success: true, url: 'https://github.com/org/repo/issues/1' };
|
||||||
|
mockCreateIssue.mockReturnValue(issueResult);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.createIssue(opts);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreateIssue).toHaveBeenCalledWith(opts);
|
||||||
|
expect(result).toBe(issueResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ラベルを含む場合、opts をそのまま委譲する', () => {
|
||||||
|
// Given
|
||||||
|
const opts = { title: 'Bug', body: 'Details', labels: ['bug', 'urgent'] };
|
||||||
|
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/org/repo/issues/2' });
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
provider.createIssue(opts);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreateIssue).toHaveBeenCalledWith(opts);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findExistingPr', () => {
|
||||||
|
it('findExistingPr(cwd, branch) に委譲し PR を返す', () => {
|
||||||
|
// Given
|
||||||
|
const pr = { number: 10, url: 'https://github.com/org/repo/pull/10' };
|
||||||
|
mockFindExistingPr.mockReturnValue(pr);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.findExistingPr('/project', 'feat/my-feature');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockFindExistingPr).toHaveBeenCalledWith('/project', 'feat/my-feature');
|
||||||
|
expect(result).toBe(pr);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PR が存在しない場合は undefined を返す', () => {
|
||||||
|
// Given
|
||||||
|
mockFindExistingPr.mockReturnValue(undefined);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.findExistingPr('/project', 'feat/no-pr');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPullRequest', () => {
|
||||||
|
it('createPullRequest(cwd, opts) に委譲し結果を返す', () => {
|
||||||
|
// Given
|
||||||
|
const opts = { branch: 'feat/new', title: 'My PR', body: 'PR body', draft: false };
|
||||||
|
const prResult = { success: true, url: 'https://github.com/org/repo/pull/5' };
|
||||||
|
mockCreatePullRequest.mockReturnValue(prResult);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.createPullRequest('/project', opts);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreatePullRequest).toHaveBeenCalledWith('/project', opts);
|
||||||
|
expect(result).toBe(prResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('draft: true の場合、opts をそのまま委譲する', () => {
|
||||||
|
// Given
|
||||||
|
const opts = { branch: 'feat/draft', title: 'Draft PR', body: 'body', draft: true };
|
||||||
|
mockCreatePullRequest.mockReturnValue({ success: true, url: 'https://github.com/org/repo/pull/6' });
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
provider.createPullRequest('/project', opts);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreatePullRequest).toHaveBeenCalledWith('/project', expect.objectContaining({ draft: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('commentOnPr', () => {
|
||||||
|
it('commentOnPr(cwd, prNumber, body) に委譲し CommentResult を返す', () => {
|
||||||
|
const commentResult: CommentResult = { success: true };
|
||||||
|
mockCommentOnPr.mockReturnValue(commentResult);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.commentOnPr('/project', 42, 'Updated!');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCommentOnPr).toHaveBeenCalledWith('/project', 42, 'Updated!');
|
||||||
|
expect(result).toBe(commentResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('コメント失敗時はエラー結果を委譲して返す', () => {
|
||||||
|
// Given
|
||||||
|
const commentResult: CommentResult = { success: false, error: 'Permission denied' };
|
||||||
|
mockCommentOnPr.mockReturnValue(commentResult);
|
||||||
|
const provider = new GitHubProvider();
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = provider.commentOnPr('/project', 42, 'comment');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Permission denied');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getGitProvider', () => {
|
||||||
|
it('GitProvider インターフェースを実装するインスタンスを返す', () => {
|
||||||
|
// When
|
||||||
|
const provider = getGitProvider();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(typeof provider.checkCliStatus).toBe('function');
|
||||||
|
expect(typeof provider.fetchIssue).toBe('function');
|
||||||
|
expect(typeof provider.createIssue).toBe('function');
|
||||||
|
expect(typeof provider.findExistingPr).toBe('function');
|
||||||
|
expect(typeof provider.createPullRequest).toBe('function');
|
||||||
|
expect(typeof provider.commentOnPr).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('呼び出しのたびに同じインスタンスを返す(シングルトン)', () => {
|
||||||
|
// When
|
||||||
|
const provider1 = getGitProvider();
|
||||||
|
const provider2 = getGitProvider();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(provider1).toBe(provider2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -59,13 +59,13 @@ vi.mock('../infra/github/issue.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../infra/github/pr.js', () => ({
|
vi.mock('../infra/github/pr.js', () => ({
|
||||||
createPullRequest: mockCreatePullRequest,
|
createPullRequest: mockCreatePullRequest,
|
||||||
pushBranch: mockPushBranch,
|
|
||||||
buildPrBody: vi.fn().mockReturnValue('PR body'),
|
buildPrBody: vi.fn().mockReturnValue('PR body'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/task/git.js', () => ({
|
vi.mock('../infra/task/git.js', () => ({
|
||||||
stageAndCommit: vi.fn().mockReturnValue('abc1234'),
|
stageAndCommit: vi.fn().mockReturnValue('abc1234'),
|
||||||
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
||||||
|
pushBranch: (...args: unknown[]) => mockPushBranch(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
|
|||||||
@ -46,7 +46,6 @@ vi.mock('../infra/github/issue.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../infra/github/pr.js', () => ({
|
vi.mock('../infra/github/pr.js', () => ({
|
||||||
createPullRequest: vi.fn(),
|
createPullRequest: vi.fn(),
|
||||||
pushBranch: vi.fn(),
|
|
||||||
buildPrBody: vi.fn().mockReturnValue('PR body'),
|
buildPrBody: vi.fn().mockReturnValue('PR body'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -22,10 +22,14 @@ const mockPushBranch = vi.fn();
|
|||||||
const mockBuildPrBody = vi.fn(() => 'Default PR body');
|
const mockBuildPrBody = vi.fn(() => 'Default PR body');
|
||||||
vi.mock('../infra/github/pr.js', () => ({
|
vi.mock('../infra/github/pr.js', () => ({
|
||||||
createPullRequest: mockCreatePullRequest,
|
createPullRequest: mockCreatePullRequest,
|
||||||
pushBranch: mockPushBranch,
|
|
||||||
buildPrBody: mockBuildPrBody,
|
buildPrBody: mockBuildPrBody,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/task/git.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
pushBranch: (...args: unknown[]) => mockPushBranch(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockExecuteTask = vi.fn();
|
const mockExecuteTask = vi.fn();
|
||||||
const mockConfirmAndCreateWorktree = vi.fn();
|
const mockConfirmAndCreateWorktree = vi.fn();
|
||||||
vi.mock('../features/tasks/index.js', () => ({
|
vi.mock('../features/tasks/index.js', () => ({
|
||||||
|
|||||||
@ -18,13 +18,18 @@ const { mockAutoCommitAndPush, mockPushBranch, mockFindExistingPr, mockCommentOn
|
|||||||
|
|
||||||
vi.mock('../infra/task/index.js', () => ({
|
vi.mock('../infra/task/index.js', () => ({
|
||||||
autoCommitAndPush: (...args: unknown[]) => mockAutoCommitAndPush(...args),
|
autoCommitAndPush: (...args: unknown[]) => mockAutoCommitAndPush(...args),
|
||||||
|
pushBranch: (...args: unknown[]) => mockPushBranch(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/index.js', () => ({
|
vi.mock('../infra/git/index.js', () => ({
|
||||||
pushBranch: (...args: unknown[]) => mockPushBranch(...args),
|
getGitProvider: () => ({
|
||||||
findExistingPr: (...args: unknown[]) => mockFindExistingPr(...args),
|
findExistingPr: (...args: unknown[]) => mockFindExistingPr(...args),
|
||||||
commentOnPr: (...args: unknown[]) => mockCommentOnPr(...args),
|
commentOnPr: (...args: unknown[]) => mockCommentOnPr(...args),
|
||||||
createPullRequest: (...args: unknown[]) => mockCreatePullRequest(...args),
|
createPullRequest: (...args: unknown[]) => mockCreatePullRequest(...args),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/github/index.js', () => ({
|
||||||
buildPrBody: (...args: unknown[]) => mockBuildPrBody(...args),
|
buildPrBody: (...args: unknown[]) => mockBuildPrBody(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -180,7 +185,7 @@ describe('postExecutionFlow', () => {
|
|||||||
await postExecutionFlow({
|
await postExecutionFlow({
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
task: 'Fix the bug',
|
task: 'Fix the bug',
|
||||||
issues: [{ number: 123, title: 'This title should not appear in PR', body: '', labels: [], comments: 0 }],
|
issues: [{ number: 123, title: 'This title should not appear in PR', body: '', labels: [], comments: [] }],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||||
@ -222,7 +227,7 @@ describe('postExecutionFlow', () => {
|
|||||||
await postExecutionFlow({
|
await postExecutionFlow({
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
task: longTask,
|
task: longTask,
|
||||||
issues: [{ number: 123, title: 'Long issue', body: '', labels: [], comments: 0 }],
|
issues: [{ number: 123, title: 'Long issue', body: '', labels: [], comments: [] }],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||||
|
|||||||
@ -148,9 +148,7 @@ vi.mock('../shared/constants.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/index.js', () => ({
|
vi.mock('../infra/github/index.js', () => ({
|
||||||
createPullRequest: vi.fn(),
|
|
||||||
buildPrBody: vi.fn(),
|
buildPrBody: vi.fn(),
|
||||||
pushBranch: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/claude/query-manager.js', () => ({
|
vi.mock('../infra/claude/query-manager.js', () => ({
|
||||||
|
|||||||
@ -72,9 +72,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/index.js', () => ({
|
vi.mock('../infra/github/index.js', () => ({
|
||||||
createPullRequest: vi.fn(),
|
|
||||||
buildPrBody: vi.fn(),
|
buildPrBody: vi.fn(),
|
||||||
pushBranch: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
||||||
|
|||||||
@ -68,11 +68,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/index.js', () => ({
|
vi.mock('../infra/github/index.js', () => ({
|
||||||
createPullRequest: vi.fn(),
|
|
||||||
buildPrBody: vi.fn(),
|
buildPrBody: vi.fn(),
|
||||||
pushBranch: vi.fn(),
|
|
||||||
findExistingPr: vi.fn(),
|
|
||||||
commentOnPr: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
||||||
|
|||||||
56
src/__tests__/taskGit.test.ts
Normal file
56
src/__tests__/taskGit.test.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Tests for pushBranch in infra/task/git.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('node:child_process', () => ({
|
||||||
|
execFileSync: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
createLogger: () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
const mockExecFileSync = vi.mocked(execFileSync);
|
||||||
|
|
||||||
|
import { pushBranch } from '../infra/task/git.js';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pushBranch', () => {
|
||||||
|
it('should call git push origin <branch>', () => {
|
||||||
|
// Given
|
||||||
|
mockExecFileSync.mockReturnValue(Buffer.from(''));
|
||||||
|
|
||||||
|
// When
|
||||||
|
pushBranch('/project', 'feature/my-branch');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||||
|
'git',
|
||||||
|
['push', 'origin', 'feature/my-branch'],
|
||||||
|
{ cwd: '/project', stdio: 'pipe' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when git push fails', () => {
|
||||||
|
// Given
|
||||||
|
mockExecFileSync.mockImplementation(() => {
|
||||||
|
throw new Error('error: failed to push some refs');
|
||||||
|
});
|
||||||
|
|
||||||
|
// When / Then
|
||||||
|
expect(() => pushBranch('/project', 'feature/my-branch')).toThrow(
|
||||||
|
'error: failed to push some refs',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -38,7 +38,8 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
resolveConfigValues: vi.fn(() => ({ provider: 'claude', model: 'sonnet' })),
|
resolveConfigValues: vi.fn(() => ({ provider: 'claude', model: 'sonnet' })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/github/index.js', () => ({
|
vi.mock('../infra/task/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
pushBranch: vi.fn(),
|
pushBranch: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ vi.mock('../shared/prompts/index.js', () => ({
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { error as logError, success } from '../shared/ui/index.js';
|
import { error as logError, success } from '../shared/ui/index.js';
|
||||||
import { pushBranch } from '../infra/github/index.js';
|
import { pushBranch } from '../infra/task/index.js';
|
||||||
import { getProvider } from '../infra/providers/index.js';
|
import { getProvider } from '../infra/providers/index.js';
|
||||||
import { syncBranchWithRoot } from '../features/tasks/list/taskSyncAction.js';
|
import { syncBranchWithRoot } from '../features/tasks/list/taskSyncAction.js';
|
||||||
import type { TaskListItem } from '../infra/task/index.js';
|
import type { TaskListItem } from '../infra/task/index.js';
|
||||||
|
|||||||
@ -8,7 +8,9 @@
|
|||||||
import { info, error as logError, withProgress } from '../../shared/ui/index.js';
|
import { info, error as logError, withProgress } from '../../shared/ui/index.js';
|
||||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { getLabel } from '../../shared/i18n/index.js';
|
import { getLabel } from '../../shared/i18n/index.js';
|
||||||
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
|
import { formatIssueAsTask, parseIssueNumbers } from '../../infra/github/index.js';
|
||||||
|
import { getGitProvider } from '../../infra/git/index.js';
|
||||||
|
import type { Issue } from '../../infra/git/index.js';
|
||||||
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueAndSaveTask, promptLabelSelection, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueAndSaveTask, promptLabelSelection, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||||
import { executePipeline } from '../../features/pipeline/index.js';
|
import { executePipeline } from '../../features/pipeline/index.js';
|
||||||
import {
|
import {
|
||||||
@ -39,22 +41,22 @@ import { loadTaskHistory } from './taskHistory.js';
|
|||||||
async function resolveIssueInput(
|
async function resolveIssueInput(
|
||||||
issueOption: number | undefined,
|
issueOption: number | undefined,
|
||||||
task: string | undefined,
|
task: string | undefined,
|
||||||
): Promise<{ issues: GitHubIssue[]; initialInput: string } | null> {
|
): Promise<{ issues: Issue[]; initialInput: string } | null> {
|
||||||
if (issueOption) {
|
if (issueOption) {
|
||||||
const ghStatus = checkGhCli();
|
const ghStatus = getGitProvider().checkCliStatus();
|
||||||
if (!ghStatus.available) {
|
if (!ghStatus.available) {
|
||||||
throw new Error(ghStatus.error);
|
throw new Error(ghStatus.error);
|
||||||
}
|
}
|
||||||
const issue = await withProgress(
|
const issue = await withProgress(
|
||||||
'Fetching GitHub Issue...',
|
'Fetching GitHub Issue...',
|
||||||
(fetchedIssue) => `GitHub Issue fetched: #${fetchedIssue.number} ${fetchedIssue.title}`,
|
(fetchedIssue) => `GitHub Issue fetched: #${fetchedIssue.number} ${fetchedIssue.title}`,
|
||||||
async () => fetchIssue(issueOption),
|
async () => getGitProvider().fetchIssue(issueOption),
|
||||||
);
|
);
|
||||||
return { issues: [issue], initialInput: formatIssueAsTask(issue) };
|
return { issues: [issue], initialInput: formatIssueAsTask(issue) };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task && isDirectTask(task)) {
|
if (task && isDirectTask(task)) {
|
||||||
const ghStatus = checkGhCli();
|
const ghStatus = getGitProvider().checkCliStatus();
|
||||||
if (!ghStatus.available) {
|
if (!ghStatus.available) {
|
||||||
throw new Error(ghStatus.error);
|
throw new Error(ghStatus.error);
|
||||||
}
|
}
|
||||||
@ -66,7 +68,7 @@ async function resolveIssueInput(
|
|||||||
const issues = await withProgress(
|
const issues = await withProgress(
|
||||||
'Fetching GitHub Issue...',
|
'Fetching GitHub Issue...',
|
||||||
(fetchedIssues) => `GitHub Issues fetched: ${fetchedIssues.map((issue) => `#${issue.number}`).join(', ')}`,
|
(fetchedIssues) => `GitHub Issues fetched: ${fetchedIssues.map((issue) => `#${issue.number}`).join(', ')}`,
|
||||||
async () => issueNumbers.map((n) => fetchIssue(n)),
|
async () => issueNumbers.map((n) => getGitProvider().fetchIssue(n)),
|
||||||
);
|
);
|
||||||
return { issues, initialInput: issues.map(formatIssueAsTask).join('\n\n---\n\n') };
|
return { issues, initialInput: issues.map(formatIssueAsTask).join('\n\n---\n\n') };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,16 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import {
|
import { formatIssueAsTask, buildPrBody } from '../../infra/github/index.js';
|
||||||
fetchIssue,
|
import { getGitProvider, type Issue } from '../../infra/git/index.js';
|
||||||
formatIssueAsTask,
|
import { stageAndCommit, resolveBaseBranch, pushBranch } from '../../infra/task/index.js';
|
||||||
checkGhCli,
|
|
||||||
createPullRequest,
|
|
||||||
pushBranch,
|
|
||||||
buildPrBody,
|
|
||||||
type GitHubIssue,
|
|
||||||
} from '../../infra/github/index.js';
|
|
||||||
import { stageAndCommit, resolveBaseBranch } from '../../infra/task/index.js';
|
|
||||||
import { executeTask, confirmAndCreateWorktree, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
import { executeTask, confirmAndCreateWorktree, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
||||||
import { info, error, success } from '../../shared/ui/index.js';
|
import { info, error, success } from '../../shared/ui/index.js';
|
||||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||||
@ -25,7 +18,7 @@ import type { PipelineConfig } from '../../core/models/index.js';
|
|||||||
|
|
||||||
export interface TaskContent {
|
export interface TaskContent {
|
||||||
task: string;
|
task: string;
|
||||||
issue?: GitHubIssue;
|
issue?: Issue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutionContext {
|
export interface ExecutionContext {
|
||||||
@ -51,7 +44,7 @@ function generatePipelineBranchName(pipelineConfig: PipelineConfig | undefined,
|
|||||||
|
|
||||||
export function buildCommitMessage(
|
export function buildCommitMessage(
|
||||||
pipelineConfig: PipelineConfig | undefined,
|
pipelineConfig: PipelineConfig | undefined,
|
||||||
issue: GitHubIssue | undefined,
|
issue: Issue | undefined,
|
||||||
taskText: string | undefined,
|
taskText: string | undefined,
|
||||||
): string {
|
): string {
|
||||||
const template = pipelineConfig?.commitMessageTemplate;
|
const template = pipelineConfig?.commitMessageTemplate;
|
||||||
@ -68,7 +61,7 @@ export function buildCommitMessage(
|
|||||||
|
|
||||||
function buildPipelinePrBody(
|
function buildPipelinePrBody(
|
||||||
pipelineConfig: PipelineConfig | undefined,
|
pipelineConfig: PipelineConfig | undefined,
|
||||||
issue: GitHubIssue | undefined,
|
issue: Issue | undefined,
|
||||||
report: string,
|
report: string,
|
||||||
): string {
|
): string {
|
||||||
const template = pipelineConfig?.prBodyTemplate;
|
const template = pipelineConfig?.prBodyTemplate;
|
||||||
@ -88,13 +81,14 @@ function buildPipelinePrBody(
|
|||||||
export function resolveTaskContent(options: PipelineExecutionOptions): TaskContent | undefined {
|
export function resolveTaskContent(options: PipelineExecutionOptions): TaskContent | undefined {
|
||||||
if (options.issueNumber) {
|
if (options.issueNumber) {
|
||||||
info(`Fetching issue #${options.issueNumber}...`);
|
info(`Fetching issue #${options.issueNumber}...`);
|
||||||
const ghStatus = checkGhCli();
|
const gitProvider = getGitProvider();
|
||||||
if (!ghStatus.available) {
|
const cliStatus = gitProvider.checkCliStatus();
|
||||||
error(ghStatus.error ?? 'gh CLI is not available');
|
if (!cliStatus.available) {
|
||||||
|
error(cliStatus.error ?? 'gh CLI is not available');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const issue = fetchIssue(options.issueNumber);
|
const issue = gitProvider.fetchIssue(options.issueNumber);
|
||||||
const task = formatIssueAsTask(issue);
|
const task = formatIssueAsTask(issue);
|
||||||
success(`Issue #${options.issueNumber} fetched: "${issue.title}"`);
|
success(`Issue #${options.issueNumber} fetched: "${issue.title}"`);
|
||||||
return { task, issue };
|
return { task, issue };
|
||||||
@ -215,7 +209,7 @@ export function submitPullRequest(
|
|||||||
const report = `Piece \`${piece}\` completed successfully.`;
|
const report = `Piece \`${piece}\` completed successfully.`;
|
||||||
const prBody = buildPipelinePrBody(pipelineConfig, taskContent.issue, report);
|
const prBody = buildPipelinePrBody(pipelineConfig, taskContent.issue, report);
|
||||||
|
|
||||||
const prResult = createPullRequest(projectCwd, {
|
const prResult = getGitProvider().createPullRequest(projectCwd, {
|
||||||
branch,
|
branch,
|
||||||
title: prTitle,
|
title: prTitle,
|
||||||
body: prBody,
|
body: prBody,
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import type { Language } from '../../../core/models/types.js';
|
|||||||
import { TaskRunner, type TaskFileData, summarizeTaskName } from '../../../infra/task/index.js';
|
import { TaskRunner, type TaskFileData, summarizeTaskName } from '../../../infra/task/index.js';
|
||||||
import { determinePiece } from '../execute/selectAndExecute.js';
|
import { determinePiece } from '../execute/selectAndExecute.js';
|
||||||
import { createLogger, getErrorMessage, generateReportDir } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage, generateReportDir } from '../../../shared/utils/index.js';
|
||||||
import { isIssueReference, resolveIssueTask, parseIssueNumbers, createIssue } from '../../../infra/github/index.js';
|
import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/index.js';
|
||||||
|
import { getGitProvider } from '../../../infra/git/index.js';
|
||||||
import { firstLine } from '../../../infra/task/naming.js';
|
import { firstLine } from '../../../infra/task/naming.js';
|
||||||
|
|
||||||
const log = createLogger('add-task');
|
const log = createLogger('add-task');
|
||||||
@ -82,7 +83,7 @@ export function createIssueFromTask(task: string, options?: { labels?: string[]
|
|||||||
const effectiveLabels = options?.labels?.filter((l) => l.length > 0) ?? [];
|
const effectiveLabels = options?.labels?.filter((l) => l.length > 0) ?? [];
|
||||||
const labels = effectiveLabels.length > 0 ? effectiveLabels : undefined;
|
const labels = effectiveLabels.length > 0 ? effectiveLabels : undefined;
|
||||||
|
|
||||||
const issueResult = createIssue({ title, body: task, labels });
|
const issueResult = getGitProvider().createIssue({ title, body: task, labels });
|
||||||
if (issueResult.success) {
|
if (issueResult.success) {
|
||||||
if (!issueResult.url) {
|
if (!issueResult.url) {
|
||||||
error('Failed to extract issue number from URL');
|
error('Failed to extract issue number from URL');
|
||||||
|
|||||||
@ -7,11 +7,12 @@
|
|||||||
|
|
||||||
import { resolvePieceConfigValue } from '../../../infra/config/index.js';
|
import { resolvePieceConfigValue } from '../../../infra/config/index.js';
|
||||||
import { confirm } from '../../../shared/prompt/index.js';
|
import { confirm } from '../../../shared/prompt/index.js';
|
||||||
import { autoCommitAndPush } from '../../../infra/task/index.js';
|
import { autoCommitAndPush, pushBranch } from '../../../infra/task/index.js';
|
||||||
import { info, error, success } from '../../../shared/ui/index.js';
|
import { info, error, success } from '../../../shared/ui/index.js';
|
||||||
import { createLogger } from '../../../shared/utils/index.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
import { createPullRequest, buildPrBody, pushBranch, findExistingPr, commentOnPr } from '../../../infra/github/index.js';
|
import { buildPrBody } from '../../../infra/github/index.js';
|
||||||
import type { GitHubIssue } from '../../../infra/github/index.js';
|
import { getGitProvider } from '../../../infra/git/index.js';
|
||||||
|
import type { Issue } from '../../../infra/git/index.js';
|
||||||
|
|
||||||
const log = createLogger('postExecution');
|
const log = createLogger('postExecution');
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ export interface PostExecutionOptions {
|
|||||||
shouldCreatePr: boolean;
|
shouldCreatePr: boolean;
|
||||||
draftPr: boolean;
|
draftPr: boolean;
|
||||||
pieceIdentifier?: string;
|
pieceIdentifier?: string;
|
||||||
issues?: GitHubIssue[];
|
issues?: Issue[];
|
||||||
repo?: string;
|
repo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,12 +88,13 @@ export async function postExecutionFlow(options: PostExecutionOptions): Promise<
|
|||||||
} catch (pushError) {
|
} catch (pushError) {
|
||||||
log.info('Branch push from project cwd failed (may already exist)', { error: pushError });
|
log.info('Branch push from project cwd failed (may already exist)', { error: pushError });
|
||||||
}
|
}
|
||||||
|
const gitProvider = getGitProvider();
|
||||||
const report = pieceIdentifier ? `Piece \`${pieceIdentifier}\` completed successfully.` : 'Task completed successfully.';
|
const report = pieceIdentifier ? `Piece \`${pieceIdentifier}\` completed successfully.` : 'Task completed successfully.';
|
||||||
const existingPr = findExistingPr(projectCwd, branch);
|
const existingPr = gitProvider.findExistingPr(projectCwd, branch);
|
||||||
if (existingPr) {
|
if (existingPr) {
|
||||||
// PRが既に存在する場合はコメントを追加(push済みなので新コミットはPRに自動反映)
|
// push済みなので、新コミットはPRに自動反映される
|
||||||
const commentBody = buildPrBody(issues, report);
|
const commentBody = buildPrBody(issues, report);
|
||||||
const commentResult = commentOnPr(projectCwd, existingPr.number, commentBody);
|
const commentResult = gitProvider.commentOnPr(projectCwd, existingPr.number, commentBody);
|
||||||
if (commentResult.success) {
|
if (commentResult.success) {
|
||||||
success(`PR updated with comment: ${existingPr.url}`);
|
success(`PR updated with comment: ${existingPr.url}`);
|
||||||
return { prUrl: existingPr.url };
|
return { prUrl: existingPr.url };
|
||||||
@ -107,7 +109,7 @@ export async function postExecutionFlow(options: PostExecutionOptions): Promise<
|
|||||||
const issuePrefix = firstIssue ? `[#${firstIssue.number}] ` : '';
|
const issuePrefix = firstIssue ? `[#${firstIssue.number}] ` : '';
|
||||||
const truncatedTask = task.length > 100 - issuePrefix.length ? `${task.slice(0, 100 - issuePrefix.length - 3)}...` : task;
|
const truncatedTask = task.length > 100 - issuePrefix.length ? `${task.slice(0, 100 - issuePrefix.length - 3)}...` : task;
|
||||||
const prTitle = issuePrefix + truncatedTask;
|
const prTitle = issuePrefix + truncatedTask;
|
||||||
const prResult = createPullRequest(projectCwd, {
|
const prResult = gitProvider.createPullRequest(projectCwd, {
|
||||||
branch,
|
branch,
|
||||||
title: prTitle,
|
title: prTitle,
|
||||||
body: prBody,
|
body: prBody,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import * as fs from 'node:fs';
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { resolvePieceConfigValue } from '../../../infra/config/index.js';
|
import { resolvePieceConfigValue } from '../../../infra/config/index.js';
|
||||||
import { type TaskInfo, createSharedClone, summarizeTaskName, detectDefaultBranch } from '../../../infra/task/index.js';
|
import { type TaskInfo, createSharedClone, summarizeTaskName, detectDefaultBranch } from '../../../infra/task/index.js';
|
||||||
import { fetchIssue, checkGhCli } from '../../../infra/github/index.js';
|
import { getGitProvider, type Issue } from '../../../infra/git/index.js';
|
||||||
import { withProgress } from '../../../shared/ui/index.js';
|
import { withProgress } from '../../../shared/ui/index.js';
|
||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
|
import { getTaskSlugFromTaskDir } from '../../../shared/utils/taskPaths.js';
|
||||||
@ -69,19 +69,20 @@ function throwIfAborted(signal?: AbortSignal): void {
|
|||||||
* Resolve a GitHub issue from task data's issue number.
|
* Resolve a GitHub issue from task data's issue number.
|
||||||
* Returns issue array for buildPrBody, or undefined if no issue or gh CLI unavailable.
|
* Returns issue array for buildPrBody, or undefined if no issue or gh CLI unavailable.
|
||||||
*/
|
*/
|
||||||
export function resolveTaskIssue(issueNumber: number | undefined): ReturnType<typeof fetchIssue>[] | undefined {
|
export function resolveTaskIssue(issueNumber: number | undefined): Issue[] | undefined {
|
||||||
if (issueNumber === undefined) {
|
if (issueNumber === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ghStatus = checkGhCli();
|
const gitProvider = getGitProvider();
|
||||||
if (!ghStatus.available) {
|
const cliStatus = gitProvider.checkCliStatus();
|
||||||
|
if (!cliStatus.available) {
|
||||||
log.info('gh CLI unavailable, skipping issue resolution for PR body', { issueNumber });
|
log.info('gh CLI unavailable, skipping issue resolution for PR body', { issueNumber });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const issue = fetchIssue(issueNumber);
|
const issue = gitProvider.fetchIssue(issueNumber);
|
||||||
return [issue];
|
return [issue];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info('Failed to fetch issue for PR body, continuing without issue info', { issueNumber, error: getErrorMessage(e) });
|
log.info('Failed to fetch issue for PR body, continuing without issue info', { issueNumber, error: getErrorMessage(e) });
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import type { PersonaProviderEntry } from '../../../core/models/persisted-global
|
|||||||
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
import type { ProviderPermissionProfiles } from '../../../core/models/provider-profiles.js';
|
||||||
import type { MovementProviderOptions } from '../../../core/models/piece-types.js';
|
import type { MovementProviderOptions } from '../../../core/models/piece-types.js';
|
||||||
import type { ProviderType } from '../../../infra/providers/index.js';
|
import type { ProviderType } from '../../../infra/providers/index.js';
|
||||||
import type { GitHubIssue } from '../../../infra/github/index.js';
|
import type { Issue } from '../../../infra/git/index.js';
|
||||||
import type { ProviderOptionsSource } from '../../../core/piece/types.js';
|
import type { ProviderOptionsSource } from '../../../core/piece/types.js';
|
||||||
|
|
||||||
/** Result of piece execution */
|
/** Result of piece execution */
|
||||||
@ -144,7 +144,7 @@ export interface SelectAndExecuteOptions {
|
|||||||
/** Interactive mode result metadata for NDJSON logging */
|
/** Interactive mode result metadata for NDJSON logging */
|
||||||
interactiveMetadata?: InteractiveMetadata;
|
interactiveMetadata?: InteractiveMetadata;
|
||||||
/** GitHub Issues to associate with the PR (adds "Closes #N" for each issue) */
|
/** GitHub Issues to associate with the PR (adds "Closes #N" for each issue) */
|
||||||
issues?: GitHubIssue[];
|
issues?: Issue[];
|
||||||
/** Skip adding task to tasks.yaml */
|
/** Skip adding task to tasks.yaml */
|
||||||
skipTaskList?: boolean;
|
skipTaskList?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { success, error as logError, StreamDisplay } from '../../../shared/ui/in
|
|||||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { getProvider, type ProviderType } from '../../../infra/providers/index.js';
|
import { getProvider, type ProviderType } from '../../../infra/providers/index.js';
|
||||||
import { resolveConfigValues } from '../../../infra/config/index.js';
|
import { resolveConfigValues } from '../../../infra/config/index.js';
|
||||||
import { pushBranch } from '../../../infra/github/index.js';
|
import { pushBranch } from '../../../infra/task/index.js';
|
||||||
import { loadTemplate } from '../../../shared/prompts/index.js';
|
import { loadTemplate } from '../../../shared/prompts/index.js';
|
||||||
import { getLanguage } from '../../../infra/config/index.js';
|
import { getLanguage } from '../../../infra/config/index.js';
|
||||||
import { type BranchActionTarget, resolveTargetBranch, resolveTargetInstruction } from './taskActionTarget.js';
|
import { type BranchActionTarget, resolveTargetBranch, resolveTargetInstruction } from './taskActionTarget.js';
|
||||||
|
|||||||
19
src/infra/git/index.ts
Normal file
19
src/infra/git/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Git provider factory
|
||||||
|
*
|
||||||
|
* Returns the singleton GitProvider instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GitHubProvider } from '../github/GitHubProvider.js';
|
||||||
|
import type { GitProvider } from './types.js';
|
||||||
|
|
||||||
|
export type { GitProvider, Issue, CliStatus, ExistingPr, CreatePrOptions, CreatePrResult, CommentResult, CreateIssueOptions, CreateIssueResult } from './types.js';
|
||||||
|
|
||||||
|
let provider: GitProvider | undefined;
|
||||||
|
|
||||||
|
export function getGitProvider(): GitProvider {
|
||||||
|
if (!provider) {
|
||||||
|
provider = new GitHubProvider();
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
86
src/infra/git/types.ts
Normal file
86
src/infra/git/types.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Git provider abstraction types
|
||||||
|
*
|
||||||
|
* Defines the GitProvider interface and its supporting types,
|
||||||
|
* decoupled from any specific provider implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CliStatus {
|
||||||
|
available: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Issue {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
labels: string[];
|
||||||
|
comments: Array<{ author: string; body: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExistingPr {
|
||||||
|
number: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePrOptions {
|
||||||
|
/** Branch to create PR from */
|
||||||
|
branch: string;
|
||||||
|
/** PR title */
|
||||||
|
title: string;
|
||||||
|
/** PR body (markdown) */
|
||||||
|
body: string;
|
||||||
|
/** Base branch (default: repo default branch) */
|
||||||
|
base?: string;
|
||||||
|
/** Repository in owner/repo format (optional, uses current repo if omitted) */
|
||||||
|
repo?: string;
|
||||||
|
/** Create PR as draft */
|
||||||
|
draft?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePrResult {
|
||||||
|
success: boolean;
|
||||||
|
/** PR URL on success */
|
||||||
|
url?: string;
|
||||||
|
/** Error message on failure */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentResult {
|
||||||
|
success: boolean;
|
||||||
|
/** Error message on failure */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateIssueOptions {
|
||||||
|
/** Issue title */
|
||||||
|
title: string;
|
||||||
|
/** Issue body (markdown) */
|
||||||
|
body: string;
|
||||||
|
/** Labels to apply */
|
||||||
|
labels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateIssueResult {
|
||||||
|
success: boolean;
|
||||||
|
/** Issue URL on success */
|
||||||
|
url?: string;
|
||||||
|
/** Error message on failure */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitProvider {
|
||||||
|
/** Check CLI tool availability and authentication status */
|
||||||
|
checkCliStatus(): CliStatus;
|
||||||
|
|
||||||
|
fetchIssue(issueNumber: number): Issue;
|
||||||
|
|
||||||
|
createIssue(options: CreateIssueOptions): CreateIssueResult;
|
||||||
|
|
||||||
|
/** Find an open PR for the given branch. Returns undefined if no PR exists. */
|
||||||
|
findExistingPr(cwd: string, branch: string): ExistingPr | undefined;
|
||||||
|
|
||||||
|
createPullRequest(cwd: string, options: CreatePrOptions): CreatePrResult;
|
||||||
|
|
||||||
|
commentOnPr(cwd: string, prNumber: number, body: string): CommentResult;
|
||||||
|
}
|
||||||
37
src/infra/github/GitHubProvider.ts
Normal file
37
src/infra/github/GitHubProvider.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* GitHub implementation of GitProvider
|
||||||
|
*
|
||||||
|
* Delegates each operation to the corresponding function in
|
||||||
|
* issue.ts and pr.ts. This class is the single place that binds
|
||||||
|
* the GitProvider contract to the GitHub/gh-CLI implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { checkGhCli, fetchIssue, createIssue } from './issue.js';
|
||||||
|
import { findExistingPr, commentOnPr, createPullRequest } from './pr.js';
|
||||||
|
import type { GitProvider, CliStatus, Issue, ExistingPr, CreateIssueOptions, CreateIssueResult, CreatePrOptions, CreatePrResult, CommentResult } from '../git/types.js';
|
||||||
|
|
||||||
|
export class GitHubProvider implements GitProvider {
|
||||||
|
checkCliStatus(): CliStatus {
|
||||||
|
return checkGhCli();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchIssue(issueNumber: number): Issue {
|
||||||
|
return fetchIssue(issueNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIssue(options: CreateIssueOptions): CreateIssueResult {
|
||||||
|
return createIssue(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
findExistingPr(cwd: string, branch: string): ExistingPr | undefined {
|
||||||
|
return findExistingPr(cwd, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
createPullRequest(cwd: string, options: CreatePrOptions): CreatePrResult {
|
||||||
|
return createPullRequest(cwd, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
commentOnPr(cwd: string, prNumber: number, body: string): CommentResult {
|
||||||
|
return commentOnPr(cwd, prNumber, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,17 +2,11 @@
|
|||||||
* GitHub integration - barrel exports
|
* GitHub integration - barrel exports
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type { GitHubIssue, GhCliStatus, CreatePrOptions, CreatePrResult, CreateIssueOptions, CreateIssueResult } from './types.js';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
checkGhCli,
|
|
||||||
fetchIssue,
|
|
||||||
formatIssueAsTask,
|
formatIssueAsTask,
|
||||||
parseIssueNumbers,
|
parseIssueNumbers,
|
||||||
isIssueReference,
|
isIssueReference,
|
||||||
resolveIssueTask,
|
resolveIssueTask,
|
||||||
createIssue,
|
|
||||||
} from './issue.js';
|
} from './issue.js';
|
||||||
|
|
||||||
export type { ExistingPr } from './pr.js';
|
export { buildPrBody } from './pr.js';
|
||||||
export { pushBranch, createPullRequest, buildPrBody, findExistingPr, commentOnPr } from './pr.js';
|
|
||||||
|
|||||||
@ -7,17 +7,10 @@
|
|||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { checkGhCli } from './issue.js';
|
import { checkGhCli } from './issue.js';
|
||||||
import type { GitHubIssue, CreatePrOptions, CreatePrResult } from './types.js';
|
import type { Issue, CreatePrOptions, CreatePrResult, ExistingPr, CommentResult } from '../git/types.js';
|
||||||
|
|
||||||
export type { CreatePrOptions, CreatePrResult };
|
|
||||||
|
|
||||||
const log = createLogger('github-pr');
|
const log = createLogger('github-pr');
|
||||||
|
|
||||||
export interface ExistingPr {
|
|
||||||
number: number;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find an open PR for the given branch.
|
* Find an open PR for the given branch.
|
||||||
* Returns undefined if no PR exists.
|
* Returns undefined if no PR exists.
|
||||||
@ -33,15 +26,13 @@ export function findExistingPr(cwd: string, branch: string): ExistingPr | undefi
|
|||||||
);
|
);
|
||||||
const prs = JSON.parse(output) as ExistingPr[];
|
const prs = JSON.parse(output) as ExistingPr[];
|
||||||
return prs[0];
|
return prs[0];
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
log.debug('gh pr list failed, treating as no PR', { error: getErrorMessage(e) });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function commentOnPr(cwd: string, prNumber: number, body: string): CommentResult {
|
||||||
* Add a comment to an existing PR.
|
|
||||||
*/
|
|
||||||
export function commentOnPr(cwd: string, prNumber: number, body: string): CreatePrResult {
|
|
||||||
const ghStatus = checkGhCli();
|
const ghStatus = checkGhCli();
|
||||||
if (!ghStatus.available) {
|
if (!ghStatus.available) {
|
||||||
return { success: false, error: ghStatus.error ?? 'gh CLI is not available' };
|
return { success: false, error: ghStatus.error ?? 'gh CLI is not available' };
|
||||||
@ -61,21 +52,6 @@ export function commentOnPr(cwd: string, prNumber: number, body: string): Create
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Push a branch to origin.
|
|
||||||
* Throws on failure.
|
|
||||||
*/
|
|
||||||
export function pushBranch(cwd: string, branch: string): void {
|
|
||||||
log.info('Pushing branch to origin', { branch });
|
|
||||||
execFileSync('git', ['push', 'origin', branch], {
|
|
||||||
cwd,
|
|
||||||
stdio: 'pipe',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Pull Request via `gh pr create`.
|
|
||||||
*/
|
|
||||||
export function createPullRequest(cwd: string, options: CreatePrOptions): CreatePrResult {
|
export function createPullRequest(cwd: string, options: CreatePrOptions): CreatePrResult {
|
||||||
const ghStatus = checkGhCli();
|
const ghStatus = checkGhCli();
|
||||||
if (!ghStatus.available) {
|
if (!ghStatus.available) {
|
||||||
@ -125,13 +101,12 @@ export function createPullRequest(cwd: string, options: CreatePrOptions): Create
|
|||||||
* Build PR body from issues and execution report.
|
* Build PR body from issues and execution report.
|
||||||
* Supports multiple issues (adds "Closes #N" for each).
|
* Supports multiple issues (adds "Closes #N" for each).
|
||||||
*/
|
*/
|
||||||
export function buildPrBody(issues: GitHubIssue[] | undefined, report: string): string {
|
export function buildPrBody(issues: Issue[] | undefined, report: string): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
parts.push('## Summary');
|
parts.push('## Summary');
|
||||||
if (issues && issues.length > 0) {
|
if (issues && issues.length > 0) {
|
||||||
parts.push('');
|
parts.push('');
|
||||||
// Use the first issue's body/title for summary
|
|
||||||
parts.push(issues[0]!.body || issues[0]!.title);
|
parts.push(issues[0]!.body || issues[0]!.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,55 +2,4 @@
|
|||||||
* GitHub module type definitions
|
* GitHub module type definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface GitHubIssue {
|
export type { Issue as GitHubIssue, CliStatus as GhCliStatus, CreateIssueOptions, CreateIssueResult } from '../git/types.js';
|
||||||
number: number;
|
|
||||||
title: string;
|
|
||||||
body: string;
|
|
||||||
labels: string[];
|
|
||||||
comments: Array<{ author: string; body: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GhCliStatus {
|
|
||||||
available: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreatePrOptions {
|
|
||||||
/** Branch to create PR from */
|
|
||||||
branch: string;
|
|
||||||
/** PR title */
|
|
||||||
title: string;
|
|
||||||
/** PR body (markdown) */
|
|
||||||
body: string;
|
|
||||||
/** Base branch (default: repo default branch) */
|
|
||||||
base?: string;
|
|
||||||
/** Repository in owner/repo format (optional, uses current repo if omitted) */
|
|
||||||
repo?: string;
|
|
||||||
/** Create PR as draft */
|
|
||||||
draft?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreatePrResult {
|
|
||||||
success: boolean;
|
|
||||||
/** PR URL on success */
|
|
||||||
url?: string;
|
|
||||||
/** Error message on failure */
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateIssueOptions {
|
|
||||||
/** Issue title */
|
|
||||||
title: string;
|
|
||||||
/** Issue body (markdown) */
|
|
||||||
body: string;
|
|
||||||
/** Labels to apply */
|
|
||||||
labels?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateIssueResult {
|
|
||||||
success: boolean;
|
|
||||||
/** Issue URL on success */
|
|
||||||
url?: string;
|
|
||||||
/** Error message on failure */
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
|
|
||||||
|
const log = createLogger('git');
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current branch name.
|
|
||||||
*/
|
|
||||||
export function getCurrentBranch(cwd: string): string {
|
export function getCurrentBranch(cwd: string): string {
|
||||||
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
||||||
cwd,
|
cwd,
|
||||||
@ -16,7 +16,6 @@ export function getCurrentBranch(cwd: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stage all changes and create a commit.
|
|
||||||
* Returns the short commit hash if changes were committed, undefined if no changes.
|
* Returns the short commit hash if changes were committed, undefined if no changes.
|
||||||
*/
|
*/
|
||||||
export function stageAndCommit(cwd: string, message: string): string | undefined {
|
export function stageAndCommit(cwd: string, message: string): string | undefined {
|
||||||
@ -40,3 +39,14 @@ export function stageAndCommit(cwd: string, message: string): string | undefined
|
|||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
}).trim();
|
}).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws on failure.
|
||||||
|
*/
|
||||||
|
export function pushBranch(cwd: string, branch: string): void {
|
||||||
|
log.info('Pushing branch to origin', { branch });
|
||||||
|
execFileSync('git', ['push', 'origin', branch], {
|
||||||
|
cwd,
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export {
|
|||||||
getOriginalInstruction,
|
getOriginalInstruction,
|
||||||
buildListItems,
|
buildListItems,
|
||||||
} from './branchList.js';
|
} from './branchList.js';
|
||||||
export { stageAndCommit, getCurrentBranch } from './git.js';
|
export { stageAndCommit, getCurrentBranch, pushBranch } from './git.js';
|
||||||
export { autoCommitAndPush, type AutoCommitResult } from './autoCommit.js';
|
export { autoCommitAndPush, type AutoCommitResult } from './autoCommit.js';
|
||||||
export { summarizeTaskName } from './summarize.js';
|
export { summarizeTaskName } from './summarize.js';
|
||||||
export { TaskWatcher, type TaskWatcherOptions } from './watcher.js';
|
export { TaskWatcher, type TaskWatcherOptions } from './watcher.js';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user