Files
Janka UrygaandGitHub 7446af4fce add git merge driver for errors.json (#80722)
Adds a merge driver that can automatically re-number messages added to
`errors.json` if a merge conflict occurs. Now, it would be easier to
just run `pnpm update-error-codes`, but that's slow and requires
installing deps + building everything, so i'm just manually merging +
renumbering instead.

To avoid disrupting people's workflows (in case this is buggy, or
something), it's currently *not enabled by default*. To enable the merge
driver, run:
```shell
scripts/merge-errors-json/install
```
which will add it to your `.git/config`. Once it's been confirmed to
work well, we can make this command run by default in `postinstall`.

### Testing

Other than the test i added (which doesn't run git), i've tested this
manually with this script that rebases some stacked branches:

<details>
<summary>merge-driver-test.sh</summary>

```bash
#!/usr/bin/env bash
set -eu -o pipefail

cd "$1"
current_branch="$(git branch --show-current)"

(
  set -eu -o pipefail
  
  git switch lubieowoce/errors-json-merge-driver

  git switch --force-create 'test/errors-json/base'
  echo '{
    "1": "base message"
  }
  ' > packages/next/errors.json
  git commit -am 'base commit'


  git switch --force-create 'test/errors-json/one';
  echo '{
    "1": "base message",
    "2": "message from branch one"
  }
  ' > packages/next/errors.json
  git commit -am 'commit 1'


  git switch 'test/errors-json/base'

  git switch --force-create 'test/errors-json/stacked-1'
  echo '{
    "1": "base message",
    "2": "message from stacked 1"
  }
  ' > packages/next/errors.json
  git commit -am 'stacked 1'


  git switch --force-create 'test/errors-json/stacked-2'
  echo '{
    "1": "base message",
    "2": "message from stacked 1",
    "3": "message from stacked 2"
  }
  ' > packages/next/errors.json
  git commit -am 'stacked 2'

  git rebase 'test/errors-json/one' --update-refs || true
  bat packages/next/errors.json
  git rebase --abort || true
)

git switch "$current_branch"
```
</details>

The script does a bunch of git operations, so it's easiest to put it
outside of your next.js repo and call `bash merge-driver-test.sh
path/to/next.js`.
2025-06-20 17:56:28 +02:00

125 lines
3.1 KiB
TypeScript

import { mkdtempSync, rmSync } from 'fs'
import { writeJSONSync, readJSONSync } from 'fs-extra'
import { join } from 'path'
import { tmpdir } from 'os'
import execa from 'execa'
describe('merge-errors-json driver', () => {
it('should correctly merge errors.json files with conflicting new entries', async () => {
const base = {
'1': 'Original message 1',
'2': 'Original message 2',
}
const current = {
...base,
'3': 'From a conflicting branch that was merged first 1',
'4': 'From a conflicting branch that was merged first 2',
}
const rebased = await simulateRebase({
base,
current,
incoming: {
...base,
'3': 'From our branch 1',
'4': 'From our branch 2',
},
})
expect(rebased).toEqual({
...current,
'5': 'From our branch 1',
'6': 'From our branch 2',
})
})
it('should handle empty base file', async () => {
const base = {}
const current = {
'1': 'Message from a conflicting branch that was merged first',
}
const rebased = await simulateRebase({
base,
current,
incoming: { '1': 'New message from our branch' },
})
expect(rebased).toEqual({
...current,
'2': 'New message from our branch',
})
})
it('should handle stacked PRs', async () => {
const base = {
'1': 'Original message 1',
'2': 'Original message 2',
}
const current = {
...base,
'3': 'Message from a conflicting branch that was merged first',
}
const ourBranch1 = {
...base,
'3': 'New message from our-branch-1',
}
const ourBranch2 = {
...ourBranch1,
'4': 'New message from our-branch-2', // will conflict after our-branch-1 is rebased and its error is renumbered to 4
}
const branch1Rebased = await simulateRebase({
base: base,
current: current,
incoming: ourBranch1,
})
expect(branch1Rebased).toEqual({
...current,
'4': 'New message from our-branch-1',
})
const branch2Rebased = await simulateRebase({
base: ourBranch1,
current: branch1Rebased,
incoming: ourBranch2,
})
expect(branch2Rebased).toEqual({
...branch1Rebased,
'5': 'New message from our-branch-2',
})
})
})
type Contents = Record<string, string>
async function simulateRebase({
base,
current,
incoming,
}: {
base: Contents
current: Contents
incoming: Contents
}) {
const tempDir = mkdtempSync(join(tmpdir(), 'merge-driver-test-'))
const paths = {
base: join(tempDir, 'base.json'),
current: join(tempDir, 'current.json'),
other: join(tempDir, 'other.json'),
}
writeJSONSync(paths.base, base, { spaces: 2 })
// during a rebase, the branch we're rebasing onto will be "current", and our commit will be "other"
writeJSONSync(paths.current, current, { spaces: 2 })
writeJSONSync(paths.other, incoming, { spaces: 2 })
await execa('node', [
'scripts/merge-errors-json/merge.mjs',
paths.current,
paths.base,
paths.other,
])
const result = readJSONSync(paths.current)
rmSync(tempDir, { recursive: true, force: true })
return result
}