feat: add support for pull requests (#156)

This commit is contained in:
Thomas Durieux
2023-01-22 12:54:14 +01:00
committed by GitHub
parent 3091b13776
commit 73e46f926f
23 changed files with 2479 additions and 28 deletions
+4
View File
@@ -1,3 +1,5 @@
import pullRequestPrivate from "./pullRequest-private";
import pullRequestPublic from "./pullRequest-public";
import repositoryPrivate from "./repository-private";
import repositoryPublic from "./repository-public";
import conference from "./conference";
@@ -8,6 +10,8 @@ import option from "./option";
import admin from "./admin";
export default {
pullRequestPrivate,
pullRequestPublic,
repositoryPrivate,
repositoryPublic,
file,
+245
View File
@@ -0,0 +1,245 @@
import * as express from "express";
import { ensureAuthenticated } from "./connection";
import {
getPullRequest,
getUser,
handleError,
isOwnerOrAdmin,
} from "./route-utils";
import AnonymousError from "../AnonymousError";
import { IAnonymizedPullRequestDocument } from "../database/anonymizedPullRequests/anonymizedPullRequests.types";
import PullRequest from "../PullRequest";
import AnonymizedPullRequestModel from "../database/anonymizedPullRequests/anonymizedPullRequests.model";
const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
// refresh pullRequest
router.post(
"/:pullRequestId/refresh",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
if (
pullRequest.status == "preparing" ||
pullRequest.status == "removing" ||
pullRequest.status == "expiring"
)
return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
await pullRequest.anonymize()
res.json({ status: pullRequest.status });
} catch (error) {
handleError(error, res, req);
}
}
);
// delete a pullRequest
router.delete(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
try {
if (pullRequest.status == "removed")
throw new AnonymousError("is_removed", {
object: req.params.pullRequestId,
httpStatus: 410,
});
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
await pullRequest.remove();
return res.json({ status: pullRequest.status });
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:owner/:repository/:pullRequestId",
async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
try {
const pullRequest = new PullRequest(
new AnonymizedPullRequestModel({
owner: user.id,
source: {
pullRequestId: parseInt(req.params.pullRequestId),
repositoryFullName: `${req.params.owner}/${req.params.repository}`,
},
})
);
await pullRequest.download();
res.json(pullRequest.toJSON());
} catch (error) {
handleError(error, res, req);
}
}
);
// get pullRequest information
router.get(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
res.json(pullRequest.toJSON());
} catch (error) {
handleError(error, res, req);
}
}
);
function validateNewPullRequest(pullRequestUpdate): void {
const validCharacters = /^[0-9a-zA-Z\-\_]+$/;
if (
!pullRequestUpdate.pullRequestId.match(validCharacters) ||
pullRequestUpdate.pullRequestId.length < 3
) {
throw new AnonymousError("invalid_pullRequestId", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.source.repositoryFullName) {
throw new AnonymousError("repository_not_specified", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.source.pullRequestId) {
throw new AnonymousError("pullRequestId_not_specified", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (
parseInt(pullRequestUpdate.source.pullRequestId) !=
pullRequestUpdate.source.pullRequestId
) {
throw new AnonymousError("pullRequestId_is_not_a_number", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.options) {
throw new AnonymousError("options_not_provided", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!Array.isArray(pullRequestUpdate.terms)) {
throw new AnonymousError("invalid_terms_format", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
}
function updatePullRequestModel(
model: IAnonymizedPullRequestDocument,
pullRequestUpdate: any
) {
model.options = {
terms: pullRequestUpdate.terms,
expirationMode: pullRequestUpdate.options.expirationMode,
expirationDate: pullRequestUpdate.options.expirationDate
? new Date(pullRequestUpdate.options.expirationDate)
: null,
update: pullRequestUpdate.options.update,
image: pullRequestUpdate.options.image,
link: pullRequestUpdate.options.link,
body: pullRequestUpdate.options.body,
title: pullRequestUpdate.options.title,
username: pullRequestUpdate.options.username,
origin: pullRequestUpdate.options.origin,
diff: pullRequestUpdate.options.diff,
comments: pullRequestUpdate.options.comments,
date: pullRequestUpdate.options.date,
};
}
// update a pullRequest
router.post(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
const pullRequestUpdate = req.body;
validateNewPullRequest(pullRequestUpdate);
pullRequest.model.anonymizeDate = new Date();
updatePullRequestModel(pullRequest.model, pullRequestUpdate);
// TODO handle conference
pullRequest.model.conference = pullRequestUpdate.conference;
await pullRequest.updateIfNeeded({ force: true });
res.json(pullRequest.toJSON());
} catch (error) {
return handleError(error, res, req);
}
}
);
// add pullRequest
router.post("/", async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
const pullRequestUpdate = req.body;
try {
validateNewPullRequest(pullRequestUpdate);
const pullRequest = new PullRequest(
new AnonymizedPullRequestModel({
owner: user.id,
options: pullRequestUpdate.options,
})
);
pullRequest.model.pullRequestId = pullRequestUpdate.pullRequestId;
pullRequest.model.anonymizeDate = new Date();
pullRequest.model.owner = user.id;
updatePullRequestModel(pullRequest.model, pullRequestUpdate);
pullRequest.source.accessToken = user.accessToken;
pullRequest.source.pullRequestId = pullRequestUpdate.source.pullRequestId;
pullRequest.source.repositoryFullName =
pullRequestUpdate.source.repositoryFullName;
pullRequest.conference = pullRequestUpdate.conference;
await pullRequest.anonymize()
res.send(pullRequest.toJSON());
} catch (error) {
if (error.message?.indexOf(" duplicate key") > -1) {
return handleError(
new AnonymousError("pullRequestId_already_used", {
httpStatus: 400,
cause: error,
object: pullRequestUpdate,
}),
res,
req
);
}
return handleError(error, res, req);
}
});
export default router;
+84
View File
@@ -0,0 +1,84 @@
import * as express from "express";
import { getPullRequest, handleError } from "./route-utils";
import AnonymousError from "../AnonymousError";
const router = express.Router();
router.get(
"/:pullRequestId/options",
async (req: express.Request, res: express.Response) => {
try {
res.header("Cache-Control", "no-cache");
const pr = await getPullRequest(req, res, { nocheck: true });
if (!pr) return;
let redirectURL = null;
if (pr.status == "expired" && pr.options.expirationMode == "redirect") {
redirectURL = `https://github.com/${pr.source.repositoryFullName}/pull/${pr.source.pullRequestId}`;
} else {
if (
pr.status == "expired" ||
pr.status == "expiring" ||
pr.status == "removing" ||
pr.status == "removed"
) {
throw new AnonymousError("pull_request_expired", {
object: pr,
httpStatus: 410,
});
}
const fiveMinuteAgo = new Date();
fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
if (pr.status != "ready") {
if (
pr.model.statusDate < fiveMinuteAgo
// && repo.status != "preparing"
) {
await pr.updateIfNeeded({ force: true });
}
if (pr.status == "error") {
throw new AnonymousError(
pr.model.statusMessage
? pr.model.statusMessage
: "pull_request_not_available",
{
object: pr,
httpStatus: 500,
}
);
}
throw new AnonymousError("pull_request_not_ready", {
httpStatus: 404,
object: pr,
});
}
await pr.updateIfNeeded();
}
res.json({
url: redirectURL,
lastUpdateDate: pr.model.statusDate,
});
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:pullRequestId/content",
async (req: express.Request, res: express.Response) => {
const pullRequest = await getPullRequest(req, res);
if (!pullRequest) return;
try {
await pullRequest.countView();
res.header("Cache-Control", "no-cache");
res.json(pullRequest.content());
} catch (error) {
handleError(error, res, req);
}
}
);
export default router;
+2 -3
View File
@@ -60,11 +60,10 @@ router.get(
router.get(
"/:repoId/files",
async (req: express.Request, res: express.Response) => {
res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res);
if (!repo) return;
try {
res.header("Cache-Control", "no-cache");
res.json(await repo.anonymizedFiles({ includeSha: false }));
} catch (error) {
handleError(error, res, req);
@@ -76,6 +75,7 @@ router.get(
"/:repoId/options",
async (req: express.Request, res: express.Response) => {
try {
res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res, { nocheck: true });
if (!repo) return;
let redirectURL = null;
@@ -146,7 +146,6 @@ router.get(
download = true;
}
res.header("Cache-Control", "no-cache");
res.json({
url: redirectURL,
download,
+30 -1
View File
@@ -5,6 +5,35 @@ import UserModel from "../database/users/users.model";
import User from "../User";
import * as io from "@pm2/io";
export async function getPullRequest(
req: express.Request,
res: express.Response,
opt?: { nocheck?: boolean }
) {
try {
const pullRequest = await db.getPullRequest(req.params.pullRequestId);
if (opt?.nocheck == true) {
} else {
// redirect if the repository is expired
if (
pullRequest.status == "expired" &&
pullRequest.options.expirationMode == "redirect"
) {
res.redirect(
`http://github.com/${pullRequest.source.repositoryFullName}/pull/${pullRequest.source.pullRequestId}`
);
return null;
}
pullRequest.check();
}
return pullRequest;
} catch (error) {
handleError(error, res, req);
return null;
}
}
export async function getRepo(
req: express.Request,
res: express.Response,
@@ -50,7 +79,7 @@ function printError(error: any, req?: express.Request) {
if (req) {
message += ` ${req.originalUrl}`;
// ignore common error
if (req.originalUrl === '/api/repo/undefined/options') return
if (req.originalUrl === "/api/repo/undefined/options") return;
}
console.error(message);
} else if (error instanceof Error) {
+15
View File
@@ -97,6 +97,21 @@ router.get(
}
}
);
router.get(
"/anonymized_pull_requests",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
res.json(
(await user.getPullRequests()).map((x) => {
return x.toJSON();
})
);
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/all_repositories",
+2 -2
View File
@@ -6,7 +6,7 @@ import GitHubDownload from "../source/GitHubDownload";
import AnonymousError from "../AnonymousError";
import { TreeElement } from "../types";
import * as marked from "marked";
import { anonymizeContent, streamToString } from "../anonymize-utils";
import { streamToString } from "../anonymize-utils";
const router = express.Router();
@@ -103,7 +103,7 @@ async function webView(req: express.Request, res: express.Response) {
}
if ((await f.extension()) == "md") {
const content = await streamToString(await f.anonymizedContent());
res.send(marked.marked(content));
res.contentType("html").send(marked.marked(content));
} else {
f.send(res);
}