@@ -18,15 +18,63 @@ function alternateTagName(tag: string): string {
1818 return tag . startsWith ( "v" ) ? tag . slice ( 1 ) : `v${ tag } ` ;
1919}
2020
21+ /**
22+ * Find the latest incremental tag matching a base version via ls-remote.
23+ * e.g., for base "4.2.0-rc.1" finds the highest "4.2.0-rc.1-N" tag.
24+ * Tries both with and without v-prefix.
25+ */
26+ async function findLatestIncrementalTag (
27+ repoUrl : string ,
28+ baseTag : string ,
29+ log ?: Logger ,
30+ repoName ?: string ,
31+ ) : Promise < string | null > {
32+ const git = simpleGit ( ) ;
33+ const bare = baseTag . startsWith ( "v" ) ? baseTag . slice ( 1 ) : baseTag ;
34+ const candidates = [ `${ bare } -*` , `v${ bare } -*` ] ;
35+
36+ for ( const pattern of candidates ) {
37+ try {
38+ const result = await git . listRemote ( [ "--tags" , repoUrl , `refs/tags/${ pattern } ` ] ) ;
39+ if ( ! result . trim ( ) ) continue ;
40+
41+ const tags = result
42+ . trim ( )
43+ . split ( "\n" )
44+ . map ( ( line ) => {
45+ const match = line . match ( / r e f s \/ t a g s \/ ( .+ ) $ / ) ;
46+ return match ? match [ 1 ] : null ;
47+ } )
48+ . filter ( ( t ) : t is string => t !== null )
49+ . sort ( ( a , b ) => {
50+ const numA = parseInt ( a . match ( / - ( \d + ) $ / ) ?. [ 1 ] || "0" , 10 ) ;
51+ const numB = parseInt ( b . match ( / - ( \d + ) $ / ) ?. [ 1 ] || "0" , 10 ) ;
52+ return numB - numA ;
53+ } ) ;
54+
55+ if ( tags . length > 0 ) {
56+ log ?.( `${ repoName } : Found incremental tags: ${ tags . join ( ", " ) } ` , "debug" ) ;
57+ return tags [ 0 ] ;
58+ }
59+ } catch {
60+ // pattern didn't match, try next
61+ }
62+ }
63+ return null ;
64+ }
65+
2166/**
2267 * Fetch a tag from origin, trying the alternate v-prefix variant on failure.
68+ * If matchLatestIncrementalTag is set on the config, also tries finding
69+ * the latest incremental tag (e.g., "4.2.0-rc.1-2" for "4.2.0-rc.1").
2370 * Returns the resolved tag name that was successfully fetched.
2471 */
2572async function fetchTag (
2673 repoGit : SimpleGit ,
2774 tag : string ,
2875 log ?: Logger ,
2976 repoName ?: string ,
77+ config ?: RepoConfig ,
3078) : Promise < string > {
3179 const fetchArgs = ( t : string ) : string [ ] => [ "--depth=1" , "origin" , `refs/tags/${ t } :refs/tags/${ t } ` ] ;
3280 try {
@@ -35,10 +83,23 @@ async function fetchTag(
3583 return tag ;
3684 } catch {
3785 const alt = alternateTagName ( tag ) ;
38- log ?.( `${ repoName } : Tag "${ tag } " not found, trying "${ alt } "` , "info" ) ;
39- await repoGit . fetch ( fetchArgs ( alt ) ) ;
40- return alt ;
86+ try {
87+ log ?.( `${ repoName } : Tag "${ tag } " not found, trying "${ alt } "` , "info" ) ;
88+ await repoGit . fetch ( fetchArgs ( alt ) ) ;
89+ return alt ;
90+ } catch {
91+ if ( ! config ?. matchLatestIncrementalTag ) throw new Error ( `Tag "${ tag } " not found (also tried "${ alt } ")` ) ;
92+ }
4193 }
94+
95+ // Incremental tag fallback: find latest tag matching baseVersion-N
96+ log ?.( `${ repoName } : Exact tags not found, searching for incremental tags matching "${ tag } "` , "info" ) ;
97+ const resolved = await findLatestIncrementalTag ( config ! . url , tag , log , repoName ) ;
98+ if ( ! resolved ) throw new Error ( `No tags found matching "${ tag } " or its variants` ) ;
99+
100+ log ?.( `${ repoName } : Using incremental tag "${ resolved } "` , "info" ) ;
101+ await repoGit . fetch ( fetchArgs ( resolved ) ) ;
102+ return resolved ;
42103}
43104
44105/** Base directory for cloned repos */
@@ -148,7 +209,7 @@ export async function cloneRepo(
148209 await repoGit . raw ( [ "config" , "gc.auto" , "0" ] ) ;
149210 log ?.( `${ config . name } : Setting sparse checkout paths: ${ config . sparse ! . join ( ", " ) } ` , "debug" ) ;
150211 await repoGit . raw ( [ "sparse-checkout" , "set" , "--skip-checks" , ...config . sparse ! ] ) ;
151- const resolvedTag = await fetchTag ( repoGit , config . tag , log , config . name ) ;
212+ const resolvedTag = await fetchTag ( repoGit , config . tag , log , config . name , config ) ;
152213 log ?.( `${ config . name } : Checking out tag` , "debug" ) ;
153214 await repoGit . checkout ( resolvedTag ) ;
154215 } else {
@@ -178,7 +239,7 @@ export async function cloneRepo(
178239 // Clone and checkout tag
179240 await git . clone ( config . url , clonePath , [ "--no-checkout" ] ) ;
180241 const repoGit = simpleGit ( { baseDir : clonePath , progress : progressHandler } ) ;
181- const resolvedTag = await fetchTag ( repoGit , config . tag , log , config . name ) ;
242+ const resolvedTag = await fetchTag ( repoGit , config . tag , log , config . name , config ) ;
182243 log ?.( `${ config . name } : Checking out tag` , "debug" ) ;
183244 await repoGit . checkout ( resolvedTag ) ;
184245 } else {
@@ -310,7 +371,20 @@ export async function needsReclone(config: RepoConfig): Promise<boolean> {
310371 if ( config . tag ) {
311372 const currentTag = await getRepoTag ( config . name ) ;
312373 if ( currentTag === null ) return true ;
313- return currentTag !== config . tag && currentTag !== alternateTagName ( config . tag ) ;
374+ if ( currentTag === config . tag || currentTag === alternateTagName ( config . tag ) ) return false ;
375+ // For incremental tags (e.g., "4.2.0-rc.1-2"), check if the current tag
376+ // is a versioned variant and whether a newer one exists upstream
377+ if ( config . matchLatestIncrementalTag ) {
378+ const bare = config . tag . startsWith ( "v" ) ? config . tag . slice ( 1 ) : config . tag ;
379+ const currentBare = currentTag . startsWith ( "v" ) ? currentTag . slice ( 1 ) : currentTag ;
380+ if ( currentBare . startsWith ( bare + "-" ) ) {
381+ const latest = await findLatestIncrementalTag ( config . url , config . tag ) ;
382+ if ( ! latest ) return false ; // can't reach remote, assume current is fine
383+ const latestBare = latest . startsWith ( "v" ) ? latest . slice ( 1 ) : latest ;
384+ return currentBare !== latestBare ;
385+ }
386+ }
387+ return true ;
314388 }
315389
316390 // For branches, we don't force re-clone (just update)
0 commit comments