Introduction: The Hidden Complexity of Repository Migrations
Repository migrations are often treated as simple infrastructure tasks. However, the reality for development teams is far more complex. The critical question isn't just moving code, but safeguarding the work in progress. What happens to the ongoing feature development when the underlying Git platform changes?
This challenge was recently highlighted during a migration from Azure Repos to GitLab. The objective was not merely to transfer code, but to ensure a seamless transition of active development without losing commits, pushing to outdated branches, creating merge conflicts, disrupting workflows, or confusing team members. This article details the lessons learned and the structured approach that facilitated a smooth migration.
The Challenge: Protecting In-Flight Development
The core problem arises when development is active during the migration window. Teams are given directives, often along the lines of:
"Stop pushing to branches original to the old platform."
This instruction, while seemingly straightforward, creates immediate friction. Developers are in the middle of coding, testing, and potentially creating pull requests. Halting all activity, especially for a migration that could span days or weeks, is impractical and detrimental to productivity. The risk of losing work or introducing inconsistencies is high if not managed meticulously.
The primary goals of a safe migration for active development are:
- Preserve Commit History: Ensure no commits are lost during the transfer.
- Prevent Stale Branches: Avoid developers continuing to work on or merge into branches that are no longer the source of truth.
- Minimize Merge Conflicts: Reduce the likelihood of developers encountering conflicts when their work eventually needs to be integrated into the new platform.
- Maintain Workflow Continuity: Keep the development process flowing as smoothly as possible, minimizing downtime and confusion.
- Ensure Team Alignment: Communicate clearly and consistently to keep all team members informed and on the same page.
Strategy: A Phased Approach to Migration
A successful migration hinges on a well-defined strategy that accounts for active development. The approach taken involved several key phases:
Phase 1: Preparation and Communication
Before any technical steps are taken, thorough preparation and clear communication are paramount. This involves:
- Establishing a Migration Window: Define a specific timeframe for the migration, including a hard cut-off for new commits to the old repository.
- Communicating the Plan: Inform all stakeholders – developers, project managers, QA, and operations – about the migration timeline, the process, and their roles.
- Creating a Read-Only State: Configure the source repository (e.g., Azure Repos) to be read-only for developers once the cut-off is reached. This prevents accidental pushes to the old system.
- Setting Up the Target Repository: Prepare the new repository (e.g., GitLab) with the correct structure, permissions, and initial setup.
Phase 2: Initial Repository Mirroring
The first technical step is to create a mirror of the existing repository in the new platform. This is typically done using Git's built-in mirroring capabilities. The goal here is to get an exact replica of the repository's state at a specific point in time.
The command used for this is generally:
git clone --mirror <source_repo_url>
git remote set-url --push origin <source_repo_url>
git push --mirror <target_repo_url>
This command clones the repository with all its history, branches, and tags, and then pushes this entire mirrored structure to the new remote. At this stage, the new repository contains the exact state of the old one as of the mirroring time.
Phase 3: Handling Active Development (The Critical Step)
This is where the migration's complexity truly lies. Developers will have been working on their local copies *before* the read-only cut-off. Their local branches contain commits that are not yet in the mirrored repository. The key is to collect these local changes and integrate them into the new platform without disruption.
The strategy here involves:
- Identifying Local Changes: Developers must identify all commits made on their active feature branches that were not pushed to the original repository before it went read-only.
- Creating a Temporary Branch: For each active feature branch, create a new, temporary branch in the *new* repository that is based on the state of the repository *after* the mirror was pushed. This ensures the new branch is built upon the latest stable code from the migration point.
- Rebasing or Cherry-Picking: Developers then need to transfer their unpushed local commits to these new temporary branches. This can be done via rebasing (`git rebase`) or cherry-picking (`git cherry-pick`). Rebasing is often preferred as it replays commits, preserving a cleaner history, but requires careful handling of potential conflicts. Cherry-picking allows selecting individual commits but can lead to a more fragmented history if not managed well.
- Resolving Conflicts: As local changes are applied to the new branches, developers must actively resolve any merge conflicts that arise. This is an unavoidable part of integrating work done in parallel or on slightly different base states.
- Testing and Validation: Thoroughly test the code on these new branches to ensure that the active development work has been migrated correctly and that no regressions have been introduced.
- Creating Pull Requests: Once validated, create new Pull Requests (or Merge Requests in GitLab terminology) targeting the appropriate main branches (e.g., `main`, `develop`) in the new GitLab repository.
Think of this phase like moving house while you're still unpacking groceries. You need to carefully transfer your partially prepared meals (active development) to the new kitchen (GitLab) and finish preparing them there, ensuring nothing spoils (commits lost) or gets mixed up (merge conflicts).

Phase 4: Post-Migration Cleanup and Verification
After the active development has been successfully transferred and integrated:
- Decommission Old Repository: Once confidence in the new repository is high, the old repository can be archived or decommissioned.
- Update CI/CD Pipelines: Ensure all continuous integration and continuous deployment pipelines are updated to point to the new repository and configured correctly.
- Verify Branch and Tag Integrity: Perform a final check to ensure all critical branches and tags have been migrated accurately.
- Team Retrospective: Conduct a retrospective to discuss what went well, what could be improved, and to document lessons learned for future migrations.
Conclusion: Proactive Planning Prevents Disruption
Migrating active development work between Git platforms is a nuanced process. It requires more than just technical commands; it demands meticulous planning, clear communication, and a deep understanding of Git workflows. By adopting a phased approach that prioritizes the protection of in-flight changes, teams can navigate these transitions with minimal disruption. The key takeaway is that proactive strategy, focused on the developer experience and the integrity of ongoing work, transforms a potentially chaotic event into a managed, successful infrastructure update.
