Skip to content

Commit

Permalink
Merge branch 'theBenForce-feat/add-gitlab-mr-context-provider' into p…
Browse files Browse the repository at this point in the history
…review
  • Loading branch information
sestinj committed Mar 17, 2024
2 parents 1e41ed6 + 9648cda commit a347f67
Show file tree
Hide file tree
Showing 10 changed files with 459 additions and 28 deletions.
277 changes: 277 additions & 0 deletions core/context/providers/GitLabMergeRequestContextProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import { AxiosInstance, AxiosError } from "axios";
import {BaseContextProvider} from "..";
import { ContextProviderExtras, ContextItem, ContextProviderDescription } from "../..";

interface RemoteBranchInfo {
branch: string | null;
project: string | null;
}

interface GitLabUser {
id: number;
username: string;
name: string;
state: "active";
locked: boolean;
avatar_url: string;
web_url: string;
}

interface GitLabMergeRequest {
iid: number;
project_id: number;
title: string;
description: string;
}

interface GitLabComment {
type: null | "DiffNote";
resolvable: boolean;
resolved?: boolean;
body: string;
created_at: string;
author: GitLabUser;
position?: {
new_path: string;
new_line: number;
head_sha: string;
line_range: {
start: {
line_code: string;
type: "new";
old_line: null;
new_line: number;
};
end: {
line_code: string;
type: "new";
old_line: null;
new_line: number;
};
};
};
}

const trimFirstElement = (args: Array<string>): string => {
return args[0].trim();
};

const getSubprocess = async (extras: ContextProviderExtras) => {
const workingDir = await extras.ide
.getWorkspaceDirs()
.then(trimFirstElement);

return (command: string) =>
extras.ide
.subprocess(`cd ${workingDir}; ${command}`)
.then(trimFirstElement);
};


class GitLabMergeRequestContextProvider extends BaseContextProvider {
static description: ContextProviderDescription = {
title: 'gitlab-mr',
displayTitle: 'GitLab Merge Request',
description: 'Reference comments in a GitLab Merge Request',
type: 'normal'
};

private async getApi(): Promise<AxiosInstance> {
const { default: Axios } = await import("axios");

const domain = this.options.domain ?? "gitlab.com";
const token = this.options.token;

if(!token) {
throw new Error(`GitLab Private Token is required!`);
}

return Axios.create({
baseURL: `https://${domain ?? "gitlab.com"}/api/v4`,
headers: {
"PRIVATE-TOKEN": token,
},
});
};


private async getRemoteBranchName(extras: ContextProviderExtras): Promise<RemoteBranchInfo> {

const subprocess = await getSubprocess(extras);

const branchName = await subprocess(`git branch --show-current`);

const branchRemote = await subprocess(
`git config branch.${branchName}.remote`
);

const branchInfo = await subprocess(`git branch -vv`);

const currentBranchInfo = branchInfo
.split("\n")
.find((line) => line.startsWith("*"));

const remoteMatches = RegExp(
`\\[${branchRemote}/(?<remote_branch>[^\\]]+)\\]`
).exec(currentBranchInfo!);

console.dir({ remoteMatches });

const remoteBranch = remoteMatches?.groups?.["remote_branch"] ?? null;

const remoteUrl = await subprocess(`git remote get-url ${branchRemote}`);

const urlMatches = RegExp(`:(?<project>.*).git`).exec(remoteUrl);

const project = urlMatches?.groups?.["project"] ?? null;

return {
branch: remoteBranch,
project,
};
};

async getContextItems(query: string, extras: ContextProviderExtras): Promise<ContextItem[]> {

const { branch, project } = await this.getRemoteBranchName(extras);

const api = await this.getApi();

const result = [] as Array<ContextItem>;

try {
const mergeRequests = await api
.get<Array<GitLabMergeRequest>>(
`/projects/${encodeURIComponent(project!)}/merge_requests`,
{
params: {
source_branch: branch,
state: "opened",
},
}
)
.then((x) => x.data);


const subprocess = await getSubprocess(extras);

for (const mergeRequest of mergeRequests) {
const parts = [
`# GitLab Merge Request\ntitle: "${mergeRequest.title}"\ndescription: "${mergeRequest.description ?? 'None'}"`,
`## Comments`,
];

const comments = await api.get<Array<GitLabComment>>(
`/projects/${mergeRequest.project_id}/merge_requests/${mergeRequest.iid}/notes`,
{
params: {
sort: "asc",
order_by: "created_at",
},
}
);

const filteredComments = comments.data.filter(
(x) => x.type === "DiffNote"
);

const locations = {} as Record<string, Array<GitLabComment>>;

for (const comment of filteredComments) {
const filename = comment.position?.new_path ?? "general";

if (!locations[filename]) {
locations[filename] = [];
}

locations[filename].push(comment);
}

if (extras.selectedCode.length && this.options.filterComments) {
const toRemove = Object.keys(locations).filter(filename => !extras.selectedCode.find(selection => selection.filepath.endsWith(filename)) && filename !== "general");
for (const filepath of toRemove) {
delete locations[filepath];
}
}

const commentFormatter = async (comment: GitLabComment) => {
const commentLabel = comment.body.includes("```suggestion") ? 'Code Suggestion' : 'Comment';
let result = `#### ${commentLabel}\nauthor: "${comment.author.name}"\ndate: "${comment.created_at}"\nresolved: ${
comment.resolved ? "Yes" : "No"
}`;

if (comment.position?.new_line) {
result += `\nline: ${comment.position.new_line}`;

if (comment.position.head_sha) {
const sourceLines = await subprocess(`git show ${comment.position.head_sha}:${comment.position.new_path}`).then(result => result.split("\n")).catch(ex => []);

const line = comment.position.new_line <= sourceLines.length ? sourceLines[comment.position.new_line - 1] : null;

if (line) {
result += `\nsource: \`${line}\``;
}
}
}

result += `\n\n${comment.body}`;

return result;
};

for (const [filename, locationComments] of Object.entries(locations)) {
if (filename !== "general") {
parts.push(`### File ${filename}`);
locationComments.sort(
(a, b) => a.position!.new_line - b.position!.new_line
);
} else {
parts.push("### General");
}

const commentSections = await Promise.all(locationComments.map(commentFormatter));
parts.push(...commentSections);
}


const content = parts.join("\n\n");

result.push(
{
name: mergeRequest.title,
content,
description: `Comments from the Merge Request for this branch.`,
},
);
}

} catch (ex) {
let content = `# GitLab Merge Request\n\nError getting merge request. `;
if (ex instanceof AxiosError) {
if (ex.response) {
const errorMessage = ex.response?.data ? ex.response.data.message ?? JSON.stringify(ex.response?.data) : `${ex.response.status}: ${ex.response.statusText}`;
content += `GitLab Error: ${errorMessage}`;
} else {
content += `GitLab Request Error ${ex.request}`;
}
} else {
// @ts-ignore
content += `Unknown error: ${ex.message ?? JSON.stringify(ex)}`;
}


result.push(
{
name: `GitLab Merge Request`,
content,
description: `Error getting the Merge Request for this branch.`,
},
);
}

return result;
}

}

export default GitLabMergeRequestContextProvider;
2 changes: 2 additions & 0 deletions core/context/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import SearchContextProvider from "./SearchContextProvider";
import TerminalContextProvider from "./TerminalContextProvider";
import LocalsProvider from "./LocalsProvider";
import URLContextProvider from "./URLContextProvider";
import GitLabMergeRequestContextProvider from "./GitLabMergeRequestContextProvider";

const Providers: (typeof BaseContextProvider)[] = [
DiffContextProvider,
Expand All @@ -34,6 +35,7 @@ const Providers: (typeof BaseContextProvider)[] = [
ProblemsContextProvider,
FolderContextProvider,
DocsContextProvider,
GitLabMergeRequestContextProvider,
// CodeHighlightsContextProvider,
// CodeOutlineContextProvider,
JiraIssuesContextProvider,
Expand Down
3 changes: 2 additions & 1 deletion core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,8 @@ type ContextProviderName =
| "postgres"
| "database"
| "code"
| "docs";
| "docs"
| "gitlab-mr";

type TemplateType =
| "llama2"
Expand Down
10 changes: 0 additions & 10 deletions core/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,6 @@
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==

"@esbuild/darwin-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz"
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==

"@eslint-community/eslint-utils@^4.2.0":
version "4.4.0"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz"
Expand Down Expand Up @@ -2667,11 +2662,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==

fsevents@^2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==

function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
Expand Down
Loading

0 comments on commit a347f67

Please sign in to comment.