gitlab_service.js 10.7 KB
Newer Older
1
const vscode = require('vscode');
2
const request = require('request-promise');
P
Pierre Carru 已提交
3
const fs = require('fs');
4
const gitService = require('./git_service');
5
const tokenService = require('./token_service');
F
Fatih Acet 已提交
6
const statusBar = require('./status_bar');
F
Fatih Acet 已提交
7

8
let version = null;
F
Fatih Acet 已提交
9
let branchMR = null;
10

F
Fatih Acet 已提交
11
async function fetch(path, method = 'GET', data = null) {
12 13 14 15 16 17 18
  const {
    instanceUrl,
    ignoreCertificateErrors,
    ca,
    cert,
    certKey,
  } = vscode.workspace.getConfiguration('gitlab');
19
  const { proxy } = vscode.workspace.getConfiguration('http');
20
  const apiRoot = `${instanceUrl}/api/v4`;
21
  const glToken = tokenService.getToken(instanceUrl);
22
  const tokens = tokenService.getInstanceUrls().join(', ');
23

F
Fatih Acet 已提交
24
  if (!glToken) {
25 26 27 28 29 30 31 32 33 34 35
    let err = `
      GitLab Workflow: Cannot make request.
      GitLab URL for this workspace is set to ${instanceUrl}
      and there is no matching token for this URL.
    `;

    if (tokens.length) {
      err = `${err} You have configured tokens for ${tokens}.`;
    }

    return vscode.window.showInformationMessage(err);
F
Fatih Acet 已提交
36 37
  }

38 39
  const config = {
    url: `${apiRoot}${path}`,
F
Fatih Acet 已提交
40
    method,
41
    headers: {
F
Fatih Acet 已提交
42
      'PRIVATE-TOKEN': glToken,
F
Fatih Acet 已提交
43
    },
44
    ecdhCurve: 'auto',
45
    rejectUnauthorized: !ignoreCertificateErrors,
46 47
  };

48
  if (proxy) {
49 50 51
    config.proxy = proxy;
  }

P
Pierre Carru 已提交
52 53 54 55 56 57 58 59
  if (ca) {
    try {
      config.ca = fs.readFileSync(ca);
    } catch (e) {
      vscode.window.showErrorMessage(`GitLab Workflow: Cannot read CA '${ca}'`);
    }
  }

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
  if (cert) {
    try {
      config.cert = fs.readFileSync(cert);
    } catch (e) {
      vscode.window.showErrorMessage(`GitLab Workflow: Cannot read CA '${cert}'`);
    }
  }

  if (certKey) {
    try {
      config.key = fs.readFileSync(certKey);
    } catch (e) {
      vscode.window.showErrorMessage(`GitLab Workflow: Cannot read CA '${certKey}'`);
    }
  }

F
Fatih Acet 已提交
76 77 78 79
  if (data) {
    config.formData = data;
  }

80 81 82 83 84
  const response = await request(config);

  try {
    return JSON.parse(response);
  } catch (e) {
85
    vscode.window.showInformationMessage('GitLab Workflow: Failed to perform your operation.');
F
Fatih Acet 已提交
86
    console.log('Failed to execute fetch', e);
87 88 89 90
    return { error: e };
  }
}

91
async function fetchProjectData(remote) {
F
Fatih Acet 已提交
92 93 94 95 96 97 98 99 100 101
  if (remote) {
    const { namespace, project } = remote;
    const projectData = await fetch(`/projects/${namespace.replace(/\//g, '%2F')}%2F${project}`);

    return projectData || null;
  }

  return null;
}

102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
async function fetchCurrentProject() {
  try {
    const remote = await gitService.fetchGitRemote();

    return await fetchProjectData(remote);
  } catch (e) {
    console.log('Failed to execute fetch', e);

    return null;
  }
}

async function fetchCurrentPipelineProject() {
  try {
    const remote = await gitService.fetchGitRemotePipeline();

    return await fetchProjectData(remote);
  } catch (e) {
    console.log('Failed to execute fetch', e);

    return null;
  }
}

F
Fatih Acet 已提交
126
async function fetchUser(userName) {
F
Fatih Acet 已提交
127 128
  let user = null;

129
  try {
F
Fatih Acet 已提交
130
    const path = userName ? `/user?search=${userName}` : '/user';
F
Fatih Acet 已提交
131

F
Fatih Acet 已提交
132
    user = await fetch(path);
133
  } catch (e) {
F
Fatih Acet 已提交
134
    let message = 'GitLab Workflow: GitLab user not found.';
F
Fatih Acet 已提交
135 136 137 138 139 140

    if (!userName) {
      message += ' Check your Personal Access Token.';
    }

    vscode.window.showInformationMessage(message);
141
  }
F
Fatih Acet 已提交
142 143

  return user;
144 145
}

146 147 148 149 150 151 152 153 154
async function fetchVersion() {
  try {
    const v = await fetch('/version');
    version = v.version;
  } catch (e) {}

  return version;
}

F
Fatih Acet 已提交
155
async function fetchIssuables(params = {}) {
156 157
  let project = null;
  let issuables = [];
158

F
Fatih Acet 已提交
159 160 161
  const { type, scope, state } = params;
  const config = {
    type: type || 'merge_requests',
162
    scope: scope || 'created_by_me',
F
Fatih Acet 已提交
163
    state: state || 'opened',
164
  };
165 166 167

  try {
    project = await fetchCurrentProject();
168 169 170 171

    if (!version) {
      version = await fetchVersion();
    }
172 173 174
  } catch (e) {
    // Fail silently
  }
F
Fatih Acet 已提交
175 176

  if (project) {
177
    // Normalize scope parameter for version < 11 instances.
178
    const [major] = version.split('.');
179 180 181 182
    if (parseInt(major, 10) < 11) {
      config.scope = config.scope.replace(/_/g, '-');
    }

183 184 185
    const path = `/projects/${project.id}/${config.type}?scope=${config.scope}&state=${
      config.state
    }`;
F
Fatih Acet 已提交
186
    issuables = await fetch(path);
F
Fatih Acet 已提交
187 188
  }

F
Fatih Acet 已提交
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
  return issuables;
}

async function fetchIssuesAssignedToMe() {
  return await fetchIssuables({
    type: 'issues',
    scope: 'assigned_to_me',
  });
}

async function fetchIssuesCreatedByMe() {
  return await fetchIssuables({
    type: 'issues',
    scope: 'created_by_me',
  });
}

async function fetchMergeRequestsAssignedToMe() {
  return await fetchIssuables({
208
    scope: 'assigned_to_me',
F
Fatih Acet 已提交
209 210 211 212 213 214 215
  });
}

async function fetchMergeRequestsCreatedByMe() {
  return await fetchIssuables();
}

216 217 218 219 220 221
async function fetchAllProjectMergeRequests() {
  return await fetchIssuables({
    scope: 'all',
  });
}

F
Fatih Acet 已提交
222 223
async function fetchMyOpenMergeRequests() {
  return await fetchIssuables();
224 225 226
}

async function fetchLastPipelineForCurrentBranch() {
227
  const project = await fetchCurrentPipelineProject();
F
Fatih Acet 已提交
228
  let pipeline = null;
229

F
Fatih Acet 已提交
230 231 232 233
  if (project) {
    const branchName = await gitService.fetchTrackingBranchName();
    const pipelinesRootPath = `/projects/${project.id}/pipelines`;
    const pipelines = await fetch(`${pipelinesRootPath}?ref=${branchName}`);
234 235

    if (pipelines.length) {
F
Fatih Acet 已提交
236
      pipeline = await fetch(`${pipelinesRootPath}/${pipelines[0].id}`);
237
    }
F
Fatih Acet 已提交
238 239
  }

F
Fatih Acet 已提交
240
  return pipeline;
F
Fatih Acet 已提交
241 242
}

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
async function fetchLastJobsForCurrentBranch(pipeline) {
  const project = await fetchCurrentPipelineProject();
  if (project) {
    let jobs = await fetch(`/projects/${project.id}/pipelines/${pipeline.id}/jobs`);

    // Gitlab return multiple jobs if you retry the pipeline we filter to keep only the last
    const alreadyProcessedJob = new Set();
    jobs = jobs.sort((one, two) => (one.created_at > two.created_at ? -1 : 1));
    jobs = jobs.filter(job => {
      if (alreadyProcessedJob.has(job.name)) {
        return false;
      }
      alreadyProcessedJob.add(job.name);
      return true;
    });

    return jobs;
  }

  return null;
}

F
Fatih Acet 已提交
265 266 267 268 269
/**
 * GitLab API doesn't support getting open MR by commit ID or branch name.
 * Using this recursive fetcher method, we fetch 100 MRs at a time and do pagination
 * until we find the MR for current branch. This method will retry max 5 times.
 */
F
Fatih Acet 已提交
270
async function fetchOpenMergeRequestForCurrentBranch() {
F
Fatih Acet 已提交
271 272 273 274
  if (branchMR) {
    return branchMR;
  }

F
Fatih Acet 已提交
275 276 277 278
  const project = await fetchCurrentProject();
  const branchName = await gitService.fetchTrackingBranchName();
  let page = 1;

F
Fatih Acet 已提交
279
  // Recursive fetcher method to find the branch MR in MR list.
F
Fatih Acet 已提交
280
  async function fetcher() {
F
Fatih Acet 已提交
281 282 283
    const path = `/projects/${project.id}/merge_requests?state=opened&per_page=100&page=${page}`;
    const mrs = await fetch(path);
    const [mr] = mrs.filter(m => m.source_branch === branchName);
F
Fatih Acet 已提交
284 285

    if (mr) {
F
Fatih Acet 已提交
286 287
      if (page > 1) {
        // Cache only if we need to do pagination.
F
Fatih Acet 已提交
288 289 290
        branchMR = mr;
      }

F
Fatih Acet 已提交
291 292 293
      return mr;
    }

F
Fatih Acet 已提交
294 295 296
    if (page <= 5 && mrs.length === 100) {
      // Retry max 5 times.
      page += 1;
F
Fatih Acet 已提交
297 298
      return await fetcher();
    }
F
Fatih Acet 已提交
299 300

    return null;
F
Fatih Acet 已提交
301 302 303 304 305
  }

  return project ? await fetcher() : null;
}

F
Fatih Acet 已提交
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
/**
 * Cancels or retries last pipeline or creates a new pipeline for current branch.
 *
 * @param {string} action create|retry|cancel
 */
async function handlePipelineAction(action) {
  const pipeline = await fetchLastPipelineForCurrentBranch();
  const project = await fetchCurrentProject();

  if (pipeline && project) {
    let endpoint = `/projects/${project.id}/pipelines/${pipeline.id}/${action}`;
    let newPipeline = null;

    if (action === 'create') {
      const branchName = await gitService.fetchTrackingBranchName();
      endpoint = `/projects/${project.id}/pipeline?ref=${branchName}`;
    }

    try {
      newPipeline = await fetch(endpoint, 'POST');
    } catch (e) {
      vscode.window.showErrorMessage(`GitLab Workflow: Failed to ${action} pipeline.`);
    }

    if (newPipeline) {
F
Fatih Acet 已提交
331
      statusBar.refreshPipeline();
F
Fatih Acet 已提交
332 333 334 335 336 337
    }
  } else {
    vscode.window.showErrorMessage('GitLab Workflow: No project or pipeline found.');
  }
}

F
Fatih Acet 已提交
338 339 340 341 342 343 344 345
async function fetchMRIssues(mrId) {
  const project = await fetchCurrentProject();
  let issues = [];

  if (project) {
    try {
      issues = await fetch(`/projects/${project.id}/merge_requests/${mrId}/closes_issues`);
    } catch (e) {
F
Fatih Acet 已提交
346
      console.log('Failed to execute fetchMRIssue', e);
F
Fatih Acet 已提交
347 348 349 350
    }
  }

  return issues;
F
Fatih Acet 已提交
351
}
F
Fatih Acet 已提交
352

F
Fatih Acet 已提交
353 354 355 356 357 358 359 360 361 362
async function createSnippet(data) {
  let snippet;

  try {
    snippet = await fetch(`/projects/${data.id}/snippets`, 'POST', data);
  } catch (e) {
    vscode.window.showInformationMessage('GitLab Workflow: Failed to create your snippet.');
  }

  return snippet;
F
Fatih Acet 已提交
363
}
F
Fatih Acet 已提交
364

F
Fatih Acet 已提交
365 366 367 368 369 370 371 372 373 374 375 376
async function validateCIConfig(content) {
  let response = null;

  try {
    response = await fetch('/ci/lint', 'POST', { content });
  } catch (e) {
    vscode.window.showInformationMessage('GitLab Workflow: Failed to validate CI configuration.');
  }

  return response;
}

F
Fatih Acet 已提交
377 378 379 380 381 382 383 384 385 386 387 388
async function fetchDiscussions(issuable) {
  let discussions = [];

  try {
    discussions = await fetch(`/projects/${issuable.project_id}/issues/${issuable.iid}/discussions?sort=asc`);
  } catch (e) {
    vscode.window.showInformationMessage('GitLab Workflow: Failed to fetch discussions for this issuable.');
  }

  return discussions;
}

F
Fatih Acet 已提交
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
// TODO: Remove project fetch
async function renderMarkdown(markdown) {
  let rendered = { html: markdown };
  const [ major ] = version.split('.');

  if (parseInt(major, 10) < 11) {
    return markdown;
  }

  try {
    const project = await fetchCurrentProject();
    rendered = await fetch('/markdown', 'POST', {
      text: markdown,
      project: project.path_with_namespace,
      gfm: 'true', // Needs to be a string for the API
    });
  } catch(e) {
    return markdown;
  }

  return rendered.html;
};

412
exports.fetchUser = fetchUser;
F
Fatih Acet 已提交
413 414 415 416
exports.fetchIssuesAssignedToMe = fetchIssuesAssignedToMe;
exports.fetchIssuesCreatedByMe = fetchIssuesCreatedByMe;
exports.fetchMergeRequestsAssignedToMe = fetchMergeRequestsAssignedToMe;
exports.fetchMergeRequestsCreatedByMe = fetchMergeRequestsCreatedByMe;
417
exports.fetchAllProjectMergeRequests = fetchAllProjectMergeRequests;
418 419 420
exports.fetchMyOpenMergeRequests = fetchMyOpenMergeRequests;
exports.fetchOpenMergeRequestForCurrentBranch = fetchOpenMergeRequestForCurrentBranch;
exports.fetchLastPipelineForCurrentBranch = fetchLastPipelineForCurrentBranch;
421
exports.fetchLastJobsForCurrentBranch = fetchLastJobsForCurrentBranch;
F
Fatih Acet 已提交
422
exports.fetchCurrentProject = fetchCurrentProject;
423
exports.fetchCurrentPipelineProject = fetchCurrentPipelineProject;
F
Fatih Acet 已提交
424
exports.handlePipelineAction = handlePipelineAction;
F
Fatih Acet 已提交
425
exports.fetchMRIssues = fetchMRIssues;
F
Fatih Acet 已提交
426
exports.createSnippet = createSnippet;
F
Fatih Acet 已提交
427
exports.validateCIConfig = validateCIConfig;
428
exports.fetchVersion = fetchVersion;
F
Fatih Acet 已提交
429
exports.fetchDiscussions = fetchDiscussions;
F
Fatih Acet 已提交
430
exports.renderMarkdown = renderMarkdown;