fix mulitple bugs

This commit is contained in:
tdurieux
2026-05-05 12:34:03 +03:00
parent 7384638993
commit 27d6b56da7
13 changed files with 242 additions and 29 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+52 -2
View File
@@ -2079,7 +2079,7 @@ code {
}
/* Progress bars on dashboard */
.progress {
.progress:not(.quota-progress) {
min-width: 120px !important;
}
@@ -3830,6 +3830,8 @@ textarea::selection {
border: 1px solid var(--border-color);
border-radius: 999px;
margin-bottom: 0;
width: 100%;
min-width: 0;
}
.paper-page .quota-progress .progress-bar {
background: var(--color);
@@ -4010,6 +4012,21 @@ textarea::selection {
background: var(--hover-bg-color);
}
.paper-table .paper-table-row.repo-inactive { opacity: 0.55; }
.paper-table .paper-table-row.repo-error {
background: rgba(180, 35, 24, 0.04);
border-left: 2px solid #B42318;
padding-left: 6px;
}
.paper-table .paper-table-row.repo-error:hover { background: rgba(180, 35, 24, 0.08); }
.dark-mode .paper-table .paper-table-row.repo-error { background: rgba(240, 138, 130, 0.05); border-left-color: #F08A82; }
.dark-mode .paper-table .paper-table-row.repo-error:hover { background: rgba(240, 138, 130, 0.1); }
.paper-table .cell-status .status-sub-error,
.cell-status .status-sub.status-sub-error {
color: #B42318;
font-family: var(--font-mono);
}
.dark-mode .paper-table .cell-status .status-sub-error,
.dark-mode .cell-status .status-sub.status-sub-error { color: #F08A82; }
.paper-table .cell-anon {
display: flex;
align-items: center;
@@ -4450,10 +4467,43 @@ textarea::selection {
}
@media (max-width: 768px) {
.paper-page .quota-row { grid-template-columns: 1fr; gap: 14px; padding: 16px; }
.paper-page .paper-page-title { font-size: clamp(1.4rem, 6vw, 1.9rem); margin: 4px 0 2px; }
.paper-page .paper-page-lede { font-size: 13px; line-height: 1.4; margin-bottom: 0; }
.paper-page .btn-ink { padding: 6px 12px; font-size: 13px; }
.paper-page .quota-row {
grid-template-columns: repeat(3, 1fr);
gap: 10px 12px;
padding: 0;
margin: 12px 0 0;
}
.paper-page .dashboard-filter-row {
padding: 8px 0;
margin-top: 10px;
}
.paper-page .quota-header {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.paper-page .quota-label { font-size: 10px; letter-spacing: 0.1em; }
.paper-page .quota-value { font-size: 11px; }
.paper-page .dashboard-filter-row { gap: 8px; }
}
@media (max-width: 480px) {
.paper-page .paper-page-lede {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.paper-page .quota-row { gap: 8px; }
.paper-page .quota-label { font-size: 9px; letter-spacing: 0.06em; }
.paper-page .quota-value { font-size: 10px; white-space: nowrap; }
}
/* Empty state */
.empty-state {
text-align: center;
+3
View File
@@ -17,6 +17,7 @@
"repository_not_accessible": "This repository is currently not accessible.",
"repo_is_updating": "Anonymous GitHub is still processing the repository, it can take several minutes.",
"invalid_repo": "The provided repository is not valid.",
"invalid_source_repository": "The repository source name is invalid; expected the form 'owner/name'.",
"repoId_not_defined": "A repository ID must be provided.",
"repoUrl_not_defined": "The repository URL needs to be defined.",
"repoId_already_used": "The repository ID is already used.",
@@ -25,6 +26,8 @@
"branch_not_specified": "The branch is not specified.",
"branch_not_found": "The branch of the repository cannot be found.",
"commit_not_specified": "A commit must be specified.",
"commit_not_found": "The configured commit no longer exists in the source repository. It may have been force-pushed, rebased, or removed.",
"repo_renamed": "The source repository appears to have been renamed or moved on GitHub. Please refresh and try again — the cached name has been updated.",
"invalid_commit_format": "The commit hash format is invalid. It must be a hexadecimal string.",
"pull_request_not_found": "The requested pull request could not be found.",
"pull_request_expired": "This pull request has expired and is no longer available.",
+1 -1
View File
@@ -81,7 +81,7 @@
<div
class="paper-table-row"
role="row"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'row-selected': selected[repo.repoId]}"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'repo-error': repo.status == 'error', 'row-selected': selected[repo.repoId]}"
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
>
<div role="cell" style="width: 28px;">
+6 -3
View File
@@ -177,7 +177,7 @@
<div
class="paper-table-row"
role="row"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed'}"
ng-class="{'repo-inactive': repo.status == 'expired' || repo.status == 'removed', 'repo-error': repo.status == 'error'}"
ng-repeat="repo in repositories | filter:repoFiler | orderBy:orderBy as filteredRepositories"
>
<div class="cell-anon" role="cell">
@@ -190,8 +190,11 @@
</div>
</div>
<div class="cell-status" role="cell">
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error'}"></span>
<span ng-bind="repo.status | title"></span>
<span class="status-line">
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error'}"></span>
<span ng-bind="repo.status | title"></span>
</span>
<span class="status-sub status-sub-error" ng-if="repo.status == 'error' && repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage"></span>
</div>
<div class="cell-views num" role="cell" ng-bind="::repo.pageView | number"></div>
<div class="cell-expires" role="cell" ng-bind="repo.anonymizeDate | humanTime"></div>
+3 -2
View File
@@ -179,7 +179,7 @@
<div
class="paper-table-row"
role="row"
ng-class="{'repo-inactive': item.status == 'expired' || item.status == 'removed' || item.status == 'error'}"
ng-class="{'repo-inactive': item.status == 'expired' || item.status == 'removed', 'repo-error': item.status == 'error'}"
ng-repeat="item in items | filter:itemFilter | orderBy:orderBy as filteredItems"
>
<div class="cell-anon" role="cell">
@@ -206,7 +206,8 @@
></span>
<span ng-bind="item.status | title"></span>
</div>
<div class="status-sub" ng-if="item.anonymizeDate" title="Last anonymized {{item.anonymizeDate | humanTime}}" ng-bind="item.anonymizeDate | humanTime"></div>
<div class="status-sub status-sub-error" ng-if="item.status == 'error' && item.statusMessage" title="{{item.statusMessage}}" ng-bind="item.statusMessage"></div>
<div class="status-sub" ng-if="item.status != 'error' && item.anonymizeDate" title="Last anonymized {{item.anonymizeDate | humanTime}}" ng-bind="item.anonymizeDate | humanTime"></div>
</div>
<div class="cell-views num" role="cell" ng-bind="item.pageView | number"></div>
<div class="cell-expires" role="cell">
+20 -1
View File
@@ -1374,7 +1374,18 @@ angular
if (res.data.options.expirationDate) {
$scope.options.expirationDate = new Date(res.data.options.expirationDate);
}
$scope.details = (await $http.get(`/api/pr/${res.data.source.repositoryFullName}/${res.data.source.pullRequestId}`)).data;
try {
$scope.details = (await $http.get(`/api/pr/${res.data.source.repositoryFullName}/${res.data.source.pullRequestId}`)).data;
} catch (error) {
const code = error && error.data && error.data.error;
if (code) {
$translate("ERRORS." + code).then((translation) => {
$scope.addToast({ title: "Error", date: new Date(), body: translation });
$scope.error = translation;
}, console.error);
displayErrorMessage(code);
}
}
$scope.$apply();
},
() => { $location.url("/404"); }
@@ -2867,3 +2878,11 @@ angular
getConference();
},
]);
$(document).on("click", "#navbarSupportedContent .nav-link", function (e) {
if ($(this).attr("data-toggle") === "dropdown") return;
var $collapse = $("#navbarSupportedContent");
if ($collapse.hasClass("show")) {
$collapse.collapse("hide");
}
});
+1 -1
View File
File diff suppressed because one or more lines are too long
+49 -8
View File
@@ -6,7 +6,7 @@ import ConferenceModel from "./model/conference/conferences.model";
import AnonymousError from "./AnonymousError";
import { IAnonymizedPullRequestDocument } from "./model/anonymizedPullRequests/anonymizedPullRequests.types";
import config from "../config";
import got from "got";
import got, { HTTPError } from "got";
import { octokit } from "./GitHubUtils";
import { ContentAnonimizer } from "./anonymize-utils";
@@ -57,20 +57,61 @@ export default class PullRequest {
const [owner, repo] = this._model.source.repositoryFullName.split("/");
const pull_number = this._model.source.pullRequestId;
const [prInfo, comments, diff] = await Promise.all([
oct.rest.pulls.get({
let prInfo;
try {
prInfo = await oct.rest.pulls.get({
owner,
repo,
pull_number,
}),
oct.paginate("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", {
});
} catch (err) {
if ((err as { status?: number }).status === 404) {
throw new AnonymousError("pull_request_not_found", {
httpStatus: 404,
object: this,
cause: err as Error,
});
}
throw err;
}
const commentsPromise = oct
.paginate("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", {
owner: owner,
repo: repo,
issue_number: pull_number,
per_page: 100,
}),
got(`https://github.com/${owner}/${repo}/pull/${pull_number}.diff`),
]);
})
.catch((err): Array<{
body?: string | null;
created_at: string;
updated_at: string;
user?: { login?: string } | null;
}> => {
if ((err as { status?: number }).status === 404) {
console.warn(
"[WARN] Failed to fetch PR comments (404), continuing without them",
`${owner}/${repo}#${pull_number}`
);
return [];
}
throw err;
});
const diffPromise = got(
`https://github.com/${owner}/${repo}/pull/${pull_number}.diff`
).catch((err) => {
if (err instanceof HTTPError && err.response.statusCode === 404) {
console.warn(
"[WARN] Failed to fetch PR diff (404), continuing without it",
`${owner}/${repo}#${pull_number}`
);
return { body: "" };
}
throw err;
});
const [comments, diff] = await Promise.all([commentsPromise, diffPromise]);
this._model.pullRequest = {
diff: diff.body,
+65
View File
@@ -3,6 +3,10 @@ import { Readable } from "stream";
import AnonymizedFile from "../AnonymizedFile";
import { SourceBase } from "./Source";
import { IFile } from "../model/files/files.types";
import { octokit } from "../GitHubUtils";
import { isConnected } from "../../server/database";
import RepositoryModel from "../model/repositories/repositories.model";
import AnonymizedRepositoryModel from "../model/anonymizedRepositories/anonymizedRepositories.model";
export interface GitHubBaseData {
getToken: () => string | Promise<string>;
@@ -25,3 +29,64 @@ export default abstract class GitHubBase implements SourceBase {
abstract getFiles(progress?: (status: string) => void): Promise<IFile[]>;
}
/**
* On a 404 from a commit/tree/zip lookup, probe `repos.get` to determine
* whether the repository itself is gone, was renamed, or only the commit is
* missing. When a rename is detected (via the cached numeric GitHub repo id
* on `RepositoryModel.externalId`), the model's `name` is updated in place so
* subsequent lookups succeed.
*/
export async function classifyGitHubMissError(
err: unknown,
data: GitHubBaseData
): Promise<"repo_not_found" | "commit_not_found" | "repo_renamed"> {
const status = (err as { status?: number }).status;
if (status !== 404) return "repo_not_found";
const oct = octokit(await data.getToken());
try {
await oct.repos.get({
owner: data.organization,
repo: data.repoName,
});
return "commit_not_found";
} catch {
// Repo no longer exists at owner/repo. Try to recover via the cached
// numeric GitHub id — if the repo was renamed, GET /repositories/{id}
// resolves to its new full_name. See #409.
if (!isConnected) return "repo_not_found";
const dbModel = await RepositoryModel.findOne({
name: data.organization + "/" + data.repoName,
});
const ghId =
typeof dbModel?.externalId === "string" &&
dbModel.externalId.startsWith("gh_")
? dbModel.externalId.slice(3)
: null;
if (!dbModel || !ghId) return "repo_not_found";
try {
const r = await oct.request("GET /repositories/{id}", { id: ghId });
const newName = (r?.data as { full_name?: string } | undefined)
?.full_name;
if (newName && newName !== dbModel.name) {
const oldName = dbModel.name;
dbModel.name = newName;
await dbModel.save();
// Propagate the rename to every anonymized repo that referenced
// the old source name, so subsequent lookups (admin diagnostic,
// streaming, download, update cron) all hit the correct GitHub
// location without the user having to recreate the configuration.
await AnonymizedRepositoryModel.updateMany(
{ "source.repositoryName": oldName },
{ $set: { "source.repositoryName": newName } }
);
data.organization = newName.split("/")[0];
data.repoName = newName.split("/")[1];
return "repo_renamed";
}
return "repo_not_found";
} catch {
return "repo_not_found";
}
}
}
+6 -2
View File
@@ -3,7 +3,10 @@ import { Readable } from "stream";
import { OctokitResponse } from "@octokit/types";
import storage from "../storage";
import GitHubBase, { GitHubBaseData } from "./GitHubBase";
import GitHubBase, {
GitHubBaseData,
classifyGitHubMissError,
} from "./GitHubBase";
import { FILE_TYPE } from "../storage/Storage";
import { octokit } from "../GitHubUtils";
import AnonymousError from "../AnonymousError";
@@ -30,7 +33,8 @@ export default class GitHubDownload extends GitHubBase {
try {
response = await this.getZipUrl();
} catch (error) {
throw new AnonymousError("repo_not_found", {
const code = await classifyGitHubMissError(error, this.data);
throw new AnonymousError(code, {
httpStatus: (error as { status?: number }).status || 404,
object: this.data,
cause: error as Error,
+29 -6
View File
@@ -55,12 +55,35 @@ export class GitHubRepository {
}
) {
const oct = octokit(opt.accessToken);
const commit = await oct.repos.getCommit({
owner: this.owner,
repo: this.repo,
ref: sha,
});
return commit.data;
try {
const commit = await oct.repos.getCommit({
owner: this.owner,
repo: this.repo,
ref: sha,
});
return commit.data;
} catch (error) {
const status = (error as { status?: number }).status;
if (status === 404) {
// Distinguish: does the repo itself still exist?
let repoExists = false;
try {
await oct.repos.get({ owner: this.owner, repo: this.repo });
repoExists = true;
} catch {
repoExists = false;
}
throw new AnonymousError(
repoExists ? "commit_not_found" : "repo_not_found",
{
httpStatus: 404,
cause: error as Error,
object: this,
}
);
}
throw error;
}
}
async branches(opt: {
+6 -2
View File
@@ -1,5 +1,8 @@
import AnonymizedFile from "../AnonymizedFile";
import GitHubBase, { GitHubBaseData } from "./GitHubBase";
import GitHubBase, {
GitHubBaseData,
classifyGitHubMissError,
} from "./GitHubBase";
import storage from "../storage";
import * as path from "path";
import got from "got";
@@ -274,7 +277,8 @@ export default class GitHubStream extends GitHubBase {
});
}
if (status === 404) {
throw new AnonymousError("repo_not_found", {
const code = await classifyGitHubMissError(error, this.data);
throw new AnonymousError(code, {
httpStatus: 404,
object: this.data,
cause: error as Error,