{"id":3211,"date":"2026-01-12T17:12:33","date_gmt":"2026-01-12T17:12:33","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/01\/12\/how-we-synchronize-nets-virtual-monorepo\/"},"modified":"2026-01-12T17:12:33","modified_gmt":"2026-01-12T17:12:33","slug":"how-we-synchronize-nets-virtual-monorepo","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/01\/12\/how-we-synchronize-nets-virtual-monorepo\/","title":{"rendered":"How We Synchronize .NET\u2019s Virtual Monorepo"},"content":{"rendered":"<p>In our previous post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/reinventing-how-dotnet-builds-and-ships-again\/\">\u201cReinventing how .NET Builds and Ships\u201d<\/a>, Matt covered our recent overhaul of .NET\u2019s building and shipping processes.<br \/>\nA key part of this multi-year effort, which we called <em>Unified Build<\/em>, is the introduction of the Virtual Monolithic Repository (VMR) that aggregates all the source code and infrastructure needed to build the .NET SDK.<br \/>\nThis article focuses on the monorepo itself: how it was created and the technical details of the two-way synchronization that keeps it alive.<\/p>\n<p>Up until recently, the .NET SDK has been built from an aggregation of build artifacts of dozens of repositories.<br \/>\nThese artifacts flowed down the repository tree where they were stitched together at the end to produce the final .NET SDK.<br \/>\nThis approach has served us well for many years, but it has also introduced significant complexity and maintenance overhead.<br \/>\nSince <em>.NET 10 Preview 4<\/em>, we have instead been building the .NET SDK from a single commit of a monorepo.<\/p>\n<h2>What is The Virtual Monolithic Repository<\/h2>\n<p>The Virtual Monolithic Repository (VMR) is a single git repository that includes all the source code and infrastructure needed to build the .NET SDK.<br \/>\nYou can find the VMR on GitHub at <a href=\"https:\/\/github.com\/dotnet\/dotnet\">dotnet\/dotnet<\/a>.<br \/>\nIn reality, it is mostly an aggregation of several dozen other standalone repositories (such as <a href=\"https:\/\/github.com\/dotnet\/runtime\">dotnet\/runtime<\/a> or <a href=\"https:\/\/github.com\/dotnet\/sdk\">dotnet\/sdk<\/a>), which we call \u201c<strong>product repositories<\/strong>\u201c.<br \/>\nOn top of that, it contains additional sources such as the build infrastructure, pipeline definitions and scripts needed to build it.<\/p>\n<p>The product repositories still exist separately and are synchronized with their counterparts as <a href=\"https:\/\/github.com\/dotnet\/dotnet\/tree\/main\/src\">subdirectories of the VMR<\/a>.<br \/>\nThis is where the <em>virtual<\/em> part comes from.<br \/>\nChanges can be made either in the product repositories or directly in the VMR.<br \/>\nOur infrastructure then keeps these two sides synchronized by creating pull requests that carry the source changes between these two sides.<\/p>\n<h2>The Road to the VMR<\/h2>\n<p>Having a two-way synchronized monorepo was always a necessary cornerstone of the Unified Build project.<br \/>\nReaching this point was, however, a multi-stage journey during which we had to keep shipping.<\/p>\n<h3>Stage #1 \u2013 Source Build Tarball<\/h3>\n<p>This journey began during the .NET 6 timeframe when we were heavily investing in our ability to make .NET available in various Linux distributions such as Ubuntu, Fedora, Debian, and package managers like Homebrew.<br \/>\nTo achieve this, we had to comply with the rules of the maintainers of these distributions.<br \/>\nThese tend to boil down to:<\/p>\n<ul>\n<li>Source code for everything, no binaries allowed<\/li>\n<li>Limited or no network access<\/li>\n<\/ul>\n<p>In other words, we had to be able to hand the maintainers a set of non-binary source files which had to compile into the .NET SDK without downloading anything from the internet.<br \/>\nWe refer to this process as the <strong>Source Build<\/strong>.<\/p>\n<p>The <em>Source Build<\/em> methodology differs from how we used to build the .NET SDK for our own releases which, before the VMR, were built from a gradual flow of build artifacts through a dependency tree of dozens of repositories.<br \/>\nBoth processes shared the same need \u2013 the dependency flow must reach the final repository (originally <code>dotnet\/installer<\/code>, later <code>dotnet\/sdk<\/code>).<br \/>\nThen, you either collect the binaries or the sources behind these binaries and feed them into your final build.<\/p>\n<p>The first iteration of Source Build would walk the commits of each repository in the tree, add the Source Build infrastructure (the logic behind the Source Build) and produce a tarball archive on-the-fly.<br \/>\nThis archive was then given to the 3rd party maintainers who built it on their systems and checked the produced packages into their package repositories.<\/p>\n<h4>Source Build Patches<\/h4>\n<p>Often, we would see that the collected sources would not successfully build from source.<br \/>\nThe build methodologies differ, and it was often too complex and expensive to discover breaks before product dependency flow completed. Sometimes this even uncovered an existing integration issue before shipping.<br \/>\nWhen a Source Build break happened, a fix was to be made in one of the product repositories and propagated down the dependency tree again.<br \/>\nThis was a tedious, lengthy, costly, and error-prone process.<\/p>\n<p>To alleviate the pain, we allowed checking in so-called \u201c<strong>Source Build patches<\/strong>\u201d into the last repository.<br \/>\nThese additional patches with fixes would be applied on top of the collected sources.<br \/>\nThen we\u2019d work the patch into the upstream original repository where the patched sources came from.<br \/>\nOnce the fixed sources flowed down the tree again, the patch could be removed \u2013 it would fail to apply on top of the collected sources since they would contain this change already at that point.<\/p>\n<h3>Stage #2 \u2013 VMR-lite<\/h3>\n<p>To make our first significant step towards full VMR code flow, we needed to move away from the tarball based approach and into a dedicated git repository.<br \/>\nThe contents would be the same as our tarball, but moving to git would involve investment in code and change management necessary for the end Unified Build VMR.<br \/>\nIn October 2022, the original <a href=\"https:\/\/github.com\/dotnet\/dotnet\">dotnet\/dotnet<\/a> repository was created.<br \/>\nIt was codenamed \u201c<strong>VMR-lite<\/strong>\u201d and was a read-only mirror (a projection) of the sources of the product repositories.<\/p>\n<p>Each time we\u2019d merge a commit into the SDK repository, a one-way synchronization pipeline would be triggered.<br \/>\nIt walked the dependency tree, collected the commits behind all dependencies, and updated the corresponding subdirectories in the VMR.<\/p>\n<p><img data-opt-id=1216721848  fetchpriority=\"high\" decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/one-way-synchronization.webp\" alt=\"One-way synchronization diagram\" \/><br \/>\n<em>A simplified diagram showing the one-way synchronization process from product repositories into the VMR-lite.<\/em><\/p>\n<p>Undesired files such as binaries forbidden by Source Build rules were excluded during this process.<br \/>\nThe Source Build patches would be applied too.<\/p>\n<p>The VMR-lite became the release vehicle for Linux distro Source Build <code>linux-x64<\/code> starting with <em>.NET 8 Preview 1<\/em> and continues to be used for .NET 8 and .NET 9 servicing to this day.<\/p>\n<h3>Stage #3 \u2013 Writeable VMR<\/h3>\n<p>Moving the Source Build development process onto the VMR was an important milestone and improved the workflow greatly.<br \/>\nAnalyzing Source Build breaks became much easier with VMR\u2019s commit history.<br \/>\nOther benefits also became apparent when we plugged the VMR into our compliance and security scanning infrastructure.<br \/>\nHowever, we were aiming much higher.<br \/>\nThe ultimate goal was to unify our binary-oriented and source-based build methodologies and use the VMR as <em>the<\/em> place we can develop in and ship from all our .NET SDK builds.<\/p>\n<p>There were two missing pieces in this picture. First, we must make the VMR writable. This also entails the ability to flow changes back into the product repositories.<br \/>\nAnd second, the sources stop coming through the tip of the dependency tree. Instead each product repository would be synchronized directly with the VMR in a \u201cflat\u201d code flow model.<\/p>\n<p><img data-opt-id=1948760004  fetchpriority=\"high\" decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/flat-flow-scaled.webp\" alt=\"Flat flow structure between repositories and the VMR\" \/><br \/>\n<em>An illustration of the flat code flow structure between repositories and the VMR.<\/em><\/p>\n<p>One other significant difference is in how the whole flow is realized.<br \/>\nIn VMR-lite, we have compiled the new set of sources in an Azure DevOps pipeline and pushed them into the VMR directly.<br \/>\nNow, our <a href=\"https:\/\/github.com\/dotnet\/arcade-services\">dependency flow cloud service<\/a> drives the flow by calculating the diffs and creating pull requests carrying the changes.<br \/>\nThis also allows us to run PR validation gates and make additional fixes in the PRs before merging the changes.<\/p>\n<p>With the VMR being writable, we can start easily introducing breaking changes in a particular repository, flow the change into a VMR PR and fix the dependent code of the other repositories in the very same PR.<br \/>\nThe changes of the dependent repository directories then flow back into their respective original repositories.<br \/>\nThe backflow also contains VMR-built binaries with the aforementioned breaking change which the repositories build against.<\/p>\n<p>The transition from the dependency tree to the flat flow happened with the release of <em>.NET 10 Preview 5<\/em> and the two-way synchronized full VMR started operating then.<\/p>\n<h2>VMR\u2019s Storage Model<\/h2>\n<p>The first decision to make was determining how to structure and create the VMR itself.<br \/>\nWe had a mix of requirements to consider coming both from our ability to meet the Source Build needs as well as our future plans for the two-way synchronization:<\/p>\n<ul>\n<li>To have a single, coherent commit that captures a consistent, buildable state at any point in time.<\/li>\n<li>To be able to apply the Source Build patches (a permanent delta).<\/li>\n<li>To be able to map additional paths \u2014 to project sources to other parts of the VMR such as <a href=\"https:\/\/github.com\/dotnet\/installer\/tree\/release\/8.0.1xx\/src\/SourceBuild\/content\">the content of the root directory<\/a>.<\/li>\n<li>To be able to exclude certain paths\/files \u2014 e.g. exclusion of binaries forbidden by some Linux distributions.<\/li>\n<li>To be able to make changes to the VMR so that they could flow back into the product repositories.<\/li>\n<\/ul>\n<p>We explored several ways how multiple repositories can be aggregated into one:<\/p>\n<ul>\n<li><a href=\"https:\/\/git-scm.com\/book\/en\/v2\/Git-Tools-Submodules\">Git Submodules<\/a><\/li>\n<li><a href=\"https:\/\/www.atlassian.com\/git\/tutorials\/git-subtree\">Git Subtree<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/ingydotnet\/git-subrepo\">Subrepo<\/a><\/li>\n<li>A custom process<\/li>\n<\/ul>\n<p>Upon closer investigation, none of these fulfilled all our requirements and so we decided to implement our own custom process which maintains the files in a single monorepository as checked-in copies of the original sources.<br \/>\nMore on the decision itself can be found in <a href=\"https:\/\/github.com\/dotnet\/arcade\/issues\/10257\">the original design<\/a>.<\/p>\n<h3>Dealing With Submodules<\/h3>\n<p>Some of our product repositories already contain <a href=\"https:\/\/www.git-scm.com\/book\/en\/v2\/Git-Tools-Submodules\">git submodules<\/a> and need them to successfully build.<br \/>\nSome of these are even external to the .NET Foundation.<br \/>\nTechnically, these could be kept as submodules in the VMR too.<br \/>\nHowever, this conflicts with some of the requirements and goals of the VMR:<\/p>\n<ul>\n<li>Any given commit contains all the sources needed to build the .NET SDK.<\/li>\n<li>Source Build requires the build to happen without internet connectivity to ensure no further artifacts are downloaded in the process.<\/li>\n<li>Source Build forbids non-text-based files in the VMR.<\/li>\n<li>Being a good Open-Source citizen, we would like to upstream as many changes back into the submodules as possible.<\/li>\n<li>We would like to avoid being dependent on the external submodule remote existence long-term to assure .NET servicing needs.<\/li>\n<\/ul>\n<p>The limitations above give us two options:<\/p>\n<ul>\n<li>We either fork all submodules, strip all non-text-based files and reference these forks as submodules in the VMR. We\u2019d then need to keep these forks in sync with the upstreams.<\/li>\n<li>We bring submodules into the VMR as hard copies of the sources instead of preserving them as a submodule link, stripping the binaries during this process.<\/li>\n<\/ul>\n<p>We considered both options and decided to go with the latter which means less friction when working with upstreams.<br \/>\nWithout the man-in-the-middle forks, we can consume new versions faster and make sure we contribute back to the upstream easier.<br \/>\nAt the same time, we have all the sources at hand always and using the VMR comes without all the complications connected to submodule usage.<\/p>\n<h2>Moving Changes<\/h2>\n<p>The core of the synchronization process is the ability to move files, or rather changes of files, between repositories effectively.<br \/>\nKeep in mind that changes can come in different shapes.<br \/>\nStarting with the obvious addition\/deletion and content modification, a file can also have its permissions (executable bit) modified, its encoding can change or the whole file can move.<\/p>\n<p>Early in the design process, it became clear that if we don\u2019t want to implement all the nitty-gritty of these operations ourselves, we need to delegate as much to git as possible.<br \/>\nThis implied that <strong>the main vehicle of moving changes between repositories would be good ol\u2019 patches<\/strong>, main reasons being:<\/p>\n<ul>\n<li>Patches fully encode all the different types of changes that can happen to files.<\/li>\n<li>Patches can be applied to different paths, e.g. mapping of the root directory.<\/li>\n<li>It is easy to exclude \/ include certain files or patterns when creating patches which allow us to filter out undesired files such as binaries.<\/li>\n<li>Patch application fails when there is unexpected content which ensures correctness of the process and safe guards against accidental overwrites.<\/li>\n<\/ul>\n<p>To illustrate how this works in practice, we just call<\/p>\n<pre><code class=\"language-bash\">git diff --patch --binary --relative -- &lt;ex\/inclusion patterns&gt;<\/code><\/pre>\n<p>We then take the resulting patch and apply it in the destination path by<\/p>\n<pre><code class=\"language-bash\">git apply --cached --ignore-space-change --directory=&lt;target dir&gt;<\/code><\/pre>\n<p>As discussed earlier, we keep this intentionally simple to leave as much heavy lifting connected to intricacies around file changes to git itself.<br \/>\nThis decision has proven quite robust, apart from some minor corner cases that need special handling. For example, <a href=\"https:\/\/github.blog\/open-source\/git\/highlights-from-git-2-39\/\"><code>git apply<\/code> limits the maximum size of a patch to &lt;1 GB<\/a>.<br \/>\nTo work around this limitation, we detect it and <a href=\"https:\/\/github.com\/dotnet\/arcade-services\/blob\/3c554926d94a90df967068396178e09c86e75196\/src\/Microsoft.DotNet.Darc\/DarcLib\/VirtualMonoRepo\/VmrPatchHandler.cs#L436-L464\">split the patch recursively into smaller chunks<\/a>.<\/p>\n<h2>Tracking the Sources<\/h2>\n<p>Since it was obvious that the synchronization will involve patches, the next step was figuring out how to keep track of which sources have been synchronized where.<br \/>\nTo track what is inside the VMR, we maintain a <a href=\"https:\/\/github.com\/dotnet\/dotnet\/blob\/main\/src\/source-manifest.json\">manifest file<\/a>.<br \/>\nThis file contains the commit SHAs of all product repositories that are currently synchronized in that VMR commit.<br \/>\nAdditionally, it also remembers the SHAs of the vendored submodules.<br \/>\nSimilarly, we also track the last synchronized VMR commit in the product repositories (for the purpose of the two-way synchronization).<br \/>\nFor that, we keep the SHA of the last synchronized VMR commit in the <a href=\"https:\/\/github.com\/dotnet\/arcade\/blob\/47a8a69721dfea57b82121ac1458d2f5bba6abd2\/eng\/Version.Details.xml#L3\"><code>Version.Details.xml<\/code> file<\/a> which we already use for tracking repo\u2019s dependencies.<\/p>\n<p>By git-blaming the tracking data, we can figure out which commit of the counterpart side was synchronized to the current repo when.<br \/>\nThis is enough to calculate how code flowed between the two sides over time.<br \/>\nWe use this later in the algorithm to determine the set of last flows and their directions.<br \/>\nThe why\u2019s and how\u2019s of this will be covered in the following sections.<\/p>\n<p>This decision has worked quite well for us so far but there were some challenges too.<br \/>\nTo name one, it can lead to erroneous situations when a repository decides to merge its own branches between each other and accidentally overwrites the tracking data.<br \/>\nIt can also be hard to \u201creset\u201d the tracking data when needed as changing the tracking data affects the git blame results.<br \/>\nWe are currently exploring more robust ways such as using <a href=\"https:\/\/git-scm.com\/docs\/git-notes\">git notes<\/a> to store the data outside of the main source tree.<\/p>\n<h2>One-Way Synchronization<\/h2>\n<p>As described earlier, the VMR-lite was a read-only projection of the product repositories.<br \/>\nSome content was excluded from synchronization such as binaries rejected by our Source Build partners.<br \/>\nAdditional content could be mapped to different paths such as the root directory which came from within <a href=\"https:\/\/github.com\/dotnet\/installer\/tree\/release\/8.0.1xx\/src\/SourceBuild\/content\">dotnet\/installer<\/a>.<br \/>\nLastly, the <em>Source Build patches<\/em> were applied on top of the synchronized content.<\/p>\n<p>To configure these synchronization rules, the VMR contained a <a href=\"https:\/\/github.com\/dotnet\/dotnet\/blob\/main\/src\/source-mappings.json\">configuration file<\/a> that looked something like this:<\/p>\n<pre><code class=\"language-json\">{\r\n  \/\/ Each mapping represents a product repository with its own content-exclusion rules\r\n  \"mappings\": [\r\n    {\r\n      \"name\": \"runtime\",\r\n      \"defaultRemote\": \"https:\/\/github.com\/dotnet\/runtime\",\r\n      \"exclude\": [\r\n        \"tests\/**\/*.dll\"\r\n      ]\r\n    },\r\n    {\r\n      \"name\": \"aspnetcore\",\r\n      \"defaultRemote\": \"https:\/\/github.com\/dotnet\/aspnetcore\",\r\n    },\r\n    \/\/ ...\r\n  ],\r\n  \/\/ Example of additional mapping of content to the root of the VMR\r\n  \"additionalMappings\": [\r\n    {\r\n      \"source\": \"src\/SourceBuild\/content\",\r\n      \"destination\": \"\/\"\r\n    }\r\n  ],\r\n  \/\/ Path to the directory containing Source Build patches\r\n  \"patchesPath\": \"src\/installer\/src\/SourceBuild\/patches\"\r\n}<\/code><\/pre>\n<p>The synchronization process itself is a basic building block not only for the VMR-lite but later also for the full two-way synchronization.<br \/>\nThe process is complicated by the fact it must handle changes of the submodules as well as <em>Source Build patches<\/em>.<br \/>\nIt\u2019s also important to note that the configuration file that dictates the synchronization rules can also change.<br \/>\nThis means that both Source Build patches and the additionally mapped content can change during the process, need to be correctly stripped away and then re-applied back.<\/p>\n<p>With everything we\u2019ve learned so far, we can now summarize the process in the following steps:<\/p>\n<ol>\n<li>Revert all Source Build patches applied in the VMR.<\/li>\n<li>Determine the set of commits representing the repository tree.<\/li>\n<li>For each repository that needs an update:\n<ol>\n<li>Revert additionally mapped content coming from this repository.<\/li>\n<li>Create a patch in the original repository between the last synchronized commit and the new commit:\n<ul>\n<li>The commit range equals the previously synchronized commit (from the manifest file) and the currently synchronized commit.<\/li>\n<li>Follow exclusion rules.<\/li>\n<li>Ignore submodule changes.<\/li>\n<li>Split the patch if it is too large.<\/li>\n<\/ul>\n<\/li>\n<li>Apply the patch to repo\u2019s subdirectory in the VMR.<\/li>\n<li>Check repository\u2019s submodules:\n<ol>\n<li>Create a patch for changes in each submodule following same pattern as above (recursively).<\/li>\n<li>Apply these patches to the respective submodule directories in the VMR.<\/li>\n<\/ol>\n<\/li>\n<li>Apply changes of additionally-mapped content coming from this repository.\n<ul>\n<li>We again create and apply patches for given paths and commit ranges.<\/li>\n<\/ul>\n<\/li>\n<li>Update the tracking information in the manifest file.<\/li>\n<\/ol>\n<\/li>\n<li>Apply Source Build patches on top of the synchronized content.<\/li>\n<\/ol>\n<p>Good, now we\u2019re able to move changes from a product repository into the VMR.<\/p>\n<h2>Two-way Synchronization<\/h2>\n<p>The end goal of this whole effort is the two-way synchronization between the VMR and the product repositories.<br \/>\nAs mentioned already, this would no longer be achieved by a pipeline pushing changes directly into the VMR.<br \/>\nInstead, our code flow service would create so-called <em>\u201ccode flow pull requests\u201d<\/em> carrying the changes.<br \/>\nWhen creating the pull request branches, we will stick to our weapon of choice for moving changes \u2013 patches.<\/p>\n<p>Though the basic building blocks of the process remain the same, the whole problem becomes considerably more complex.<br \/>\nOutside of making sure the right changes materialize in the right way on the other side, we must also account for the flows happening in parallel, often at varying frequencies.<br \/>\nIn other words, it can very well happen that changes keep flowing in one direction at a daily cadence while the pull request in the other direction gets blocked by an integration build break and takes days, or even weeks, to be merged.<br \/>\nThe code flow algorithm must be able to understand these situations and make sure conflicts surface only when an actual conflicting changes were made.<\/p>\n<p>Being correct is not everything either.<br \/>\nThe pull requests must also convey what changes are included, where they come from and attempt to offer guidance when conflict resolution is needed.<br \/>\nWhen flowing millions of lines of code monthly across dozens of repositories, chaos is easily introduced and developers must be equipped with the right tools and information so that they can make the right decisions and stay on top of everything.<br \/>\nLastly, we also need to understand the holistic state of the system to identify interruptions in the flow, long-living PRs, and other potential bottlenecks.<br \/>\nDeveloper experience and observability are crucial for us to be able to maintain a healthy system.<\/p>\n<p>Similarly as before, we have developed a custom algorithm on which we iterated.<br \/>\nWe will walk through the evolution too to better illustrate our learnings.<br \/>\nHopefully, some of these can be useful for anyone trying to solve a similar problem.<\/p>\n<h3>Terminology<\/h3>\n<p>Let\u2019s look at some of our terminology we\u2019ll be using in the rest of this section:<\/p>\n<ul>\n<li><strong>Source\/Product repository<\/strong> \u2013 One of the current development repositories, e.g., <code>dotnet\/runtime<\/code>. Not the VMR.<\/li>\n<li><strong>Forward flow<\/strong> \u2013 The process of moving changes from an product repository to the VMR.<\/li>\n<li><strong>Backflow<\/strong> \u2013 The process of moving changes from the VMR to an product repository.<\/li>\n<li><strong>Code flow<\/strong> \u2013 The process of moving changes between the VMR and product repositories. This is a generic term that can refer to both forward flow and backflow.<\/li>\n<li><strong>Code flow PR<\/strong> \u2013 A pull request carrying the code changes that is opened as part of the code flow process. This can be a forward flow PR or a backflow PR.<\/li>\n<\/ul>\n<h3>Two-way Code Flow v1<\/h3>\n<p>The first iteration of the code flow algorithm was designed with a goal that every time we need to flow changes, we must be able to create <em>some<\/em> pull request in the target repository.<br \/>\nThis pull request must contain the desired changes but might conflict with the target branch.<br \/>\nWe will show how we later realized this was a misguided north star as it introduced some interesting problems.<\/p>\n<p>The <em>TL;DR<\/em> of how the algorithm works is that we keep track of the last flows between the two sides using the <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/how-we-synchronize-dotnets-virtual-monorepo\/#tracking-the-sources\">tracking metadata<\/a> described above.<br \/>\nWe then find the right place (commit) to create the PR branch in the destination repository from, materialize the changes on top of it and open a pull request.<br \/>\nWe must assure you that if there are conflicting changes between the two sides, these are also present in the PR.<br \/>\nThis means that the PR branch must be based on an old enough commit to bring both the commit from the source as well as the change in the destination branch into the conflicting state.<\/p>\n<p>The first iteration of the code flow algorithm was used to ship most .NET 10 previews as well as the 10.0 release.<br \/>\nThe algorithm considers the direction of the previous flow and applies different strategies based on that.<br \/>\nTechnically, there are four scenarios to consider (forward-forward, forward-backward, backward-forward, backward-backward) but the latter two are symmetrical so we will not discuss them separately.<\/p>\n<h4>Flows in Opposite Directions<\/h4>\n<p>Let\u2019s have a look at the more complex scenario from the two first \u2013 when we have two flows in opposite directions.<br \/>\nThe diagrams in this section use the following notation:<\/p>\n<ul>\n<li><img data-opt-id=1414027222  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f7e0.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udfe0\" class=\"wp-smiley\" \/> <strong>Orange<\/strong> \u2013 File content transformations. A file starts with content <code>A<\/code>, and <code>B -&gt; C<\/code> means a commit changed content from <code>B<\/code> to <code>C<\/code>.<\/li>\n<li><img data-opt-id=1105730153  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f7e2.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udfe2\" class=\"wp-smiley\" \/> <strong>Green<\/strong> \u2013 The previous successful flow. Shows which commit is being flown (dashed) and what the PR branch on the other side would form like (solid).<\/li>\n<li><img data-opt-id=1806922902  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f535.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udd35\" class=\"wp-smiley\" \/> <strong>Blue<\/strong> \u2013 The current flow being discussed.<\/li>\n<li><img data-opt-id=992430270  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f7e3.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udfe3\" class=\"wp-smiley\" \/> <strong>Purple<\/strong> \u2013 The diff being carried to the counterpart repository.<\/li>\n<li><img data-opt-id=613594721  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/26ab.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\u26ab\" class=\"wp-smiley\" \/> <strong>Grey<\/strong> \u2013 Unrelated commits that don\u2019t affect the tracked file.<\/li>\n<li>Commits are numbered in chronological order. Points <code>1<\/code> and <code>2<\/code> typically denote some previous synchronization.<\/li>\n<\/ul>\n<p><img data-opt-id=824312030  data-opt-src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/backward-forward-codeflow.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"Flows in opposite directions\" \/><br \/>\n<em>A code flow diagram showing two consecutive flows between a repository and a VMR, each in a different direction.<\/em><\/p>\n<p><strong>The flow of changes in the diagram is as follows:<\/strong><\/p>\n<ul>\n<li><code>1<\/code> and <code>2<\/code> denote some previous synchronization point.<\/li>\n<li><code>4<\/code> Commit in the VMR changes the contents of <code>A<\/code> to <code>B<\/code>.<\/li>\n<li><code>5<\/code> A backflow starts at that point.<\/li>\n<li><code>6<\/code> A backflow branch (green) is created in the repository. The branch is based on the commit of last synchronization (<code>1<\/code>). How this flow is created is not the subject of this diagram. Here, we are interested in the following flow. A PR from this branch is opened.<\/li>\n<li><code>7<\/code> Backflow PR is merged, effectively updating <code>A<\/code> from <code>A<\/code> to <code>B<\/code> in repository\u2019s main branch.<\/li>\n<li><code>8<\/code> A commit is made in the repository, changing content from <code>B<\/code> to <code>C<\/code>.<\/li>\n<li><code>9<\/code> An unrelated commit is made in the VMR.<\/li>\n<li><code>10<\/code> A forward flow starts at that point.<\/li>\n<li><code>11<\/code> A forward flow branch (blue) is created in the repository. The branch is based on the commit of last synchronization\u2019s (<code>5<\/code>) base commit. A PR from this branch is opened.<br \/>\nAn additional commit is made in the forward flow PR which changes the contents of <code>A<\/code> to <code>D<\/code>.<\/li>\n<li><code>12<\/code> The PR is merged, effectively updating <code>A<\/code> from <code>B<\/code> to <code>D<\/code>.<\/li>\n<\/ul>\n<p>You can notice several features:<\/p>\n<ul>\n<li>No (git) conflicts appear. This is because this concrete example considers a single file that is chronologically changed from <code>A<\/code> to <code>D<\/code> in gradual steps. In cases where most of the changes happen in the individual repository, we expect the code to flow fluently.<\/li>\n<li>The whole flow is comparable to a dev working in a dev branch within a single repository. The dev then opens a PR against the main branch (the repository in this case). Wherever there are conflicts in a single repository case, we would get conflicts here too and this is by design.<\/li>\n<\/ul>\n<p>What is left to discuss is how we create the commit (<code>11<\/code>) of the forward flow branch.<br \/>\nWe know that we received the delta from the repository as part of the commit <code>7<\/code> after the last backflow PR was merged.<br \/>\nWe account for the fact that a squash merge was used and commit <code>6<\/code> might not be available anymore.<br \/>\nThere could have also been additional commits on the backflow PR branch between commits <code>6<\/code> and <code>7<\/code>.<br \/>\nThe set of changes we need to flow when we are flowing commit <code>10<\/code> technically consists of commits <code>3<\/code>, <code>6<\/code>, <code>7<\/code>, <code>8<\/code> and <code>10<\/code>.<br \/>\nBasically, everything that happened on the repository side that has not yet been flowed into the VMR.<br \/>\nIt is visualized as the purple diff between <code>10<\/code> and <code>6<\/code>.<br \/>\nThis diff correctly represents the delta because:<\/p>\n<ul>\n<li>It contains the last known snapshot of the VMR (<code>6<\/code>)<\/li>\n<li>All commits that happened in the VMR in the meantime (since the last commit) \u2013 the commits <code>3<\/code> and <code>7<\/code>.<\/li>\n<li>The other commits that happened in the VMR since the sync <code>8<\/code> and <code>10<\/code>.<\/li>\n<\/ul>\n<p>The base commit of the forward flow branch is then the base commit of the last backflow as that\u2019s what we\u2019re applying the delta to.<br \/>\nIf commit <code>9<\/code> had conflicting changes compared to the delta, the PR would show these conflicts, and the dev would have to resolve them.<\/p>\n<h4>Two Flows in the Same Direction<\/h4>\n<p>The situation is a bit simpler when we have two consecutive flows leading in the same direction:<\/p>\n<p><img data-opt-id=1540245329  data-opt-src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/forward-forward-codeflow.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"Two flows in the same direction\" \/><br \/>\n<em>A code flow diagram showing two consecutive flows from a repository into a VMR.<\/em><\/p>\n<p>When we are forming the forward flow commit (<code>10<\/code>), we know that the only things that happened since we last sent all our updates to the VMR are the commits <code>9<\/code> and <code>10<\/code>.<br \/>\nWe can then just apply this new delta on top of the last forward flow commit (<code>8<\/code>).<\/p>\n<h4>Conflicts<\/h4>\n<p>A conflict is a situation when the same chunk of the same file is changed in diverging ways in the repository and the VMR at the same time.<br \/>\nHuman intervention is then needed to decide which change wins.<br \/>\nThe goal of the algorithm is to make sure that these conflicts are surfaced and dealt with before changes can flow again.<br \/>\nHowever, we will show how it can matter in what exact way a conflict is introduced with respect to the ongoing code flow.<br \/>\nWe will also show how conflicts can appear even when no conflicting changes were made at all.<\/p>\n<p>Let\u2019s consider the following example where a conflict is introduced by an extraneous commit that was made in a forward flow PR:<\/p>\n<p><img data-opt-id=672920869  data-opt-src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/forward-forward-codeflow-with-conflict-scaled.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"Conflict introduced in a code flow PR\" \/><br \/>\n<em>A code flow diagram showing a conflict introduced in a code flow PR.<\/em><\/p>\n<p>In this situation, the additional commit that was made in the first forward flow PR (<code>6<\/code>) conflicts with a commit made in the repository (<code>10<\/code>). Since there was no backflow, this information is unknown to the repository side.<br \/>\nThe follow-up forward flow is problematic because changes <code>10<\/code> and <code>11<\/code> cannot be applied on top of <code>8<\/code>.<br \/>\nWe are, in fact, even unable to create any commits for the PR branch at all!<br \/>\nIn such a case the only thing left to do is to base the PR branch on the last known good commit (<code>2<\/code>) and reconstruct the previous flow (by re-applying <code>5<\/code> which is technically <code>1<\/code>, <code>3<\/code> and <code>4<\/code>), apply <code>10<\/code> and <code>11<\/code> on top and create a PR branch that will be conflicting with the target branch because of the <code>6<\/code>\/<code>10<\/code> conflict.<br \/>\nThe user would then be instructed to merge the target branch (<code>9<\/code>) into the PR branch and resolve the conflict. For those changes contained in <code>5<\/code> that are more or less the same as the ones in <code>8<\/code>, git will transparently match up and only the actual conflicting files will be left for resolution.<br \/>\nThe next backflow will then bring this resolution over to the repository.<\/p>\n<p>There are countless other examples of conflicts that can occur, but these will usually manifest as conflicts in the PR. The example above is more interesting because the forward flow is unable to even create the PR branch in the first place. This is because <code>8<\/code> (the previous forward flow commit) contains <code>6<\/code> which conflicts with <code>10<\/code>.<br \/>\nIt\u2019s important to note that the set of conflicting files will not only contain the problematic file but also the manifest file that tracks the last synchronized commits. This is because the manifest file was updated in commit <code>8<\/code> where it contains SHA of commit <code>4<\/code> while the PR branch is updating the same line to <code>11<\/code>.<br \/>\nIt is impossible for us to partially resolve this even when we know the desired content of the source manifest because git does not allow partial merge resolutions.<br \/>\nThis means that when a real conflict forces us to rebase to an older commit, it brings trouble\u2026<\/p>\n<h4>Conflicts, Conflicts Everywhere<\/h4>\n<p>Once we started testing the algorithm in practice, we started realizing that the real complexity lies in the dynamicity of the problem happening in a real development rhythm.<br \/>\nFlows rarely happen in a ping-pong-like sets where changes flow back and forth nicely in a predictable manner.<br \/>\nInstead, we must expect flows happening in both directions in parallel, on their own frequency, often taking a long time between opening and merging.<\/p>\n<p>Let\u2019s look at a scenario where a single file goes through a series of gradual changes in the source repository and while we don\u2019t make any actual conflicting changes to it, we will still see conflicts in the code flow PR:<\/p>\n<p><img data-opt-id=725180929  data-opt-src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/conflict-during-gradual-changes-scaled.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"Conflict between gradual changes\" \/><br \/>\n<em>A code flow diagram illustrating problems arising during gradual changes of a file.<\/em><\/p>\n<p>In this example, a file in the repository had its content gradually changed from <code>A<\/code> to <code>B<\/code> to <code>C<\/code>.<br \/>\nNo changes were done to it in the VMR.<br \/>\nA forward flow <img data-opt-id=1806922902  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f535.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udd35\" class=\"wp-smiley\" \/> and a backflow <img data-opt-id=1105730153  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f7e2.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udfe2\" class=\"wp-smiley\" \/> started in parallel and each had been successfully merged.<br \/>\nThe forward flow <img data-opt-id=1806922902  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f535.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udd35\" class=\"wp-smiley\" \/> successfully updated the file in the VMR from <code>A<\/code> to <code>B<\/code>.<br \/>\nNow let\u2019s look at the second forward flow <img data-opt-id=1460382226  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f534.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udd34\" class=\"wp-smiley\" \/> which should technically bring over the change <code>B<\/code> to <code>C<\/code>.<br \/>\nThe second forward flow is following the algorithm of the opposite direction flow <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/how-we-synchronize-dotnets-virtual-monorepo\/#flows-in-opposite-directions\">described above<\/a> and its PR branch is based on the commit of the last backflow <img data-opt-id=1105730153  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f7e2.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udfe2\" class=\"wp-smiley\" \/>.<br \/>\nThis effectively means that the PR branch contains the change from <code>A<\/code> to <code>C<\/code>.<br \/>\nHowever, the target branch already contains the change from <code>A<\/code> to <code>B<\/code>.<br \/>\nWhen we try to merge the PR, git sees a conflict between the change from <code>A<\/code> to <code>C<\/code> (the PR branch) and the change from <code>A<\/code> to <code>B<\/code> (the target branch) and we fail to merge the branch.<\/p>\n<p>You can see on this example that even though there were no conflicting changes made to the file, the way the flows interleaved caused a conflict regardless.<br \/>\nTo address this, we can use the information about both of the previous flows \u2013 not just the last one \u2013 merge the target branch into the PR branch and resolve the conflicts programmatically as we understand the desired end state of the files.<br \/>\nHowever, this only works when there\u2019s no additional changes to the file that we don\u2019t expect.<br \/>\nFurthermore, when a real conflict is thrown into the mix, merging the branches becomes impossible again.<br \/>\nThat also means the user will not only have to deal with the real conflict but also with a conflict in the gradually changing file that should not be a conflict in the first place!<br \/>\nOne such file is the source manifest which changes in each flow, and which always ends up in conflict in this situation.<br \/>\nThe developer then must resolve a conflict in the tracking data themselves which is not ideal as it can easily lead to a disaster.<\/p>\n<p>These situations are not rare. Workflows which introduce new files that are quickly modified in a follow-up PR are common (e.g. localization).<\/p>\n<h4>The Revert Problem<\/h4>\n<p>The problem described above, though workable, already points to some inherent limitations of the approach.<br \/>\nThe last straw that broke this camel\u2019s back and made us rethink some of our goals was the so-called <em>revert problem<\/em>.<\/p>\n<p>The idea is similar to the previously described scenario but instead of gradually changing a file, we make a change to it and later revert this change back.<br \/>\nIf we manage to flow the change separately from the revert, and the second flow with the revert also ends up in a conflict, this perfect storm prevents us from merging branches automatically and we will lose the revert completely!<\/p>\n<p><img data-opt-id=1299042406  data-opt-src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/the-revert-problem-scaled.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"The revert problem diagram\" \/><br \/>\n<em>A code flow diagram illustrating a problem that arises when a file change is reverted.<\/em><\/p>\n<p>In this example, we can observe the following:<\/p>\n<ul>\n<li>File <code>B<\/code> is added <img data-opt-id=1105730153  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f7e2.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udfe2\" class=\"wp-smiley\" \/> and then removed (reverted) <img data-opt-id=1460382226  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f534.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udd34\" class=\"wp-smiley\" \/>, while file <code>A<\/code> receives unrelated conflicting changes <img data-opt-id=1414027222  data-opt-src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f7e0.png\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"\ud83d\udfe0\" class=\"wp-smiley\" \/>.<\/li>\n<li>The forward flow PR branch contains all three changes where the revert negates the original change which manifests as the file <code>B<\/code> not changing at all.<\/li>\n<li>The conflict in file <code>A<\/code> prevents us from basing the PR branch on top of commit <code>8<\/code> (the last backflow) and we must instead base it on commit <code>2<\/code>, while we recreate the previous flow.<\/li>\n<li>Since the newly rebased PR branch now technically does not contain any change to file <code>B<\/code> (the change and its revert cancel each other out), when we merge the PR, the revert is lost completely and the file <code>B<\/code> stays in the VMR while it was removed from the original repository.<\/li>\n<\/ul>\n<p>Furthermore, this does not only apply to full file reverts but as well to any change, however small, that is later reverted back. Scary!<\/p>\n<p>Even though several conditions must click together, this can happen in practice.<br \/>\nSpecifically, we\u2019ve seen this in cases where a temporary workaround or feature flag was introduced in the code and later it got removed again.<br \/>\nIt also happened when PRs were reverted in full in a busy repository where actual conflicts can occur frequently.<br \/>\nRegardless, it was not acceptable for us to lose changes silently like this.<br \/>\nBack to the drawing board!<\/p>\n<h3>Rebasing Our Approach<\/h3>\n<p>At this point, we have depleted all possibilities offered by plain branching and merging.<br \/>\nFor every workaround and idea we\u2019d come up with, there\u2019d be a counterexample that shatters it!<\/p>\n<h4>Changing the Playing Field<\/h4>\n<p>This led us to reconsider our goal of being able to always create a PR with some content in the target repository.<br \/>\nSince we cannot partially resolve conflicts, the resolution must happen in a different environment than the GitHub PR UI.<br \/>\nCan it be the dev\u2019s local machine?<br \/>\nCan we let the user run a command that would perform the flow locally which would bring the local repository in the conflicting state, resolve the known (non-)conflicts such as source manifest changes, and let the user deal with the actual problems?<\/p>\n<h4>Different Game Too<\/h4>\n<p>One other design guide we tried to follow was comparing how our dual-repository solution differs from having just a single repository with multiple branches.<br \/>\nHow do our flows and git operations differ from a regular day of work in a feature branch leading to a feature PR?<br \/>\nIn what other way do people deal with conflicts?<br \/>\nAnd when they do, how does the process look like?<br \/>\nThis led us to explore a different approach altogether.<br \/>\nWe\u2019re talking about the <code>git rebase<\/code> flow.<\/p>\n<h4>Let\u2019s Get Interactive<\/h4>\n<p>When we combine the ideas above, we arrive at a new code flow experience that is more interactive and relies on a different user intervention when conflicts arise.<br \/>\nThe new process does not look too different from a regular git rebase flow:<\/p>\n<ul>\n<li>The code flow service still calculates the changes the same way as before \u2013 taking previous flows into account, constructing a branch based on the last flow, etc.<\/li>\n<li>We then attempt to rebase the PR branch onto the tip of the target branch. This fails when there are conflicts and leaves the repository in a conflicting state.<\/li>\n<li>When no conflict occurs, the rebase is committed and pushed into a new PR and we\u2019re done.<\/li>\n<li>In case of a conflict, the code flow service cannot proceed further. It instead opens an empty PR and instructs the user to perform the flow locally using custom tooling.<\/li>\n<li>The service then blocks the PR merge with a custom status check until it sees the desired changes pushed to the PR branch.<\/li>\n<li>The custom tooling fetches necessary information and performs the same code flow locally.<\/li>\n<li>Upon conflicting, it resolves any known conflicts which can be the result of flows happening in both directions in parallel as discussed <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/how-we-synchronize-dotnets-virtual-monorepo\/#conflicts-conflicts-everywhere\">above<\/a>. It then leaves the user to resolve the <em>actual<\/em> conflicts.<\/li>\n<li>The user then commits and pushes the changes.<\/li>\n<li>Finally, the service validates the pushed contents and unblocks the PR for merging (it greenlights a custom status check).<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/dotnet\/pull\/3629\">This PR<\/a> shows an example of what this PR looks like.<\/li>\n<\/ul>\n<p>The fact that we can resolve known conflicts partially while leaving the actual conflicts to the user is the key to success here.<br \/>\nWe don\u2019t suffer from the revert problem anymore as we can correctly compute the deltas, using all the information about the previous flows.<br \/>\nBut since we are already on top of the target branch, we can correct any missing reverts too.<br \/>\nWe can detect missing reverts by trying to reverse-apply the last flowed change and seeing for which files this fails.<br \/>\nThat can happen only when they are missing a change.<\/p>\n<p>We rolled out this new experience in December and so far, we\u2019ve had much better experience with it.<br \/>\nThere is still one experimental improvement we\u2019re looking into where we would not create the working branch in the target repository but rather apply the patch direcly on top of the target branch.<br \/>\nHowever, this does not work out of the box in git as some operations such as modifications to files not known to git (e.g. file is modified in one side while removed in the other) will fail the application process.<br \/>\nSo far it showed that it\u2019s easier to manifest the required changes in a working branch of the target repository first and then rebase it onto the target branch because git can work with more information from the commit graph while rebasing.<br \/>\nHowever, if we managed to solve the unsupported cases, it would mean a big simplification of the process since part of the working branch creation is the previous flow reconstruction.<\/p>\n<h2>Present Challenges<\/h2>\n<p>We\u2019ve traveled a long way, but it would be naive to think we\u2019ve arrived at the destination.<br \/>\nYes, we were able to ship a dozen releases using the VMR, and yes, it has already brought fruits like allowing us to finalize on a release build earlier while being able to accept last-minute fixes in much later in the process.<br \/>\nSurprisingly, even switching from the repository dependency tree to a flat topology did not meaningfully disrupt our day-to-day development of .NET 10.<br \/>\nWith some careful planning, we were able to make the move in mere hours.<br \/>\nNonetheless, it\u2019s necessary to mention there were hiccups along the way.<\/p>\n<h3>Branching &amp; Product Lifecycles<\/h3>\n<p>.NET is a huge platform and consists of many different products that often ship at their own cadence.<br \/>\nVisual Studio, Aspire, .NET MAUI, Entity Framework, to name a few, have different lifecycle models and require a different rhythm.<br \/>\nOne that does not always align with .NET SDK\u2019s.<br \/>\nIn short, there are several main groups of repositories based on their product lifecycle:<\/p>\n<ul>\n<li><strong>SDK band centric<\/strong> \u2013 repositories such as <code>dotnet\/sdk<\/code> that ship different variants per each SDK band. They would branch in the same way as the VMR itself, e.g. <code>release\/10.0.1xx<\/code> or <code>release\/10.0.2xx<\/code>.<\/li>\n<li><strong>Shared components \/ runtimes<\/strong> \u2013 repositories such as <code>dotnet\/runtime<\/code> or <code>dotnet\/aspnetcore<\/code> that ship components shared between multiple SDK bands. They would usually branch per major version, e.g. <code>release\/10.0<\/code> or <code>release\/11.0<\/code>.<\/li>\n<li><strong>VS centric<\/strong> \u2013 repositories such as <code>dotnet\/roslyn<\/code> that ship components tightly coupled with Visual Studio releases. They would usually branch per Visual Studio version, e.g. <code>release\/17.14<\/code> or <code>release\/dev18.0<\/code>.<\/li>\n<\/ul>\n<p>You can read more about our branching strategy in detail the <a href=\"https:\/\/github.com\/dotnet\/dotnet\/blob\/dc803dea8a5917a87a812a05bae596c299368a43\/docs\/VMR-Managing-SDK-Bands.md\">VMR SDK Bands documentation<\/a>.<\/p>\n<p>These differences in the lifecycle started surfacing when repositories needed to branch at separate times than the VMR which follows the SDK band centric model.<br \/>\nThe code flow algorithm was designed to handle synchronization of two branches between each other only.<br \/>\nIn practice this means that when a repository needs to start synchronizing a different branch with a given VMR branch, we must reset the contents of the VMR manually to make it match the repository.<br \/>\nThis is a complicated process as we must make sure that no changes made to the VMR are lost in the process.<\/p>\n<p>Situations like the one above have happened dozens of times during the .NET 10 product cycle already as we are busy working on shipping the .NET 10.0.200 release.<br \/>\nWe are still working through this problem where we plan to detect changes in our code flow configuration and issue an automated content reset PR that tries to gracefully handle the transition.<\/p>\n<h3>Snapping Release Branches<\/h3>\n<p>Another problematic situation can occur when we\u2019re snapping branches for release, and each product repository would snap at their own time.<br \/>\nDuring the development of a new major .NET version (e.g., .NET 10), each month we\u2019d snap our <code>main<\/code> development branches into the respective preview release branches (e.g., <code>release\/10.0.1xx-previewN<\/code>).<br \/>\nConsider this case when the repository snaps its branch before the VMR does:<\/p>\n<p><img data-opt-id=910543300  data-opt-src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/wrong-branch-snap.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20100%%20100%%22%20width%3D%22100%%22%20height%3D%22100%%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22100%%22%20height%3D%22100%%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"Branches snapped in the wrong order\" \/><br \/>\n<em>A code flow diagram illustrating problems arising when branches are snapped in the wrong order.<\/em><\/p>\n<p>Notice how the commits <code>3<\/code> and <code>5<\/code> get flowed into the VMR and depending on when each repo snaps it branches, they either are or are not in a parent\/child relationship.<br \/>\nIn the diagram, they are not related in the repository while they are parent\/child in the VMR.<br \/>\nThis is obviously wrong as both can be making conflicting changes but are totally valid in the context of their own branch\u2019s history.<\/p>\n<p>We prevent such situations from being introduced by driving the snaps centrally, starting in the VMR.<br \/>\nThe VMR goes first upon which we find the latest commits in each product repository where the snap still makes sense.<br \/>\nWe then create the release branch there.<\/p>\n<h3>Metadata Corruption<\/h3>\n<p>Another challenge we\u2019ve encountered involves the synchronization tracking data.<br \/>\nA common practice in the product repositories is to merge release branches between each other.<br \/>\nFor example, changes made to the <code>release\/10.0.1xx<\/code> can usually be merged into the higher band\u2019s <code>release\/10.0.2xx<\/code> branch.<br \/>\nDuring this process, the tracking metadata, if overwritten, can become inconsistent.<br \/>\nThe 2xx branch of the repository ends up referencing a VMR commit that was synchronized with the 1xx branch.<\/p>\n<p>We\u2019re currently experimenting with using <a href=\"https:\/\/git-scm.com\/docs\/git-notes\">git notes<\/a> as an alternative storage mechanism for tracking metadata.<br \/>\nGit notes attach metadata to commits without modifying the commit itself, which could help us avoid some of the problems arising from having the metadata as part of the working tree.<\/p>\n<h2>What Was Not Covered<\/h2>\n<p>This article grew long very fast and for that I apologize.<br \/>\nNonetheless, there are still numerous related interesting aspects that would deserve more attention since they are as vital for the overall success as the synchronization algorithm itself:<\/p>\n<ul>\n<li><strong>Developer experience<\/strong> \u2013 What all we did in terms of UX to help developers navigate code flow PRs and track where their changes are synchronized into.<\/li>\n<li><strong>Monitoring and observability<\/strong> \u2013 How we track code flow state and health, detect stuck flows, or alert on issues across repositories.<\/li>\n<li><strong>Tooling<\/strong> \u2013 What custom tools we built to help developers perform local code flows, resolve conflicts, and validate changes before pushing.<\/li>\n<\/ul>\n<p>Let us know if you\u2019d be interested in reading about any of these topics in more detail.<\/p>\n<h2>Conclusion<\/h2>\n<p>If you made it this far \u2014 first, thank you \u2014 and second, hopefully, you gained some insight into our journey from a tarball-based Source Build to a fully synchronized monorepo, all while keeping hundreds of developers productive across dozens of repositories, shipping monthly releases without interruption.<br \/>\nThe Virtual Monolithic Repository has become a foundational pillar of .NET\u2019s infrastructure, enabling us to unify and streamline our build and release processes while preserving the flexibility and autonomy of individual repositories and their communities.<br \/>\nThese wins, however, come at a cost of the complexities involved in synchronizing the repositories.<br \/>\nIn case you\u2019re embarking on a similar journey, where maybe our current setup can be but a stepping stone on a path to a full monorepo, we hope our experiences and learnings prove useful.<br \/>\nDon\u2019t hesitate to reach out too, this is a niche problem and we\u2019re happy to talk!<\/p>\n<h2>Resources<\/h2>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/dotnet\/tree\/main\/docs\">Unified Build Design Documentation<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/dotnet\/blob\/dc803dea8a5917a87a812a05bae596c299368a43\/docs\/VMR-Full-Code-Flow.md\">Original design document for the code flow algorithm<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/arcade-services\/tree\/main\/src\/Microsoft.DotNet.Darc\/DarcLib\/VirtualMonoRepo\">Implementation of the code flow<\/a><\/li>\n<\/ul>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/how-we-synchronize-dotnets-virtual-monorepo\/\">How We Synchronize .NET\u2019s Virtual Monorepo<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In our previous post \u201cReinventing how .NET Builds and Ships\u201d, Matt covered our recent overhaul of .NET\u2019s building and shipping [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3212,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-3211","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/3211","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=3211"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/3211\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/3212"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=3211"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=3211"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=3211"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}