diff --git a/tools/pipelines/localization-update.yml b/tools/pipelines/localization-update.yml index 704ae14876e47..8849d85eaa0ff 100644 --- a/tools/pipelines/localization-update.yml +++ b/tools/pipelines/localization-update.yml @@ -26,7 +26,7 @@ jobs: steps: - checkout: self clean: true - fetchDepth: 1 + fetchDepth: 0 fetchTags: false submodules: false @@ -207,16 +207,27 @@ jobs: git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $basicAuth" git remote set-url origin https://github.com/microsoft/TypeScript.git + $existingPr = gh pr list --repo microsoft/TypeScript --base main --head $branch --state open --json url --jq '.[0].url // empty' $remoteBranch = git ls-remote --heads origin $branch $branchExists = [bool] $remoteBranch + $remoteCommit = "" if ($branchExists) { - git fetch --depth=1 --no-tags --progress origin "refs/heads/${branch}:refs/remotes/origin/${branch}" + git fetch --no-tags --progress origin "refs/heads/${branch}:refs/remotes/origin/${branch}" + $remoteCommit = git rev-parse "origin/${branch}" + } + + $candidate = git rev-parse HEAD + if ($existingPr) { + if (!$branchExists) { + throw "The open localization pull request no longer has a remote branch." + } + $candidate = node tools/scripts/preserve-localization-base.mjs $remoteCommit } $shouldPush = $true if ($branchExists) { - $localTree = git rev-parse 'HEAD^{tree}' - $remoteTree = git rev-parse "origin/${branch}^{tree}" + $localTree = git rev-parse "${candidate}^{tree}" + $remoteTree = git rev-parse "${remoteCommit}^{tree}" if ($localTree -eq $remoteTree) { Write-Host "Remote branch already contains the same files." $shouldPush = $false @@ -224,10 +235,9 @@ jobs: } if ($shouldPush) { - git push --force-with-lease --set-upstream origin $branch + git push "--force-with-lease=refs/heads/${branch}:${remoteCommit}" origin "${candidate}:refs/heads/${branch}" } - $existingPr = gh pr list --repo microsoft/TypeScript --base main --head $branch --state open --json url --jq '.[0].url // empty' if ($existingPr) { Write-Host "Updated existing pull request: $existingPr" } diff --git a/tools/scripts/preserve-localization-base.mjs b/tools/scripts/preserve-localization-base.mjs new file mode 100644 index 0000000000000..c1679c0061369 --- /dev/null +++ b/tools/scripts/preserve-localization-base.mjs @@ -0,0 +1,48 @@ +import { + execFileSync, + spawnSync, +} from "node:child_process"; + +const [previous] = process.argv.slice(2); +if (!previous || process.argv.length !== 3) { + throw new Error("Usage: node tools/scripts/preserve-localization-base.mjs "); +} + +console.log(prepareUpdate()); + +function prepareUpdate() { + const generated = git("rev-parse", "HEAD"); + const main = git("rev-parse", `${generated}^`); + const previousHead = git("rev-parse", "--verify", `${previous}^{commit}`); + const base = git("merge-base", main, previousHead); + if (base === main) return generated; + + // Apply only the generated delta to the old base, without changing the checkout. + const tree = mergeTree(`--merge-base=${main}`, base, generated); + if (!tree) { + console.error("Localization changes conflict with the existing PR base; using current main."); + return generated; + } + + const candidate = git("commit-tree", tree, "-p", base, "-m", "Update localization files"); + if (mergeTree(main, candidate) !== git("rev-parse", `${generated}^{tree}`)) { + console.error("Preserving the PR base would conflict or change the generated merge result; using current main."); + return generated; + } + + return tree === git("rev-parse", `${previousHead}^{tree}`) ? previousHead : candidate; +} + +function git(...args) { + return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); +} + +function mergeTree(...args) { + const result = spawnSync("git", ["merge-tree", "--write-tree", ...args], { encoding: "utf8" }); + if (result.error) throw result.error; + if (result.status === 1) return undefined; + if (result.status !== 0) { + throw new Error(`git merge-tree failed (${result.status}): ${result.stderr}`); + } + return result.stdout.trim().split(/\r?\n/, 1)[0]; +}