4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
6
import { EventEmitter , LogOutputChannel , Memento , Uri , workspace } from 'vscode' ;
7
- import { getOctokit } from './auth' ;
7
+ import { Repository as GitHubRepository , RepositoryRuleset } from '@octokit/graphql-schema' ;
8
+ import { getOctokitGraphql } from './auth' ;
8
9
import { API , BranchProtection , BranchProtectionProvider , BranchProtectionRule , Repository } from './typings/git' ;
9
10
import { DisposableStore , getRepositoryFromUrl } from './util' ;
10
11
11
- interface RepositoryRuleset {
12
- readonly id : number ;
13
- readonly conditions : {
14
- ref_name : {
15
- exclude : string [ ] ;
16
- include : string [ ] ;
17
- } ;
18
- } ;
19
- readonly enforcement : 'active' | 'disabled' | 'evaluate' ;
20
- readonly rules : RepositoryRule [ ] ;
21
- readonly target : 'branch' | 'tag' ;
22
- }
23
-
24
- interface RepositoryRule {
25
- readonly type : string ;
26
- }
12
+ const REPOSITORY_QUERY = `
13
+ query repositoryPermissions($owner: String!, $repo: String!) {
14
+ repository(owner: $owner, name: $repo) {
15
+ defaultBranchRef {
16
+ name
17
+ },
18
+ viewerPermission
19
+ }
20
+ }
21
+ ` ;
22
+
23
+ const REPOSITORY_RULESETS_QUERY = `
24
+ query repositoryRulesets($owner: String!, $repo: String!, $cursor: String, $limit: Int = 100) {
25
+ repository(owner: $owner, name: $repo) {
26
+ rulesets(includeParents: true, first: $limit, after: $cursor) {
27
+ nodes {
28
+ name
29
+ enforcement
30
+ rules(type: PULL_REQUEST) {
31
+ totalCount
32
+ }
33
+ conditions {
34
+ refName {
35
+ include
36
+ exclude
37
+ }
38
+ }
39
+ target
40
+ },
41
+ pageInfo {
42
+ endCursor,
43
+ hasNextPage
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ` ;
27
49
28
50
export class GithubBranchProtectionProviderManager {
29
51
@@ -92,130 +114,41 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider
92
114
// Restore branch protection from global state
93
115
this . branchProtection = this . globalState . get < BranchProtection [ ] > ( this . globalStateKey , [ ] ) ;
94
116
95
- repository . status ( )
96
- . then ( ( ) => this . initializeBranchProtection ( ) ) ;
117
+ repository . status ( ) . then ( ( ) => this . updateRepositoryBranchProtection ( ) ) ;
97
118
}
98
119
99
120
provideBranchProtection ( ) : BranchProtection [ ] {
100
121
return this . branchProtection ;
101
122
}
102
123
103
- private async initializeBranchProtection ( ) : Promise < void > {
104
- try {
105
- // Branch protection (HEAD)
106
- await this . updateHEADBranchProtection ( ) ;
124
+ private async getRepositoryDetails ( owner : string , repo : string ) : Promise < GitHubRepository > {
125
+ const graphql = await getOctokitGraphql ( ) ;
126
+ const { repository } = await graphql < { repository : GitHubRepository } > ( REPOSITORY_QUERY , { owner, repo } ) ;
107
127
108
- // Branch protection (remotes)
109
- await this . updateRepositoryBranchProtection ( ) ;
110
- } catch ( err ) {
111
- // noop
112
- this . logger . warn ( `Failed to initialize branch protection: ${ this . formatErrorMessage ( err ) } ` ) ;
113
- }
128
+ return repository ;
114
129
}
115
130
116
- private async hasPushPermission ( repository : { owner : string ; repo : string } ) : Promise < boolean > {
117
- try {
118
- const octokit = await getOctokit ( ) ;
119
- const response = await octokit . repos . get ( { ...repository } ) ;
131
+ private async getRepositoryRulesets ( owner : string , repo : string ) : Promise < RepositoryRuleset [ ] > {
132
+ const rulesets : RepositoryRuleset [ ] = [ ] ;
120
133
121
- return response . data . permissions ?. push === true ;
122
- } catch ( err ) {
123
- this . logger . warn ( `Failed to get repository permissions for repository (${ repository . owner } /${ repository . repo } ): ${ this . formatErrorMessage ( err ) } ` ) ;
124
- throw err ;
125
- }
126
- }
127
-
128
- private async getBranchRules ( repository : { owner : string ; repo : string } , branch : string ) : Promise < RepositoryRule [ ] > {
129
- try {
130
- const octokit = await getOctokit ( ) ;
131
- const response = await octokit . request ( 'GET /repos/{owner}/{repo}/rules/branches/{branch}' , {
132
- ...repository ,
133
- branch,
134
- headers : {
135
- 'X-GitHub-Api-Version' : '2022-11-28'
136
- }
137
- } ) ;
138
- return response . data as RepositoryRule [ ] ;
139
- } catch ( err ) {
140
- this . logger . warn ( `Failed to get branch rules for repository (${ repository . owner } /${ repository . repo } ), branch (${ branch } ): ${ this . formatErrorMessage ( err ) } ` ) ;
141
- throw err ;
142
- }
143
- }
144
-
145
- private async getRepositoryRulesets ( repository : { owner : string ; repo : string } ) : Promise < RepositoryRuleset [ ] > {
134
+ let cursor : string | undefined = undefined ;
135
+ const graphql = await getOctokitGraphql ( ) ;
146
136
147
- try {
148
- const rulesets : RepositoryRuleset [ ] = [ ] ;
149
- const octokit = await getOctokit ( ) ;
150
- for await ( const response of octokit . paginate . iterator ( 'GET /repos/{owner}/{repo}/rulesets' , { ...repository , includes_parents : true } ) ) {
151
- if ( response . status !== 200 ) {
152
- continue ;
153
- }
137
+ while ( true ) {
138
+ const { repository } = await graphql < { repository : GitHubRepository } > ( REPOSITORY_RULESETS_QUERY , { owner, repo, cursor } ) ;
154
139
155
- for ( const ruleset of response . data as RepositoryRuleset [ ] ) {
156
- if ( ruleset . target !== 'branch' || ruleset . enforcement !== 'active' ) {
157
- continue ;
158
- }
140
+ rulesets . push ( ...( repository . rulesets ?. nodes ?? [ ] )
141
+ // Active branch ruleset that contains the pull request required rule
142
+ . filter ( node => node && node . target === 'BRANCH' && node . enforcement === 'ACTIVE' && ( node . rules ?. totalCount ?? 0 ) > 0 ) as RepositoryRuleset [ ] ) ;
159
143
160
- const response = await octokit . request ( 'GET /repos/{owner}/{repo}/rulesets/{id}' , {
161
- ...repository ,
162
- id : ruleset . id ,
163
- headers : {
164
- 'X-GitHub-Api-Version' : '2022-11-28'
165
- }
166
- } ) ;
167
-
168
- const rulesetWithDetails = response . data as RepositoryRuleset ;
169
- if ( rulesetWithDetails ?. rules . find ( r => r . type === 'pull_request' ) ) {
170
- rulesets . push ( rulesetWithDetails ) ;
171
- }
172
- }
144
+ if ( repository . rulesets ?. pageInfo . hasNextPage ) {
145
+ cursor = repository . rulesets . pageInfo . endCursor as string | undefined ;
146
+ } else {
147
+ break ;
173
148
}
174
-
175
- return rulesets ;
176
149
}
177
- catch ( err ) {
178
- this . logger . warn ( `Failed to get repository rulesets for repository (${ repository . owner } /${ repository . repo } ): ${ this . formatErrorMessage ( err ) } ` ) ;
179
- throw err ;
180
- }
181
- }
182
-
183
- private async updateHEADBranchProtection ( ) : Promise < void > {
184
- try {
185
- const HEAD = this . repository . state . HEAD ;
186
-
187
- if ( ! HEAD ?. name || ! HEAD ?. upstream ?. remote ) {
188
- return ;
189
- }
190
-
191
- const remoteName = HEAD . upstream . remote ;
192
- const remote = this . repository . state . remotes . find ( r => r . name === remoteName ) ;
193
-
194
- if ( ! remote ) {
195
- return ;
196
- }
197
-
198
- const repository = getRepositoryFromUrl ( remote . pushUrl ?? remote . fetchUrl ?? '' ) ;
199
-
200
- if ( ! repository ) {
201
- return ;
202
- }
203
150
204
- if ( ! ( await this . hasPushPermission ( repository ) ) ) {
205
- return ;
206
- }
207
-
208
- const rules = await this . getBranchRules ( repository , HEAD . name ) ;
209
- if ( ! rules . find ( r => r . type === 'pull_request' ) ) {
210
- return ;
211
- }
212
-
213
- this . branchProtection = [ { remote : remote . name , rules : [ { include : [ HEAD . name ] } ] } ] ;
214
- this . _onDidChangeBranchProtection . fire ( this . repository . rootUri ) ;
215
- } catch ( err ) {
216
- this . logger . warn ( `Failed to update HEAD branch protection: ${ this . formatErrorMessage ( err ) } ` ) ;
217
- throw err ;
218
- }
151
+ return rulesets ;
219
152
}
220
153
221
154
private async updateRepositoryBranchProtection ( ) : Promise < void > {
@@ -229,38 +162,26 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider
229
162
continue ;
230
163
}
231
164
232
- if ( ! ( await this . hasPushPermission ( repository ) ) ) {
165
+ // Repository details
166
+ const repositoryDetails = await this . getRepositoryDetails ( repository . owner , repository . repo ) ;
167
+
168
+ // Check repository write permission
169
+ if ( repositoryDetails . viewerPermission !== 'ADMIN' && repositoryDetails . viewerPermission !== 'MAINTAIN' && repositoryDetails . viewerPermission !== 'WRITE' ) {
233
170
continue ;
234
171
}
235
172
236
- // Repository details
237
- const octokit = await getOctokit ( ) ;
238
- const response = await octokit . repos . get ( { ...repository } ) ;
239
-
240
- // Repository rulesets
241
- const rulesets = await this . getRepositoryRulesets ( repository ) ;
242
-
243
- const parseRef = ( ref : string ) : string => {
244
- if ( ref . startsWith ( 'refs/heads/' ) ) {
245
- return ref . substring ( 11 ) ;
246
- } else if ( ref === '~DEFAULT_BRANCH' ) {
247
- return response . data . default_branch ;
248
- } else if ( ref === '~ALL' ) {
249
- return '**/*' ;
250
- }
251
-
252
- return ref ;
253
- } ;
173
+ // Get repository rulesets
174
+ const branchProtectionRules : BranchProtectionRule [ ] = [ ] ;
175
+ const repositoryRulesets = await this . getRepositoryRulesets ( repository . owner , repository . repo ) ;
254
176
255
- const rules : BranchProtectionRule [ ] = [ ] ;
256
- for ( const ruleset of rulesets ) {
257
- rules . push ( {
258
- include : ruleset . conditions . ref_name . include . map ( r => parseRef ( r ) ) ,
259
- exclude : ruleset . conditions . ref_name . exclude . map ( r => parseRef ( r ) )
177
+ for ( const ruleset of repositoryRulesets ) {
178
+ branchProtectionRules . push ( {
179
+ include : ( ruleset . conditions . refName ?. include ?? [ ] ) . map ( r => this . parseRulesetRefName ( repositoryDetails , r ) ) ,
180
+ exclude : ( ruleset . conditions . refName ?. exclude ?? [ ] ) . map ( r => this . parseRulesetRefName ( repositoryDetails , r ) )
260
181
} ) ;
261
182
}
262
183
263
- branchProtection . push ( { remote : remote . name , rules } ) ;
184
+ branchProtection . push ( { remote : remote . name , rules : branchProtectionRules } ) ;
264
185
}
265
186
266
187
this . branchProtection = branchProtection ;
@@ -269,12 +190,23 @@ export class GithubBranchProtectionProvider implements BranchProtectionProvider
269
190
// Save branch protection to global state
270
191
await this . globalState . update ( this . globalStateKey , branchProtection ) ;
271
192
} catch ( err ) {
272
- this . logger . warn ( `Failed to update repository branch protection: ${ this . formatErrorMessage ( err ) } ` ) ;
273
- throw err ;
193
+ // noop
194
+ this . logger . warn ( `Failed to update repository branch protection: ${ err . message } ` ) ;
274
195
}
275
196
}
276
197
277
- private formatErrorMessage ( err : any ) : string {
278
- return `${ err . message ?? '' } ${ err . status ? ` (${ err . status } )` : '' } ` ;
198
+ private parseRulesetRefName ( repository : GitHubRepository , refName : string ) : string {
199
+ if ( refName . startsWith ( 'refs/heads/' ) ) {
200
+ return refName . substring ( 11 ) ;
201
+ }
202
+
203
+ switch ( refName ) {
204
+ case '~ALL' :
205
+ return '**/*' ;
206
+ case '~DEFAULT_BRANCH' :
207
+ return repository . defaultBranchRef ! . name ;
208
+ default :
209
+ return refName ;
210
+ }
279
211
}
280
212
}
0 commit comments