@@ -70,79 +70,107 @@ function parseFromRevArg(rawArgs: ReadonlyArray<string>): string | null {
7070 ) ;
7171}
7272
73- function resolveChangelogRangeConfig (
73+ function getTaggedVersionCommit ( version : string ) : string | null {
74+ const tag = `v${ version } ` ;
75+ if ( ! git ( ) . tagExists ( tag ) ) {
76+ return null ;
77+ }
78+ return git ( { quiet : true } ) . revParse ( `${ tag } ^{}` ) ;
79+ }
80+
81+ function getFirstParentCommit ( commit : string ) : string | null {
82+ const [ commitWithParents ] = git ( ) . revList ( '--parents' , '-n' , '1' , commit ) ;
83+ if ( commitWithParents == null ) {
84+ return null ;
85+ }
86+
87+ const [ , firstParent ] = commitWithParents . split ( ' ' ) ;
88+ return firstParent ?? null ;
89+ }
90+
91+ function resolveCommitRefOrThrow ( ref : string ) : string {
92+ try {
93+ return git ( ) . revParse ( ref ) ;
94+ } catch ( error ) {
95+ throw new Error (
96+ `Unable to resolve fromRev "${ ref } " to a local commit. ` +
97+ 'Pass a reachable first-parent revision:\n' +
98+ ' npm run changelog -- <fromRev>' ,
99+ { cause : error } ,
100+ ) ;
101+ }
102+ }
103+
104+ function resolveChangeLogConfig (
74105 workingTreeVersion : string ,
75106 fromRev : string | null ,
76107) : {
77108 title : string ;
78- rangeStart : string ;
79- rangeEnd : string ;
109+ commitsList : Array < string > ;
80110} {
81111 const workingTreeReleaseTag = `v${ workingTreeVersion } ` ;
112+ const title = git ( ) . tagExists ( workingTreeReleaseTag )
113+ ? 'Unreleased'
114+ : workingTreeReleaseTag ;
115+
116+ const commitsList : Array < string > = [ ] ;
117+ let rangeStart =
118+ fromRev != null
119+ ? resolveCommitRefOrThrow ( fromRev )
120+ : getTaggedVersionCommit ( workingTreeVersion ) ;
121+
122+ let rangeStartReached = false ;
123+ let lastCheckedVersion = workingTreeVersion ;
124+ let newerCommit : string | null = null ;
125+ let newerVersion : string | null = null ;
126+ let commit : string | null = git ( ) . revParse ( 'HEAD' ) ;
127+
128+ while ( commit != null ) {
129+ const commitVersion = readPackageJSONAtRef ( commit ) . version ;
130+
131+ if ( rangeStart == null && commitVersion !== lastCheckedVersion ) {
132+ rangeStart = getTaggedVersionCommit ( commitVersion ) ;
133+ lastCheckedVersion = commitVersion ;
134+ }
82135
83- // packageJSON in the working tree can differ from HEAD:package.json during
84- // release:prepare after npm version updates files but before committing.
85- // Supported scenario 1: release preparation not started
86- // - working-tree version tag exists
87- // - HEAD version older than or equal to working-tree version, must also exist
88- if ( git ( ) . tagExists ( workingTreeReleaseTag ) ) {
89- return {
90- title : 'Unreleased' ,
91- rangeStart : fromRev ?? workingTreeReleaseTag ,
92- rangeEnd : 'HEAD' ,
93- } ;
94- }
136+ if ( newerCommit != null && newerVersion === commitVersion ) {
137+ commitsList . push ( newerCommit ) ;
138+ }
139+
140+ if ( rangeStart != null && commit === rangeStart ) {
141+ rangeStartReached = true ;
142+ break ;
143+ }
95144
96- const headVersion = readPackageJSONAtRef ( 'HEAD' ) . version ;
97- const headReleaseTag = `v${ headVersion } ` ;
98-
99- // Supported scenario 2: release preparation started
100- // - working-tree version tag not yet created
101- // - HEAD version tag exists
102- if ( git ( ) . tagExists ( headReleaseTag ) ) {
103- return {
104- title : workingTreeReleaseTag ,
105- rangeStart : fromRev ?? headReleaseTag ,
106- rangeEnd : 'HEAD' ,
107- } ;
145+ newerCommit = commit ;
146+ newerVersion = commitVersion ;
147+ commit = getFirstParentCommit ( commit ) ;
108148 }
109149
110- // Supported scenario 3:
111- // - release preparation committed
112- // - working-tree version tag equal to HEAD version tag, both not yet created
113- // - HEAD~1 version tag exists
114- const parentVersion = readPackageJSONAtRef ( 'HEAD~1' ) . version ;
115- const parentTag = `v${ parentVersion } ` ;
116- const parentTagExists = git ( ) . tagExists ( parentTag ) ;
117- if ( workingTreeReleaseTag === headReleaseTag && parentTagExists ) {
118- console . warn ( `Release committed, should already contain this changelog!` ) ;
119-
120- return {
121- title : workingTreeReleaseTag ,
122- rangeStart : fromRev ?? parentTag ,
123- rangeEnd : 'HEAD~1' ,
124- } ;
150+ if ( rangeStart == null || ! rangeStartReached ) {
151+ throw new Error (
152+ 'Unable to determine changelog range from local first-parent history.\n' +
153+ 'This can happen with a shallow clone, missing tags, or an unreachable fromRev.\n' +
154+ 'Fetch more history/tags (for example, "git fetch --tags --deepen=200") ' +
155+ 'or pass an explicit reachable first-parent fromRev:\n' +
156+ ' npm run changelog -- <fromRev>' ,
157+ ) ;
125158 }
126159
127- throw new Error (
128- 'Unable to determine changelog range. One of the following scenarios must be true:\n' +
129- `1) HEAD/working-tree release tags exist, i.e. release preparation not started.\n` +
130- `2) HEAD release tag exists, but working-tree release tag not yet created, i.e. release preparation started, not yet committed.\n` +
131- `3) HEAD/working-tree release tags not yet created, i.e. release preparation committed, not yet released, no additional commits on branch.` ,
132- ) ;
160+ return {
161+ title,
162+ commitsList : commitsList . reverse ( ) ,
163+ } ;
133164}
134165
135166async function genChangeLog ( ) : Promise < string > {
136167 const workingTreeVersion = packageJSON . version ;
137168 const fromRev = parseFromRevArg ( process . argv . slice ( 2 ) ) ;
138- const { title, rangeStart , rangeEnd } = resolveChangelogRangeConfig (
169+ const { title, commitsList } = resolveChangeLogConfig (
139170 workingTreeVersion ,
140171 fromRev ,
141172 ) ;
142173
143- const commitsRange = `${ rangeStart } ..${ rangeEnd } ` ;
144- const commitsList = git ( ) . revList ( '--reverse' , commitsRange ) ;
145-
146174 const allPRs = await getPRsInfo ( commitsList ) ;
147175 const date = git ( ) . log ( '-1' , '--format=%cd' , '--date=short' ) ;
148176
0 commit comments