@@ -70,14 +70,10 @@ getChangeLog()
7070function getChangeLog ( ) {
7171 const workingTreeVersion = packageJSON . version ;
7272 const fromRev = parseFromRevArg ( process . argv . slice ( 2 ) ) ;
73- const { title, rangeStart , rangeEnd } = resolveChangelogRangeConfig (
73+ const { title, commitsList } = resolveChangeLogConfig (
7474 workingTreeVersion ,
7575 fromRev ,
7676 ) ;
77- const commitsRange = `${ rangeStart } ..${ rangeEnd } ` ;
78- const commitsListOutput = exec ( `git rev-list --reverse ${ commitsRange } ` ) ;
79- const commitsList =
80- commitsListOutput === '' ? [ ] : commitsListOutput . split ( '\n' ) ;
8177
8278 const date = exec ( 'git log -1 --format=%cd --date=short' ) ;
8379 return getCommitsInfo ( commitsList )
@@ -100,59 +96,91 @@ function parseFromRevArg(rawArgs) {
10096 ) ;
10197}
10298
103- function resolveChangelogRangeConfig ( workingTreeVersion , fromRev ) {
104- const workingTreeReleaseTag = `v${ workingTreeVersion } ` ;
99+ function getTaggedVersionCommit ( version ) {
100+ const tag = `v${ version } ` ;
101+ if ( ! tagExists ( tag ) ) {
102+ return null ;
103+ }
104+ return exec ( `git rev-parse ${ tag } ^{}` ) ;
105+ }
105106
106- // packageJSON in the working tree can differ from HEAD:package.json during
107- // release:prepare after npm version updates files but before committing.
108- // Supported scenario 1: release preparation not started
109- // - working-tree version tag exists
110- // - HEAD version older than or equal to working-tree version, must also exist
111- if ( tagExists ( workingTreeReleaseTag ) ) {
112- return {
113- title : 'Unreleased' ,
114- rangeStart : fromRev || workingTreeReleaseTag ,
115- rangeEnd : 'HEAD' ,
116- } ;
107+ function getFirstParentCommit ( commit ) {
108+ const commitWithParents = exec ( `git rev-list --parents -n 1 ${ commit } ` ) ;
109+ if ( commitWithParents === '' ) {
110+ return null ;
117111 }
118112
119- const headVersion = readPackageJSONAtRef ( 'HEAD' ) . version ;
120- const headReleaseTag = `v${ headVersion } ` ;
121-
122- // Supported scenario 2: release preparation started
123- // - working-tree version tag not yet created
124- // - HEAD version tag exists
125- if ( tagExists ( headReleaseTag ) ) {
126- return {
127- title : workingTreeReleaseTag ,
128- rangeStart : fromRev || headReleaseTag ,
129- rangeEnd : 'HEAD' ,
130- } ;
113+ const [ , firstParent ] = commitWithParents . split ( ' ' ) ;
114+ return firstParent || null ;
115+ }
116+
117+ function resolveCommitRefOrThrow ( ref ) {
118+ try {
119+ return exec ( `git rev-parse ${ ref } ` ) ;
120+ } catch ( error ) {
121+ throw new Error (
122+ `Unable to resolve fromRev "${ ref } " to a local commit. ` +
123+ 'Pass a reachable first-parent revision:\n' +
124+ ' npm run changelog -- <fromRev>' ,
125+ { cause : error } ,
126+ ) ;
131127 }
128+ }
132129
133- // Supported scenario 3:
134- // - release preparation committed
135- // - working-tree version tag equal to HEAD version tag, both not yet created
136- // - HEAD~1 version tag exists
137- const parentVersion = readPackageJSONAtRef ( 'HEAD~1' ) . version ;
138- const parentTag = `v${ parentVersion } ` ;
139- const parentTagExists = tagExists ( parentTag ) ;
140- if ( workingTreeReleaseTag === headReleaseTag && parentTagExists ) {
141- console . warn ( 'Release committed, should already contain this changelog!' ) ;
142-
143- return {
144- title : workingTreeReleaseTag ,
145- rangeStart : fromRev || parentTag ,
146- rangeEnd : 'HEAD~1' ,
147- } ;
130+ function resolveChangeLogConfig ( workingTreeVersion , fromRev ) {
131+ const workingTreeReleaseTag = `v${ workingTreeVersion } ` ;
132+ const title = tagExists ( workingTreeReleaseTag )
133+ ? 'Unreleased'
134+ : workingTreeReleaseTag ;
135+
136+ const commitsList = [ ] ;
137+ let rangeStart =
138+ fromRev != null
139+ ? resolveCommitRefOrThrow ( fromRev )
140+ : getTaggedVersionCommit ( workingTreeVersion ) ;
141+
142+ let rangeStartReached = false ;
143+ let lastCheckedVersion = workingTreeVersion ;
144+ let newerCommit = null ;
145+ let newerVersion = null ;
146+ let commit = exec ( 'git rev-parse HEAD' ) ;
147+
148+ while ( commit != null ) {
149+ const commitVersion = readPackageJSONAtRef ( commit ) . version ;
150+
151+ if ( rangeStart == null && commitVersion !== lastCheckedVersion ) {
152+ rangeStart = getTaggedVersionCommit ( commitVersion ) ;
153+ lastCheckedVersion = commitVersion ;
154+ }
155+
156+ if ( newerCommit != null && newerVersion === commitVersion ) {
157+ commitsList . push ( newerCommit ) ;
158+ }
159+
160+ if ( rangeStart != null && commit === rangeStart ) {
161+ rangeStartReached = true ;
162+ break ;
163+ }
164+
165+ newerCommit = commit ;
166+ newerVersion = commitVersion ;
167+ commit = getFirstParentCommit ( commit ) ;
148168 }
149169
150- throw new Error (
151- 'Unable to determine changelog range. One of the following scenarios must be true:\n' +
152- '1) HEAD/working-tree release tags exist, i.e. release preparation not started.\n' +
153- '2) HEAD release tag exists, but working-tree release tag not yet created, i.e. release preparation started, not yet committed.\n' +
154- '3) HEAD/working-tree release tags not yet created, i.e. release preparation committed, not yet released, no additional commits on branch.' ,
155- ) ;
170+ if ( rangeStart == null || ! rangeStartReached ) {
171+ throw new Error (
172+ 'Unable to determine changelog range from local first-parent history.\n' +
173+ 'This can happen with a shallow clone, missing tags, or an unreachable fromRev.\n' +
174+ 'Fetch more history/tags (for example, "git fetch --tags --deepen=200") ' +
175+ 'or pass an explicit reachable first-parent fromRev:\n' +
176+ ' npm run changelog -- <fromRev>' ,
177+ ) ;
178+ }
179+
180+ return {
181+ title,
182+ commitsList : commitsList . reverse ( ) ,
183+ } ;
156184}
157185
158186function genChangeLog ( title , date , allPRs ) {
0 commit comments