import { Violation } from './violations' ; import { isFile } from './utils' ; import { Octokit } from '@octokit/rest' ; import parseDiff from 'parse-diff' ; import fetch from 'node-fetch' ; import { logger } from 'gcf-utils' ; /**
 * The result for unmatched region tag checks.
 *
 * We want to keep track of which region tags are in which files.
 */ export interface ParseResult { result : boolean ; violations : Violation [ ] ; tagsFound : boolean ; startTags : string [ ] ; } type ChangeTypes = 'add' | 'del' | 'unknown' ; /**
 * A single region tag change in a pull request.
 */ export interface RegionTagLocation { type : ChangeTypes ; regionTag : string ; owner : string ; repo : string ; file ? : string ; sha : string ; line : number ; } /**
 * The summary of the region tag changes in a pull request.
 */ export interface ChangesInPullRequest { changes : RegionTagLocation [ ] ; added : number ; deleted : number ; files : string [ ] ; } export const START_TAG_REGEX = /\[START ([^\]]*)\]/ ; const END_TAG_REGEX = /\[END ([^\]]*)\]/ ; /**
 * Detects region tag changes in a pull request and return the summary.
 */ export async function parseRegionTagsInPullRequest ( octokit , diffUrl , owner , repo , sha , headOwner , headRepo , headSha ) { const changes = [ ] ; const files = [ ] ; const ret = { changes : changes , added : 0 , deleted : 0 , files : files , } ; const response = await fetch ( diffUrl ) ; const diff = await response . text ( ) ; const diffResult = parseDiff ( diff ) ; for ( const file of diffResult ) { if ( file . to !== undefined && file . to !== '/dev/null' ) { ret . files . push ( file . to ) ; } if ( file . to !== undefined && file . to !== '/dev/null' && file . from !== undefined && file . from !== '/dev/null' && file . from !== file . to ) { try { const blobBeforeRename = await octokit . repos . getContent ( { owner : owner , repo : repo , path : file . from , ref : sha , } ) ; if ( ! isFile ( blobBeforeRename . data ) ) { continue ; } const fileContentsBeforeRename = Buffer . from ( blobBeforeRename . data . content , 'base64' ) . toString ( 'utf8' ) ; const linesBeforeRename = fileContentsBeforeRename . split ( '\n' ) ; for ( let i = 0 ; i < linesBeforeRename . length ; i ++ ) { const startMatch = linesBeforeRename [ i ] . match ( START_TAG_REGEX ) ; if ( startMatch ) { ret . deleted += 1 ; ret . changes . push ( { type : 'del' , regionTag : startMatch [ 1 ] , owner : owner , repo : repo , file : file . from , sha : sha , line : i + 1 , } ) ; } } const blobAfterRename = await octokit . repos . getContent ( { owner : headOwner , repo : headRepo , path : file . to , ref : headSha , } ) ; if ( ! isFile ( blobAfterRename . data ) ) { continue ; } const fileContentsAfterRename = Buffer . from ( blobAfterRename . data . content , 'base64' ) . toString ( 'utf8' ) ; const linesAfterRename = fileContentsAfterRename . split ( '\n' ) ; for ( let i = 0 ; i < linesAfterRename . length ; i ++ ) { const startMatch = linesAfterRename [ i ] . match ( START_TAG_REGEX ) ; if ( startMatch ) { ret . added += 1 ; ret . changes . push ( { type : 'add' , regionTag : startMatch [ 1 ] , owner : headOwner , repo : headRepo , file : file . to , sha : headSha , line : i + 1 , } ) ; } } } catch ( e ) { const err = e as Error ; err . message = 'Skipping the diff entry because it failed to read the' + ` file: ${ err . message }` ; logger . error ( err ) ; continue ; } } else { for ( const chunk of file . chunks ) { for ( const change of chunk . changes ) { if ( change . type === 'normal' ) { continue ; } const startMatch = change . content . match ( START_TAG_REGEX ) ; if ( startMatch ) { if ( change . type === 'add' ) { ret . added += 1 ; } if ( change . type === 'del' ) { ret . deleted += 1 ; } ret . changes . push ( { type : change . type === 'del' ? 'del' : 'add' , regionTag : startMatch [ 1 ] , owner : change . type === 'del' ? owner : headOwner , repo : change . type === 'del' ? repo : headRepo , file : change . type === 'del' ? file . from : file . to , sha : change . type === 'del' ? sha : headSha , line : change . ln , } ) ; } } } } } return ret ; } /**
 * Parses a single file and checks unmatched region tags.
 */ export function parseRegionTags ( contents , filename , owner , repo , sha ) { const result = { result : true , violations : [ ] , tagsFound : false , startTags : [ ] , } ; const tags = [ ] ; let lineno = 0 ; contents . split ( /\r?\n/ ) . forEach ( line => { lineno ++ ; const startMatch = line . match ( START_TAG_REGEX ) ; if ( startMatch ) { result . tagsFound = true ; if ( ! result . startTags . includes ( startMatch [ 1 ] ) ) { result . startTags . push ( startMatch [ 1 ] ) ; } let alreadyStarted = false ; for ( const tag of tags ) { if ( tag [ 1 ] === startMatch [ 1 ] ) { alreadyStarted = true ; result . result = false ; result . violations . push ( { violationType : 'TAG_ALREADY_STARTED' , location : { type : 'unknown' , regionTag : startMatch [ 1 ] , owner : owner , repo : repo , file : filename , sha : sha , line : lineno , } , devsite_urls : [ ] , } ) ; } } if ( ! alreadyStarted ) { tags . push ( [ lineno , startMatch [ 1 ] ] ) ; } } const endMatch = line . match ( END_TAG_REGEX ) ; if ( endMatch ) { let startTagIndex = - 1 ; for ( let i = 0 ; i < tags . length ; i ++ ) { if ( tags [ i ] [ 1 ] === endMatch [ 1 ] ) { startTagIndex = i ; } } if ( startTagIndex === - 1 ) { result . result = false ; result . violations . push ( { violationType : 'NO_MATCHING_START_TAG' , location : { type : 'unknown' , regionTag : endMatch [ 1 ] , owner : owner , repo : repo , file : filename , sha : sha , line : lineno , } , devsite_urls : [ ] , } ) ; } else { tags . splice ( startTagIndex , 1 ) ; } } } ) ; for ( const tag of tags ) { result . result = false ; result . violations . push ( { violationType : 'NO_MATCHING_END_TAG' , location : { type : 'unknown' , regionTag : tag [ 1 ] , owner : owner , repo : repo , file : filename , sha : sha , line : tag [ 0 ] , } , devsite_urls : [ ] , } ) ; } return result ; }