@@ -6,7 +6,13 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
66import { execSync } from "node:child_process"
77import { existsSync , mkdirSync , rmSync , writeFileSync } from "node:fs"
88import { join } from "node:path"
9- import { commitChanges , generateCommitMessage , hasChanges , pushChanges } from "../src/git.ts"
9+ import {
10+ commitChanges ,
11+ generateCommitMessage ,
12+ hasChanges ,
13+ hasUnpushedCommits ,
14+ pushChanges ,
15+ } from "../src/git.ts"
1016import type { Logger } from "../src/logger.ts"
1117
1218const TEST_DIR = "/tmp/opencoder-test-git"
@@ -326,4 +332,126 @@ describe("git", () => {
326332 rmSync ( nonGitDir , { recursive : true } )
327333 } )
328334 } )
335+
336+ describe ( "hasUnpushedCommits" , ( ) => {
337+ let testGitDir : string
338+
339+ beforeEach ( ( ) => {
340+ // Create a test git repository
341+ testGitDir = "/tmp/opencoder-test-git-unpushed"
342+ if ( existsSync ( testGitDir ) ) {
343+ rmSync ( testGitDir , { recursive : true } )
344+ }
345+ mkdirSync ( testGitDir , { recursive : true } )
346+ execSync ( "git init" , { cwd : testGitDir } )
347+ execSync ( 'git config user.email "test@test.com"' , { cwd : testGitDir } )
348+ execSync ( 'git config user.name "Test User"' , { cwd : testGitDir } )
349+ } )
350+
351+ afterEach ( ( ) => {
352+ if ( existsSync ( testGitDir ) ) {
353+ rmSync ( testGitDir , { recursive : true } )
354+ }
355+ } )
356+
357+ test ( "returns false for non-git directory" , ( ) => {
358+ const nonGitDir = "/tmp/opencoder-test-non-git-unpushed"
359+ if ( existsSync ( nonGitDir ) ) {
360+ rmSync ( nonGitDir , { recursive : true } )
361+ }
362+ mkdirSync ( nonGitDir , { recursive : true } )
363+
364+ expect ( hasUnpushedCommits ( nonGitDir ) ) . toBe ( false )
365+
366+ rmSync ( nonGitDir , { recursive : true } )
367+ } )
368+
369+ test ( "returns false for non-existent directory" , ( ) => {
370+ expect ( hasUnpushedCommits ( "/tmp/does-not-exist-xyz" ) ) . toBe ( false )
371+ } )
372+
373+ test ( "returns false when no upstream is configured" , ( ) => {
374+ // Create a commit but no upstream
375+ writeFileSync ( join ( testGitDir , "test.txt" ) , "test content" )
376+ execSync ( "git add . && git commit -m 'Initial commit'" , { cwd : testGitDir } )
377+
378+ expect ( hasUnpushedCommits ( testGitDir ) ) . toBe ( false )
379+ } )
380+
381+ test ( "returns false for empty repo" , ( ) => {
382+ expect ( hasUnpushedCommits ( testGitDir ) ) . toBe ( false )
383+ } )
384+ } )
385+
386+ describe ( "commitChanges with special characters" , ( ) => {
387+ let mockLogger : Logger
388+ let testGitDir : string
389+
390+ beforeEach ( ( ) => {
391+ mockLogger = {
392+ log : mock ( ( ) => { } ) ,
393+ logError : mock ( ( ) => { } ) ,
394+ logVerbose : mock ( ( ) => { } ) ,
395+ say : mock ( ( ) => { } ) ,
396+ info : mock ( ( ) => { } ) ,
397+ success : mock ( ( ) => { } ) ,
398+ warn : mock ( ( ) => { } ) ,
399+ alert : mock ( ( ) => { } ) ,
400+ flush : mock ( ( ) => { } ) ,
401+ setCycleLog : mock ( ( ) => { } ) ,
402+ cleanup : mock ( ( ) => 0 ) ,
403+ } as unknown as Logger
404+
405+ testGitDir = "/tmp/opencoder-test-git-special"
406+ if ( existsSync ( testGitDir ) ) {
407+ rmSync ( testGitDir , { recursive : true } )
408+ }
409+ mkdirSync ( testGitDir , { recursive : true } )
410+ execSync ( "git init" , { cwd : testGitDir } )
411+ execSync ( 'git config user.email "test@test.com"' , { cwd : testGitDir } )
412+ execSync ( 'git config user.name "Test User"' , { cwd : testGitDir } )
413+ } )
414+
415+ afterEach ( ( ) => {
416+ if ( existsSync ( testGitDir ) ) {
417+ rmSync ( testGitDir , { recursive : true } )
418+ }
419+ } )
420+
421+ test ( "handles double quotes in commit message" , ( ) => {
422+ writeFileSync ( join ( testGitDir , "test.txt" ) , "test content" )
423+
424+ commitChanges ( testGitDir , mockLogger , 'feat: add "quoted" feature' , false )
425+
426+ const log = execSync ( "git log --oneline" , { cwd : testGitDir , encoding : "utf-8" } )
427+ expect ( log ) . toContain ( 'add "quoted" feature' )
428+ } )
429+
430+ test ( "handles dollar signs in commit message" , ( ) => {
431+ writeFileSync ( join ( testGitDir , "test.txt" ) , "test content" )
432+
433+ commitChanges ( testGitDir , mockLogger , "feat: update $variable handling" , false )
434+
435+ const log = execSync ( "git log --format=%B -n 1" , { cwd : testGitDir , encoding : "utf-8" } )
436+ expect ( log ) . toContain ( "$variable" )
437+ } )
438+
439+ test ( "handles backticks in commit message" , ( ) => {
440+ writeFileSync ( join ( testGitDir , "test.txt" ) , "test content" )
441+
442+ commitChanges ( testGitDir , mockLogger , "feat: add `code` formatting" , false )
443+
444+ const log = execSync ( "git log --format=%B -n 1" , { cwd : testGitDir , encoding : "utf-8" } )
445+ expect ( log ) . toContain ( "`code`" )
446+ } )
447+
448+ test ( "handles backslashes in commit message" , ( ) => {
449+ writeFileSync ( join ( testGitDir , "test.txt" ) , "test content" )
450+
451+ commitChanges ( testGitDir , mockLogger , "feat: fix path\\to\\file handling" , false )
452+
453+ const log = execSync ( "git log --format=%B -n 1" , { cwd : testGitDir , encoding : "utf-8" } )
454+ expect ( log ) . toContain ( "path\\to\\file" )
455+ } )
456+ } )
329457} )
0 commit comments