Skip to content

Commit eb12815

Browse files
committed
Create replacement milestones
1 parent c4951e5 commit eb12815

7 files changed

Lines changed: 259 additions & 154 deletions

File tree

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ After doing this, the autolinking of issues, commits, and branches will work. Se
3434

3535
## Usage
3636

37-
The user must be a member of the project you want to copy or else you won't see it in the first step.
37+
The user must be a member of the project you want to copy. This user must be the one
3838

3939
1. `cp sample_settings.ts settings.ts`
4040
1. edit settings.ts
@@ -114,11 +114,15 @@ As default it is set to false. Doesn't fire the requests to github api and only
114114

115115
#### usePlaceholderIssuesForMissingIssues
116116

117-
If this is set to true (default) then the migration process will automatically create empty dummy issues for every 'missing' GitLab issue (if you deleted an GitLab issue for example). Those issues will be closed on Github and they ensure, that the issue ids stay the same on both, GitLab and Github.
117+
If this is set to true (default) then the migration process will automatically create empty dummy issues for every 'missing' GitLab issue (if you deleted a GitLab issue for example). Those issues will be closed on Github and they ensure that the issue ids stay the same on both GitLab and Github.
118+
119+
#### usePlaceholderMilestonesForMissingMilestones
120+
121+
If this is set to true (default) then the migration process will automatically create empty dummy milestones for every 'missing' GitLab milestone (if you deleted a GitLab milestone for example). Those milestones will be closed on Github and they ensure that the milestone ids stay the same on both GitLab and Github.
118122

119123
#### useReplacementIssuesForCreationFails
120124

121-
If this is set to true (default) then the migration process will automatically create so called "replacement-issues" for every issue where the migration fails. This replacement issue will be exactly the same, but the original description will be lost. In the future, the description of the replacement issue will also contain a link to the original issue on GitLab. This way users, who still have access to the GitLab repository can still view its content. However, this is still an open task. (TODO)
125+
If this is set to true (default) then the migration process will automatically create so called "replacement-issues" for every issue where the migration fails. This replacement issue will be exactly the same, but the original description will be lost. In the future, the description of the replacement issue will also contain a link to the original issue on GitLab. This way, users who still have access to the GitLab repository can still view its content. However, this is still an open task. (TODO)
122126

123127
It would of course be better to find the cause for migration fails, so that no replacement issues would be needed. Finding the cause together with a retry-mechanism would be optimal, and will maybe come in the future - currently the replacement-issue-mechanism helps to keep things in order.
124128

@@ -150,11 +154,11 @@ Maps the usernames from gitlab to github. If the assinee of the gitlab issue is
150154

151155
### projectmap
152156

153-
When one renames the project while transfering so that the projects don't loose there links to the mentioned issues.
157+
This is useful when migrating multiple projects if they are renamed at destination. Provide a map from gitlab names to github names so that any cross-project references (e.g. issues) are not lost.
154158

155159
## Import limit
156160

157-
Because Github has a limit of 5000 Api requests per hour one has to watch out that one doesn't get over this limit. I transferred one of my project with it ~ 300 issues with ~ 200 notes. This totals to some 500 objects excluding commits which are imported through githubs importer. I never got under 3800 remaining requests (while testing it two times in one hour).
161+
Because Github has a limit of 5000 Api requests per hour one has to be careful not to go over this limit. I transferred one of my project with it ~ 300 issues with ~ 200 notes. This totals to some 500 objects excluding commits which are imported through githubs importer. I never got under 3800 remaining requests (while testing it two times in one hour).
158162

159163
So the rule of thumb should be that one can import a repo with ~ 2500 issues without a problem.
160164

@@ -165,7 +169,9 @@ So the rule of thumb should be that one can import a repo with ~ 2500 issues wit
165169
See section 'useReplacementIssuesForCreationFails' above for more infos!
166170
One reason seems to be some error with `Octokit` (error message snippet: https://pastebin.com/3VNUNYLh)
167171

168-
### Milestone refs and issue refs
172+
### Milestone, MR and issue references
173+
174+
This is WIP
169175

170176
the milestone refs and issue refs do not seem to be rewritten properly at the
171177
moment. specifically, milestones show up like `%4` in comments
@@ -182,6 +188,6 @@ A throttling mechanism could maybe help to avoid api rate limit errors.
182188
In some scenarios the ability to migrate is probably more important than the total
183189
duration of the migration process. Some users may even be willing to accept a very long duration (> 1 day if necessary?), if they can get the migration done at all, in return.
184190

185-
### Make request run in parallel
191+
### Make requests run in parallel
186192

187-
Some requests could be run in parallel, to shorten the total duration. Currently all GitLab- and Github-Api-Requests are being run sequentially.
193+
Some requests could be run in parallel, to shorten the total duration. Currently all GitLab- and Github-Api-Requests are run sequentially.

sample_settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default {
3636
mergeRequests: true,
3737
},
3838
debug: false,
39+
usePlaceholderMilestonesForMissingMilestones: true,
3940
usePlaceholderIssuesForMissingIssues: true,
4041
useReplacementIssuesForCreationFails: true,
4142
useIssuesForAllMergeRequests: false,

src/githubHelper.ts

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import settings from '../settings';
22
import { GithubSettings } from './settings';
33
import * as utils from './utils';
4-
import {Octokit as GitHubApi, RestEndpointMethodTypes} from '@octokit/rest';
5-
import { IssuesListForRepoResponseData, PullsListResponseData } from "@octokit/types";
6-
import GitlabHelper from './gitlabHelper';
4+
import { Octokit as GitHubApi, RestEndpointMethodTypes } from '@octokit/rest';
5+
import {
6+
IssuesListForRepoResponseData,
7+
PullsListResponseData,
8+
} from '@octokit/types';
9+
import { GitlabHelper } from './gitlabHelper';
710

811
const gitHubLocation = 'https://github.com';
912

10-
export default class GithubHelper {
13+
interface Milestone {
14+
number: number;
15+
title: string;
16+
}
17+
18+
export class GithubHelper {
1119
githubApi: GitHubApi;
1220
githubUrl: string;
1321
githubOwner: string;
@@ -20,10 +28,12 @@ export default class GithubHelper {
2028
delayInMs: number;
2129
useIssuesForAllMergeRequests: boolean;
2230

23-
constructor(githubApi: GitHubApi,
24-
githubSettings: GithubSettings,
25-
gitlabHelper: GitlabHelper,
26-
useIssuesForAllMergeRequests: boolean) {
31+
constructor(
32+
githubApi: GitHubApi,
33+
githubSettings: GithubSettings,
34+
gitlabHelper: GitlabHelper,
35+
useIssuesForAllMergeRequests: boolean
36+
) {
2737
this.githubApi = githubApi;
2838
this.githubUrl = githubSettings.baseUrl
2939
? githubSettings.baseUrl
@@ -51,10 +61,9 @@ export default class GithubHelper {
5161
async registerRepoId() {
5262
try {
5363
await utils.sleep(this.delayInMs);
54-
// get an array of GitHub milestones for the new repo
5564
let result = await this.githubApi.repos.get({
5665
owner: this.githubOwner,
57-
repo: this.githubRepo
66+
repo: this.githubRepo,
5867
});
5968

6069
this.repoId = result.data.id;
@@ -70,7 +79,7 @@ export default class GithubHelper {
7079
/**
7180
* Get a list of all GitHub milestones currently in new repo
7281
*/
73-
async getAllGithubMilestones() {
82+
async getAllGithubMilestones(): Promise<Milestone[]> {
7483
try {
7584
await utils.sleep(this.delayInMs);
7685
// get an array of GitHub milestones for the new repo
@@ -81,12 +90,10 @@ export default class GithubHelper {
8190
});
8291

8392
// extract the milestone number and title and put into a new array
84-
const milestones = result.data.map(x => ({
93+
return result.data.map(x => ({
8594
number: x.number,
8695
title: x.title,
8796
}));
88-
89-
return milestones;
9097
} catch (err) {
9198
console.error('Could not access all GitHub milestones');
9299
console.error(err);
@@ -209,9 +216,12 @@ export default class GithubHelper {
209216
* TODO description
210217
*/
211218
async createIssue(milestones, issue) {
212-
let bodyConverted = await this.convertIssuesAndComments(issue.description, issue);
219+
let bodyConverted = await this.convertIssuesAndComments(
220+
issue.description,
221+
issue
222+
);
213223

214-
let props : RestEndpointMethodTypes["issues"]["create"]["parameters"] = {
224+
let props: RestEndpointMethodTypes['issues']['create']['parameters'] = {
215225
owner: this.githubOwner,
216226
repo: this.githubRepo,
217227
title: issue.title.trim(),
@@ -316,8 +326,9 @@ export default class GithubHelper {
316326
}
317327

318328
console.log(
319-
`\t...Done creating issue comments (migrated ${nrOfMigratedNotes} comments, skipped ${notes.length -
320-
nrOfMigratedNotes} comments)`
329+
`\t...Done creating issue comments (migrated ${nrOfMigratedNotes} comments, skipped ${
330+
notes.length - nrOfMigratedNotes
331+
} comments)`
321332
);
322333
}
323334

@@ -395,7 +406,7 @@ export default class GithubHelper {
395406
// default state is open so we don't have to update if the issue is closed.
396407
if (issue.state !== 'closed' || githubIssue.state === 'closed') return;
397408

398-
let props: RestEndpointMethodTypes["issues"]["update"]["parameters"] = {
409+
let props: RestEndpointMethodTypes['issues']['update']['parameters'] = {
399410
owner: this.githubOwner,
400411
repo: this.githubRepo,
401412
issue_number: githubIssue.number,
@@ -404,27 +415,28 @@ export default class GithubHelper {
404415

405416
await utils.sleep(this.delayInMs);
406417

407-
if (settings.debug) {
408-
return Promise.resolve();
409-
}
410-
// make the state update
418+
if (settings.debug) return Promise.resolve();
419+
411420
return await this.githubApi.issues.update(props);
412421
}
413422

414423
// ----------------------------------------------------------------------------
415424

416425
/**
417426
* Create a GitHub milestone from a GitLab milestone
427+
* @param milestone GitLab milestone data
428+
* @return Created milestone data (or void if debugging => nothing created)
418429
*/
419-
async createMilestone(milestone) {
430+
async createMilestone(milestone): Promise<Milestone | void> {
420431
// convert from GitLab to GitHub
421-
let githubMilestone : RestEndpointMethodTypes["issues"]["createMilestone"]["parameters"] = {
422-
owner: this.githubOwner,
423-
repo: this.githubRepo,
424-
title: milestone.title,
425-
description: milestone.description,
426-
state: milestone.state === 'active' ? 'open' : 'closed',
427-
};
432+
let githubMilestone: RestEndpointMethodTypes['issues']['createMilestone']['parameters'] =
433+
{
434+
owner: this.githubOwner,
435+
repo: this.githubRepo,
436+
title: milestone.title,
437+
description: milestone.description,
438+
state: milestone.state === 'active' ? 'open' : 'closed',
439+
};
428440

429441
if (milestone.due_date) {
430442
githubMilestone.due_on = milestone.due_date + 'T00:00:00Z';
@@ -433,8 +445,12 @@ export default class GithubHelper {
433445
await utils.sleep(this.delayInMs);
434446

435447
if (settings.debug) return Promise.resolve();
436-
// create the GitHub milestone
437-
return await this.githubApi.issues.createMilestone(githubMilestone);
448+
449+
const created = await this.githubApi.issues.createMilestone(
450+
githubMilestone
451+
);
452+
453+
return { number: created.data.number, title: created.data.title };
438454
}
439455

440456
// ----------------------------------------------------------------------------
@@ -574,13 +590,13 @@ export default class GithubHelper {
574590
await this.githubApi.pulls.create(props);
575591
return Promise.resolve({ data: null }); // need to return null promise for parent to wait on
576592
} catch (err) {
577-
if(err.status === 422) {
593+
if (err.status === 422) {
578594
console.error(
579595
`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.`
580596
);
581597
// fall through to next section
582598
} else {
583-
throw (err);
599+
throw err;
584600
}
585601
}
586602
}
@@ -651,8 +667,9 @@ export default class GithubHelper {
651667
}
652668

653669
console.log(
654-
`\t...Done creating pull request comments (migrated ${nrOfMigratedNotes} pull request comments, skipped ${notes.length -
655-
nrOfMigratedNotes} pull request comments)`
670+
`\t...Done creating pull request comments (migrated ${nrOfMigratedNotes} pull request comments, skipped ${
671+
notes.length - nrOfMigratedNotes
672+
} pull request comments)`
656673
);
657674
}
658675

@@ -667,7 +684,7 @@ export default class GithubHelper {
667684
* @returns {Promise<Github.Response<Github.IssuesUpdateResponse>>}
668685
*/
669686
async updatePullRequestData(githubPullRequest, pullRequest, milestones) {
670-
let props: RestEndpointMethodTypes["issues"]["update"]["parameters"] = {
687+
let props: RestEndpointMethodTypes['issues']['update']['parameters'] = {
671688
owner: this.githubOwner,
672689
repo: this.githubRepo,
673690
issue_number: githubPullRequest.number || githubPullRequest.iid,
@@ -746,7 +763,7 @@ export default class GithubHelper {
746763
if (pullRequest.state !== 'closed' || githubPullRequest.state === 'closed')
747764
return;
748765

749-
let props : RestEndpointMethodTypes["issues"]["update"]["parameters"] = {
766+
let props: RestEndpointMethodTypes['issues']['update']['parameters'] = {
750767
owner: this.githubOwner,
751768
repo: this.githubRepo,
752769
issue_number: githubPullRequest.number,
@@ -825,7 +842,12 @@ export default class GithubHelper {
825842
}
826843
);
827844

828-
strWithMigLine = await utils.migrateAttachments(strWithMigLine, this.repoId, settings.s3, this.gitlabHelper);
845+
strWithMigLine = await utils.migrateAttachments(
846+
strWithMigLine,
847+
this.repoId,
848+
settings.s3,
849+
this.gitlabHelper
850+
);
829851

830852
return strWithMigLine;
831853
}
@@ -899,10 +921,7 @@ export default class GithubHelper {
899921
line = position.old_line;
900922
}
901923
const crypto = require('crypto');
902-
const hash = crypto
903-
.createHash('md5')
904-
.update(path)
905-
.digest('hex');
924+
const hash = crypto.createHash('md5').update(path).digest('hex');
906925
slug = `#diff-${hash}${side}${line}`;
907926
}
908927
// Mention the file and line number. If we can't get this for some reason then use the commit id instead.

src/gitlabHelper.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { Gitlab } from '@gitbeaker/node';
22
import { GitlabSettings } from './settings';
33
import axios from 'axios';
4+
// import { MilestoneSchema } from '@gitbeaker/core/dist/types/types';
5+
// export type Milestone = Partial<MilestoneSchema>;
46

7+
export interface Milestone {
8+
id?: number; // internal gitlab identifier
9+
iid: number; // milestone number, equivalent to github number
10+
title: string;
11+
description: string;
12+
state: string;
13+
}
514

6-
export default class GitlabHelper {
15+
export class GitlabHelper {
716
// Wait for this issue to be resolved
817
// https://github.com/jdalrymple/gitbeaker/issues/793
918
gitlabApi: InstanceType<typeof Gitlab>;
@@ -94,14 +103,17 @@ export default class GitlabHelper {
94103
try {
95104
const host = this.host.endsWith('/') ? this.host : this.host + '/';
96105
const attachmentUrl = host + this.projectPath + relurl;
97-
const data = (await axios.get(attachmentUrl, {
106+
const data = (
107+
await axios.get(attachmentUrl, {
98108
responseType: 'arraybuffer',
99109
headers: {
100110
// HACK: work around GitLab's API lack of GET for attachments
101111
// See https://gitlab.com/gitlab-org/gitlab/-/issues/24155
102-
Cookie: `_gitlab_session=${this.sessionCookie}`
103-
}})).data;
104-
return Buffer.from(data, 'binary')
112+
Cookie: `_gitlab_session=${this.sessionCookie}`,
113+
},
114+
})
115+
).data;
116+
return Buffer.from(data, 'binary');
105117
} catch (err) {
106118
console.error(`Could not download attachment #${relurl}.`);
107119
return null;
@@ -120,11 +132,11 @@ export default class GitlabHelper {
120132
*/
121133
async getAllMergeRequestNotes(pullRequestIid: number) {
122134
try {
123-
return (this.gitlabApi.MergeRequestNotes.all(
135+
return this.gitlabApi.MergeRequestNotes.all(
124136
this.gitlabProjectId,
125137
pullRequestIid,
126138
{}
127-
) as any) as any[];
139+
) as any as any[];
128140
} catch (err) {
129141
console.error(
130142
`Could not fetch notes for GitLab merge request #${pullRequestIid}.`

0 commit comments

Comments
 (0)