1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+
4
+ # #############################
5
+ # . SYNOPSIS
6
+ # Generate the draft change log of the PowerShell Extension for VSCode
7
+ #
8
+ # . PARAMETER LastReleaseTag
9
+ # The last release tag
10
+ #
11
+ # . PARAMETER Token
12
+ # The authentication token to use for retrieving the GitHub user log-in names for external contributors. Get it from:
13
+ # https://github.com/settings/tokens
14
+ #
15
+ # . PARAMETER NewReleaseTag
16
+ # The github tag that will be associated with the next release
17
+ #
18
+ # . PARAMETER HasCherryPick
19
+ # Indicate whether there are any commits in the last release branch that were cherry-picked from the master branch
20
+ #
21
+ # . OUTPUTS
22
+ # The generated change log draft of vscode-powershell AND PowerShellEditorServices
23
+ #
24
+ # . NOTES
25
+ # Run from the path to /vscode-powershell
26
+ #
27
+ # . EXAMPLE
28
+ #
29
+ # .\tools\Get-PowerShellExtensionChangelog.ps1 -LastReleaseTag v1.7.0 -Token $TOKENSTR -NewReleaseTag v1.8.0
30
+ #
31
+ # #############################
32
+ param (
33
+ [Parameter (Mandatory )]
34
+ [string ]$LastReleaseTag ,
35
+
36
+ [Parameter (Mandatory )]
37
+ [string ]$Token ,
38
+
39
+ [Parameter (Mandatory )]
40
+ [string ]$NewReleaseTag ,
41
+
42
+ [Parameter ()]
43
+ [switch ]$HasCherryPick
44
+ )
45
+
46
+ # These powershell team members don't use 'microsoft.com' for Github email or choose to not show their emails.
47
+ # We have their names in this array so that we don't need to query Github to find out if they are powershell team members.
48
+ $Script :powershell_team = @ (
49
+ " Robert Holt"
50
+ " Tyler Leonhardt"
51
+ )
52
+
53
+ $Script :powershell_team_emails = @ (
54
+
55
+ )
56
+
57
+ class CommitNode {
58
+ [string ] $Hash
59
+ [string []] $Parents
60
+ [string ] $AuthorName
61
+ [string ] $AuthorGitHubLogin
62
+ [string ] $AuthorEmail
63
+ [string ] $Subject
64
+ [string ] $Body
65
+ [string ] $PullRequest
66
+ [string ] $ChangeLogMessage
67
+ [bool ] $IsBreakingChange
68
+
69
+ CommitNode($hash , $parents , $name , $email , $subject , $body ) {
70
+ $this.Hash = $hash
71
+ $this.Parents = $parents
72
+ $this.AuthorName = $name
73
+ $this.AuthorEmail = $email
74
+ $this.Subject = $subject
75
+ $this.Body = $body
76
+ $this.IsBreakingChange = $body -match " \[breaking change\]"
77
+
78
+ if ($subject -match " \(#(\d+)\)" ) {
79
+ $this.PullRequest = $Matches [1 ]
80
+ }
81
+ }
82
+ }
83
+
84
+ # #############################
85
+ # . SYNOPSIS
86
+ # In the release workflow, the release branch will be merged back to master after the release is done,
87
+ # and a merge commit will be created as the child of the release tag commit.
88
+ # This cmdlet takes a release tag or the corresponding commit hash, find its child merge commit, and
89
+ # return its metadata in this format: <merge-commit-hash>|<parent-commit-hashes>
90
+ #
91
+ # . PARAMETER LastReleaseTag
92
+ # The last release tag
93
+ #
94
+ # . PARAMETER CommitHash
95
+ # The commit hash of the last release tag
96
+ #
97
+ # . OUTPUTS
98
+ # Return the metadata of the child merge commit, in this format: <merge-commit-hash>|<parent-commit-hashes>
99
+ # #############################
100
+ function Get-ChildMergeCommit
101
+ {
102
+ [CmdletBinding (DefaultParameterSetName = " TagName" )]
103
+ param (
104
+ [Parameter (Mandatory , ParameterSetName = " TagName" )]
105
+ [string ]$LastReleaseTag ,
106
+
107
+ [Parameter (Mandatory , ParameterSetName = " CommitHash" )]
108
+ [string ]$CommitHash
109
+ )
110
+
111
+ $tag_hash = $CommitHash
112
+ if ($PSCmdlet.ParameterSetName -eq " TagName" ) { $tag_hash = git rev- parse " $LastReleaseTag ^0" }
113
+
114
+ # # Get the merge commits that are reachable from 'HEAD' but not from the release tag
115
+ $merge_commits_not_in_release_branch = git -- no- pager log -- merges " $tag_hash ..HEAD" -- format= ' %H||%P'
116
+ # # Find the child merge commit, whose parent-commit-hashes contains the release tag hash
117
+ $child_merge_commit = $merge_commits_not_in_release_branch | Select-String - SimpleMatch $tag_hash
118
+ return $child_merge_commit.Line
119
+ }
120
+
121
+ # #############################
122
+ # . SYNOPSIS
123
+ # Create a CommitNode instance to represent a commit.
124
+ #
125
+ # . PARAMETER CommitMetadata
126
+ # The commit metadata. It's in this format:
127
+ # <commit-hash>|<parent-hashes>|<author-name>|<author-email>|<commit-subject>
128
+ #
129
+ # . PARAMETER CommitMetadata
130
+ # The commit metadata, in this format:
131
+ # <commit-hash>|<parent-hashes>|<author-name>|<author-email>|<commit-subject>
132
+ #
133
+ # . OUTPUTS
134
+ # Return the 'CommitNode' object
135
+ # #############################
136
+ function New-CommitNode
137
+ {
138
+ param (
139
+ [Parameter (ValueFromPipeline )]
140
+ [ValidatePattern (" ^.+\|.+\|.+\|.+\|.+$" )]
141
+ [string ]$CommitMetadata
142
+ )
143
+
144
+ Process {
145
+ $hash , $parents , $name , $email , $subject = $CommitMetadata.Split (" ||" )
146
+ $body = (git -- no- pager show $hash - s -- format=% b) -join " `n "
147
+ return [CommitNode ]::new($hash , $parents , $name , $email , $subject , $body )
148
+ }
149
+ }
150
+
151
+ # #############################
152
+ # . SYNOPSIS
153
+ # Generate the draft change log of the git repo in the current directory
154
+ #
155
+ # . PARAMETER LastReleaseTag
156
+ # The last release tag
157
+ #
158
+ # . PARAMETER Token
159
+ # The authentication token to use for retrieving the GitHub user log-in names for external contributors
160
+ #
161
+ # . PARAMETER RepoUri
162
+ # The uri of the API endpoint. For example: https://api.github.com/repos/PowerShell/vscode-powershell
163
+ #
164
+ # . PARAMETER HasCherryPick
165
+ # Indicate whether there are any commits in the last release branch that were cherry-picked from the master branch
166
+ #
167
+ # . OUTPUTS
168
+ # The generated change log draft.
169
+ # #############################
170
+ function Get-ChangeLog
171
+ {
172
+ param (
173
+ [Parameter (Mandatory )]
174
+ [string ]$LastReleaseTag ,
175
+
176
+ [Parameter (Mandatory )]
177
+ [string ]$Token ,
178
+
179
+ [Parameter (Mandatory )]
180
+ [string ]$RepoUri ,
181
+
182
+ [Parameter ()]
183
+ [switch ]$HasCherryPick
184
+ )
185
+
186
+ $tag_hash = git rev- parse " $LastReleaseTag ^0"
187
+ $format = ' %H||%P||%aN||%aE||%s'
188
+ $header = @ {" Authorization" = " token $Token " }
189
+
190
+ # Find the merge commit that merged the release branch to master.
191
+ $child_merge_commit = Get-ChildMergeCommit - CommitHash $tag_hash
192
+ $commit_hash , $parent_hashes = $child_merge_commit.Split (" ||" )
193
+ # Find the other parent of the merge commit, which represents the original head of master right before merging.
194
+ $other_parent_hash = ($parent_hashes.Trim () -replace $tag_hash ).Trim()
195
+
196
+ if ($HasCherryPick ) {
197
+ # # Sometimes we need to cherry-pick some commits from the master branch to the release branch during the release,
198
+ # # and eventually merge the release branch back to the master branch. This will result in different commit nodes
199
+ # # in master branch that actually represent same set of changes.
200
+ # #
201
+ # # In this case, we cannot simply use the revision range "$tag_hash..HEAD" becuase it will include the original
202
+ # # commits in the master branch that were cherry-picked to the release branch -- they are reachable from 'HEAD'
203
+ # # but not reachable from the last release tag. Instead, we need to exclude the commits that were cherry-picked,
204
+ # # and only include the commits that are not in the last release into the change log.
205
+
206
+ # Find the commits that were only in the orginal master, excluding those that were cherry-picked to release branch.
207
+ $new_commits_from_other_parent = git -- no- pager log -- first- parent -- cherry- pick -- right- only " $tag_hash ...$other_parent_hash " -- format= $format | New-CommitNode
208
+ # Find the commits that were only in the release branch, excluding those that were cherry-picked from master branch.
209
+ $new_commits_from_last_release = git -- no- pager log -- first- parent -- cherry- pick -- left- only " $tag_hash ...$other_parent_hash " -- format= $format | New-CommitNode
210
+ # Find the commits that are actually duplicate but having different patch-ids due to resolving conflicts during the cherry-pick.
211
+ $duplicate_commits = Compare-Object $new_commits_from_last_release $new_commits_from_other_parent - Property PullRequest - ExcludeDifferent - IncludeEqual - PassThru
212
+ if ($duplicate_commits ) {
213
+ $duplicate_pr_numbers = @ ($duplicate_commits | ForEach-Object - MemberName PullRequest)
214
+ $new_commits_from_other_parent = $new_commits_from_other_parent | Where-Object PullRequest -NotIn $duplicate_pr_numbers
215
+ }
216
+
217
+ # Find the commits that were made after the merge commit.
218
+ $new_commits_after_merge_commit = @ (git -- no- pager log -- first- parent " $commit_hash ..HEAD" -- format= $format | New-CommitNode )
219
+ $new_commits = $new_commits_after_merge_commit + $new_commits_from_other_parent
220
+ } else {
221
+ # # No cherry-pick was involved in the last release branch.
222
+ # # Using a ref rang like "$tag_hash..HEAD" with 'git log' means getting the commits that are reachable from 'HEAD' but not reachable from the last release tag.
223
+
224
+ # # We use '--first-parent' for 'git log'. It means for any merge node, only follow the parent node on the master branch side.
225
+ # # In case we merge a branch to master for a PR, only the merge node will show up in this way, the individual commits from that branch will be ignored.
226
+ # # This is what we want because the merge commit itself already represents the PR.
227
+
228
+ # # First, we want to get all new commits merged during the last release
229
+ # $new_commits_during_last_release = @(git --no-pager log --first-parent "$tag_hash..$($other_parent_hash.TrimStart(" "))" --format=$format | New-CommitNode)
230
+ # # Then, we want to get all new commits merged after the last release
231
+ $new_commits_after_last_release = @ (git -- no- pager log -- first- parent " $commit_hash ..HEAD" -- format= $format | New-CommitNode )
232
+ # # Last, we get the full list of new commits
233
+ $new_commits = $new_commits_during_last_release + $new_commits_after_last_release
234
+ }
235
+
236
+ # They are very active contributors, so we keep their email-login mappings here to save a few queries to Github.
237
+ $community_login_map = @ {}
238
+
239
+ foreach ($commit in $new_commits ) {
240
+ if ($commit.AuthorEmail.EndsWith (" @microsoft.com" ) -or $powershell_team -contains $commit.AuthorName -or $powershell_team_emails -contains $commit.AuthorEmail ) {
241
+ $commit.ChangeLogMessage = " - {0}" -f $commit.Subject
242
+ } else {
243
+ if ($community_login_map.ContainsKey ($commit.AuthorEmail )) {
244
+ $commit.AuthorGitHubLogin = $community_login_map [$commit.AuthorEmail ]
245
+ } else {
246
+ $uri = " $RepoUri /commits/$ ( $commit.Hash ) "
247
+ $response = Invoke-WebRequest - Uri $uri - Method Get - Headers $header - ErrorAction SilentlyContinue
248
+ if ($response )
249
+ {
250
+ $content = ConvertFrom-Json - InputObject $response.Content
251
+ $commit.AuthorGitHubLogin = $content.author.login
252
+ $community_login_map [$commit.AuthorEmail ] = $commit.AuthorGitHubLogin
253
+ }
254
+ }
255
+ $commit.ChangeLogMessage = " - {0} (Thanks @{1}!)" -f $commit.Subject , $commit.AuthorGitHubLogin
256
+ }
257
+
258
+ if ($commit.IsBreakingChange ) {
259
+ $commit.ChangeLogMessage = " {0} [Breaking Change]" -f $commit.ChangeLogMessage
260
+ }
261
+ }
262
+
263
+ $new_commits | Sort-Object - Descending - Property IsBreakingChange | ForEach-Object - MemberName ChangeLogMessage
264
+ }
265
+
266
+ # #############################
267
+ # . SYNOPSIS
268
+ # Generate the draft change log of the PowerShell Extension for VSCode
269
+ #
270
+ # . PARAMETER LastReleaseTag
271
+ # The last release tag
272
+ #
273
+ # . PARAMETER Token
274
+ # The authentication token to use for retrieving the GitHub user log-in names for external contributors. Get it from:
275
+ # https://github.com/settings/tokens
276
+ #
277
+ # . PARAMETER NewReleaseTag
278
+ # The github tag that will be associated with the next release
279
+ #
280
+ # . PARAMETER HasCherryPick
281
+ # Indicate whether there are any commits in the last release branch that were cherry-picked from the master branch
282
+ #
283
+ # . OUTPUTS
284
+ # The generated change log draft of vscode-powershell AND PowerShellEditorServices
285
+ #
286
+ # . NOTES
287
+ # Run from the path to /vscode-powershell
288
+ # #############################
289
+ function Get-PowerShellExtensionChangeLog {
290
+ param (
291
+ [Parameter (Mandatory )]
292
+ [string ]$LastReleaseTag ,
293
+
294
+ [Parameter (Mandatory )]
295
+ [string ]$Token ,
296
+
297
+ [Parameter (Mandatory )]
298
+ [string ]$NewReleaseTag ,
299
+
300
+ [Parameter ()]
301
+ [switch ]$HasCherryPick
302
+ )
303
+
304
+ $vscodePowerShell = Get-ChangeLog - LastReleaseTag $LastReleaseTag - Token $Token - HasCherryPick:$HasCherryPick.IsPresent - RepoUri ' https://api.github.com/repos/PowerShell/vscode-powershell'
305
+ Push-Location ../ PowerShellEditorServices
306
+ $pses = Get-ChangeLog - LastReleaseTag $LastReleaseTag - Token $Token - HasCherryPick:$HasCherryPick.IsPresent - RepoUri ' https://api.github.com/repos/PowerShell/PowerShellEditorServices'
307
+ Pop-Location
308
+
309
+ return @"
310
+ ## $NewReleaseTag
311
+ ### $ ( [datetime ]::Today.ToString(" D" ))
312
+ #### [vscode-powershell](https://github.com/powershell/vscode-powershell)
313
+
314
+ $ ( $vscodePowerShell -join " `n " )
315
+
316
+ #### [PowerShellEditorServices](https://github.com/powershell/PowerShellEditorServices)
317
+
318
+ $ ( $pses -join " `n " )
319
+
320
+ "@
321
+ }
322
+
323
+ Get-PowerShellExtensionChangeLog - LastReleaseTag $LastReleaseTag - Token $Token - NewReleaseTag $NewReleaseTag - HasCherryPick:$HasCherryPick.IsPresent
0 commit comments