mr_item_model.ts 4.1 KB
Newer Older
1
import * as vscode from 'vscode';
2
import * as assert from 'assert';
3
import { PROGRAMMATIC_COMMANDS } from '../../command_names';
4
import { toReviewUri } from '../../review/review_uri';
5 6 7
import { createGitLabNewService } from '../../service_factory';
import { ChangedFileItem } from './changed_file_item';
import { ItemModel } from './item_model';
8 9
import { GqlDiscussion, GqlPosition } from '../../gitlab/gitlab_new_service';
import { handleError } from '../../log';
10
import { UserFriendlyError } from '../../errors/user_friendly_error';
11 12 13 14 15 16 17 18 19 20 21 22

const containsTextPosition = (discussion: GqlDiscussion): boolean => {
  const firstNote = discussion.notes.nodes[0];
  return firstNote?.position?.positionType === 'text';
};

const commentRangeFromPosition = (position: GqlPosition): vscode.Range => {
  const glLine = position.oldLine || position.newLine;
  assert(glLine, 'there is always eitehr new or old line');
  const vsPosition = new vscode.Position(glLine - 1, 0);
  return new vscode.Range(vsPosition, vsPosition);
};
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

export class MrItemModel extends ItemModel {
  constructor(readonly mr: RestIssuable, readonly project: VsProject) {
    super();
  }

  getTreeItem(): vscode.TreeItem {
    const { iid, title, author } = this.mr;
    const item = new vscode.TreeItem(
      `!${iid} · ${title}`,
      vscode.TreeItemCollapsibleState.Collapsed,
    );
    item.iconPath = vscode.Uri.parse(author.avatar_url);
    return item;
  }

  async getChildren(): Promise<vscode.TreeItem[]> {
    const description = new vscode.TreeItem('Description');
    description.iconPath = new vscode.ThemeIcon('note');
    description.command = {
      command: PROGRAMMATIC_COMMANDS.SHOW_RICH_CONTENT,
      arguments: [this.mr, this.project.uri],
      title: 'Show MR',
    };
47 48 49
    try {
      await this.getMrDiscussions();
    } catch (e) {
50 51
      handleError(
        new UserFriendlyError(
52 53 54
          `The extension failed to preload discussions on the MR diff.
            It's possible that you've encountered
            https://gitlab.com/gitlab-org/gitlab/-/issues/298827.`,
55 56 57
          e,
        ),
      );
58
    }
59 60 61 62
    const changedFiles = await this.getChangedFiles();
    return [description, ...changedFiles];
  }

63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  private uriFromPosition(position: GqlPosition): vscode.Uri {
    const onOldVersion = Boolean(position.oldLine);
    const path = onOldVersion ? position.oldPath : position.newPath;
    const commit = onOldVersion ? position.diffRefs.baseSha : position.diffRefs.headSha;
    return toReviewUri({
      path,
      commit,
      workspacePath: this.project.uri,
      projectId: this.mr.project_id,
    });
  }

  private async getMrDiscussions(): Promise<void> {
    const commentController = vscode.comments.createCommentController(
      this.mr.references.full,
      this.mr.title,
    );

    const gitlabService = await createGitLabNewService(this.project.uri);

83 84 85 86
    const discussions = await gitlabService.getDiscussions({
      issuable: this.mr,
      includePosition: true,
    });
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
    const discussionsOnDiff = discussions.filter(containsTextPosition);
    const threads = discussionsOnDiff.map(({ notes }) => {
      const comments = notes.nodes.map(({ body, author }) => ({
        body,
        mode: vscode.CommentMode.Preview,
        author: {
          name: author.name,
          iconPath: vscode.Uri.parse(author.avatarUrl),
        },
      }));
      const position = notes.nodes[0]?.position as GqlPosition; // we filtered out all discussions without position
      const thread = commentController.createCommentThread(
        this.uriFromPosition(position),
        commentRangeFromPosition(position),
        comments,
      );
      thread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
      thread.canReply = false;
      return thread;
    });
    this.setDisposableChildren([...threads, commentController]);
  }

110 111 112 113 114 115
  private async getChangedFiles(): Promise<vscode.TreeItem[]> {
    const gitlabService = await createGitLabNewService(this.project.uri);
    const mrVersion = await gitlabService.getMrDiff(this.mr);
    return mrVersion.diffs.map(d => new ChangedFileItem(this.mr, mrVersion, d, this.project));
  }
}