Easier merges on lockfiles

Easier merges on lockfiles

Lockfiles are a strange case of code which is checked into your repository, but not really editable. When you change your third-party dependencies, you'll typically be forced to update a corresponding lockfile at the same time. What happens when you rebase your changes on upstream, and someone else also updated the lockfile? Merge conflicts! Yuck!

Resolving a merge conflict in some generated file is annoying. Do you always accept your changes? Always accept the "incoming"? Some package managers know how to resolve merge conflicts on their own, if you remember this feature then your happy path is just to run the package manager again (e.g. pnpm install) and then the file is fixed. But not all engineers know that this works, and not all package managers know to do the resolution. (For example, Bazel's bzlmod, see https://github.com/bazelbuild/bazel/issues/20272#issuecomment-1819889397)

Git already has a way to improve the situation though. The .gitattributes file has a bunch of helpful bits you should know about, such as linguist-generated=true (mark some files as generated so GitHub doesn't show diffs in their content) and export-ignore (omit some files when packaging up an archive of the repo). Let's look at another one: merge=

Git has a bunch of strategies for merging file contents, called "drivers". You can register some of these in your personal git configuration, see https://git-scm.com/docs/merge-config. The driver is a program that is expected to perform the merge. Again from the docs:

a command to run to merge ancestor’s version (%O), current version (%A) and the other branches' version (%B)
The merge driver is expected to leave the result of the merge in the file named with %A by overwriting it, and exit with zero status if it managed to merge them cleanly, or non-zero if there were conflicts.

Let's say we want to take the "current version" (whatever we have in our local source tree) and make that the merge result. It's already the %A file, so there's nothing to do. All we have to do is "exit with zero status" and that's what the true builtin does. So while the merge driver could be a complex topic, there's a trivial way to define one that always takes "our" file as the merge result:

git config --global merge.ours.driver true

Now that you've got that in your git configuration (and every other developer on your team does as well...) you can specify that the lockfiles in your repo are meant to always use this merge driver, by adding to your .gitattributes file like

path/to/LOCKFILE merge=ours

Now you shouldn't be bothered with merge conflicts like the default driver would report.