Large-scale code changes: how to do them safely
How to make large-scale code changes across every repo: find every occurrence, apply changes at scale, and track each one to merge with Batch Changes.

How to make large-scale code changes across every repo: find every occurrence, apply changes at scale, and track each one to merge with Batch Changes.
You found the deprecated function. It lives in 1,400 files across 80 repositories, and three of those repos belong to teams you've never met. The change itself is a two-line edit. The hard part is everything around it: finding every call site, opening a pull request in every repo, and then tracking 80 separate reviews until the last one merges. That coordination problem is what makes a large-scale code change different from a normal one, and it's where most teams stall.
This guide is for platform and DevEx engineers who already know how to write a codemod and are stuck on the part that comes after: doing it everywhere, safely, and proving you didn't miss anything. We'll cover what counts as a large-scale change, why they stall, the process the largest engineering orgs use, and how to run the whole loop with Sourcegraph.
The term has a precise meaning, and it isn't "a big diff." Google's engineering teams, who run more of these than almost anyone else, define it by a property of the change rather than by its raw size. In Software Engineering at Google, the rule is that an LSC is "any set of changes that are logically related but cannot practically be submitted as a single atomic unit" (Software Engineering at Google, Ch. 22). The change is one logical idea. The delivery has many commits because a single commit won't work.
Why won't a single commit work? Two reasons, and both are mechanical. The change might touch so many files that the underlying tooling can't commit them all at once, or it's large enough that it would always hit merge conflicts. Most version control systems have operations that scale linearly with change size, so a system that handles tens of files fine "might not have sufficient memory or processing power to atomically commit thousands of files at once."
Different orgs draw the line in different places, but they all draw one. Chromium's public process doc puts the threshold at roughly "changes covering more than 10 distinct directories with OWNERS files" (Chromium LSC docs). Google's rule of thumb is edit count: "if a change requires more than 500 edits, it's usually more efficient for an engineer to learn and execute our change-generation tools rather than manually execute that edit."
The common thread is reach, not complexity. A one-character change becomes a large-scale code change the moment it has to land in 200 repositories at the same level of confidence as one.
Most large-scale code changes don't die because the transform is hard. They die in the coordination layer. Four specific frictions recur, and naming them is the first step in designing around them.
Atomicity is the root constraint. You can't ship the change as one reviewable, revertible unit, so you're forced to split it. As the file count climbs, "the probability of encountering a merge conflict also grows and is compounded by the number of engineers working in the repository." The bigger the change, the more it collides with everyone else's work while it's in flight.
Visibility is the part teams underestimate. Before you change anything, you need a complete and trustworthy list of where the old pattern lives. Grep across cloned repos breaks down the moment multiple teams work on multiple branches, and a missed call site means the migration quietly stays incomplete. If you can't prove the list is complete, you can't prove the change is done.
Consistency degrades across repos. When 80 pull requests are authored by hand or by 80 separate bot runs, they drift. One half-applies the pattern, one carries a typo in the commit message, and the change that was supposed to be uniform becomes 80 slightly different changes.
Tracking is where momentum dies. Splitting the change is the right move, but it multiplies the bookkeeping. The canonical advice is explicit: the most important LSC infrastructure is "the set of tooling that shards a master change into smaller pieces and manages the process of testing, mailing, reviewing, and committing them independently." Without that management layer, a 200-changeset rollout becomes 200 browser tabs and a spreadsheet that's wrong by Wednesday.
Hold onto that last point. The transform is a solved problem; codemods have existed for years. The unsolved problem for most teams is to enumerate, apply, and track at scale, and that's the lens for everything below.
Strip away the tooling, and every large-scale change runs the same four-step loop: find every occurrence, generate the change, get it reviewed, and merge it everywhere. The orgs that do this well industrialize each step instead of doing it by hand.
Google formalizes this into a process that "roughly breaks down into four phases... 1. Authorization 2. Change creation 3. Shard management 4. Cleanup." The shard-management phase is the heart of it. Their tooling "takes a large change and shards it based upon project boundaries and ownership rules into changes that can be submitted atomically. It then puts each individually sharded change through an independent test-mail-submit pipeline."
The scale this enables is the reason the model matters. The migration from scoped_ptr to std::unique_ptr touched "more than 500,000 references... scattered among millions of source files" and was the largest such change Google had attempted at the time (Software Engineering at Google). No human merges half a million edits by hand; you enumerate, generate, shard, and track.
There's a governance step baked in, and it's worth getting right. Code owners are expected to understand the changes happening to their code, but they "don't hold a veto over the broader LSC." That balance keeps a migration from being blocked forever by one unresponsive team while still giving each owner a real review of their slice.
Here's the model to carry forward. Splitting a large change into smaller independent chunks "gets around these limitations, although it makes the execution of the change more complex." So the job of any good large-scale change tool is to make that added execution complexity disappear: keep the change as one declarative idea, fan it out into independently reviewable pieces, and give you one place to watch them all land.
This is exactly the loop we built Batch Changes around, and it maps cleanly onto find, change, and track. Batch Changes lets you "change code everywhere with a single declarative file" and then "track and manage all of your changesets" through checks and code reviews until each change is merged. The point is that the declarative file stays the single source of truth while the rollout fans out underneath it.
Step one is to find, and it's where completeness gets decided. Code Search runs literal, keyword, and regex queries across your codebase "whether 100 or 1M+ repositories," with navigation "powered by SCIP-based semantic analysis." It indexes GitHub, GitLab, Bitbucket, Gerrit, Perforce, and more and resolves symbol definitions across repositories. That cross-repo enumeration is what turns "we think it's in about 80 repos" into a list you can stand behind.
Step two is change, expressed as a batch spec. A batch spec is "a YAML file that defines a batch change... which changes should be made in which repositories." You match repos with a Sourcegraph search query, and "each matched repository is added to the list of repositories on which the batch change will run." Each step runs a shell command inside a Docker container you specify, so any codemod, script, or formatter you already trust drops straight in. A real version 2 spec that bumps a dependency across every matched repo looks like this:
version: 2
name: bump-internal-sdk-to-3-2-0
description: Upgrade internal-sdk from 3.1.x to 3.2.0 everywhere it's used
on:
- repositoriesMatchingQuery: file:package.json internal-sdk 3.1 patterntype:keyword
steps:
- run: ./upgrade-internal-sdk.sh
container: node:20-alpine
env:
OLD_VERSION: 3.1.7
NEW_VERSION: 3.2.0
changesetTemplate:
title: Upgrade internal-sdk to 3.2.0
body: Bumps internal-sdk from 3.1.x to 3.2.0. Generated with Batch Changes.
branch: deps/internal-sdk-3.2.0
commit:
message: Upgrade internal-sdk to 3.2.0
published: false
The published: false line matters more than it looks. Running src batch preview computes every diff but publishes "no commits, branches, or changesets... (i.e., the repositories on your code host will be untouched)" until you review the preview and click Apply. You see the full blast radius before a single pull request exists. When you do publish, each changeset "results in the creation of a merge request on your code host (e.g., a Pull Request on GitHub)," and the template defines exactly what those pull requests and merge requests look like.
Step three is track, and this is the part the per-repo tools leave to you. Because the changesets are managed objects, an unpublished one "can be previewed on Sourcegraph... but its commit, branch, and pull requests aren't created on the code host." You can roll out gradually with published: draft to open draft pull requests first. You can also import changesets that already exist on a code host by their pull request or merge request number, so PRs from other tools open and land in the same dashboard. From there, you filter for "ready to merge" versus "needs attention," bulk-rebase to stay merge-ready, and on GitHub code hosts, turn on auto-merge so each pull request lands the moment its checks pass (Bulk Operations on Changesets, docs.sourcegraph.com). We call this the "outer loop": the review, security, and rollout work that surrounds the diff.
The three approaches differ most in who owns enumeration and tracking:
| Approach | Discovery | Transform | Cross-repo tracking | Scope |
|---|---|---|---|---|
| Hand-edited PRs | Manual grep, per repo | Manual edits | Spreadsheet or browser tabs | A handful of repos |
| Per-repo codemod or bot | Per-repo config | Deterministic, runs in each repo | None (one PR per repo) | One repo at a time |
| Batch Changes | Code Search across all repos | One declarative spec with any container step | One tracked changeset view, auto-merge on GitHub | 100 to 1M+ repos |
One honest boundary: this assumes your change can be expressed as a script over matched repositories. For a monorepo, transformChanges splits one repo's diff into several changesets grouped by directory so each owner reviews their own slice. For judgment-heavy rewrites that differ per repo, you're still doing real per-repo work; what Sourcegraph removes is the enumeration and tracking overhead, not the thinking. For rewrites that need that per-repo judgment, Agentic Batch Changes puts an agent inside the same find-and-track loop.
The model above isn't theoretical. Two engineering teams ran exactly this loop at scale, and their numbers are the proof.
Workiva's Client Platform team maintains shared libraries that the rest of the company builds on, including dozens of Dart packages. Every release had to propagate across 70+ repositories to avoid breaking other teams. Their home-grown tool worked, but it "didn't provide end-to-end visibility into the path to completion." After moving the work to Batch Changes, they used it to push a new React version to every frontend repo, update Kubernetes API versions, and migrate a CDN across all code references.
"Sourcegraph Batch Changes gives us the confidence we need to understand the total impact of large-scale code changes before we make them. This enables the entire team to make more impactful decisions more often." — Director of Architecture, Workiva
The headline result: Batch Changes cut the time to make these large-scale code changes by 80% compared with making them manually (Workiva case study). As Evan Weible, Staff Software Engineer at Workiva, put it: "As long as there's a tool out there that does what you need, you can incorporate it into Batch Changes with minimal effort."
Quantcast hit the visibility problem first. Founded in 2006, the company's engineering team "had amassed thousands of repositories," which made any refactor a research project before it was a coding project. Facing a GDPR deadline, they used an org-wide regex search to find every place personal data flowed through the code.
"Since Sourcegraph searches every repository, a single engineer took only a few days to analyze thousands of them, which would have taken months if they were each examined individually." — Quantcast case study
That's the enumeration step paying off directly. Quantcast reported that the approach made large-scale refactoring "systematic, safe, and efficient... saving hundreds of developer hours without risking production stability" (Quantcast case study). The reason the search mattered so much: "Sourcegraph returns all search results; it doesn't drop or elide them..." For a migration, an incomplete list is worse than no list, because it gives you false confidence that you're done.
A large-scale change is as much a social event as a technical one. You're editing code other teams own, and how you roll it out determines whether they trust the next one. A few practices separate the migrations that finish from the ones that stall halfway.
Decide push versus pull deliberately. A push model means the platform team opens every pull request and drives them to merge. A pull model means you publish the change and ask owning teams to merge their own slice on their schedule. Push is faster for urgent security work; pull respects team autonomy for non-urgent code migrations. Batch Changes supports both because publication is per-changeset: you can publish everything at once, or use a per-repo published array to stage the rollout team by team.
Keep the diff reviewable and the owner in the loop. The governance lesson from Google holds for any org: owners should genuinely review their slice, but a single unresponsive team shouldn't be able to veto a fleet-wide change. Sharding the change into one changeset per repo (or per directory in a monorepo) makes that code review humane rather than a 1,400-file wall.
Preview before you publish, every time. The single most useful habit is to run the preview and read it. Because the spec produces no code-host changes until you Apply, the preview is a free dry run of the entire blast radius. Treat a surprising preview as a signal that your search query is wrong, not as noise to click through.
Roll out in waves for risky changes. Start with published: draft so the first batch opens as draft pull requests you can sanity-check in CI before they're review-ready. Promote to published once a representative sample passes. This is the same risk discipline behind Martin Fowler's strangler fig approach, applied to the rollout itself; for the larger modernization picture, our guide to legacy code modernization covers where this fits.
Make completeness checkable. End every migration the way Quantcast did: a saved search that should return zero results when the change is fully landed. If the query still matches, you're not done, and now you know exactly where. Recipe-based engines like OpenRewrite apply deterministic transforms well within their language ecosystems; where they stop is the cross-repo enumeration and the single-tracked view of every changeset's state, which is the gap Batch Changes is built to fill.
A large-scale code change is not a bigger version of a normal change; it's a coordination problem wearing a diff's clothing. The transform is the easy part. The work that decides whether the change actually lands everywhere is enumerating every occurrence, applying one consistent change across all of them, and tracking each pull request to merge without leaving any gaps.
That's the loop we built: Code Search to find every occurrence, Batch Changes to apply one declarative change across every repo, and a single changeset view to drive them all to merge. If you're staring at a change that has to land in dozens or hundreds of repositories, start by writing the search query that shows how big it really is, then run a single coordinated rollout instead of 200 manual ones.
Is a large-scale code change just a big pull request? No. The defining trait is that the change can't practically ship as one atomic commit, usually because it touches too many files for the version control system to commit at once or would constantly hit merge conflicts. A large diff in one repo is just a large diff; an LSC is one logical change delivered as many independently reviewable, independently mergeable pieces.
How do I track hundreds of pull requests across repos without losing the thread? Use a system that treats each pull request as a managed changeset rather than a browser tab. With Batch Changes, you filter changesets by state ("ready to merge" versus "needs attention"), bulk-rebase to keep them merge-ready, and on GitHub code hosts, enable auto-merge so each one lands when its checks pass. You can also import pull requests that other tools have already opened by their PR or MR number, so everything appears in a single view.
Does this work for a monorepo, or only for many separate repos? Both. For separate repos, a search query matches the set, and the change fans out across them. For a monorepo, transformChanges splits one repo's diff into multiple changesets grouped by directory, so each code owner reviews only their slice.
Do I need an approval committee like Google's? Not at most companies' scale. Google's authorization phase exists because its LSCs touch hundreds of thousands of references at once. The portable lesson is lighter: get genuine reviews from each affected owner, but design the rollout so one unresponsive team can't block a fleet-wide change indefinitely.
Can an AI agent run the large-scale change for me? An agent can draft the transform and the search query, but the safety still comes from the same loop: enumerate completely, preview the full blast radius before anything publishes, and track every changeset to merge. Our role is the deterministic find-and-track layer underneath the agent, so the change is grounded in a complete list rather than a confident guess. Agentic Batch Changes packages exactly that pairing: an agent drafts and applies the change while the same enumeration, preview, and changeset tracking keep it honest.

With Sourcegraph, the code understanding platform for enterprise.
Schedule a demo