diff --git a/src/api.ts b/src/api.ts index 66ccc216..a8886da2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -28,6 +28,7 @@ import { MergeRequestApproval, MergeRequestState, Milestone, + Note, Project, Scope, Tag, @@ -425,6 +426,16 @@ export class Api { return this.makeGitLabAPIRequest(`projects/${projectId}/milestones`) as Promise; } + /** + * Get notes for issue + * + * @param projectId Project ID of issue to get notes for + * @param issue Issue to get notes for + */ + public async getNotes(projectId: number, issue: Issue) { + return this.makeGitLabAPIRequest(`/projects/${projectId}/issues/${issue.iid}/notes?sort=asc`) as Promise; + } + /** * Get projects for a group * @@ -503,6 +514,7 @@ export class Api { json: true, method: _options.method, timeout: 60000, + followAllRedirects: true, transform: (bodyToTransform, response) => { const xTotalPages = response.headers['x-total-pages']; @@ -514,7 +526,7 @@ export class Api { }, }); } catch (error) { - if (error.error.message.includes('not responding') || _options.retryOnAnyError) { + if (error.message.includes('not responding') || _options.retryOnAnyError) { const seconds = 5; Logger.warn(`GitLab was not responding. Waiting ${seconds}s and retrying...`); @@ -525,6 +537,9 @@ export class Api { continue; } + Logger.log(url); + Logger.log(JSON.stringify(options)); + throw error; } } diff --git a/src/cli.ts b/src/cli.ts index e02dbf18..c586cf59 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,7 +20,7 @@ import {Command} from 'commander'; import {readFileSync} from 'fs'; import {join} from 'path'; import {Api, ApiRequestOptions} from './api'; -import {Issue} from './types'; +import {Issue, IssueState, MembershipScope, Scope} from './types'; Logger.setTransformations([ new AddLogLevel(), @@ -97,6 +97,80 @@ commander Logger.ok('Processed all issues.'); }); +commander + .command('copy ') + .action(async (projectId, targetUrl, targetToken, targetProjectId) => { + const api = new Api(commander.url, commander.token); + const targetApi = new Api(targetUrl, targetToken); + + // get all issues from project + const issues = await api.makeGitLabAPIRequest(`/projects/${projectId}/issues`, { + retryOnAnyError: true, + tries: 10, + }) as Issue[]; + + // sort issues by their project specific ids + issues.sort((a, b) => { + return a.iid - b.iid; + }); + + // get members of target project + const members = await targetApi.getMembers(MembershipScope.PROJECTS, targetProjectId); + + let idx = 0; + + // tslint:disable-next-line:no-magic-numbers + await asyncPool(2, issues, async (issue) => { + // get notes of old issue + const notes = await api.getNotes(projectId, issue); + + // create new issue + const newIssue = await targetApi.createIssue(targetProjectId, issue.title, issue.description === null ? '---' : `${issue.web_url} + +${issue.description}`); + + for (const note of notes) { + // skip system notes + if (note.system) { + continue; + } + + // create new note in new issue for every note in issue + await targetApi.createNote(targetProjectId, Scope.ISSUES, newIssue.iid, `**${note.author.name} (@${note.author.username}):** + +${note.body}`); + } + + // close newly created issue if original is closed to + if (issue.state === IssueState.CLOSED) { + await targetApi.makeGitLabAPIRequest(`/projects/${targetProjectId}/issues/${newIssue.iid}`, { + data: { + state_event: 'close', + }, + method: 'PUT', + retryOnAnyError: true, + tries: 10, + }); + } + + // search for member in target group with same username + const assignee = members.find((member) => { + if (issue.assignee === null) { + return false; + } + + return member.username === issue.assignee.username; + }); + + // set assignee if usernames match + if (typeof assignee !== 'undefined') { + await targetApi.setAssigneeForIssue(newIssue, assignee.id); + } + + Logger.log(`Finished issue ${++idx} of ${issues.length}.`); + }); + }); + commander .parse(process.argv);