Skip to content

Commit 6615530

Browse files
authored
Use official merge request API (#9)
1 parent 3ad5d22 commit 6615530

File tree

6 files changed

+42
-161
lines changed

6 files changed

+42
-161
lines changed

Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ ENV DATA_DIR=/data
1919

2020
RUN set -ex \
2121
&& apk --no-cache --update add \
22-
git \
2322
ca-certificates \
2423
libstdc++ \
2524
libgcc \

data/.gitignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/Git.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/GitlabApi.ts

Lines changed: 33 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import * as fs from 'fs';
21
import fetch, { RequestInit, Response } from 'node-fetch';
3-
import { Git } from './Git';
2+
import queryString from 'querystring';
43

54
export interface User {
65
id: number;
@@ -41,6 +40,7 @@ export interface MergeRequest {
4140
state: MergeState;
4241
force_remove_source_branch: boolean;
4342
labels: string[];
43+
squash: boolean;
4444
}
4545

4646
interface MergeRequestUpdateData {
@@ -73,6 +73,8 @@ export interface MergeRequestInfo extends MergeRequest {
7373
head_sha: string,
7474
};
7575
pipeline: MergeRequestPipeline | null;
76+
diverged_commits_count: number;
77+
rebase_in_progress: boolean;
7678
}
7779

7880
export interface DiscussionNote {
@@ -84,16 +86,6 @@ export interface MergeRequestDiscussion {
8486
notes: DiscussionNote[];
8587
}
8688

87-
interface Commit {
88-
id: string;
89-
}
90-
91-
interface Project {
92-
id: number;
93-
ssh_url_to_repo: string;
94-
path_with_namespace: string;
95-
}
96-
9789
interface Pipeline {
9890
user: {
9991
id: number,
@@ -110,28 +102,25 @@ export class GitlabApi {
110102

111103
private readonly gitlabUrl: string;
112104
private readonly authToken: string;
113-
private readonly repositoryDir: string;
114105

115-
constructor(gitlabUrl: string, authToken: string, repositoryDir: string) {
106+
constructor(gitlabUrl: string, authToken: string) {
116107
this.gitlabUrl = gitlabUrl;
117108
this.authToken = authToken;
118-
this.repositoryDir = repositoryDir;
119109
}
120110

121111
public async getMe(): Promise<User> {
122112
return this.sendRequestWithSingleResponse(`/api/v4/user`, RequestMethod.Get);
123113
}
124114

125-
public async getLastCommitOnTarget(projectId: number, branch: string): Promise<Commit> {
126-
return this.sendRequestWithSingleResponse(`/api/v4/projects/${projectId}/repository/commits/${branch}`, RequestMethod.Get);
127-
}
128-
129115
public async getAssignedOpenedMergeRequests(): Promise<MergeRequest[]> {
130116
return this.sendRequestWithMultiResponse(`/api/v4/merge_requests?scope=assigned_to_me&state=opened`, RequestMethod.Get);
131117
}
132118

133119
public async getMergeRequestInfo(projectId: number, mergeRequestIid: number): Promise<MergeRequestInfo> {
134-
return this.sendRequestWithSingleResponse(`/api/v4/projects/${projectId}/merge_requests/${mergeRequestIid}`, RequestMethod.Get);
120+
return this.sendRequestWithSingleResponse(`/api/v4/projects/${projectId}/merge_requests/${mergeRequestIid}`, RequestMethod.Get, {
121+
include_diverged_commits_count: true,
122+
include_rebase_in_progress: true,
123+
});
135124
}
136125

137126
public async getMergeRequestDiscussions(projectId: number, mergeRequestIid: number): Promise<MergeRequestDiscussion[]> {
@@ -160,70 +149,14 @@ export class GitlabApi {
160149
});
161150
}
162151

163-
public async rebaseMergeRequest(mergeRequest: MergeRequest, user: User): Promise<void> {
164-
const sourceProject = await this.getProject(mergeRequest.source_project_id);
165-
const targetProject = await this.getProject(mergeRequest.target_project_id);
166-
167-
if (!fs.existsSync(this.repositoryDir)) {
168-
fs.mkdirSync(this.repositoryDir, {
169-
recursive: true,
170-
});
171-
}
172-
173-
const git = await Git.create(`${this.repositoryDir}/${mergeRequest.target_project_id}`);
174-
175-
const remoteRepositories = [
176-
targetProject.path_with_namespace,
177-
];
178-
179-
if (targetProject.path_with_namespace !== sourceProject.path_with_namespace) {
180-
remoteRepositories.push(sourceProject.path_with_namespace);
181-
}
182-
183-
remoteRepositories.forEach(async (remoteRepository: string) => {
184-
try {
185-
await git.run(`remote add ${remoteRepository} ${this.gitlabUrl}:${this.authToken}@gitlab.com/${remoteRepository}.git`);
186-
} catch (e) {
187-
if (e.message.indexOf(`fatal: remote ${remoteRepository} already exists.`) === -1) {
188-
throw e;
189-
}
190-
}
191-
});
192-
193-
await git.run(`config user.name "${user.name}"`);
194-
await git.run(`config user.email "${user.email}"`);
195-
196-
await git.run(`fetch ${targetProject.path_with_namespace} ${mergeRequest.target_branch}`);
197-
await git.run(`fetch ${sourceProject.path_with_namespace} ${mergeRequest.source_branch}`);
198-
199-
await git.run(`checkout ${targetProject.path_with_namespace}/${mergeRequest.target_branch}`);
200-
201-
try {
202-
await git.run(`branch -D ${mergeRequest.source_branch}`);
203-
} catch (e) {
204-
if (e.message.indexOf(`error: branch '${mergeRequest.source_branch}' not found.`) === -1) {
205-
throw e;
206-
}
207-
}
208-
209-
await git.run(`checkout -b ${mergeRequest.source_branch} ${sourceProject.path_with_namespace}/${mergeRequest.source_branch}`);
210-
await git.run(`rebase ${targetProject.path_with_namespace}/${mergeRequest.target_branch} ${mergeRequest.source_branch}`);
211-
212-
await git.run(`push --force-with-lease ${sourceProject.path_with_namespace} ${mergeRequest.source_branch}:${mergeRequest.source_branch}`);
213-
await git.run(`checkout ${targetProject.path_with_namespace}/${mergeRequest.target_branch}`);
214-
await git.run(`branch -D ${mergeRequest.source_branch}`);
215-
}
216-
217-
private async getProject(projectId: number): Promise<Project> {
218-
return this.sendRequestWithSingleResponse(`/api/v4/projects/${projectId}`, RequestMethod.Get);
152+
public async rebaseMergeRequest(projectId: number, mergeRequestIid: number): Promise<void> {
153+
const response = await this.sendRawRequest(`/api/v4/projects/${projectId}/merge_requests/${mergeRequestIid}/rebase`, RequestMethod.Put);
154+
this.validateResponseStatus(response);
219155
}
220156

221157
private async sendRequestWithSingleResponse(url: string, method: RequestMethod, body?: object): Promise<any> {
222158
const response = await this.sendRawRequest(url, method, body);
223-
224-
if (response.status === 401) {
225-
throw new Error('Unauthorized');
226-
}
159+
this.validateResponseStatus(response);
227160

228161
const data = await response.json();
229162
if (typeof data !== 'object' && data.id === undefined) {
@@ -236,10 +169,7 @@ export class GitlabApi {
236169

237170
private async sendRequestWithMultiResponse(url: string, method: RequestMethod, body?: object): Promise<any> {
238171
const response = await this.sendRawRequest(url, method, body);
239-
240-
if (response.status === 401) {
241-
throw new Error('Unauthorized');
242-
}
172+
this.validateResponseStatus(response);
243173

244174
const data = await response.json();
245175
if (!Array.isArray(data)) {
@@ -250,6 +180,20 @@ export class GitlabApi {
250180
return data;
251181
}
252182

183+
private validateResponseStatus(response: Response): void {
184+
if (response.status === 401) {
185+
throw new Error('Unauthorized');
186+
}
187+
188+
if (response.status === 403) {
189+
throw new Error('Forbidden');
190+
}
191+
192+
if (response.status < 200 || response.status >= 300) {
193+
throw new Error('Unexpected status code');
194+
}
195+
}
196+
253197
public sendRawRequest(url: string, method: RequestMethod, body?: object): Promise<Response> {
254198
const options: RequestInit = {
255199
method,
@@ -260,7 +204,11 @@ export class GitlabApi {
260204
};
261205

262206
if (body !== undefined) {
263-
options.body = JSON.stringify(body);
207+
if (method === RequestMethod.Get) {
208+
url = url + '?' + queryString.stringify(body);
209+
} else {
210+
options.body = JSON.stringify(body);
211+
}
264212
}
265213

266214
return fetch(`${this.gitlabUrl}${url}`, options);

src/MergeRequestAcceptor.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ export const filterBotLabels = (labels: string[]) => {
7979

8080
export const acceptMergeRequest = async (gitlabApi: GitlabApi, mergeRequest: MergeRequest, user: User, options: AcceptMergeRequestOptions): Promise<AcceptMergeRequestResult> => {
8181
let mergeRequestInfo;
82-
let lastCommitOnTarget;
8382

8483
while (true) {
8584
const tasks: Array<Promise<any>> = [sleep(options.ciInterval)];
@@ -117,15 +116,20 @@ export const acceptMergeRequest = async (gitlabApi: GitlabApi, mergeRequest: Mer
117116
};
118117
}
119118

120-
lastCommitOnTarget = await gitlabApi.getLastCommitOnTarget(mergeRequest.project_id, mergeRequest.target_branch);
121-
if (mergeRequestInfo.diff_refs.base_sha !== lastCommitOnTarget.id) {
119+
if (mergeRequestInfo.rebase_in_progress) {
120+
console.log(`[MR] Still rebasing`);
121+
await Promise.all(tasks);
122+
continue;
123+
}
124+
125+
if (mergeRequestInfo.diverged_commits_count > 0) {
122126
await gitlabApi.updateMergeRequest(mergeRequest.project_id, mergeRequest.iid, {
123127
labels: [...filterBotLabels(mergeRequestInfo.labels), BotLabels.Rebasing].join(','),
124128
});
125129
console.log(`[MR] source branch is not up to date, rebasing`);
126130
await Promise.all([
127131
tryCancelPipeline(gitlabApi, mergeRequestInfo, user),
128-
gitlabApi.rebaseMergeRequest(mergeRequest, user),
132+
gitlabApi.rebaseMergeRequest(mergeRequest.project_id, mergeRequest.iid),
129133
]);
130134
await gitlabApi.updateMergeRequest(mergeRequest.project_id, mergeRequest.iid, {
131135
labels: [...filterBotLabels(mergeRequestInfo.labels), BotLabels.Accepting].join(','),

src/index.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as env from 'env-var';
2-
import * as fs from 'fs';
32
import { assignToAuthorAndResetLabels } from './AssignToAuthor';
43
import { setBotLabels } from './BotLabelsSetter';
54
import { DiscussionNote, GitlabApi, MergeRequest, MergeRequestDiscussion, MergeStatus, User } from './GitlabApi';
@@ -18,13 +17,8 @@ const GITLAB_AUTH_TOKEN = env.get('GITLAB_AUTH_TOKEN').required().asString();
1817
const CI_CHECK_INTERVAL = env.get('CI_CHECK_INTERVAL', '10').asIntPositive() * 1000;
1918
const MR_CHECK_INTERVAL = env.get('MR_CHECK_INTERVAL', '20').asIntPositive() * 1000;
2019
const REMOVE_BRANCH_AFTER_MERGE = env.get('REMOVE_BRANCH_AFTER_MERGE', 'true').asBoolStrict();
21-
const dataDir = env.get('DATA_DIR', `${__dirname}/../data`).asString();
2220

23-
if (!fs.existsSync(dataDir)) {
24-
throw new Error(`Data directory ${dataDir} does not exist`);
25-
}
26-
27-
const gitlabApi = new GitlabApi(GITLAB_URL, GITLAB_AUTH_TOKEN, `${dataDir}/repository`);
21+
const gitlabApi = new GitlabApi(GITLAB_URL, GITLAB_AUTH_TOKEN);
2822
const worker = new Worker();
2923

3024
const runMergeRequestCheckerLoop = async (user: User) => {

0 commit comments

Comments
 (0)