import settings from '../settings' ; import { GithubSettings } from './settings' ; import * as utils from './utils' ; import { Octokit as GitHubApi , RestEndpointMethodTypes } from '@octokit/rest' ; import { Endpoints } from '@octokit/types' ; import GitlabHelper from './gitlabHelper' ; import { LabelSchema } from '@gitbeaker/core/dist/types/types' ; type IssuesListForRepoResponseData = Endpoints [ 'GET /repos/{owner}/{repo}/issues' ] [ 'response' ] [ 'data' ] ; type PullsListResponseData = Endpoints [ 'GET /repos/{owner}/{repo}/pulls' ] [ 'response' ] [ 'data' ] ; const gitHubLocation = 'https://github.com' ; interface CommentImport { created_at ? : string ; body : string ; } ; interface IssueImport { title : string ; body : string ; created_at : string ; closed : boolean ; assignee ? : string ; updated_at ? : string ; milestone ? : number ; labels ? : Array < string > ; } export type SimpleLabel = Pick < LabelSchema , 'name' | 'color' > ; export class GithubHelper { githubApi : GitHubApi ; githubUrl : string ; githubOwner : string ; githubToken : string ; githubRepo : string ; githubTimeout ? : number ; gitlabHelper : GitlabHelper ; userProjectRegex : RegExp ; repoId ? : number ; delayInMs : number ; useIssuesForAllMergeRequests : boolean ; constructor ( githubApi , githubSettings , gitlabHelper , useIssuesForAllMergeRequests ) { this . githubApi = githubApi ; this . githubUrl = githubSettings . baseUrl ? githubSettings . baseUrl : gitHubLocation ; this . githubOwner = githubSettings . owner ; this . githubToken = githubSettings . token ; this . githubRepo = githubSettings . repo ; this . githubTimeout = githubSettings . timeout ; this . gitlabHelper = gitlabHelper ; this . userProjectRegex = utils . generateUserProjectRegex ( ) ; this . delayInMs = 2000 ; this . useIssuesForAllMergeRequests = useIssuesForAllMergeRequests ; } /**
     * Store the new repo id
     */ async registerRepoId ( ) { try { await utils . sleep ( this . delayInMs ) ; let result = await this . githubApi . repos . get ( { owner : this . githubOwner , repo : this . githubRepo , } ) ; this . repoId = result . data . id ; } catch ( err ) { console . error ( 'Could not access GitHub repo' ) ; console . error ( err ) ; process . exit ( 1 ) ; } } /**
     * Get a list of all GitHub milestones currently in new repo
     */ async getAllGithubMilestones ( ) { try { await utils . sleep ( this . delayInMs ) ; let result = await this . githubApi . issues . listMilestones ( { owner : this . githubOwner , repo : this . githubRepo , state : 'all' , } ) ; const milestones = result . data . map ( x => ( { number : x . number , title : x . title , } ) ) ; return milestones ; } catch ( err ) { console . error ( 'Could not access all GitHub milestones' ) ; console . error ( err ) ; process . exit ( 1 ) ; } } /**
     * Get a list of all the current GitHub issues.
     * This uses a while loop to make sure that each page of issues is received.
     */ async getAllGithubIssues ( ) { let allIssues = [ ] ; let page = 1 ; const perPage = 100 ; while ( true ) { await utils . sleep ( this . delayInMs ) ; const issues = await this . githubApi . issues . listForRepo ( { owner : this . githubOwner , repo : this . githubRepo , state : 'all' , per_page : perPage , page : page , } ) ; if ( issues . data . length === 0 ) break ; allIssues = allIssues . concat ( issues . data ) ; if ( issues . data . length < perPage ) break ; page ++ ; } return allIssues ; } /**
     * Get a list of all GitHub label names currently in new repo
     */ async getAllGithubLabelNames ( ) { try { await utils . sleep ( this . delayInMs ) ; let result = await this . githubApi . issues . listLabelsForRepo ( { owner : this . githubOwner , repo : this . githubRepo , } ) ; let labels = result . data . map ( x => x . name ) ; return labels ; } catch ( err ) { console . error ( 'Could not access all GitHub label names' ) ; console . error ( err ) ; process . exit ( 1 ) ; } } param tag { string } returns async getReleaseByTag ( tag ) { try { await utils . sleep ( this . delayInMs ) ; let result = await this . githubApi . repos . getReleaseByTag ( { owner : this . githubOwner , repo : this . githubRepo , tag : tag } ) ; return result ; } catch ( err ) { console . error ( 'No existing release for this tag on github' ) ; return null ; } } param tag_name { string } param name { string } param body { string } async createRelease ( tag_name , name , body ) { try { await utils . sleep ( this . delayInMs ) ; let result = await this . githubApi . repos . createRelease ( { owner : this . githubOwner , repo : this . githubRepo , tag_name , name , body , } ) ; return result ; } catch ( err ) { console . error ( 'Could not create release on github' ) ; console . error ( err ) ; return null ; } } /**
     * Get a list of all the current GitHub pull requests.
     * This uses a while loop to make sure that each page of issues is received.
     */ async getAllGithubPullRequests ( ) { let allPullRequests = [ ] ; let page = 1 ; const perPage = 100 ; while ( true ) { await utils . sleep ( this . delayInMs ) ; const pullRequests = await this . githubApi . pulls . list ( { owner : this . githubOwner , repo : this . githubRepo , state : 'all' , per_page : perPage , page : page , } ) ; if ( pullRequests . data . length === 0 ) break ; allPullRequests = allPullRequests . concat ( pullRequests . data ) ; if ( pullRequests . data . length < perPage ) break ; page ++ ; } return allPullRequests ; } userIsCreator ( author ) { return author && ( ( settings . usermap && settings . usermap [ author . username ] === settings . github . token_owner ) || ( author . username === settings . github . token_owner ) ) ; } /**
     * Update the description of the repository on GitHub.
     * Replaces newlines and tabs with spaces. No attempt is made to remove e.g. Markdown
     * links or other special formatting.
     */ async updateRepositoryDescription ( description ) { let props = { owner : this . githubOwner , repo : this . githubRepo , description : description . replace ( /\s+/g , " " ) } ; return this . githubApi . repos . update ( props ) ; } param milestones param issue async createIssue ( milestones , issue ) { let bodyConverted = await this . convertIssuesAndComments ( issue . description , issue ) ; let props = { owner : this . githubOwner , repo : this . githubRepo , title : issue . title . trim ( ) , body : bodyConverted , } ; if ( issue . assignee ) { props . assignees = [ ] ; if ( issue . assignee . username === settings . github . username ) { props . assignees . push ( settings . github . username ) ; } else if ( settings . usermap && settings . usermap [ issue . assignee . username ] ) { props . assignees . push ( settings . usermap [ issue . assignee . username ] ) ; } } if ( issue . milestone ) { let milestone = milestones . find ( m => m . title === issue . milestone . title ) ; if ( milestone ) { props . milestone = milestone . number ; } } if ( issue . labels ) { props . labels = issue . labels . filter ( l => { if ( issue . state !== 'closed' ) return true ; let lower = l . toLowerCase ( ) ; return lower !== 'doing' && lower !== 'to do' ; } ) ; if ( settings . conversion . useLowerCaseLabels ) { props . labels = props . labels . map ( ( el ) => el . toLowerCase ( ) ) ; } } if ( props . body && props . body . indexOf ( '/uploads/' ) > - 1 && ! settings . s3 ) { props . labels . push ( 'has attachment' ) ; } await utils . sleep ( this . delayInMs ) ; if ( settings . debug ) return Promise . resolve ( { data : issue } ) ; return this . githubApi . issues . create ( props ) ; } param milestones param issue async importIssueAndComments ( milestones , issue ) { let props = { title : issue . title . trim ( ) , body : await this . convertIssuesAndComments ( issue . description , issue , ! this . userIsCreator ( issue . author ) || ! issue . description ) , created_at : issue . created_at , updated_at : issue . updated_at , closed : issue . state === 'closed' } ; let comments ; if ( issue . assignee ) { if ( issue . assignee . username === settings . github . username ) { props . assignee = settings . github . username ; } else if ( settings . usermap && settings . usermap [ issue . assignee . username ] ) { props . assignee = settings . usermap [ issue . assignee . username ] ; } else if ( issue . assignee . username === settings . github . token_owner ) { props . assignee = settings . github . token_owner ; } } if ( issue . milestone ) { let milestone = milestones . find ( m => m . title === issue . milestone . title ) ; if ( milestone ) { props . milestone = milestone . number ; } } if ( issue . labels ) { props . labels = issue . labels . filter ( l => { if ( issue . state !== 'closed' ) return true ; let lower = l . toLowerCase ( ) ; return lower !== 'doing' && lower !== 'to do' ; } ) ; if ( settings . conversion . useLowerCaseLabels ) { props . labels = props . labels . map ( ( el ) => el . toLowerCase ( ) ) ; } } if ( props . body && props . body . indexOf ( '/uploads/' ) > - 1 && ! settings . s3 ) { props . labels . push ( 'has attachment' ) ; } if ( settings . debug ) return Promise . resolve ( { data : issue } ) ; console . log ( '\tMigrating issue comments...' ) ; if ( issue . isPlaceholder ) { console . log ( '\t...this is a placeholder issue, no comments are migrated.' ) ; } else { let notes = await this . gitlabHelper . getIssueNotes ( issue . iid ) ; comments = await this . processNotesIntoComments ( notes ) ; } return this . requestImportIssue ( props , comments ) ; } param notes returns async processNotesIntoComments ( notes ) { let comments = [ ] ; notes = notes . sort ( ( a , b ) => a . id - b . id ) ; if ( notes . length === 0 ) { console . log ( `\t...no comments available, nothing to migrate.` ) ; return [ ] ; } let nrOfMigratedNotes = 0 ; for ( let note of notes ) { if ( this . checkIfNoteCanBeSkipped ( note . body ) ) { continue ; } let userIsPoster = ( settings . usermap && settings . usermap [ note . author . username ] === settings . github . token_owner ) || note . author . username === settings . github . token_owner ; comments . push ( { created_at : note . created_at , body : await this . convertIssuesAndComments ( note . body , note , ! userIsPoster || ! note . body ) } ) ; nrOfMigratedNotes ++ ; } console . log ( `\t...Done creating comments (migrated ${ nrOfMigratedNotes } comments, skipped ${ notes . length - nrOfMigratedNotes } comments)` ) ; return comments ; } param issue param comments returns async requestImportIssue ( issue , comments ) { let pending = await this . githubApi . request ( `POST /repos/${ settings . github . owner }/${ settings . github . repo }/import/issues` , { "issue" : issue , "comments" : comments } ) ; let result = null ; while ( true ) { await utils . sleep ( this . delayInMs ) ; result = await this . githubApi . request ( `GET /repos/${ settings . github . owner }/${ settings . github . repo }/import/issues/${ pending . data . id }` ) ; if ( result . data . status === "imported" || result . data . status === "failed" ) { break ; } } if ( result . data . status === "failed" ) { console . log ( "\tFAILED: " ) ; console . log ( result ) ; console . log ( "\tERRORS:" ) ; console . log ( result . data . errors ) ; return null ; } let issue_number = result . data . issue_url . split ( '/' ) . splice ( - 1 ) [ 0 ] ; return issue_number ; } /**
     * TODO description
     */ async createIssueComments ( githubIssue , issue ) { console . log ( '\tMigrating issue comments...' ) ; if ( issue . isPlaceholder ) { console . log ( '\t...this is a placeholder issue, no comments are migrated.' ) ; return ; } let notes = await this . gitlabHelper . getIssueNotes ( issue . iid ) ; if ( notes . length === 0 ) { console . log ( `\t...no issue comments available, nothing to migrate.` ) ; return ; } notes = notes . sort ( ( a , b ) => a . id - b . id ) ; let nrOfMigratedNotes = 0 ; for ( let note of notes ) { const gotMigrated = await this . processNote ( note , githubIssue ) ; if ( gotMigrated ) { nrOfMigratedNotes ++ ; } } console . log ( `\t...Done creating issue comments (migrated ${ nrOfMigratedNotes } comments, skipped ${ notes . length - nrOfMigratedNotes } comments)` ) ; } /**
     * This function checks if a note needs to be processed or if it can be skipped.
     * A note can be skipped if it contains predefined terms (like 'Status changed to...')
     * or if it contains any value from settings.skipMatchingComments ->
     * Note that this is case insensitive!
     *
     */ checkIfNoteCanBeSkipped ( noteBody ) { const stateChange = ( /Status changed to .*/i . test ( noteBody ) && ! /Status changed to closed by commit.*/i . test ( noteBody ) ) || /^changed milestone to .*/i . test ( noteBody ) || /^Milestone changed to .*/i . test ( noteBody ) || /^(Re)*assigned to /i . test ( noteBody ) || /^added .* labels/i . test ( noteBody ) || /^Added ~.* label/i . test ( noteBody ) || /^removed ~.* label/i . test ( noteBody ) || /^mentioned in issue #\d+.*/i . test ( noteBody ) || /^mentioned in merge request !\d+/i . test ( noteBody ) || /^changed the description.*/i . test ( noteBody ) || /^changed title from.*to.*/i . test ( noteBody ) ; const matchingComment = settings . skipMatchingComments . reduce ( ( a , b ) => a || new RegExp ( b , 'i' ) . test ( noteBody ) , false ) ; return stateChange || matchingComment ; } async processNote ( note , githubIssue ) { if ( this . checkIfNoteCanBeSkipped ( note . body ) ) { return false ; } else { let bodyConverted = await this . convertIssuesAndComments ( note . body , note ) ; await utils . sleep ( this . delayInMs ) ; if ( settings . debug ) { return true ; } await this . githubApi . issues . createComment ( { owner : this . githubOwner , repo : this . githubRepo , issue_number : githubIssue . number , body : bodyConverted , } ) . catch ( x => { console . error ( 'could not create GitHub issue comment!' ) ; console . error ( x ) ; process . exit ( 1 ) ; } ) ; return true ; } } /**
     * Update the issue state (i.e., closed or open).
     */ async updateIssueState ( githubIssue , issue ) { if ( issue . state !== 'closed' || githubIssue . state === 'closed' ) return ; let props = { owner : this . githubOwner , repo : this . githubRepo , issue_number : githubIssue . number , state : issue . state , } ; await utils . sleep ( this . delayInMs ) ; if ( settings . debug ) { return Promise . resolve ( ) ; } return await this . githubApi . issues . update ( props ) ; } /**
     * Create a GitHub milestone from a GitLab milestone
     */ async createMilestone ( milestone ) { let githubMilestone = { owner : this . githubOwner , repo : this . githubRepo , title : milestone . title , description : milestone . description , state : milestone . state === 'active' ? 'open' : 'closed' , } ; if ( milestone . due_date ) { githubMilestone . due_on = milestone . due_date + 'T00:00:00Z' ; } await utils . sleep ( this . delayInMs ) ; if ( settings . debug ) return Promise . resolve ( ) ; return await this . githubApi . issues . createMilestone ( githubMilestone ) ; } /**
     * Create a GitHub label from a GitLab label
     */ async createLabel ( label ) { let githubLabel = { owner : this . githubOwner , repo : this . githubRepo , name : label . name , color : label . color . substr ( 1 ) , } ; await utils . sleep ( this . delayInMs ) ; if ( settings . debug ) return Promise . resolve ( ) ; return await this . githubApi . issues . createLabel ( githubLabel ) ; } param milestones param pullRequest returns { Promise < void > } async createPullRequestAndComments ( milestones , pullRequest ) { let githubPullRequestData = await this . createPullRequest ( pullRequest ) ; if ( typeof githubPullRequestData === 'number' ) return ; let githubPullRequest = githubPullRequestData . data ; if ( githubPullRequest ) { await this . updatePullRequestData ( githubPullRequest , pullRequest , milestones ) ; await this . createPullRequestComments ( githubPullRequest , pullRequest ) ; await this . updatePullRequestState ( githubPullRequest , pullRequest ) ; } } param pullRequest returns { Promise < Promise < { data : null } > | Promise < Github . Response < Github . PullsCreateResponse > > | Promise < { data : * } > > } async createPullRequest ( pullRequest ) { let canCreate = ! this . useIssuesForAllMergeRequests ; if ( canCreate ) { try { await this . githubApi . repos . getBranch ( { owner : this . githubOwner , repo : this . githubRepo , branch : pullRequest . target_branch , } ) ; } catch ( err ) { let gitlabBranches = await this . gitlabHelper . getAllBranches ( ) ; if ( gitlabBranches . find ( m => m . name === pullRequest . target_branch ) ) { console . error ( `The '${ pullRequest . target_branch }' branch exists on GitLab but has not been migrated to GitHub. Please migrate the branch before migrating pull request #${ pullRequest . iid }.` ) ; return Promise . resolve ( { data : null } ) ; } else { console . error ( `Merge request ${ pullRequest . iid } (target branch '${ pullRequest . target_branch }' does not exist => cannot migrate pull request, creating an issue instead.` ) ; canCreate = false ; } } } if ( canCreate ) { try { await this . githubApi . repos . getBranch ( { owner : this . githubOwner , repo : this . githubRepo , branch : pullRequest . source_branch , } ) ; } catch ( err ) { let gitlabBranches = await this . gitlabHelper . getAllBranches ( ) ; if ( gitlabBranches . find ( m => m . name === pullRequest . source_branch ) ) { console . error ( `The '${ pullRequest . source_branch }' branch exists on GitLab but has not been migrated to GitHub. Please migrate the branch before migrating pull request #${ pullRequest . iid }.` ) ; return Promise . resolve ( { data : null } ) ; } else { console . error ( `Pull request #${ pullRequest . iid } (source branch '${ pullRequest . source_branch }' does not exist => cannot migrate pull request, creating an issue instead.` ) ; canCreate = false ; } } } if ( settings . debug ) return Promise . resolve ( { data : pullRequest } ) ; if ( canCreate ) { let bodyConverted = await this . convertIssuesAndComments ( pullRequest . description , pullRequest ) ; let props = { owner : this . githubOwner , repo : this . githubRepo , title : pullRequest . title . trim ( ) , body : bodyConverted , head : pullRequest . source_branch , base : pullRequest . target_branch , } ; await utils . sleep ( this . delayInMs ) ; try { await this . githubApi . pulls . create ( props ) ; return Promise . resolve ( { data : null } ) ; } catch ( err ) { if ( err . status === 422 ) { console . error ( `Pull request #${ pullRequest . iid } - attempt to create has failed, assume '${ pullRequest . source_branch }' has already been merged => cannot migrate pull request, creating an issue instead.` ) ; } else { throw err ; } } } let mergeStr = '_Merges ' + pullRequest . source_branch + ' -> ' + pullRequest . target_branch + '_\n\n' ; let bodyConverted = await this . convertIssuesAndComments ( mergeStr + pullRequest . description , pullRequest , ! this . userIsCreator ( pullRequest . author ) || ! settings . useIssueImportAPI ) ; if ( settings . useIssueImportAPI ) { let props = { title : pullRequest . title . trim ( ) + ' - [' + pullRequest . state + ']' , body : bodyConverted , created_at : pullRequest . created_at , updated_at : pullRequest . updated_at , closed : pullRequest . state === 'merged' || pullRequest . state === 'closed' , labels : [ 'gitlab merge request' ] } ; if ( pullRequest . assignee ) { if ( pullRequest . assignee . username === settings . github . username ) { props . assignee = settings . github . username ; } else if ( settings . usermap && settings . usermap [ pullRequest . assignee . username ] ) { props . assignee = settings . usermap [ pullRequest . assignee . username ] ; } else if ( pullRequest . assignee . username === settings . github . token_owner ) { props . assignee = settings . github . token_owner ; } } console . log ( '\tMigrating pull request comments...' ) ; let comments = [ ] ; if ( ! pullRequest . iid ) { console . log ( '\t...this is a placeholder for a deleted GitLab merge request, no comments are created.' ) ; } else { let notes = await this . gitlabHelper . getAllMergeRequestNotes ( pullRequest . iid ) ; comments = await this . processNotesIntoComments ( notes ) ; } return this . requestImportIssue ( props , comments ) ; } else { let props = { owner : this . githubOwner , repo : this . githubRepo , title : pullRequest . title . trim ( ) + ' - [' + pullRequest . state + ']' , body : bodyConverted , } ; pullRequest . labels . push ( 'gitlab merge request' ) ; return this . githubApi . issues . create ( props ) ; } } param githubPullRequest param pullRequest returns { Promise < void > } async createPullRequestComments ( githubPullRequest , pullRequest ) { console . log ( '\tMigrating pull request comments...' ) ; if ( ! pullRequest . iid ) { console . log ( '\t...this is a placeholder for a deleted GitLab merge request, no comments are created.' ) ; return Promise . resolve ( ) ; } let notes = await this . gitlabHelper . getAllMergeRequestNotes ( pullRequest . iid ) ; if ( notes . length === 0 ) { console . log ( `\t...no pull request comments available, nothing to migrate.` ) ; return ; } notes = notes . sort ( ( a , b ) => a . id - b . id ) ; let nrOfMigratedNotes = 0 ; for ( let note of notes ) { const gotMigrated = await this . processNote ( note , githubPullRequest ) ; if ( gotMigrated ) { nrOfMigratedNotes ++ ; } } console . log ( `\t...Done creating pull request comments (migrated ${ nrOfMigratedNotes } pull request comments, skipped ${ notes . length - nrOfMigratedNotes } pull request comments)` ) ; } param githubPullRequest param pullRequest param milestones returns { Promise < Github . Response < Github . IssuesUpdateResponse > > } async updatePullRequestData ( githubPullRequest , pullRequest , milestones ) { let props = { owner : this . githubOwner , repo : this . githubRepo , issue_number : githubPullRequest . number || githubPullRequest . iid , } ; if ( pullRequest . assignee ) { props . assignees = [ ] ; if ( pullRequest . assignee . username === settings . github . username ) { props . assignees . push ( settings . github . username ) ; } else if ( settings . usermap && settings . usermap [ pullRequest . assignee . username ] ) { props . assignees . push ( settings . usermap [ pullRequest . assignee . username ] ) ; } } if ( pullRequest . milestone ) { let milestone = milestones . find ( m => m . title === pullRequest . milestone . title ) ; if ( milestone ) { props . milestone = milestone . number ; } } if ( pullRequest . labels ) { props . labels = pullRequest . labels . filter ( l => { if ( pullRequest . state !== 'closed' ) return true ; let lower = l . toLowerCase ( ) ; return lower !== 'doing' && lower !== 'to do' ; } ) ; } return await this . githubApi . issues . update ( props ) ; } param githubPullRequest param pullRequest returns { Promise < Promise < Github . AnyResponse > | Github . Response < Github . PullsUpdateResponse > | Promise < void > > } async updatePullRequestState ( githubPullRequest , pullRequest ) { if ( pullRequest . state === 'merged' && githubPullRequest . state !== 'closed' && ! settings . debug ) { pullRequest . state = 'closed' ; } if ( pullRequest . state !== 'closed' || githubPullRequest . state === 'closed' ) return ; let props = { owner : this . githubOwner , repo : this . githubRepo , issue_number : githubPullRequest . number , state : pullRequest . state , } ; await utils . sleep ( this . delayInMs ) ; if ( settings . debug ) { return Promise . resolve ( ) ; } return await this . githubApi . issues . update ( props ) ; } param milestones param issue async createIssueAndComments ( milestones , issue ) { if ( settings . useIssueImportAPI ) { await this . importIssueAndComments ( milestones , issue ) ; } else { const githubIssueData = await this . createIssue ( milestones , issue ) ; const githubIssue = githubIssueData . data ; await this . createIssueComments ( githubIssue , issue ) ; await this . updateIssueState ( githubIssue , issue ) ; } } param str param item param add_line async convertIssuesAndComments ( str , item , add_line = true ) { const repoLink = `${ this . githubUrl }/${ this . githubOwner }/${ this . githubRepo }` ; if ( ( ! settings . usermap || Object . keys ( settings . usermap ) . length === 0 ) && ( ! settings . projectmap || Object . keys ( settings . projectmap ) . length === 0 ) ) { return add_line ? GithubHelper . addMigrationLine ( str , item , repoLink ) : str ; } else { let strWithMigLine = add_line ? GithubHelper . addMigrationLine ( str , item , repoLink ) : str ; strWithMigLine = strWithMigLine . replace ( this . userProjectRegex , matched => { if ( matched . startsWith ( '@' ) ) { return '@' + settings . usermap [ matched . substr ( 1 ) ] ; } else if ( matched . endsWith ( '#' ) ) { return ( settings . projectmap [ matched . substring ( 0 , matched . length - 1 ) ] + '#' ) ; } else { return matched ; } } ) ; strWithMigLine = await utils . migrateAttachments ( strWithMigLine , this . repoId , settings . s3 , this . gitlabHelper ) ; return strWithMigLine ; } } /**
     * Adds a line of text at the beginning of a comment that indicates who, when
     * and from GitLab.
     */ static addMigrationLine ( str , item , repoLink ) { if ( ! item || ! item . author || ! item . author . username || ! item . created_at ) { return str ; } const dateformatOptions = { month : 'short' , day : 'numeric' , year : 'numeric' , hour : 'numeric' , minute : 'numeric' , hour12 : false , } ; const formattedDate = new Date ( item . created_at ) . toLocaleString ( 'en-US' , dateformatOptions ) ; const attribution = `In GitLab by @${ item . author . username } on ${ formattedDate }` ; const lineRef = item && item . position ? GithubHelper . createLineRef ( item . position , repoLink ) : '' ; const summary = attribution + ( lineRef ? `\n\n${ lineRef }` : '' ) ; return `${ summary }\n\n${ str }` ; } /**
     * When migrating in-line comments to GitHub then creates a link to the
     * appropriate line of the diff.
     */ static createLineRef ( position , repoLink ) { if ( ! repoLink || ! repoLink . startsWith ( gitHubLocation ) || ! position || ! position . head_sha ) { return '' ; } const base_sha = position . base_sha ; const head_sha = position . head_sha ; var path = '' ; var line = '' ; var slug = '' ; if ( ( position . new_line && position . new_path ) || ( position . old_line && position . old_path ) ) { var side ; if ( ! position . old_line || ! position . old_path ) { side = 'R' ; path = position . new_path ; line = position . new_line ; } else { side = 'L' ; path = position . old_path ; line = position . old_line ; } const crypto = require ( 'crypto' ) ; const hash = crypto . createHash ( 'md5' ) . update ( path ) . digest ( 'hex' ) ; slug = `#diff-${ hash }${ side }${ line }` ; } const ref = path && line ? `${ path } line ${ line }` : `${ head_sha }` ; return `Commented on [${ ref }](${ repoLink }/compare/${ base_sha }..${ head_sha }${ slug })\n\n` ; } }