wrapped_repository.ts 6.5 KB
Newer Older
1
import * as url from 'url';
2
import { basename, join } from 'path';
3
import * as assert from 'assert';
4 5
import { Repository } from '../api/git';

6 7 8
import { GITLAB_COM_URL } from '../constants';
import { tokenService } from '../services/token_service';
import { log } from '../log';
9
import { GitRemote, parseGitRemote } from './git_remote_parser';
10 11
import { getExtensionConfiguration } from '../utils/get_extension_configuration';
import { GitLabNewService } from '../gitlab/gitlab_new_service';
12
import { GitLabProject } from '../gitlab/gitlab_project';
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

function intersectionOfInstanceAndTokenUrls(gitRemoteHosts: string[]) {
  const instanceUrls = tokenService.getInstanceUrls();

  return instanceUrls.filter(instanceUrl =>
    gitRemoteHosts.includes(url.parse(instanceUrl).host || ''),
  );
}

function heuristicInstanceUrl(gitRemoteHosts: string[]) {
  // if the intersection of git remotes and configured PATs exists and is exactly
  // one hostname, use it
  const intersection = intersectionOfInstanceAndTokenUrls(gitRemoteHosts);
  if (intersection.length === 1) {
    const heuristicUrl = intersection[0];
    log(`Found ${heuristicUrl} in the PAT list and git remotes, using it as the instanceUrl`);
    return heuristicUrl;
  }

  if (intersection.length > 1) {
    log(`Found more than one intersection of git remotes and configured PATs, ${intersection}`);
  }

  return null;
}

39
function getInstanceUrlFromRemotes(gitRemoteUrls: string[]): string {
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
  const { instanceUrl } = getExtensionConfiguration();
  // if the workspace setting exists, use it
  if (instanceUrl) {
    return instanceUrl;
  }

  // try to determine the instance URL heuristically
  const gitRemoteHosts = gitRemoteUrls
    .map((uri: string) => parseGitRemote(uri)?.host)
    .filter((h): h is string => Boolean(h));
  const heuristicUrl = heuristicInstanceUrl(gitRemoteHosts);
  if (heuristicUrl) {
    return heuristicUrl;
  }

  // default to Gitlab cloud
  return GITLAB_COM_URL;
}

T
Tomas Vik 已提交
59
export interface CachedMr {
60
  mr: RestMr;
T
Tomas Vik 已提交
61 62 63
  mrVersion: RestMrVersion;
}

64
export class WrappedRepository {
65
  private readonly rawRepository: Repository;
66

67 68
  private cachedProject?: GitLabProject;

T
Tomas Vik 已提交
69 70
  private mrCache: Record<number, CachedMr> = {};

71 72 73 74
  constructor(rawRepository: Repository) {
    this.rawRepository = rawRepository;
  }

75 76 77 78 79 80 81
  private get remoteName(): string {
    const preferredRemote = getExtensionConfiguration().remoteName;
    const branchRemote = this.rawRepository.state.HEAD?.remote;
    const firstRemote = this.rawRepository.state.remotes[0]?.name;
    return preferredRemote || branchRemote || firstRemote || 'origin';
  }

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
  async fetch(): Promise<void> {
    await this.rawRepository.fetch();
  }

  async checkout(branchName: string): Promise<void> {
    await this.rawRepository.checkout(branchName);

    assert(
      this.rawRepository.state.HEAD,
      "We can't read repository HEAD. We suspect that your `git head` command fails and we can't continue till it succeeds",
    );

    const currentBranchName = this.rawRepository.state.HEAD.name;
    assert(
      currentBranchName === branchName,
      `The branch name after the checkout (${currentBranchName}) is not the branch that the extension tried to check out (${branchName}). Inspect your repository before making any more changes.`,
    );
  }

101
  getRemoteByName(remoteName: string): GitRemote {
102 103 104 105 106 107 108
    const remoteUrl = this.rawRepository.state.remotes.find(r => r.name === remoteName)?.fetchUrl;
    assert(remoteUrl, `could not find any URL for git remote with name '${this.remoteName}'`);
    const parsedRemote = parseGitRemote(remoteUrl, this.instanceUrl);
    assert(parsedRemote, `git remote "${remoteUrl}" could not be parsed`);
    return parsedRemote;
  }

109 110 111
  async getProject(): Promise<GitLabProject | undefined> {
    if (!this.cachedProject) {
      const { namespace, project } = this.remote;
112
      this.cachedProject = await this.getGitLabService().getProject(`${namespace}/${project}`);
113 114
    }
    return this.cachedProject;
115 116
  }

117 118 119 120
  get containsGitLabProject(): boolean {
    return Boolean(this.cachedProject);
  }

121
  async reloadMr(mr: RestMr): Promise<CachedMr> {
T
Tomas Vik 已提交
122 123 124 125 126 127 128 129 130 131 132 133 134
    const mrVersion = await this.getGitLabService().getMrDiff(mr);
    const cachedMr = {
      mr,
      mrVersion,
    };
    this.mrCache[mr.id] = cachedMr;
    return cachedMr;
  }

  getMr(id: number): CachedMr | undefined {
    return this.mrCache[id];
  }

135 136
  get remote(): GitRemote {
    return this.getRemoteByName(this.remoteName);
137 138
  }

139 140 141 142
  get lastCommitSha(): string | undefined {
    return this.rawRepository.state.HEAD?.commit;
  }

143 144 145 146 147 148 149
  get instanceUrl(): string {
    const remoteUrls = this.rawRepository.state.remotes
      .map(r => r.fetchUrl)
      .filter((r): r is string => Boolean(r));
    return getInstanceUrlFromRemotes(remoteUrls);
  }

150
  getGitLabService(): GitLabNewService {
151 152 153
    return new GitLabNewService(this.instanceUrl);
  }

154
  get name(): string {
155
    return this.cachedProject?.name ?? basename(this.rawRepository.rootUri.fsPath);
156 157
  }

158 159 160
  get rootFsPath(): string {
    return this.rawRepository.rootUri.fsPath;
  }
161

162 163 164 165 166 167 168
  async getFileContent(path: string, sha: string): Promise<string | null> {
    // even on Windows, the git show command accepts only POSIX paths
    const absolutePath = join(this.rootFsPath, path).replace(/\\/g, '/');
    // null sufficiently signalises that the file has not been found
    // this scenario is going to happen often (for open and squashed MRs)
    return this.rawRepository.show(sha, absolutePath).catch(() => null);
  }
T
Tomas Vik 已提交
169 170 171 172

  async diff(): Promise<string> {
    return this.rawRepository.diff();
  }
173

174 175 176 177 178 179 180 181 182 183 184 185 186
  async getTrackingBranchName(): Promise<string> {
    const branchName = this.rawRepository.state.HEAD?.name;
    assert(
      branchName,
      'The repository seems to be in a detached HEAD state. Please checkout a branch.',
    );
    const trackingBranch = await this.rawRepository
      .getConfig(`branch.${branchName}.merge`)
      .catch(() => ''); // the tracking branch is going to be empty most of the time, we'll swallow the error instead of logging it every time

    return trackingBranch.replace('refs/heads/', '') || branchName;
  }

187 188 189 190 191 192 193 194 195 196
  /**
   * Compares, whether this wrapper contains repository for the
   * same folder as the method argument.
   *
   * The VS Code Git extension can produce more instances of `Repository`
   * interface for the same git folder. We can't simply compare references with `===`.
   */
  hasSameRootAs(repository: Repository): boolean {
    return this.rootFsPath === repository.rootUri.fsPath;
  }
197 198 199 200

  getVersion() {
    return this.getGitLabService().getVersion();
  }
201
}