From 62464a97452047141768b38b442f43ffaa3c4bb5 Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Sat, 22 Nov 2025 04:37:30 +0530 Subject: [PATCH 1/2] fix: auto remote + upstream for push action --- .gitignore | 3 ++- src/managers/RemoteManager.ts | 50 ++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 6f66877..36d161c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules dist .DS_Store .env -coverage \ No newline at end of file +coverage +tests/sandbox \ No newline at end of file diff --git a/src/managers/RemoteManager.ts b/src/managers/RemoteManager.ts index 3e690c8..844d833 100644 --- a/src/managers/RemoteManager.ts +++ b/src/managers/RemoteManager.ts @@ -10,10 +10,9 @@ import inquirer from 'inquirer'; import { execSync } from 'child_process'; -import { GitExecutor } from '../core/GitExecutor'; +import { GitExecutor } from '../core/GitExecutor'; import { Logger } from '../utils/Logger'; - /** * Helper: get list of remote names */ @@ -122,18 +121,43 @@ export const RemoteManager = { */ async pushChanges() { const remotes = getRemoteList(); - const { remote } = await inquirer.prompt([ - { - type: 'list', - name: 'remote', - message: 'Select remote to push to:', - choices: remotes.length ? remotes : ['origin'], - }, - ]); + // Step 1 — If no remotes, ask user to add one + if (remotes.length === 0) { + Logger.error('❌ No remote found for this repository.'); + + const { url } = await inquirer.prompt([ + { type: 'input', name: 'url', message: "Enter remote URL to add as 'origin':" }, + ]); + + Logger.info(`🔗 Adding remote origin → ${url}`); + await GitExecutor.run(`git remote add origin ${url}`); + remotes.push('origin'); + } + + // Step 2 — Detect current branch + const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim(); + + // Step 3 — Check if branch has upstream + let hasUpstream = true; + try { + execSync('git rev-parse --abbrev-ref --symbolic-full-name @{u}'); + } catch { + hasUpstream = false; + } + + // Step 4 — If no upstream, push -u + if (!hasUpstream) { + Logger.info(`🚀 First-time push detected for branch '${currentBranch}'.`); + Logger.info(`Setting upstream to origin/${currentBranch}...`); + await GitExecutor.run(`git push -u origin ${currentBranch}`); + Logger.success('✅ Pushed & upstream tracking set!'); + return; + } - Logger.info(`🚀 Pushing changes to '${remote}'...`); - await GitExecutor.run(`git push ${remote}`); - Logger.success(`✅ Changes pushed to '${remote}'!`); + // Step 5 — Normal push + Logger.info(`🚀 Pushing changes to remote...`); + await GitExecutor.run(`git push`); + Logger.success('✅ Changes pushed!'); }, /** From 28e78892abf70e7b5b1129a295f00cac0b234acd Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Sat, 22 Nov 2025 05:02:20 +0530 Subject: [PATCH 2/2] test: RemoteManager --- tests/mocks/inquirerMock.ts | 4 + tests/unit/RemoteManager.test.ts | 259 +++++++++++++++++++------------ 2 files changed, 166 insertions(+), 97 deletions(-) diff --git a/tests/mocks/inquirerMock.ts b/tests/mocks/inquirerMock.ts index ccfdedb..a8aa85b 100644 --- a/tests/mocks/inquirerMock.ts +++ b/tests/mocks/inquirerMock.ts @@ -49,3 +49,7 @@ export function mockInquirer(answers: Record) { // ignore - tests will fail later if mock not present } } + +export function clearInquirerQueue() { + _answersQueue.length = 0; +} \ No newline at end of file diff --git a/tests/unit/RemoteManager.test.ts b/tests/unit/RemoteManager.test.ts index 1f2adf9..495fb51 100644 --- a/tests/unit/RemoteManager.test.ts +++ b/tests/unit/RemoteManager.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { GitExecutor } from "@/core/GitExecutor"; -import { mockInquirer } from "../mocks/inquirerMock"; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { GitExecutor } from '@/core/GitExecutor'; +import { mockInquirer, clearInquirerQueue } from '../mocks/inquirerMock'; // Mock execSync BEFORE importing RemoteManager vi.mock('child_process', async (importOriginal) => { @@ -13,67 +13,66 @@ vi.mock('child_process', async (importOriginal) => { } as any; }); -import { execSync } from "child_process"; +import { execSync } from 'child_process'; -describe("RemoteManager – Full Test Suite", () => { +describe('RemoteManager – Full Test Suite', () => { beforeEach(() => { vi.clearAllMocks(); + clearInquirerQueue(); }); // -------------------------------------------------------------- // 1️⃣ listRemotes() // -------------------------------------------------------------- - it("should list remotes", async () => { - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + it('should list remotes', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.listRemotes(); - expect(spy).toHaveBeenCalledWith("git remote -v"); + expect(spy).toHaveBeenCalledWith('git remote -v'); }); // -------------------------------------------------------------- // 2️⃣ addRemote() // -------------------------------------------------------------- - it("should add a remote", async () => { - mockInquirer({ name: "origin", url: "https://github.com/test/repo" }); + it('should add a remote', async () => { + mockInquirer({ name: 'origin', url: 'https://github.com/test/repo' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.addRemote(); - expect(spy).toHaveBeenCalledWith( - "git remote add origin https://github.com/test/repo" - ); + expect(spy).toHaveBeenCalledWith('git remote add origin https://github.com/test/repo'); }); // -------------------------------------------------------------- // 3️⃣ renameRemote() // -------------------------------------------------------------- - it("should rename a remote", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should rename a remote', async () => { + (execSync as any).mockReturnValue('origin\nbackup'); - mockInquirer({ oldName: "origin" }); - mockInquirer({ newName: "main-origin" }); + mockInquirer({ oldName: 'origin' }); + mockInquirer({ newName: 'main-origin' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.renameRemote(); - expect(spy).toHaveBeenCalledWith("git remote rename origin main-origin"); + expect(spy).toHaveBeenCalledWith('git remote rename origin main-origin'); }); - it("should not rename if no remotes exist", async () => { - (execSync as any).mockReturnValue(""); + it('should not rename if no remotes exist', async () => { + (execSync as any).mockReturnValue(''); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.renameRemote(); @@ -83,26 +82,26 @@ describe("RemoteManager – Full Test Suite", () => { // -------------------------------------------------------------- // 4️⃣ removeRemote() // -------------------------------------------------------------- - it("should remove remote", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should remove remote', async () => { + (execSync as any).mockReturnValue('origin\nbackup'); - mockInquirer({ name: "backup" }); + mockInquirer({ name: 'backup' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.removeRemote(); - expect(spy).toHaveBeenCalledWith("git remote remove backup"); + expect(spy).toHaveBeenCalledWith('git remote remove backup'); }); - it("should not remove if no remotes", async () => { - (execSync as any).mockReturnValue(""); + it('should not remove if no remotes', async () => { + (execSync as any).mockReturnValue(''); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.removeRemote(); @@ -112,138 +111,204 @@ describe("RemoteManager – Full Test Suite", () => { // -------------------------------------------------------------- // 5️⃣ updateRemoteUrl() // -------------------------------------------------------------- - it("should update remote URL", async () => { - (execSync as any).mockReturnValue("origin"); + it('should update remote URL', async () => { + (execSync as any).mockReturnValue('origin'); - mockInquirer({ name: "origin" }); - mockInquirer({ url: "https://github.com/new/url" }); + mockInquirer({ name: 'origin' }); + mockInquirer({ url: 'https://github.com/new/url' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.updateRemoteUrl(); - expect(spy).toHaveBeenCalledWith( - "git remote set-url origin https://github.com/new/url" - ); + expect(spy).toHaveBeenCalledWith('git remote set-url origin https://github.com/new/url'); }); // -------------------------------------------------------------- // 6️⃣ pushChanges() // -------------------------------------------------------------- - it("should push changes to selected remote", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should ask for remote URL and perform first push when NO remotes exist', async () => { + // No remotes in repo + (execSync as any).mockReturnValueOnce(''); // getRemoteList() + + // User enters remote URL + mockInquirer({ url: 'https://github.com/test/repo.git' }); - mockInquirer({ remote: "backup" }); + const executorSpy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); - const { RemoteManager } = await import("@/managers"); + // Mock: detecting currentBranch + (execSync as any).mockReturnValueOnce('main'); // current branch + (execSync as any).mockImplementationOnce(() => { + throw new Error(); + }); // no upstream await RemoteManager.pushChanges(); - expect(spy).toHaveBeenCalledWith("git push backup"); + expect(executorSpy).toHaveBeenCalledWith( + 'git remote add origin https://github.com/test/repo.git', + ); + + expect(executorSpy).toHaveBeenCalledWith('git push -u origin main'); }); - // If no remotes → default to origin - it("should push to origin when no remotes exist", async () => { - (execSync as any).mockReturnValue(""); + it('should push with -u when upstream is missing', async () => { + // Mock remotes exist + (execSync as any).mockReturnValueOnce('origin\n'); + + // No inquirer needed (remote auto-chooses origin) + mockInquirer({}); + + const executorSpy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { RemoteManager } = await import('@/managers'); + + // Mock branch name + (execSync as any).mockReturnValueOnce('dev'); // current branch + + // Mock: upstream missing -> execSync throws + (execSync as any).mockImplementationOnce(() => { + throw new Error('No upstream'); + }); + + await RemoteManager.pushChanges(); - mockInquirer({ remote: "origin" }); + expect(executorSpy).toHaveBeenCalledWith('git push -u origin dev'); + }); + + it('should perform normal push when upstream exists', async () => { + // Remote present + (execSync as any).mockReturnValueOnce('origin\n'); + + // No prompts necessary + mockInquirer({}); + + const executorSpy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { RemoteManager } = await import('@/managers'); + + // Mock branch = main + (execSync as any).mockReturnValueOnce('main'); + + // Mock upstream exists (does NOT throw) + (execSync as any).mockReturnValueOnce('origin/main'); + + await RemoteManager.pushChanges(); + + expect(executorSpy).toHaveBeenCalledWith('git push'); + }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + // If no remotes → ask for URL, add remote, then push + it('should push to origin when no remotes exist', async () => { + mockInquirer({ url: 'https://github.com/test/repo.git' }); + + // Mock execSync calls in order: + // 1. getRemoteList() -> execSync('git remote') -> returns '' + // 2. pushChanges() -> execSync('git rev-parse --abbrev-ref HEAD') -> returns 'main' + // 3. pushChanges() -> execSync('git rev-parse --abbrev-ref --symbolic-full-name @{u}') -> throws + (execSync as any) + .mockReturnValueOnce('') // getRemoteList() - no remotes + .mockReturnValueOnce('main') // current branch + .mockImplementationOnce(() => { + throw new Error(); // no upstream + }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.pushChanges(); - expect(spy).toHaveBeenCalledWith("git push origin"); + expect(spy).toHaveBeenCalledWith('git remote add origin https://github.com/test/repo.git'); + expect(spy).toHaveBeenCalledWith('git push -u origin main'); }); // -------------------------------------------------------------- // 7️⃣ pushWithUpstream() // -------------------------------------------------------------- - it("should push with upstream", async () => { - (execSync as any).mockReturnValue("origin"); + it('should push with upstream', async () => { + (execSync as any).mockReturnValue('origin'); - mockInquirer({ remote: "origin" }); - mockInquirer({ branch: "dev" }); + // pushWithUpstream uses a single prompt with array of 2 questions + mockInquirer({ remote: 'origin', branch: 'dev' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.pushWithUpstream(); - expect(spy).toHaveBeenCalledWith("git push -u origin dev"); + expect(spy).toHaveBeenCalledWith('git push -u origin dev'); }); // -------------------------------------------------------------- // 8️⃣ pullChanges() // -------------------------------------------------------------- - it("should pull from selected remote", async () => { - (execSync as any).mockReturnValue("origin\nteam"); + it('should pull from selected remote', async () => { + (execSync as any).mockReturnValue('origin\nteam'); - mockInquirer({ remote: "team" }); + mockInquirer({ remote: 'team' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.pullChanges(); - expect(spy).toHaveBeenCalledWith("git pull team"); + expect(spy).toHaveBeenCalledWith('git pull team'); }); // -------------------------------------------------------------- // 9️⃣ fetchUpdates() // -------------------------------------------------------------- - it("should fetch from selected remote", async () => { - (execSync as any).mockReturnValue("origin\ntest"); + it('should fetch from selected remote', async () => { + (execSync as any).mockReturnValue('origin\ntest'); - mockInquirer({ remote: "test" }); + mockInquirer({ remote: 'test' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.fetchUpdates(); - expect(spy).toHaveBeenCalledWith("git fetch test"); + expect(spy).toHaveBeenCalledWith('git fetch test'); }); it("should fetch --all when user selects 'all'", async () => { - (execSync as any).mockReturnValue("origin\nteam"); + (execSync as any).mockReturnValue('origin\nteam'); - mockInquirer({ remote: "all" }); + mockInquirer({ remote: 'all' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.fetchUpdates(); - expect(spy).toHaveBeenCalledWith("git fetch --all"); + expect(spy).toHaveBeenCalledWith('git fetch --all'); }); // -------------------------------------------------------------- // 🔟 showRemoteInfo() // -------------------------------------------------------------- - it("should show remote info", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should show remote info', async () => { + (execSync as any).mockReturnValue('origin\nbackup'); - mockInquirer({ remote: "backup" }); + mockInquirer({ remote: 'backup' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.showRemoteInfo(); - expect(spy).toHaveBeenCalledWith("git remote show backup"); + expect(spy).toHaveBeenCalledWith('git remote show backup'); }); - it("should do nothing if no remotes exist", async () => { - (execSync as any).mockReturnValue(""); + it('should do nothing if no remotes exist', async () => { + (execSync as any).mockReturnValue(''); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.showRemoteInfo(); @@ -253,13 +318,13 @@ describe("RemoteManager – Full Test Suite", () => { // -------------------------------------------------------------- // 1️⃣1️⃣ syncAll() // -------------------------------------------------------------- - it("should sync all remotes", async () => { - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + it('should sync all remotes', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.syncAll(); - expect(spy).toHaveBeenCalledWith("git fetch --all && git pull --all"); + expect(spy).toHaveBeenCalledWith('git fetch --all && git pull --all'); }); });