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+ " tylerl0706@gmail.com"
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