@@ -881,6 +881,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
881881 hasUpstream : false ,
882882 aheadCount : 0 ,
883883 behindCount : 0 ,
884+ aheadOfDefaultCount : 0 ,
884885 pr : null ,
885886 } ) ;
886887 } ) ,
@@ -910,6 +911,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
910911 hasUpstream : false ,
911912 aheadCount : 0 ,
912913 behindCount : 0 ,
914+ aheadOfDefaultCount : 0 ,
913915 pr : null ,
914916 } ) ;
915917 } ) ,
@@ -1672,6 +1674,41 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
16721674 } ) ,
16731675 ) ;
16741676
1677+ it . effect ( "pushes existing commits without committing dirty worktree changes" , ( ) =>
1678+ Effect . gen ( function * ( ) {
1679+ const repoDir = yield * makeTempDir ( "t3code-git-manager-" ) ;
1680+ yield * initRepo ( repoDir ) ;
1681+ yield * runGit ( repoDir , [ "checkout" , "-b" , "feature/push-dirty" ] ) ;
1682+ const remoteDir = yield * createBareRemote ( ) ;
1683+ yield * runGit ( repoDir , [ "remote" , "add" , "origin" , remoteDir ] ) ;
1684+ fs . writeFileSync ( path . join ( repoDir , "push-dirty.txt" ) , "push dirty\n" ) ;
1685+ yield * runGit ( repoDir , [ "add" , "push-dirty.txt" ] ) ;
1686+ yield * runGit ( repoDir , [ "commit" , "-m" , "Push dirty branch" ] ) ;
1687+ fs . mkdirSync ( path . join ( repoDir , ".vercel" ) ) ;
1688+ fs . writeFileSync ( path . join ( repoDir , ".vercel" , "project.json" ) , "{}\n" ) ;
1689+
1690+ const { manager } = yield * makeManager ( ) ;
1691+ const result = yield * runStackedAction ( manager , {
1692+ cwd : repoDir ,
1693+ action : "push" ,
1694+ } ) ;
1695+
1696+ expect ( result . commit . status ) . toBe ( "skipped_not_requested" ) ;
1697+ expect ( result . push . status ) . toBe ( "pushed" ) ;
1698+ expect ( result . pr . status ) . toBe ( "skipped_not_requested" ) ;
1699+ expect (
1700+ yield * runGit ( repoDir , [ "status" , "--porcelain" ] ) . pipe (
1701+ Effect . map ( ( output ) => output . stdout . trim ( ) ) ,
1702+ ) ,
1703+ ) . toContain ( "?? .vercel/" ) ;
1704+ expect (
1705+ yield * runGit ( remoteDir , [ "log" , "-1" , "--pretty=%s" , "feature/push-dirty" ] ) . pipe (
1706+ Effect . map ( ( output ) => output . stdout . trim ( ) ) ,
1707+ ) ,
1708+ ) . toBe ( "Push dirty branch" ) ;
1709+ } ) ,
1710+ ) ;
1711+
16751712 it . effect ( "create_pr pushes a clean branch before creating the PR when needed" , ( ) =>
16761713 Effect . gen ( function * ( ) {
16771714 const repoDir = yield * makeTempDir ( "t3code-git-manager-" ) ;
@@ -2418,6 +2455,113 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
24182455 } ) ,
24192456 ) ;
24202457
2458+ it . effect (
2459+ "restores same-repository upstream tracking after local PR checkout without a remote ref" ,
2460+ ( ) =>
2461+ Effect . gen ( function * ( ) {
2462+ const repoDir = yield * makeTempDir ( "t3code-git-manager-" ) ;
2463+ yield * initRepo ( repoDir ) ;
2464+ const remoteDir = yield * createBareRemote ( ) ;
2465+ yield * runGit ( repoDir , [ "remote" , "add" , "origin" , remoteDir ] ) ;
2466+ yield * runGit ( repoDir , [ "push" , "-u" , "origin" , "main" ] ) ;
2467+ yield * runGit ( repoDir , [ "checkout" , "-b" , "feature/pr-local-upstream" ] ) ;
2468+ fs . writeFileSync ( path . join ( repoDir , "upstream.txt" ) , "upstream\n" ) ;
2469+ yield * runGit ( repoDir , [ "add" , "upstream.txt" ] ) ;
2470+ yield * runGit ( repoDir , [ "commit" , "-m" , "Local upstream PR branch" ] ) ;
2471+ yield * runGit ( repoDir , [ "push" , "-u" , "origin" , "feature/pr-local-upstream" ] ) ;
2472+ yield * runGit ( repoDir , [ "checkout" , "main" ] ) ;
2473+ yield * runGit ( repoDir , [ "branch" , "-D" , "feature/pr-local-upstream" ] ) ;
2474+ yield * runGit ( repoDir , [
2475+ "update-ref" ,
2476+ "-d" ,
2477+ "refs/remotes/origin/feature/pr-local-upstream" ,
2478+ ] ) ;
2479+
2480+ const { manager } = yield * makeManager ( {
2481+ ghScenario : {
2482+ pullRequest : {
2483+ number : 65 ,
2484+ title : "Local upstream PR" ,
2485+ url : "https://github.com/pingdotgg/codething-mvp/pull/65" ,
2486+ baseRefName : "main" ,
2487+ headRefName : "feature/pr-local-upstream" ,
2488+ state : "open" ,
2489+ isCrossRepository : false ,
2490+ headRepositoryNameWithOwner : "pingdotgg/codething-mvp" ,
2491+ headRepositoryOwnerLogin : "pingdotgg" ,
2492+ } ,
2493+ repositoryCloneUrls : {
2494+ "pingdotgg/codething-mvp" : {
2495+ url : remoteDir ,
2496+ sshUrl : remoteDir ,
2497+ } ,
2498+ } ,
2499+ } ,
2500+ } ) ;
2501+
2502+ const result = yield * preparePullRequestThread ( manager , {
2503+ cwd : repoDir ,
2504+ reference : "65" ,
2505+ mode : "local" ,
2506+ } ) ;
2507+
2508+ expect ( result . worktreePath ) . toBeNull ( ) ;
2509+ expect ( result . branch ) . toBe ( "feature/pr-local-upstream" ) ;
2510+ expect (
2511+ ( yield * runGit ( repoDir , [ "rev-parse" , "--abbrev-ref" , "@{upstream}" ] ) ) . stdout . trim ( ) ,
2512+ ) . toBe ( "origin/feature/pr-local-upstream" ) ;
2513+ } ) ,
2514+ ) ;
2515+
2516+ it . effect (
2517+ "restores same-repository upstream tracking when provider omits head repository metadata" ,
2518+ ( ) =>
2519+ Effect . gen ( function * ( ) {
2520+ const repoDir = yield * makeTempDir ( "t3code-git-manager-" ) ;
2521+ yield * initRepo ( repoDir ) ;
2522+ const remoteDir = yield * createBareRemote ( ) ;
2523+ yield * runGit ( repoDir , [ "remote" , "add" , "origin" , remoteDir ] ) ;
2524+ yield * runGit ( repoDir , [ "push" , "-u" , "origin" , "main" ] ) ;
2525+ yield * runGit ( repoDir , [ "checkout" , "-b" , "feature/pr-local-no-head-repo" ] ) ;
2526+ fs . writeFileSync ( path . join ( repoDir , "no-head-repo.txt" ) , "upstream\n" ) ;
2527+ yield * runGit ( repoDir , [ "add" , "no-head-repo.txt" ] ) ;
2528+ yield * runGit ( repoDir , [ "commit" , "-m" , "Local PR branch without repo metadata" ] ) ;
2529+ yield * runGit ( repoDir , [ "push" , "-u" , "origin" , "feature/pr-local-no-head-repo" ] ) ;
2530+ yield * runGit ( repoDir , [ "checkout" , "main" ] ) ;
2531+ yield * runGit ( repoDir , [ "branch" , "-D" , "feature/pr-local-no-head-repo" ] ) ;
2532+ yield * runGit ( repoDir , [
2533+ "update-ref" ,
2534+ "-d" ,
2535+ "refs/remotes/origin/feature/pr-local-no-head-repo" ,
2536+ ] ) ;
2537+
2538+ const { manager } = yield * makeManager ( {
2539+ ghScenario : {
2540+ pullRequest : {
2541+ number : 66 ,
2542+ title : "Local upstream PR without repo metadata" ,
2543+ url : "https://github.com/pingdotgg/codething-mvp/pull/66" ,
2544+ baseRefName : "main" ,
2545+ headRefName : "feature/pr-local-no-head-repo" ,
2546+ state : "open" ,
2547+ } ,
2548+ } ,
2549+ } ) ;
2550+
2551+ const result = yield * preparePullRequestThread ( manager , {
2552+ cwd : repoDir ,
2553+ reference : "66" ,
2554+ mode : "local" ,
2555+ } ) ;
2556+
2557+ expect ( result . worktreePath ) . toBeNull ( ) ;
2558+ expect ( result . branch ) . toBe ( "feature/pr-local-no-head-repo" ) ;
2559+ expect (
2560+ ( yield * runGit ( repoDir , [ "rev-parse" , "--abbrev-ref" , "@{upstream}" ] ) ) . stdout . trim ( ) ,
2561+ ) . toBe ( "origin/feature/pr-local-no-head-repo" ) ;
2562+ } ) ,
2563+ ) ;
2564+
24212565 it . effect ( "prepares pull request threads in worktree mode on the PR head branch" , ( ) =>
24222566 Effect . gen ( function * ( ) {
24232567 const repoDir = yield * makeTempDir ( "t3code-git-manager-" ) ;
0 commit comments