mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-09-29 21:51:44 +02:00
fix: distinguish repository publication and settings dates
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"core.min.js": "core.c5bd53363a.min.js",
|
||||
"vendor.min.js": "vendor.3188bc70a6.min.js",
|
||||
"vendor.min.js": "vendor.7f9da8be8e.min.js",
|
||||
"mermaid.min.js": "mermaid.f848a72d16.min.js",
|
||||
"all.min.css": "all.87fb103470.min.css",
|
||||
"all.min.css": "all.5fbafcda4e.min.css",
|
||||
"markdown.min.js": "markdown.ad7b1d71c3.min.js",
|
||||
"pdf.min.js": "pdf.eaa7573247.min.js",
|
||||
"notebook.min.js": "notebook.8844e2735f.min.js",
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -2906,6 +2906,7 @@ code {
|
||||
background: var(--paper-bg-alt);
|
||||
}
|
||||
.leftCol-foot .last-update {
|
||||
display: block;
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,17 @@
|
||||
<tree class="files" :file="files" :search-query="fileSearchQuery" :search-results="fileSearchResults" :page="viewState"></tree>
|
||||
</div>
|
||||
<div class="leftCol-foot">
|
||||
<span class="last-update" data-toggle="tooltip" data-placement="top" :title="(options?.lastUpdateDate)">
|
||||
Updated {{ fmt?.date(options?.lastUpdateDate) }}
|
||||
<span class="last-update" :title="options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate">
|
||||
{{ options?.sourceCommitDate ? 'Source commit' : options?.anonymizedAt ? 'Anonymized' : 'Updated' }} {{ fmt?.date(options?.sourceCommitDate || options?.anonymizedAt || options?.lastUpdateDate) }}
|
||||
</span>
|
||||
<span class="last-update" v-if="options?.settingsSavedAt" :title="options.settingsSavedAt">
|
||||
Settings saved {{ fmt?.date(options.settingsSavedAt) }}
|
||||
</span>
|
||||
<span class="last-update" v-if="options?.publishedAt" :title="options.publishedAt">
|
||||
Snapshot ready {{ fmt?.date(options.publishedAt) }}
|
||||
</span>
|
||||
<span class="last-update" v-if="options?.anonymizedAt && !options?.publishedAt" title="Older repositories have no recorded publication date. The source commit date does not establish when files became available here.">
|
||||
Snapshot ready date not recorded
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Vendored
+12
-12
File diff suppressed because one or more lines are too long
@@ -475,6 +475,8 @@ export default class Repository {
|
||||
if (status !== RepositoryStatus.ARCHIVED) this.assertNotArchived();
|
||||
if (!status) return this.model;
|
||||
const statusDate = new Date();
|
||||
const publishedAt = status === RepositoryStatus.READY && this.status !== RepositoryStatus.READY
|
||||
? statusDate : undefined;
|
||||
if (isConnected) {
|
||||
const result = await AnonymizedRepositoryModel.updateOne(
|
||||
{
|
||||
@@ -486,12 +488,13 @@ export default class Repository {
|
||||
"githubAccess.revision": this._model.githubAccess?.revision || { $exists: false },
|
||||
} : {}),
|
||||
},
|
||||
{ $set: { status, statusDate, statusMessage } }
|
||||
{ $set: { status, statusDate, statusMessage, ...(publishedAt ? { publishedAt } : {}) } }
|
||||
).exec();
|
||||
if (this.protectLifecycle && result.matchedCount === 0) {
|
||||
throw new AnonymousError("repository_job_cancelled", { httpStatus: 410 });
|
||||
}
|
||||
}
|
||||
if (publishedAt) this._model.publishedAt = publishedAt;
|
||||
this._model.status = status;
|
||||
this._model.statusDate = statusDate;
|
||||
this._model.statusMessage = statusMessage;
|
||||
|
||||
@@ -16,6 +16,8 @@ const AnonymizedRepositorySchema = new Schema({
|
||||
archiveCachePending: Boolean,
|
||||
statusMessage: String,
|
||||
anonymizeDate: Date,
|
||||
settingsSavedAt: Date,
|
||||
publishedAt: Date,
|
||||
lastView: Date,
|
||||
pageView: Number,
|
||||
accessToken: { type: String, select: false },
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface IAnonymizedRepository {
|
||||
archiveReason?: string;
|
||||
archiveCachePending?: boolean;
|
||||
anonymizeDate: Date;
|
||||
settingsSavedAt?: Date;
|
||||
publishedAt?: Date;
|
||||
source: {
|
||||
type: "GitHubDownload" | "GitHubStream" | "Zip";
|
||||
branch?: string;
|
||||
|
||||
@@ -173,8 +173,10 @@ router.post(
|
||||
const reactivating = repo.status === RepositoryStatus.EXPIRED;
|
||||
const updates: Record<string, Date> = {
|
||||
"options.expirationDate": newExpiration,
|
||||
settingsSavedAt: new Date(),
|
||||
};
|
||||
repo.model.options.expirationDate = newExpiration;
|
||||
repo.model.settingsSavedAt = updates.settingsSavedAt;
|
||||
if (reactivating) {
|
||||
repo.model.anonymizeDate = new Date();
|
||||
updates.anonymizeDate = repo.model.anonymizeDate;
|
||||
@@ -572,6 +574,7 @@ router.post(
|
||||
{ _id: repo.model._id, "githubAccess.revision": previousAccessRevision || { $exists: false } },
|
||||
{
|
||||
$set: {
|
||||
settingsSavedAt: new Date(),
|
||||
options: repo.model.options,
|
||||
source: repo.model.source,
|
||||
githubAccess: repo.model.githubAccess,
|
||||
@@ -647,6 +650,7 @@ router.post("/", async (req, res) => {
|
||||
const repo = new AnonymizedRepositoryModel();
|
||||
repo.repoId = repoUpdate.repoId;
|
||||
repo.anonymizeDate = new Date();
|
||||
repo.settingsSavedAt = repo.anonymizeDate;
|
||||
repo.owner = user.id;
|
||||
repo.githubAccess = selectedAccess.binding;
|
||||
|
||||
|
||||
@@ -385,9 +385,15 @@ router.get(
|
||||
res.json({
|
||||
url: redirectURL,
|
||||
download: download || user?.isAdmin === true,
|
||||
lastUpdateDate: repo.model.source.commitDate
|
||||
? repo.model.source.commitDate
|
||||
: repo.model.anonymizeDate,
|
||||
// Source commits can predate publication by months and do not record
|
||||
// changes to anonymization settings or expiration.
|
||||
lastUpdateDate: new Date(Math.max(...[
|
||||
repo.model.anonymizeDate, repo.model.settingsSavedAt, repo.model.publishedAt,
|
||||
].filter((date): date is Date => !!date).map(date => new Date(date).getTime()))),
|
||||
anonymizedAt: repo.model.anonymizeDate,
|
||||
sourceCommitDate: repo.model.source.commitDate,
|
||||
settingsSavedAt: repo.model.settingsSavedAt,
|
||||
publishedAt: repo.model.publishedAt,
|
||||
isAdmin: user?.isAdmin === true,
|
||||
isOwner: user?.id == repo.model.owner,
|
||||
hasWebsite: !!repo.options.page && !!repo.options.pageSource,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
const { expect } = require("chai");
|
||||
require("ts-node/register/transpile-only");
|
||||
const db = require("../src/server/database");
|
||||
const Model = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
|
||||
const Files = require("../src/core/model/files/files.model").default;
|
||||
const Repository = require("../src/core/Repository").default;
|
||||
const utils = require("../src/server/routes/route-utils");
|
||||
const queue = require("../src/queue");
|
||||
|
||||
function handler(name, path, method) {
|
||||
return require(`../src/server/routes/${name}`).default.stack.find(layer =>
|
||||
layer.route?.path === path && layer.route.methods[method]).route.stack[0].handle;
|
||||
}
|
||||
|
||||
describe("repository activity dates", () => {
|
||||
const restores = [];
|
||||
function stub(object, key, value) {
|
||||
const original = object[key]; restores.push(() => { object[key] = original; }); object[key] = value;
|
||||
}
|
||||
afterEach(() => { while (restores.length) restores.pop()(); });
|
||||
function repository(status = "ready") {
|
||||
return new Repository(new Model({ repoId: "activity", status,
|
||||
owner: "507f1f77bcf86cd799439011", anonymizeDate: new Date("2026-01-02"),
|
||||
source: { repositoryName: "owner/repo", commit: "abc123", branch: "main", commitDate: new Date("2020-01-01") },
|
||||
options: { terms: [], expirationMode: "never" },
|
||||
}));
|
||||
}
|
||||
function auth(repo) {
|
||||
stub(utils, "getRepo", async () => repo);
|
||||
stub(utils, "getUser", async () => ({ isAdmin: true }));
|
||||
stub(utils, "handleError", error => { throw error; });
|
||||
}
|
||||
it("records readiness after a download, preserves it for progress messages, and advances it after restoration", async () => {
|
||||
const repo = repository("download"); let saved;
|
||||
stub(db, "isConnected", true);
|
||||
stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; return { matchedCount: 1 }; } }));
|
||||
await repo.updateStatus("ready");
|
||||
expect(saved.publishedAt).to.be.instanceOf(Date);
|
||||
const first = repo.model.publishedAt;
|
||||
await repo.updateStatus("ready", "progress");
|
||||
expect(saved).not.to.have.property("publishedAt");
|
||||
expect(repo.model.publishedAt).to.deep.equal(first);
|
||||
await repo.updateStatus("expired");
|
||||
await repo.updateStatus("preparing");
|
||||
repo.model.publishedAt = new Date("2026-01-01");
|
||||
await repo.updateStatus("ready");
|
||||
expect(repo.model.publishedAt.getTime()).to.be.greaterThan(new Date("2026-01-01").getTime());
|
||||
});
|
||||
it("does not record publication when a concurrent removal cancels the worker", async () => {
|
||||
const repo = repository("download"); repo.protectLifecycle = true;
|
||||
stub(db, "isConnected", true);
|
||||
stub(Model, "updateOne", () => ({ exec: async () => ({ matchedCount: 0 }) }));
|
||||
try { await repo.updateStatus("ready"); throw new Error("expected cancellation"); }
|
||||
catch (error) { expect(error.message).to.equal("repository_job_cancelled"); }
|
||||
expect(repo.model.publishedAt).to.equal(undefined);
|
||||
});
|
||||
it("dates a settings-only save without rebuilding or changing the commit", async () => {
|
||||
const repo = repository(); auth(repo); let saved;
|
||||
stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; return { matchedCount: 1 }; } }));
|
||||
stub(queue, "downloadQueue", { add: async () => { throw new Error("must not rebuild"); } });
|
||||
await handler("repository-private", "/:repoId/", "post")({ body: {
|
||||
repoId: repo.repoId, fullName: "owner/repo", source: { commit: "abc123", branch: "main" },
|
||||
terms: ["author"], options: { expirationMode: "never", image: false },
|
||||
} }, { json: () => {} });
|
||||
expect(saved.settingsSavedAt).to.be.instanceOf(Date);
|
||||
expect(saved.source.commit).to.equal("abc123");
|
||||
expect(saved).not.to.have.property("publishedAt");
|
||||
});
|
||||
it("dates a dashboard expiration extension", async () => {
|
||||
const repo = repository(); auth(repo); let saved;
|
||||
stub(Model, "updateOne", (_filter, update) => ({ exec: async () => { saved = update.$set; } }));
|
||||
await handler("repository-private", "/:repoId/extend", "post")({}, { json: () => {} });
|
||||
expect(saved.settingsSavedAt).to.be.instanceOf(Date);
|
||||
expect(saved["options.expirationDate"]).to.be.instanceOf(Date);
|
||||
});
|
||||
it("returns publication and settings dates independently of the older source commit", async () => {
|
||||
const repo = repository(); auth(repo);
|
||||
repo.model.settingsSavedAt = new Date("2026-03-01");
|
||||
repo.model.publishedAt = new Date("2026-02-01");
|
||||
stub(Files, "exists", async () => null);
|
||||
let body;
|
||||
await handler("repository-public", "/:repoId/options", "get")({}, { header: () => {}, json: value => { body = value; } });
|
||||
expect(body.lastUpdateDate).to.deep.equal(repo.model.settingsSavedAt);
|
||||
expect(body.sourceCommitDate).to.deep.equal(new Date("2020-01-01"));
|
||||
expect(body.publishedAt).to.deep.equal(repo.model.publishedAt);
|
||||
});
|
||||
it("does not invent publication or settings dates for legacy repositories", async () => {
|
||||
const repo = repository(); auth(repo);
|
||||
stub(Files, "exists", async () => null);
|
||||
let body;
|
||||
await handler("repository-public", "/:repoId/options", "get")({}, { header: () => {}, json: value => { body = value; } });
|
||||
expect(body.publishedAt).to.equal(undefined);
|
||||
expect(body.settingsSavedAt).to.equal(undefined);
|
||||
expect(body.lastUpdateDate).to.deep.equal(repo.model.anonymizeDate);
|
||||
});
|
||||
});
|
||||
@@ -341,6 +341,29 @@ describe("Vue 3 UI", function () {
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("distinguishes source, settings and readiness dates in the explorer", async function () {
|
||||
ui = await browser("/r/test/hello.txt", {
|
||||
"/api/repo/test/options": { anonymizedAt: "2026-01-01T00:00:00Z", sourceCommitDate: "2020-01-01T00:00:00Z",
|
||||
settingsSavedAt: "2026-02-01T00:00:00Z", publishedAt: "2026-03-01T00:00:00Z" },
|
||||
});
|
||||
const footer = ui.window.document.querySelector(".leftCol-foot");
|
||||
expect(footer.textContent).to.include("Source commit").and.include("Settings saved").and.include("Snapshot ready");
|
||||
expect([...footer.querySelectorAll(".last-update")].map(node => node.title)).to.deep.equal([
|
||||
"2020-01-01T00:00:00Z", "2026-02-01T00:00:00Z", "2026-03-01T00:00:00Z",
|
||||
]);
|
||||
expect(footer.textContent).not.to.include("not recorded");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
it("identifies an unknown legacy publication date", async function () {
|
||||
ui = await browser("/r/test/hello.txt", {
|
||||
"/api/repo/test/options": { anonymizedAt: "2026-01-01T00:00:00Z", sourceCommitDate: "2020-01-01T00:00:00Z" },
|
||||
});
|
||||
const footer = ui.window.document.querySelector(".leftCol-foot");
|
||||
expect(footer.textContent).to.include("Snapshot ready date not recorded");
|
||||
expect(footer.textContent).not.to.include("Settings saved");
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("renders hostile filenames as text and updates the explorer without losing the tree", async function () {
|
||||
const filename = '{{constructor.constructor("window.probe=1")()}}.txt';
|
||||
ui = await browser("/r/test/hello.txt", {
|
||||
|
||||
Reference in New Issue
Block a user