feat(FN-1612): merge fusion/fn-1612
This commit is contained in:
@@ -1,8 +1,43 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
execSync: vi.fn(),
|
||||
}));
|
||||
// Store for mock results - shared between callback and promisified paths
|
||||
let mockResults: (string | Error)[] = [];
|
||||
let resultIndex = 0;
|
||||
const execCalls: [string, object | undefined][] = [];
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const { promisify } = await import("node:util");
|
||||
const execFn: typeof vi.fn = vi.fn((cmd: string, opts: object | undefined, cb: (err: Error | null, stdout: string, stderr: string) => void) => {
|
||||
// Track the call for assertion purposes
|
||||
execCalls.push([cmd, opts]);
|
||||
const callback = typeof opts === "function" ? opts : cb;
|
||||
// promisify path - callback is undefined
|
||||
if (callback === undefined) {
|
||||
return; // promisify.custom handles the Promise
|
||||
}
|
||||
try {
|
||||
const result = mockResults[resultIndex++] || "";
|
||||
const stdout = result instanceof Error ? "" : result.toString();
|
||||
callback(null, stdout, "");
|
||||
} catch (err) {
|
||||
callback(err as Error, "", "");
|
||||
}
|
||||
});
|
||||
// Mirror real child_process.exec: promisify resolves to { stdout, stderr }.
|
||||
execFn[promisify.custom] = (cmd: string, opts?: object) => {
|
||||
// Track the call for assertion purposes
|
||||
execCalls.push([cmd, opts]);
|
||||
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
const result = mockResults[resultIndex++] || "";
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
resolve({ stdout: result.toString(), stderr: "" });
|
||||
}
|
||||
});
|
||||
};
|
||||
return { exec: execFn, execSync: vi.fn() };
|
||||
});
|
||||
|
||||
vi.mock("node:readline/promises", () => ({
|
||||
createInterface: vi.fn(() => ({
|
||||
@@ -15,26 +50,34 @@ vi.mock("../project-context.js", () => ({
|
||||
resolveProject: vi.fn(),
|
||||
}));
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
import {
|
||||
isGitRepo,
|
||||
getGitStatus,
|
||||
getDirtyFileCount,
|
||||
isValidBranchName,
|
||||
fetchGitRemote,
|
||||
pullGitBranch,
|
||||
pushGitBranch,
|
||||
runGitStatus,
|
||||
runGitFetch,
|
||||
runGitPull,
|
||||
runGitPush,
|
||||
} from "./git.js";
|
||||
|
||||
const mockExecSync = vi.mocked(execSync);
|
||||
const mockCreateInterface = vi.mocked(createInterface);
|
||||
|
||||
// Helper to set up sequential mock results
|
||||
function mockNextResult(result: string) {
|
||||
mockResults.push(result);
|
||||
}
|
||||
|
||||
// Helper to check if exec was called with specific command
|
||||
function wasExecCalled(cmd: string): boolean {
|
||||
return execCalls.some(([c]) => c === cmd);
|
||||
}
|
||||
|
||||
// Helper to get last exec call
|
||||
function getLastExecCall(): [string, object | undefined] | undefined {
|
||||
return execCalls[execCalls.length - 1];
|
||||
}
|
||||
|
||||
describe("git commands", () => {
|
||||
let logSpy: ReturnType<typeof vi.spyOn>;
|
||||
let errorSpy: ReturnType<typeof vi.spyOn>;
|
||||
@@ -42,6 +85,9 @@ describe("git commands", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockResults = [];
|
||||
resultIndex = 0;
|
||||
execCalls.length = 0;
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: string | number | null) => {
|
||||
@@ -52,7 +98,7 @@ describe("git commands", () => {
|
||||
projectName: "demo-project",
|
||||
projectPath: "/projects/demo",
|
||||
isRegistered: true,
|
||||
store: {} as any,
|
||||
store: {} as ReturnType<typeof vi.fn>,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,67 +108,76 @@ describe("git commands", () => {
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("core helpers work", () => {
|
||||
mockExecSync.mockReturnValueOnce(".git");
|
||||
expect(isGitRepo()).toBe(true);
|
||||
it("core helpers work", async () => {
|
||||
mockNextResult(".git");
|
||||
expect(await isGitRepo()).toBe(true);
|
||||
expect(isValidBranchName("main")).toBe(true);
|
||||
expect(isValidBranchName("--bad")).toBe(false);
|
||||
});
|
||||
|
||||
it("runGitStatus uses resolved project path", async () => {
|
||||
mockExecSync
|
||||
.mockReturnValueOnce(".git")
|
||||
.mockReturnValueOnce("main\n")
|
||||
.mockReturnValueOnce("a1b2c3d\n")
|
||||
.mockReturnValueOnce(" M file.ts\n")
|
||||
.mockReturnValueOnce("0\t0\n")
|
||||
.mockReturnValueOnce(" M file.ts\n");
|
||||
// isGitRepo, branch, commit, status, rev-list, dirty count
|
||||
mockNextResult(".git");
|
||||
mockNextResult("main\n");
|
||||
mockNextResult("a1b2c3d\n");
|
||||
mockNextResult(" M file.ts\n");
|
||||
mockNextResult("0\t0\n");
|
||||
mockNextResult(" M file.ts\n");
|
||||
|
||||
await runGitStatus("demo-project");
|
||||
|
||||
expect(resolveProject).toHaveBeenCalledWith("demo-project");
|
||||
expect(mockExecSync).toHaveBeenCalledWith("git status --porcelain", expect.objectContaining({ cwd: "/projects/demo" }));
|
||||
expect(wasExecCalled("git status --porcelain")).toBe(true);
|
||||
const lastCall = getLastExecCall();
|
||||
expect(lastCall).toBeDefined();
|
||||
expect(lastCall![1]).toMatchObject({ cwd: "/projects/demo" });
|
||||
});
|
||||
|
||||
it("runGitStatus without project uses shared resolution flow", async () => {
|
||||
mockExecSync
|
||||
.mockReturnValueOnce(".git")
|
||||
.mockReturnValueOnce("main\n")
|
||||
.mockReturnValueOnce("a1b2c3d\n")
|
||||
.mockReturnValueOnce("")
|
||||
.mockReturnValueOnce("0\t0\n");
|
||||
mockNextResult(".git");
|
||||
mockNextResult("main\n");
|
||||
mockNextResult("a1b2c3d\n");
|
||||
mockNextResult("");
|
||||
mockNextResult("0\t0\n");
|
||||
|
||||
await runGitStatus();
|
||||
|
||||
expect(resolveProject).toHaveBeenCalledWith(undefined);
|
||||
expect(mockExecSync).toHaveBeenCalledWith("git rev-parse --git-dir", expect.objectContaining({ cwd: "/projects/demo" }));
|
||||
expect(wasExecCalled("git rev-parse --git-dir")).toBe(true);
|
||||
const lastCall = getLastExecCall();
|
||||
expect(lastCall).toBeDefined();
|
||||
expect(lastCall![1]).toMatchObject({ cwd: "/projects/demo" });
|
||||
});
|
||||
|
||||
it("runGitStatus without project falls back to current working directory when resolution fails", async () => {
|
||||
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/local/project");
|
||||
vi.mocked(resolveProject).mockRejectedValueOnce(new Error("No fusion project found"));
|
||||
mockExecSync
|
||||
.mockReturnValueOnce(".git")
|
||||
.mockReturnValueOnce("main\n")
|
||||
.mockReturnValueOnce("a1b2c3d\n")
|
||||
.mockReturnValueOnce("")
|
||||
.mockReturnValueOnce("0\t0\n");
|
||||
mockNextResult(".git");
|
||||
mockNextResult("main\n");
|
||||
mockNextResult("a1b2c3d\n");
|
||||
mockNextResult("");
|
||||
mockNextResult("0\t0\n");
|
||||
|
||||
await runGitStatus();
|
||||
|
||||
expect(resolveProject).toHaveBeenCalledWith(undefined);
|
||||
expect(mockExecSync).toHaveBeenCalledWith("git rev-parse --git-dir", expect.objectContaining({ cwd: "/local/project" }));
|
||||
expect(wasExecCalled("git rev-parse --git-dir")).toBe(true);
|
||||
const lastCall = getLastExecCall();
|
||||
expect(lastCall).toBeDefined();
|
||||
expect(lastCall![1]).toMatchObject({ cwd: "/local/project" });
|
||||
cwdSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("runGitFetch uses resolved project path", async () => {
|
||||
mockExecSync
|
||||
.mockReturnValueOnce(".git")
|
||||
.mockReturnValueOnce("Fetch completed");
|
||||
mockNextResult(".git");
|
||||
mockNextResult("Fetch completed");
|
||||
|
||||
await runGitFetch("origin", "demo-project");
|
||||
|
||||
expect(mockExecSync).toHaveBeenCalledWith("git fetch origin", expect.objectContaining({ cwd: "/projects/demo" }));
|
||||
expect(wasExecCalled("git fetch origin")).toBe(true);
|
||||
const lastCall = getLastExecCall();
|
||||
expect(lastCall).toBeDefined();
|
||||
expect(lastCall![1]).toMatchObject({ cwd: "/projects/demo" });
|
||||
});
|
||||
|
||||
it("propagates project resolution errors for git commands", async () => {
|
||||
@@ -133,35 +188,39 @@ describe("git commands", () => {
|
||||
|
||||
it("runGitPull uses resolved project path", async () => {
|
||||
const question = vi.fn().mockResolvedValue("y");
|
||||
mockCreateInterface.mockReturnValue({ question, close: vi.fn() } as any);
|
||||
mockExecSync
|
||||
.mockReturnValueOnce(".git")
|
||||
.mockReturnValueOnce("main\n")
|
||||
.mockReturnValueOnce("a1b2c3d\n")
|
||||
.mockReturnValueOnce("")
|
||||
.mockReturnValueOnce("0\t0\n")
|
||||
.mockReturnValueOnce("Already up to date.")
|
||||
.mockReturnValueOnce("Already up to date.");
|
||||
mockCreateInterface.mockReturnValue({ question, close: vi.fn() } as ReturnType<typeof createInterface>);
|
||||
mockNextResult(".git");
|
||||
mockNextResult("main\n");
|
||||
mockNextResult("a1b2c3d\n");
|
||||
mockNextResult("");
|
||||
mockNextResult("0\t0\n");
|
||||
mockNextResult("Already up to date.");
|
||||
mockNextResult("Already up to date.");
|
||||
|
||||
await runGitPull({ projectName: "demo-project" });
|
||||
|
||||
expect(mockExecSync).toHaveBeenCalledWith("git pull", expect.objectContaining({ cwd: "/projects/demo" }));
|
||||
expect(wasExecCalled("git pull")).toBe(true);
|
||||
const lastCall = getLastExecCall();
|
||||
expect(lastCall).toBeDefined();
|
||||
expect(lastCall![1]).toMatchObject({ cwd: "/projects/demo" });
|
||||
});
|
||||
|
||||
it("runGitPush uses resolved project path", async () => {
|
||||
const question = vi.fn().mockResolvedValue("y");
|
||||
mockCreateInterface.mockReturnValue({ question, close: vi.fn() } as any);
|
||||
mockExecSync
|
||||
.mockReturnValueOnce(".git")
|
||||
.mockReturnValueOnce("main\n")
|
||||
.mockReturnValueOnce("a1b2c3d\n")
|
||||
.mockReturnValueOnce("")
|
||||
.mockReturnValueOnce("0\t0\n")
|
||||
.mockReturnValueOnce("")
|
||||
.mockReturnValueOnce("");
|
||||
mockCreateInterface.mockReturnValue({ question, close: vi.fn() } as ReturnType<typeof createInterface>);
|
||||
mockNextResult(".git");
|
||||
mockNextResult("main\n");
|
||||
mockNextResult("a1b2c3d\n");
|
||||
mockNextResult("");
|
||||
mockNextResult("0\t0\n");
|
||||
mockNextResult("");
|
||||
mockNextResult("");
|
||||
|
||||
await runGitPush({ projectName: "demo-project" });
|
||||
|
||||
expect(mockExecSync).toHaveBeenCalledWith("git push", expect.objectContaining({ cwd: "/projects/demo" }));
|
||||
expect(wasExecCalled("git push")).toBe(true);
|
||||
const lastCall = getLastExecCall();
|
||||
expect(lastCall).toBeDefined();
|
||||
expect(lastCall![1]).toMatchObject({ cwd: "/projects/demo" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
|
||||
@@ -49,9 +52,9 @@ export type GitPushResult = {
|
||||
/**
|
||||
* Check if a directory is a git repository.
|
||||
*/
|
||||
export function isGitRepo(cwd: string = process.cwd()): boolean {
|
||||
export async function isGitRepo(cwd: string = process.cwd()): Promise<boolean> {
|
||||
try {
|
||||
execSync("git rev-parse --git-dir", { encoding: "utf-8", timeout: 5000, cwd });
|
||||
await execAsync("git rev-parse --git-dir", { encoding: "utf-8", timeout: 5000, cwd });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -85,25 +88,26 @@ export function isValidBranchName(name: string): boolean {
|
||||
* Get the current git status including branch, commit hash, and dirty state.
|
||||
* Returns structured data for CLI display.
|
||||
*/
|
||||
export function getGitStatus(cwd: string = process.cwd()): GitStatus | null {
|
||||
export async function getGitStatus(cwd: string = process.cwd()): Promise<GitStatus | null> {
|
||||
try {
|
||||
// Get current branch (empty string means detached HEAD)
|
||||
const branchOutput = execSync("git branch --show-current", { encoding: "utf-8", timeout: 5000, cwd }).trim();
|
||||
const branch = branchOutput || "HEAD detached";
|
||||
const { stdout: branchOutput } = await execAsync("git branch --show-current", { encoding: "utf-8", timeout: 5000, cwd });
|
||||
const branch = branchOutput.trim() || "HEAD detached";
|
||||
|
||||
// Get current commit hash (short)
|
||||
const commit = execSync("git rev-parse --short HEAD", { encoding: "utf-8", timeout: 5000, cwd }).trim();
|
||||
const { stdout: commitOut } = await execAsync("git rev-parse --short HEAD", { encoding: "utf-8", timeout: 5000, cwd });
|
||||
const commit = commitOut.trim();
|
||||
|
||||
// Check if working directory is dirty
|
||||
const statusOutput = execSync("git status --porcelain", { encoding: "utf-8", timeout: 5000, cwd }).trim();
|
||||
const isDirty = statusOutput.length > 0;
|
||||
const { stdout: statusOutput } = await execAsync("git status --porcelain", { encoding: "utf-8", timeout: 5000, cwd });
|
||||
const isDirty = statusOutput.trim().length > 0;
|
||||
|
||||
// Get ahead/behind counts from upstream
|
||||
let ahead = 0;
|
||||
let behind = 0;
|
||||
try {
|
||||
const revListOutput = execSync("git rev-list --left-right --count HEAD...@{u}", { encoding: "utf-8", timeout: 5000, cwd }).trim();
|
||||
const match = revListOutput.match(/(\d+)\s+(\d+)/);
|
||||
const { stdout: revListOutput } = await execAsync("git rev-list --left-right --count HEAD...@{u}", { encoding: "utf-8", timeout: 5000, cwd });
|
||||
const match = revListOutput.trim().match(/(\d+)\s+(\d+)/);
|
||||
if (match) {
|
||||
ahead = parseInt(match[1], 10);
|
||||
behind = parseInt(match[2], 10);
|
||||
@@ -121,9 +125,10 @@ export function getGitStatus(cwd: string = process.cwd()): GitStatus | null {
|
||||
/**
|
||||
* Count dirty files by parsing git status output.
|
||||
*/
|
||||
export function getDirtyFileCount(cwd: string = process.cwd()): { added: number; modified: number; deleted: number } {
|
||||
export async function getDirtyFileCount(cwd: string = process.cwd()): Promise<{ added: number; modified: number; deleted: number }> {
|
||||
try {
|
||||
const output = execSync("git status --porcelain", { encoding: "utf-8", timeout: 5000, cwd }).trim();
|
||||
const { stdout } = await execAsync("git status --porcelain", { encoding: "utf-8", timeout: 5000, cwd });
|
||||
const output = stdout.trim();
|
||||
if (!output) return { added: 0, modified: 0, deleted: 0 };
|
||||
|
||||
const lines = output.split("\n").filter(Boolean);
|
||||
@@ -148,13 +153,13 @@ export function getDirtyFileCount(cwd: string = process.cwd()): { added: number;
|
||||
/**
|
||||
* Fetch from origin or specified remote.
|
||||
*/
|
||||
export function fetchGitRemote(remote: string = "origin", cwd: string = process.cwd()): GitFetchResult {
|
||||
export async function fetchGitRemote(remote: string = "origin", cwd: string = process.cwd()): Promise<GitFetchResult> {
|
||||
if (!isValidBranchName(remote)) {
|
||||
throw new Error("Invalid remote name");
|
||||
}
|
||||
try {
|
||||
const output = execSync(`git fetch ${remote}`, { encoding: "utf-8", timeout: 30000, cwd });
|
||||
return { fetched: true, message: output.trim() || "Fetch completed" };
|
||||
const { stdout } = await execAsync(`git fetch ${remote}`, { encoding: "utf-8", timeout: 30000, cwd });
|
||||
return { fetched: true, message: stdout.trim() || "Fetch completed" };
|
||||
} catch (err: any) {
|
||||
const message = err.message || String(err);
|
||||
if (message.includes("Could not resolve host") || message.includes("Connection refused")) {
|
||||
@@ -168,10 +173,10 @@ export function fetchGitRemote(remote: string = "origin", cwd: string = process.
|
||||
/**
|
||||
* Pull the current branch.
|
||||
*/
|
||||
export function pullGitBranch(cwd: string = process.cwd()): GitPullResult {
|
||||
export async function pullGitBranch(cwd: string = process.cwd()): Promise<GitPullResult> {
|
||||
try {
|
||||
const output = execSync("git pull", { encoding: "utf-8", timeout: 30000, cwd });
|
||||
return { success: true, message: output.trim() };
|
||||
const { stdout } = await execAsync("git pull", { encoding: "utf-8", timeout: 30000, cwd });
|
||||
return { success: true, message: stdout.trim() };
|
||||
} catch (err: any) {
|
||||
const message = err.message || String(err);
|
||||
if (message.includes("CONFLICT") || message.includes("Merge conflict")) {
|
||||
@@ -184,10 +189,10 @@ export function pullGitBranch(cwd: string = process.cwd()): GitPullResult {
|
||||
/**
|
||||
* Push the current branch.
|
||||
*/
|
||||
export function pushGitBranch(cwd: string = process.cwd()): GitPushResult {
|
||||
export async function pushGitBranch(cwd: string = process.cwd()): Promise<GitPushResult> {
|
||||
try {
|
||||
const output = execSync("git push", { encoding: "utf-8", timeout: 30000, cwd });
|
||||
return { success: true, message: output.trim() || "Push completed" };
|
||||
const { stdout } = await execAsync("git push", { encoding: "utf-8", timeout: 30000, cwd });
|
||||
return { success: true, message: stdout.trim() || "Push completed" };
|
||||
} catch (err: any) {
|
||||
const message = err.message || String(err);
|
||||
if (message.includes("rejected") || message.includes("non-fast-forward")) {
|
||||
@@ -209,12 +214,12 @@ export async function runGitStatus(projectName?: string): Promise<void> {
|
||||
const projectPath = await resolveGitCwd(projectName);
|
||||
|
||||
// Validate directory is a git repo
|
||||
if (!isGitRepo(projectPath)) {
|
||||
if (!(await isGitRepo(projectPath))) {
|
||||
console.error("Error: Not a git repository");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const status = getGitStatus(projectPath);
|
||||
const status = await getGitStatus(projectPath);
|
||||
if (!status) {
|
||||
console.error("Error: Failed to get git status");
|
||||
process.exit(1);
|
||||
@@ -226,7 +231,7 @@ export async function runGitStatus(projectName?: string): Promise<void> {
|
||||
|
||||
// Status line
|
||||
if (status.isDirty) {
|
||||
const counts = getDirtyFileCount(projectPath);
|
||||
const counts = await getDirtyFileCount(projectPath);
|
||||
const parts: string[] = [];
|
||||
if (counts.added) parts.push(`+${counts.added}`);
|
||||
if (counts.modified) parts.push(`~${counts.modified}`);
|
||||
@@ -260,7 +265,7 @@ export async function runGitFetch(remote?: string, projectName?: string): Promis
|
||||
const projectPath = await resolveGitCwd(projectName);
|
||||
|
||||
// Validate directory is a git repo
|
||||
if (!isGitRepo(projectPath)) {
|
||||
if (!(await isGitRepo(projectPath))) {
|
||||
console.error("Error: Not a git repository");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -272,7 +277,7 @@ export async function runGitFetch(remote?: string, projectName?: string): Promis
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`git fetch ${targetRemote}`, { encoding: "utf-8", timeout: 30000, cwd: projectPath });
|
||||
await execAsync(`git fetch ${targetRemote}`, { encoding: "utf-8", timeout: 30000, cwd: projectPath });
|
||||
console.log();
|
||||
console.log(` ✓ Fetched from ${targetRemote}`);
|
||||
console.log();
|
||||
@@ -291,13 +296,13 @@ export async function runGitPull(options: { skipConfirm?: boolean; projectName?:
|
||||
const projectPath = await resolveGitCwd(options.projectName);
|
||||
|
||||
// Validate directory is a git repo
|
||||
if (!isGitRepo(projectPath)) {
|
||||
if (!(await isGitRepo(projectPath))) {
|
||||
console.error("Error: Not a git repository");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check for dirty state
|
||||
const status = getGitStatus(projectPath);
|
||||
const status = await getGitStatus(projectPath);
|
||||
if (!status) {
|
||||
console.error("Error: Failed to get git status");
|
||||
process.exit(1);
|
||||
@@ -308,7 +313,7 @@ export async function runGitPull(options: { skipConfirm?: boolean; projectName?:
|
||||
console.log();
|
||||
console.log(" ⚠ Warning: You have uncommitted changes.");
|
||||
console.log(` Branch: ${status.branch}`);
|
||||
|
||||
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const answer = await rl.question(" Continue with pull? [y/N] ");
|
||||
rl.close();
|
||||
@@ -321,11 +326,11 @@ export async function runGitPull(options: { skipConfirm?: boolean; projectName?:
|
||||
}
|
||||
|
||||
try {
|
||||
const output = execSync("git pull", { encoding: "utf-8", timeout: 30000, cwd: projectPath });
|
||||
const { stdout } = await execAsync("git pull", { encoding: "utf-8", timeout: 30000, cwd: projectPath });
|
||||
console.log();
|
||||
console.log(` ✓ Pulled latest changes for ${status.branch}`);
|
||||
if (output.trim() && output.trim() !== "Already up to date.") {
|
||||
console.log(` ${output.trim()}`);
|
||||
if (stdout.trim() && stdout.trim() !== "Already up to date.") {
|
||||
console.log(` ${stdout.trim()}`);
|
||||
}
|
||||
console.log();
|
||||
} catch (err: any) {
|
||||
@@ -348,13 +353,13 @@ export async function runGitPush(options: { skipConfirm?: boolean; projectName?:
|
||||
const projectPath = await resolveGitCwd(options.projectName);
|
||||
|
||||
// Validate directory is a git repo
|
||||
if (!isGitRepo(projectPath)) {
|
||||
if (!(await isGitRepo(projectPath))) {
|
||||
console.error("Error: Not a git repository");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const status = getGitStatus(projectPath);
|
||||
const status = await getGitStatus(projectPath);
|
||||
if (!status) {
|
||||
console.error("Error: Failed to get git status");
|
||||
process.exit(1);
|
||||
@@ -367,7 +372,7 @@ export async function runGitPush(options: { skipConfirm?: boolean; projectName?:
|
||||
|
||||
// Check for upstream
|
||||
try {
|
||||
execSync("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { encoding: "utf-8", timeout: 5000, cwd: projectPath });
|
||||
await execAsync("git rev-parse --abbrev-ref --symbolic-full-name @{u}", { encoding: "utf-8", timeout: 5000, cwd: projectPath });
|
||||
} catch {
|
||||
console.error("Error: No upstream configured for current branch");
|
||||
console.error(` Run: git push -u origin ${status.branch}`);
|
||||
@@ -389,11 +394,11 @@ export async function runGitPush(options: { skipConfirm?: boolean; projectName?:
|
||||
}
|
||||
|
||||
try {
|
||||
const output = execSync("git push", { encoding: "utf-8", timeout: 30000, cwd: projectPath });
|
||||
const { stdout } = await execAsync("git push", { encoding: "utf-8", timeout: 30000, cwd: projectPath });
|
||||
console.log();
|
||||
console.log(` ✓ Pushed ${status.branch} to origin`);
|
||||
if (output.trim()) {
|
||||
console.log(` ${output.trim()}`);
|
||||
if (stdout.trim()) {
|
||||
console.log(` ${stdout.trim()}`);
|
||||
}
|
||||
console.log();
|
||||
} catch (err: any) {
|
||||
|
||||
Reference in New Issue
Block a user