@@ -15,6 +15,7 @@ import {
1515 getRepoInfo ,
1616 normalizePathspec ,
1717 parseRepoUrl ,
18+ resolveFirstSyncBoundary ,
1819} from "./git" ;
1920
2021describe ( "normalizePathspec" , ( ) => {
@@ -449,6 +450,29 @@ function runGit(command: string, cwd: string): string {
449450 } ) . trim ( ) ;
450451}
451452
453+ /**
454+ * Initializes a tmpdir repo, configures user, creates the listed directories,
455+ * lands a seed commit, and renames the branch to `main`. Returns the cwd and
456+ * base SHA.
457+ */
458+ function initTempRepo ( opts : { prefix : string ; dirs : string [ ] ; seedFile : { path : string ; content : string } } ) : {
459+ cwd : string ;
460+ base : string ;
461+ } {
462+ const cwd = mkdtempSync ( join ( tmpdir ( ) , opts . prefix ) ) ;
463+ runGit ( "init" , cwd ) ;
464+ runGit ( 'config user.email "test@example.com"' , cwd ) ;
465+ runGit ( 'config user.name "Test User"' , cwd ) ;
466+ for ( const dir of opts . dirs ) {
467+ mkdirSync ( join ( cwd , dir ) , { recursive : true } ) ;
468+ }
469+ writeFileSync ( join ( cwd , opts . seedFile . path ) , opts . seedFile . content ) ;
470+ runGit ( "add ." , cwd ) ;
471+ runGit ( 'commit -m "Initial"' , cwd ) ;
472+ runGit ( "branch -M main" , cwd ) ;
473+ return { cwd, base : runGit ( "rev-parse HEAD" , cwd ) } ;
474+ }
475+
452476/**
453477 * Cuts `branch` off `baseBranch`, lands one file change, merges back via
454478 * `--no-ff` with a GitHub-style PR-merge message, then deletes `branch` to
@@ -569,19 +593,11 @@ function createTempRepoWithMerge(): TempRepoWithMerge {
569593 * commit merged back as HEAD. `merge300` touches `infra/` only.
570594 */
571595function createTempRepoWithMultipleMerges ( ) : TempRepoWithMultipleMerges {
572- const cwd = mkdtempSync ( join ( tmpdir ( ) , "linear-release-multi-merge-" ) ) ;
573- runGit ( "init" , cwd ) ;
574- runGit ( 'config user.email "test@example.com"' , cwd ) ;
575- runGit ( 'config user.name "Test User"' , cwd ) ;
576-
577- mkdirSync ( join ( cwd , "frontend" ) , { recursive : true } ) ;
578- mkdirSync ( join ( cwd , "backend" ) , { recursive : true } ) ;
579- mkdirSync ( join ( cwd , "infra" ) , { recursive : true } ) ;
580- writeFileSync ( join ( cwd , "frontend" , "seed.txt" ) , "seed" ) ;
581- runGit ( "add ." , cwd ) ;
582- runGit ( 'commit -m "Initial"' , cwd ) ;
583- runGit ( "branch -M main" , cwd ) ;
584- const base = runGit ( "rev-parse HEAD" , cwd ) ;
596+ const { cwd, base } = initTempRepo ( {
597+ prefix : "linear-release-multi-merge-" ,
598+ dirs : [ "frontend" , "backend" , "infra" ] ,
599+ seedFile : { path : "frontend/seed.txt" , content : "seed" } ,
600+ } ) ;
585601
586602 const merge100 = mergeFeatureBranch ( {
587603 cwd,
@@ -625,19 +641,11 @@ function createTempRepoWithMultipleMerges(): TempRepoWithMultipleMerges {
625641 * only.
626642 */
627643function createTempRepoReleaseBranch ( ) : TempRepoReleaseBranch {
628- const cwd = mkdtempSync ( join ( tmpdir ( ) , "linear-release-rel-branch-" ) ) ;
629- runGit ( "init" , cwd ) ;
630- runGit ( 'config user.email "test@example.com"' , cwd ) ;
631- runGit ( 'config user.name "Test User"' , cwd ) ;
632-
633- mkdirSync ( join ( cwd , "frontend-nuxt3" ) , { recursive : true } ) ;
634- mkdirSync ( join ( cwd , "backend" ) , { recursive : true } ) ;
635- mkdirSync ( join ( cwd , "mobile-android" ) , { recursive : true } ) ;
636- writeFileSync ( join ( cwd , "frontend-nuxt3" , "seed.ts" ) , "seed" ) ;
637- runGit ( "add ." , cwd ) ;
638- runGit ( 'commit -m "Initial"' , cwd ) ;
639- runGit ( "branch -M main" , cwd ) ;
640- const base = runGit ( "rev-parse HEAD" , cwd ) ;
644+ const { cwd, base } = initTempRepo ( {
645+ prefix : "linear-release-rel-branch-" ,
646+ dirs : [ "frontend-nuxt3" , "backend" , "mobile-android" ] ,
647+ seedFile : { path : "frontend-nuxt3/seed.ts" , content : "seed" } ,
648+ } ) ;
641649
642650 runGit ( "checkout -b rel/2026-05-06 main" , cwd ) ;
643651 mergeFeatureBranch ( {
@@ -887,6 +895,22 @@ describe("merge commit handling", () => {
887895 } ) ;
888896 } ) ;
889897
898+ describe ( "resolveFirstSyncBoundary" , ( ) => {
899+ it ( "expands to HEAD^1 when HEAD is a merge commit" , ( ) => {
900+ expect ( resolveFirstSyncBoundary ( mergeRepo . commits . mergeCommit , mergeRepo . cwd ) ) . toBe ( mergeRepo . commits . base ) ;
901+ } ) ;
902+
903+ it ( "returns the commit itself when HEAD is a regular commit" , ( ) => {
904+ expect ( resolveFirstSyncBoundary ( mergeRepo . commits . featureBranch , mergeRepo . cwd ) ) . toBe (
905+ mergeRepo . commits . featureBranch ,
906+ ) ;
907+ } ) ;
908+
909+ it ( "returns the commit itself when HEAD is the root commit" , ( ) => {
910+ expect ( resolveFirstSyncBoundary ( mergeRepo . commits . base , mergeRepo . cwd ) ) . toBe ( mergeRepo . commits . base ) ;
911+ } ) ;
912+ } ) ;
913+
890914 describe ( "getCommitContextsBetweenShas with merge commits" , ( ) => {
891915 it ( "should include merge commit when path filtering would exclude it" , ( ) => {
892916 // The merge node itself adds no file changes, so default simplification
@@ -997,12 +1021,14 @@ describe("merge commit handling", () => {
9971021 rmSync ( relRepo . cwd , { recursive : true , force : true } ) ;
9981022 } ) ;
9991023
1000- it ( "should surface feature merges from inside the rel branch when scanning HEAD^1..HEAD" , ( ) => {
1001- const parents = getCommitParents ( relRepo . commits . headMerge , relRepo . cwd ) ;
1002- expect ( parents . length ) . toBeGreaterThanOrEqual ( 1 ) ;
1003- const parent = parents [ 0 ] ! ;
1024+ it ( "should surface feature merges from inside the rel branch when scanning the resolved first-sync boundary" , ( ) => {
1025+ // Mirrors the customer's first-sync flow: resolveFirstSyncBoundary picks
1026+ // HEAD^1 because HEAD is a merge, then getCommitContextsBetweenShas runs
1027+ // over that range.
1028+ const boundary = resolveFirstSyncBoundary ( relRepo . commits . headMerge , relRepo . cwd ) ;
1029+ expect ( boundary ) . not . toBe ( relRepo . commits . headMerge ) ;
10041030
1005- const result = getCommitContextsBetweenShas ( parent , relRepo . commits . headMerge , {
1031+ const result = getCommitContextsBetweenShas ( boundary , relRepo . commits . headMerge , {
10061032 includePaths : [ "frontend-nuxt3/**" , "backend/**" ] ,
10071033 cwd : relRepo . cwd ,
10081034 } ) ;
0 commit comments