Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bot): auto update links of moved files #31091

Merged
merged 2 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Create Markdownlint auto-fix PR
name: Create content auto-fix PR

on:
schedule:
Expand Down Expand Up @@ -31,14 +31,15 @@ jobs:
yarn content fix-flaws
yarn fix:md
yarn fix:fm
node scripts/update-moved-file-links.js

- name: Create PR with only fixable issues
if: success()
uses: peter-evans/create-pull-request@v5
with:
commit-message: "chore: auto-fix Markdownlint issues"
commit-message: "chore: auto-fix Markdownlint, Prettier, front-matter, redirects issues"
branch: markdownlint-auto-cleanup
title: "Markdownlint auto-cleanup"
title: "fix: auto-cleanup by bot"
author: mdn-bot <108879845+mdn-bot@users.noreply.github.com>
body: |
All issues auto-fixed
Expand All @@ -50,7 +51,7 @@ jobs:
with:
commit-message: "chore: auto-fix Markdownlint issues"
branch: markdownlint-auto-cleanup
title: "Markdownlint auto-cleanup"
title: "fix: auto-cleanup by bot"
author: mdn-bot <108879845+mdn-bot@users.noreply.github.com>
body: |
Auto-fix was run, but additional issues found.
Expand Down
138 changes: 138 additions & 0 deletions scripts/update-moved-file-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import fs from "node:fs/promises";
import path from "node:path";
import { execGit, getRootDir, walkSync, isImagePath } from "./utils.js";

const SLUG_RX = /(?<=\nslug: ).*?$/gm;
const HELP_MSG =
"Usage:\n\t" +
"node scripts/update-moved-file-links.js\n\t" +
"node scripts/update-moved-file-links.js [movedFromPath] [movedToPath]\n";

/**
* Try to get slug for an image from file path
*/
export async function getImageSlug(imagePath, root) {
const nodePath = path.parse(imagePath);
const absolutePath = `${root}/files/en-us/${nodePath.dir}/index.md`;
let content;
try {
content = await fs.readFile(absolutePath, "utf-8");
} catch (e) {}

if (content) {
return `/en-US/docs/${(content.match(SLUG_RX) || [])[0]}/${nodePath.base}`;
} else {
return `/en-US/docs/${imagePath}`;
}
}

let movedFiles = [];
const rootDir = getRootDir();
const argLength = process.argv.length;

if (process.argv[2] === "--help" || process.argv[2] === "-h") {
console.error(HELP_MSG);
process.exit(0);
} else if (argLength === 2 && argLength > 3) {
console.error(HELP_MSG);
process.exit(1);
} else if (argLength === 3) {
movedFiles.push({ from: process.argv[2], to: process.argv[3] });
} else {
// git log --name-status --pretty=format:"" --since "1 day ago" --diff-filter=R
let result = execGit(
[
"log",
"--name-status",
"--pretty=format:",
'--since="1 day ago"',
"--diff-filter=R",
],
{ cwd: "." },
);

if (result.trim()) {
movedFiles.push(
...result
.split("\n")
.filter((line) => line.trim() !== "" && line.includes("files/en-us"))
.map((line) => line.replaceAll(/files\/en-us\/|\/index.md/gm, ""))
.map((line) => line.split(/\s/))
.map((tuple) => {
return { from: tuple[1], to: tuple[2] };
}),
);
}
}

if (movedFiles.length < 1) {
console.log("No content files were moved. Nothing to update! 🎉");
process.exit(0);
}

const redirectsText = await fs.readFile(
`${rootDir}/files/en-us/_redirects.txt`,
"utf-8",
);

// convert file paths to slugs
movedFiles = (
await Promise.all(
movedFiles.map(async (tuple) => {
const movedLineRg = new RegExp(`\n.*?${tuple.from}\\s+.*?\n`, "gmi");
const redirectLine = (redirectsText.match(movedLineRg) || [])[0];

if (redirectLine) {
const urls = redirectLine.trim().split(/\s+/);
return { from: urls[0], to: urls[1] };
}

if (isImagePath(tuple.from)) {
return {
from: await getImageSlug(tuple.from, rootDir),
to: await getImageSlug(tuple.to, rootDir),
};
}

console.warn("No redirect entry found for: ", tuple.from);
}),
)
).filter((e) => !!e);

console.log(`Number of moved files to consider: ${movedFiles.length}`);

let totalNo = 0;
let updatedNo = 0;
for await (const filePath of walkSync(getRootDir())) {
if (filePath.endsWith("index.md")) {
try {
totalNo++;
const content = await fs.readFile(filePath, "utf-8");
let updated = new String(content);
for (const moved of movedFiles) {
// [text](link)
updated = updated.replaceAll(`${moved.from})`, `${moved.to})`);
// <link>
updated = updated.replaceAll(`${moved.from}>`, `${moved.to}>`);
// [text](link#)
updated = updated.replaceAll(`${moved.from}#`, `${moved.to}#`);
// [text](link "tool tip")
updated = updated.replaceAll(`${moved.from} `, `${moved.to} `);
// <a href="link">
updated = updated.replaceAll(`${moved.from}"`, `${moved.to}"`);
// <a href='link'>
updated = updated.replaceAll(`${moved.from}'`, `${moved.to}'`);
}

if (content !== updated) {
updatedNo++;
await fs.writeFile(filePath, updated);
}
} catch (e) {
console.error(`Error processing ${filePath}: ${e.message}`);
throw e;
}
}
}

console.log(`Updated moved file links in ${updatedNo}/${totalNo} files.`);
51 changes: 51 additions & 0 deletions scripts/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from "node:fs/promises";
import path from "node:path";
import childProcess from "node:child_process";

const IMG_RX = /(\.png|\.jpg|\.svg|\.gif)$/gim;

export async function* walkSync(dir) {
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
yield* walkSync(path.join(dir, file.name));
} else {
yield path.join(dir, file.name);
}
}
}

export function execGit(args, opts = {}, root = null) {
const gitRoot = root || getRootDir();
const { status, error, stdout, stderr } = childProcess.spawnSync(
"git",
args,
{
cwd: gitRoot,
// Default is 1MB
maxBuffer: 1024 * 1024 * 100, // 100MB
},
);
if (error || status !== 0) {
if (stderr) {
console.log(args);
console.log(`Error running git ${args}`);
console.error(stderr);
}
if (error) {
throw error;
}
throw new Error(
`git command failed: ${stderr.toString() || stdout.toString()}`,
);
}
return stdout.toString().trim();
}

export function getRootDir() {
return execGit(["rev-parse", "--show-toplevel"], {}, process.cwd());
}

export function isImagePath(path) {
return IMG_RX.test(path);
}
Loading