mr_item_model.ts 4.2 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

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,
    );
35 36 37
    if (author.avatar_url) {
      item.iconPath = vscode.Uri.parse(author.avatar_url);
    }
38 39 40 41 42 43 44 45 46 47 48
    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',
    };
49 50 51
    try {
      await this.getMrDiscussions();
    } catch (e) {
52 53
      handleError(
        new UserFriendlyError(
54 55 56
          `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.`,
57 58 59
          e,
        ),
      );
60
    }
61 62 63 64
    const changedFiles = await this.getChangedFiles();
    return [description, ...changedFiles];
  }

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
  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);

85 86 87 88
    const discussions = await gitlabService.getDiscussions({
      issuable: this.mr,
      includePosition: true,
    });
89 90 91 92 93 94 95
    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,
96
          iconPath: author.avatarUrl !== null ? vscode.Uri.parse(author.avatarUrl) : undefined,
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
        },
      }));
      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]);
  }

112 113 114 115 116 117
  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));
  }
}