@@ -8,7 +8,7 @@ import { describe, expect, vi } from "vitest";
88
99import { GitCoreLive , makeGitCore } from "./GitCore.ts" ;
1010import { GitCore , type GitCoreShape } from "../Services/GitCore.ts" ;
11- import { GitCommandError } from "../Errors.ts" ;
11+ import { GitCheckoutDirtyWorktreeError , GitCommandError } from "../Errors.ts" ;
1212import { type ProcessRunResult , runProcess } from "../../processRunner.ts" ;
1313import { ServerConfig } from "../../config.ts" ;
1414
@@ -40,6 +40,15 @@ function writeTextFile(
4040 } ) ;
4141}
4242
43+ function readTextFile (
44+ filePath : string ,
45+ ) : Effect . Effect < string , PlatformError . PlatformError , FileSystem . FileSystem > {
46+ return Effect . gen ( function * ( ) {
47+ const fileSystem = yield * FileSystem . FileSystem ;
48+ return yield * fileSystem . readFileString ( filePath ) ;
49+ } ) ;
50+ }
51+
4352/** Run a raw git command for test setup (not under test). */
4453function git (
4554 cwd : string ,
@@ -755,6 +764,124 @@ it.layer(TestLayer)("git integration", (it) => {
755764 ( yield * GitCore ) . checkoutBranch ( { cwd : tmp , branch : "other" } ) ,
756765 ) ;
757766 expect ( result . _tag ) . toBe ( "Failure" ) ;
767+ if ( result . _tag === "Failure" ) {
768+ const error = result . failure ;
769+ expect ( error ) . toBeInstanceOf ( GitCheckoutDirtyWorktreeError ) ;
770+ if ( error instanceof GitCheckoutDirtyWorktreeError ) {
771+ expect ( error . branch ) . toBe ( "other" ) ;
772+ expect ( error . conflictingFiles ) . toContain ( "README.md" ) ;
773+ expect ( error . message ) . toContain ( "Uncommitted changes block checkout to other:" ) ;
774+ }
775+ }
776+ } ) ,
777+ ) ;
778+ } ) ;
779+
780+ describe ( "stashAndCheckout" , ( ) => {
781+ it . effect ( "stashes dirty changes, switches branches, and reapplies the stash" , ( ) =>
782+ Effect . gen ( function * ( ) {
783+ const tmp = yield * makeTmpDir ( ) ;
784+ const { initialBranch } = yield * initRepoWithCommit ( tmp ) ;
785+ const core = yield * GitCore ;
786+
787+ yield * core . createBranch ( { cwd : tmp , branch : "feature" } ) ;
788+ yield * core . checkoutBranch ( { cwd : tmp , branch : "feature" } ) ;
789+ yield * writeTextFile ( path . join ( tmp , "feature.txt" ) , "feature content\n" ) ;
790+ yield * git ( tmp , [ "add" , "." ] ) ;
791+ yield * git ( tmp , [ "commit" , "-m" , "add feature file" ] ) ;
792+ yield * core . checkoutBranch ( { cwd : tmp , branch : initialBranch } ) ;
793+
794+ yield * writeTextFile ( path . join ( tmp , "README.md" ) , "dirty changes\n" ) ;
795+
796+ yield * core . stashAndCheckout ( { cwd : tmp , branch : "feature" } ) ;
797+
798+ const branches = yield * core . listBranches ( { cwd : tmp } ) ;
799+ expect ( branches . branches . find ( ( branch ) => branch . current ) ?. name ) . toBe ( "feature" ) ;
800+ expect ( yield * readTextFile ( path . join ( tmp , "README.md" ) ) ) . toBe ( "dirty changes\n" ) ;
801+ expect ( ( yield * git ( tmp , [ "stash" , "list" ] ) ) . trim ( ) ) . toBe ( "" ) ;
802+ } ) ,
803+ ) ;
804+
805+ it . effect ( "keeps the stash when reapplying dirty changes conflicts" , ( ) =>
806+ Effect . gen ( function * ( ) {
807+ const tmp = yield * makeTmpDir ( ) ;
808+ const { initialBranch } = yield * initRepoWithCommit ( tmp ) ;
809+ const core = yield * GitCore ;
810+
811+ yield * core . createBranch ( { cwd : tmp , branch : "conflicting" } ) ;
812+ yield * core . checkoutBranch ( { cwd : tmp , branch : "conflicting" } ) ;
813+ yield * writeTextFile ( path . join ( tmp , "README.md" ) , "conflicting content\n" ) ;
814+ yield * git ( tmp , [ "add" , "." ] ) ;
815+ yield * git ( tmp , [ "commit" , "-m" , "conflicting change" ] ) ;
816+ yield * core . checkoutBranch ( { cwd : tmp , branch : initialBranch } ) ;
817+
818+ yield * writeTextFile ( path . join ( tmp , "README.md" ) , "local edits that will conflict\n" ) ;
819+
820+ const result = yield * Effect . result (
821+ core . stashAndCheckout ( { cwd : tmp , branch : "conflicting" } ) ,
822+ ) ;
823+
824+ expect ( result . _tag ) . toBe ( "Failure" ) ;
825+ expect ( ( yield * git ( tmp , [ "stash" , "list" ] ) ) ) . toContain (
826+ "dpcode: stash before switching to conflicting" ,
827+ ) ;
828+ } ) ,
829+ ) ;
830+ } ) ;
831+
832+ describe ( "stashDrop" , ( ) => {
833+ it . effect ( "reads the top stash details" , ( ) =>
834+ Effect . gen ( function * ( ) {
835+ const tmp = yield * makeTmpDir ( ) ;
836+ const core = yield * GitCore ;
837+ const { initialBranch } = yield * initRepoWithCommit ( tmp ) ;
838+
839+ yield * writeTextFile ( path . join ( tmp , "README.md" ) , "stashed changes\n" ) ;
840+ yield * writeTextFile ( path . join ( tmp , "new-file.txt" ) , "new file\n" ) ;
841+ yield * git ( tmp , [ "stash" , "push" , "-u" , "-m" , "test stash" ] ) ;
842+
843+ const info = yield * core . stashInfo ( { cwd : tmp } ) ;
844+
845+ expect ( info . cwd ) . toBe ( tmp ) ;
846+ expect ( info . branch ) . toBe ( initialBranch ) ;
847+ expect ( info . stashRef ) . toBe ( "stash@{0}" ) ;
848+ expect ( info . message ) . toContain ( "test stash" ) ;
849+ expect ( info . files ) . toContain ( "README.md" ) ;
850+ expect ( info . files ) . toContain ( "new-file.txt" ) ;
851+ } ) ,
852+ ) ;
853+
854+ it . effect ( "drops the top stash entry" , ( ) =>
855+ Effect . gen ( function * ( ) {
856+ const tmp = yield * makeTmpDir ( ) ;
857+ const core = yield * GitCore ;
858+ yield * initRepoWithCommit ( tmp ) ;
859+
860+ yield * writeTextFile ( path . join ( tmp , "README.md" ) , "stashed changes\n" ) ;
861+ yield * git ( tmp , [ "stash" , "push" , "-m" , "test stash" ] ) ;
862+ expect ( yield * git ( tmp , [ "stash" , "list" ] ) ) . toContain ( "test stash" ) ;
863+
864+ yield * core . stashDrop ( { cwd : tmp } ) ;
865+
866+ expect ( ( yield * git ( tmp , [ "stash" , "list" ] ) ) . trim ( ) ) . toBe ( "" ) ;
867+ } ) ,
868+ ) ;
869+ } ) ;
870+
871+ describe ( "removeIndexLock" , ( ) => {
872+ it . effect ( "removes the repository index lock path reported by git" , ( ) =>
873+ Effect . gen ( function * ( ) {
874+ const tmp = yield * makeTmpDir ( ) ;
875+ const core = yield * GitCore ;
876+ yield * initRepoWithCommit ( tmp ) ;
877+
878+ const lockPath = path . join ( tmp , ".git" , "index.lock" ) ;
879+ yield * writeTextFile ( lockPath , "" ) ;
880+ expect ( existsSync ( lockPath ) ) . toBe ( true ) ;
881+
882+ yield * core . removeIndexLock ( { cwd : tmp } ) ;
883+
884+ expect ( existsSync ( lockPath ) ) . toBe ( false ) ;
758885 } ) ,
759886 ) ;
760887 } ) ;
0 commit comments