Skip to content

Commit 42ab3e5

Browse files
hochan222LeoMcA
andauthored
feat(translations/differences): visualize how many commits behind translations are (#8338)
* PoC: visualization for how far behind the lastest commit * feat: tag for Metadata does not exist. * fix: filter available * refactor: execSync cwd option * fix: filter priority * Optimization through cache * reduce ja time to 214s with git rev-list * feat: source commit error report * Optimize source commit logic Generate source commit error report * feat: source commit importance color * new approach for traversing git commit graph Co-authored-by: LeoMcA <[email protected]> * remove p tag in table tag Co-authored-by: Leo McArdle <[email protected]> * add details to report message and file name Co-authored-by: LeoMcA <[email protected]> * modify source commit filter condition Co-authored-by: Leo McArdle <[email protected]> * fix: consider sourceCommitsBehindCount undefined * check automatic cache validation * refactor: types and naming * execSync -> spawn, various fixes for optimization Co-authored-by: LeoMcA <[email protected]> * fix: make sourceCommitCache work correctly * remove log --------- Co-authored-by: LeoMcA <[email protected]> Co-authored-by: Leo McArdle <[email protected]>
1 parent 0423309 commit 42ab3e5

File tree

3 files changed

+236
-16
lines changed

3 files changed

+236
-16
lines changed

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ testing/content/files/en-us/markdown/tool/m2h/index.html
103103
# eslintcache
104104
client/.eslintcache
105105
popularities.json
106+
source-commit.json
107+
source-commit-invalid-report.txt
106108

107109
client/src/.linaria_cache
108110
ssr/.linaria-cache/

Diff for: client/src/translations/differences/index.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ interface DocumentEdits {
3535
parentModified: string;
3636
commitURL: string;
3737
parentCommitURL: string;
38+
sourceCommitsBehindCount?: number;
39+
sourceCommitURL?: string;
3840
}
3941

4042
interface Document {
@@ -574,6 +576,10 @@ function DocumentsTable({
574576
const a = A.mdn_url;
575577
const b = B.mdn_url;
576578
return reverse * a.localeCompare(b);
579+
} else if (sort === "sourceCommit") {
580+
const a = A.edits.sourceCommitsBehindCount ?? -1;
581+
const b = B.edits.sourceCommitsBehindCount ?? -1;
582+
return reverse * (b - a);
577583
} else {
578584
throw new Error(`Unrecognized sort '${sort}'`);
579585
}
@@ -606,6 +612,7 @@ function DocumentsTable({
606612
<TableHead id="popularity" title="Popularity" />
607613
<TableHead id="modified" title="Last modified" />
608614
<TableHead id="differences" title="Differences" />
615+
<TableHead id="sourceCommit" title="Source Commit" />
609616
</tr>
610617
</thead>
611618
<tbody>
@@ -658,6 +665,14 @@ function DocumentsTable({
658665
<LastModified edits={doc.edits} />
659666
</td>
660667
<td>{doc.differences.total.toLocaleString()}</td>
668+
<td>
669+
<L10nSourceCommitModified
670+
sourceCommitsBehindCount={
671+
doc.edits.sourceCommitsBehindCount
672+
}
673+
sourceCommitURL={doc.edits.sourceCommitURL}
674+
/>
675+
</td>
661676
</tr>
662677
);
663678
})}
@@ -680,6 +695,29 @@ function DocumentsTable({
680695
);
681696
}
682697

698+
function L10nSourceCommitModified({
699+
sourceCommitsBehindCount,
700+
sourceCommitURL,
701+
}: Pick<DocumentEdits, "sourceCommitsBehindCount" | "sourceCommitURL">) {
702+
if (
703+
!sourceCommitURL ||
704+
(!sourceCommitsBehindCount && sourceCommitsBehindCount !== 0)
705+
) {
706+
return <>Metadata does not exist.</>;
707+
}
708+
709+
const getImportanceColor = () => {
710+
if (sourceCommitsBehindCount === 0) return "🟢";
711+
return sourceCommitsBehindCount < 10 ? "🟠" : "🔴";
712+
};
713+
714+
return (
715+
<a
716+
href={sourceCommitURL}
717+
>{`${getImportanceColor()} ${sourceCommitsBehindCount} commits behind`}</a>
718+
);
719+
}
720+
683721
function LastModified({ edits }: { edits: DocumentEdits }) {
684722
const modified = dayjs(edits.modified);
685723
const parentModified = dayjs(edits.parentModified);

Diff for: server/translations.ts

+196-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path";
33

44
import express from "express";
55
import { fdir } from "fdir";
6+
import { execSync, spawn } from "node:child_process";
67

78
import { getPopularities, Document, Translation } from "../content/index.js";
89
import {
@@ -59,8 +60,52 @@ function packageTranslationDifferences(translationDifferences) {
5960
return { total, countByType };
6061
}
6162

63+
type RecentRepoHashType = string;
64+
6265
const _foundDocumentsCache = new Map();
66+
const sourceCommitCache = fs.existsSync("./source-commit.json")
67+
? new Map<string, number | RecentRepoHashType>(
68+
Object.entries(
69+
JSON.parse(fs.readFileSync("./source-commit.json", "utf8"))
70+
)
71+
)
72+
: new Map<string, number>();
73+
const commitFiles = new Map<string, string[]>();
74+
let commitFilesOldest = "HEAD";
6375
export async function findDocuments({ locale }) {
76+
function checkCacheValidation(prevCache: Map<any, any>): void {
77+
const contentHash = getRecentRepoHash(CONTENT_ROOT);
78+
const translatedContentHash = getRecentRepoHash(CONTENT_TRANSLATED_ROOT);
79+
80+
function getRecentRepoHash(cwd: string): string {
81+
return execSync("git rev-parse HEAD", { cwd }).toString().trimEnd();
82+
}
83+
function updateRecentRepoHash(cache: Map<string, any>): void {
84+
cache.set(CONTENT_ROOT, contentHash);
85+
cache.set(CONTENT_TRANSLATED_ROOT, translatedContentHash);
86+
}
87+
function isValidCache(cache: Map<string, any>): boolean {
88+
return (
89+
cache.has(CONTENT_ROOT) &&
90+
cache.has(CONTENT_TRANSLATED_ROOT) &&
91+
cache.get(CONTENT_ROOT) === contentHash &&
92+
cache.get(CONTENT_TRANSLATED_ROOT) === translatedContentHash
93+
);
94+
}
95+
96+
if (isValidCache(sourceCommitCache)) {
97+
return;
98+
}
99+
if (!isValidCache(prevCache)) {
100+
prevCache.clear();
101+
sourceCommitCache.clear();
102+
commitFiles.clear();
103+
commitFilesOldest = "HEAD";
104+
updateRecentRepoHash(prevCache);
105+
updateRecentRepoHash(sourceCommitCache);
106+
}
107+
}
108+
64109
const counts = {
65110
// Number of documents found that aren't skipped
66111
found: 0,
@@ -81,6 +126,7 @@ export async function findDocuments({ locale }) {
81126
});
82127
counts.total = documentsFound.count;
83128

129+
checkCacheValidation(_foundDocumentsCache);
84130
if (!_foundDocumentsCache.has(locale)) {
85131
_foundDocumentsCache.set(locale, new Map());
86132
}
@@ -91,7 +137,7 @@ export async function findDocuments({ locale }) {
91137

92138
if (!cache.has(filePath) || cache.get(filePath).mtime < mtime) {
93139
counts.cacheMisses++;
94-
const document = getDocument(filePath);
140+
const document = await getDocument(filePath);
95141
cache.set(filePath, {
96142
document,
97143
mtime,
@@ -114,14 +160,20 @@ export async function findDocuments({ locale }) {
114160
took,
115161
};
116162

163+
fs.writeFileSync(
164+
"./source-commit.json",
165+
JSON.stringify(Object.fromEntries(sourceCommitCache)),
166+
"utf8"
167+
);
168+
117169
return {
118170
counts,
119171
times,
120172
documents,
121173
};
122174
}
123175

124-
function getDocument(filePath) {
176+
async function getDocument(filePath) {
125177
function packagePopularity(document, parentDocument) {
126178
return {
127179
value: document.metadata.popularity,
@@ -141,34 +193,162 @@ function getDocument(filePath) {
141193
};
142194
}
143195

144-
function packageEdits(document, parentDocument) {
145-
const commitURL = getLastCommitURL(
146-
document.fileInfo.root,
147-
document.metadata.hash
148-
);
149-
const parentCommitURL = getLastCommitURL(
150-
parentDocument.fileInfo.root,
151-
parentDocument.metadata.hash
152-
);
153-
const modified = document.metadata.modified;
154-
const parentModified = parentDocument.metadata.modified;
196+
function recordInvalidSourceCommit(
197+
fileFolder: string,
198+
commitHash: string,
199+
message: string
200+
) {
201+
const filePath = "./source-commit-invalid-report.txt";
202+
const errorMessage = `- ${commitHash} commit hash is invalid in ${fileFolder}: ${message.replace(
203+
/\n/g,
204+
" "
205+
)}`;
206+
if (!fs.existsSync(filePath)) {
207+
fs.writeFileSync(filePath, "");
208+
}
209+
210+
fs.appendFile(filePath, `${errorMessage}\n`, function (err) {
211+
if (err) throw err;
212+
});
213+
}
214+
215+
class GitError extends Error {
216+
constructor(stderr: string) {
217+
super(stderr);
218+
this.name = "GitError";
219+
}
220+
}
221+
222+
function fillMemStore(commitHash: string) {
223+
return new Promise((resolve, reject) => {
224+
const git = spawn(
225+
"git",
226+
[
227+
"log",
228+
"--pretty=format:%x00%x00%H",
229+
"--name-only",
230+
"-z",
231+
`${commitHash}..${commitFilesOldest}`,
232+
],
233+
{
234+
cwd: CONTENT_ROOT,
235+
}
236+
);
237+
238+
let stdoutBuffer = "";
239+
240+
git.stdout.on("data", (data) => {
241+
stdoutBuffer += data.toString();
242+
const commits = stdoutBuffer.split("\0\0");
243+
const partial = commits.pop();
244+
stdoutBuffer = partial;
245+
commits.forEach((commit) => {
246+
const [dirtyHash, files] = commit.split("\n");
247+
// necessary for commits following those with no changes:
248+
const hash = dirtyHash.replace(/\0/g, "");
249+
commitFiles.set(hash, files ? files.split("\0") : []);
250+
});
251+
});
252+
253+
let stderr = "";
254+
255+
git.stderr.on("data", (data) => {
256+
stderr += data.toString();
257+
});
258+
259+
git.on("close", (code) => {
260+
commitFilesOldest = commitHash;
261+
code ? reject(new GitError(stderr)) : resolve(null);
262+
});
263+
});
264+
}
265+
266+
async function getCommitBehindFromLatest(
267+
fileFolder: string,
268+
parentFilePath: string,
269+
commitHash: string
270+
): Promise<number> {
271+
if (sourceCommitCache.has(fileFolder)) {
272+
return sourceCommitCache.get(fileFolder) as number;
273+
}
274+
275+
try {
276+
let count = 0;
277+
if (!commitFiles.has(commitHash)) {
278+
await fillMemStore(commitHash);
279+
}
280+
for (const [hash, files] of commitFiles.entries()) {
281+
if (hash === commitHash) {
282+
if (!files.includes(parentFilePath)) {
283+
recordInvalidSourceCommit(
284+
fileFolder,
285+
commitHash,
286+
"file isn't changed in this commit"
287+
);
288+
}
289+
break;
290+
}
291+
if (files.includes(parentFilePath)) count++;
292+
}
293+
sourceCommitCache.set(fileFolder, count);
294+
} catch (err) {
295+
if (err instanceof GitError) {
296+
recordInvalidSourceCommit(fileFolder, commitHash, err.message);
297+
} else {
298+
throw err;
299+
}
300+
}
301+
302+
return sourceCommitCache.get(fileFolder) as number;
303+
}
304+
305+
async function packageEdits(document, parentDocument) {
306+
const {
307+
fileInfo: { root: fileRoot, folder: fileFolder },
308+
metadata: { hash: fileHash, modified, l10n },
309+
} = document;
310+
const {
311+
fileInfo: { root: parentFileRoot, path: parentFilePath },
312+
metadata: { hash: parentFileHash, parentModified },
313+
} = parentDocument;
314+
315+
const commitURL = getLastCommitURL(fileRoot, fileHash);
316+
const parentCommitURL = getLastCommitURL(parentFileRoot, parentFileHash);
317+
let sourceCommitURL;
318+
let sourceCommitsBehindCount;
319+
320+
if (l10n?.sourceCommit) {
321+
sourceCommitURL = getLastCommitURL(CONTENT_ROOT, l10n.sourceCommit);
322+
sourceCommitsBehindCount = await getCommitBehindFromLatest(
323+
fileFolder,
324+
parentFilePath.replace(parentFileRoot, "files"),
325+
l10n.sourceCommit
326+
);
327+
}
328+
155329
return {
156330
commitURL,
157331
parentCommitURL,
158332
modified,
159333
parentModified,
334+
sourceCommitURL,
335+
sourceCommitsBehindCount,
160336
};
161337
}
162338

163339
// We can't just open the `index.json` and return it like that in the XHR
164340
// payload. It's too much stuff and some values need to be repackaged/
165341
// serialized or some other transformation computation.
166-
function packageDocument(document, englishDocument, translationDifferences) {
342+
async function packageDocument(
343+
document,
344+
englishDocument,
345+
translationDifferences
346+
) {
167347
const mdn_url = document.url;
168348
const { title } = document.metadata;
169349
const popularity = packagePopularity(document, englishDocument);
170350
const differences = packageTranslationDifferences(translationDifferences);
171-
const edits = packageEdits(document, englishDocument);
351+
const edits = await packageEdits(document, englishDocument);
172352
return { popularity, differences, edits, mdn_url, title };
173353
}
174354

@@ -191,7 +371,7 @@ function getDocument(filePath) {
191371
)) {
192372
differences.push(difference);
193373
}
194-
return packageDocument(document, englishDocument, differences);
374+
return await packageDocument(document, englishDocument, differences);
195375
}
196376

197377
const _defaultLocaleDocumentsCache = new Map();

0 commit comments

Comments
 (0)