Migrate from Perforce, TFVC or CVS to GitLab

Moving from Perforce, TFVC or CVS to self-managed GitLab is mostly a history conversion problem, not an import click. This page explains what maps, which paths are documented, and what drives effort.

On this page

Why moving Perforce, TFVC or CVS to GitLab is a history conversion project

GitLab's documented importers do not cover Perforce, TFVC or CVS directly, so this move is a conversion project: convert the history to Git, import the result into self-managed GitLab, then cut over teams and tooling. Plan each stage as a separate piece of work with its own owner and acceptance check. The conversion step is where most of the judgment sits, because it decides what the Git history will look like for every developer who clones it afterwards.

The effort depends on the inventory, not on the source system name alone. Ask for the size of the history in each depot, module or CVS module, the branching model in use (Perforce streams, TFVC branches or CVS tags and vendor branches), how many large binary files are tracked, and how many committer identities must be mapped to GitLab accounts. Binary assets generally belong in Git LFS, and that decision needs a storage plan on your side before conversion starts. Confirm these figures with your own team before anyone quotes a schedule.

GitLab Self-Managed is available in the Free, Premium and Ultimate tiers, so the edition decision can be made in parallel with the conversion design. Features such as advanced CI/CD and priority support are tied to Premium, while security and compliance features are tied to Ultimate, according to the GitLab pricing page. Confirm with your team which of those features the migrated repositories must use on day one, because that choice affects both licensing and the pipelines you rebuild.

What maps cleanly from Perforce, TFVC and CVS to GitLab

This table compares the version control concepts in Perforce, TFVC and CVS with their closest Git and GitLab equivalents. Read the middle columns for what converts directly and the notes for where history is lost, reshaped or must be rebuilt by hand. Most rows describe a mapping that needs a decision, not a one-to-one copy.

Source conceptGit and GitLab equivalentConversion notes
Perforce changelistsCommitsEach changelist usually becomes one commit, but changelist descriptions, job links and submit metadata need an explicit mapping. Confirm how your conversion tool handles multi-file changelists.
Perforce streams or branchesBranches, with possible reshapingStream hierarchies do not map to a single Git construct. Decide which streams become long-lived branches and which are retired before conversion.
Perforce labelsTagsLabels are usually moved to annotated tags, but a label covers a file set rather than a commit. Expect some labels to need manual review.
Perforce shelvesDraft merge requests or branches, not historyShelved files are not part of submitted history. Plan to recreate open work as branches or drafts, or to finish it in Perforce before cutover.
TFVC changesets and work item linksCommits and merge request or issue referencesChangeset history converts, but links to work items usually need a separate mapping to GitLab issues. Confirm what the import path carries over.
TFVC check-in policiesProtected branches, approvals and pipeline rulesPolicies are not converted as-is. Rebuild them as protected branch settings, approval rules and CI/CD jobs, then test them.
CVS modules and tagsRepositories, subdirectories and tagsModules often map to separate repositories or subdirectories. CVS tags are file-level, so a consistent tag per commit is not guaranteed.
CVS revision-only historyCommit history, with reconstructed commitsPer-file revisions are grouped into commits by heuristics. Commit boundaries, timestamps and authorship may be approximate.
File-level locksGit LFS file locking, or a processLocking is not history. Check whether your GitLab edition and Git LFS configuration cover the locking workflow your teams need.
Large binary files and deltasGit LFS pointers in the repositoryBinary history is stored differently. Large assets move to LFS, and older binary revisions may be trimmed or stored as full copies. Confirm storage limits with your instance administrator.
Access controls and permissionsGroup and project roles, protected branchesSource permissions do not carry over directly. Map roles to GitLab groups and projects before migration, and confirm access rules with the owners of each area.
Build and deployment stepsGitLab CI/CD pipelines and runnersBuild jobs are not converted automatically. Rewrite them as pipelines, then provision runners with the right executor and network access.

Documented GitLab import paths and the conversion tools they depend on

GitLab's import documentation covers a defined set of sources, and Perforce, TFVC and CVS are not among them. Teams moving from these systems usually combine a conversion step with one of the documented Git import paths, so each card below marks a point where the plan can succeed or stall.

Git import by URL or manifest

This is the documented route for bringing a Git repository into GitLab once it exists as Git. For Perforce, TFVC or CVS, it is the last step after history has been converted, not the conversion itself. Check that the converted repository is complete, that branches and tags survived, and that the import runs against the exact history you reviewed. Confirm import permissions and whether your instance allows the source URL before the cutover window opens.

Manifest-based bulk import

When many converted repositories need to move together, a manifest lets you list them in one run instead of creating each project by hand. The manifest must match your group structure and naming plan, so decide those before you generate it. Ask which fields the manifest accepts in your GitLab version and how it handles failures in the middle of a batch.

Direct transfer and file export

Direct transfer and file exports move projects between GitLab instances, which matters if a team already runs GitLab elsewhere and is consolidating. This path does not help with Perforce, TFVC or CVS directly, but it can matter for a second-stage move. Confirm what each method carries, including issues and merge requests, before choosing it as the destination for converted history.

External conversion to Git

Subversion needs external conversion tools, and the same principle applies to Perforce, TFVC and CVS: history is converted to Git outside GitLab. The choice of tool, its handling of branches, tags, authors and binary files, and its output format determine what reaches GitLab. Confirm the tool version, supported source system version and fidelity with the tool's own documentation, and run a test conversion on a representative repository.

Gaps the importer will not close

GitLab's import documentation does not list Perforce, TFVC or CVS as direct importers, so changesets, shelved work, labels, workspace mappings and file locks will not arrive by default. Some of these may need to be recorded in a migration log, a wiki or an archive. Ask your team to inventory which of these artifacts they rely on before the conversion scope is fixed.

Authorship and ownership mapping

Conversion tools write commit authors from the source system, and those names rarely match GitLab accounts. Build an author map before conversion, covering service accounts, departed staff and shared logins. Decide whether commit authors stay as recorded and who verifies the mapping. This is effort most teams underestimate, and the quote for the work should name it explicitly.

Choosing how to convert history: full, trimmed or snapshot

A full conversion replays every changelist, changeset or revision from the legacy system as a Git commit, so the history in GitLab matches what the old system recorded. Teams keep the complete record of who changed which line and when, which matters most for regulated code where blame and traceability are inspected. The cost is mapping work that is easy to underestimate: every legacy username must become a commit identity, stream or branch structure must be translated into Git branches, and labels or tags must be recreated as Git tags.

  • Author mapping: Build a table from legacy accounts to name and email pairs, and decide what to do with departed staff and service accounts.
  • Branch structure: Map Perforce streams, TFVC branches or CVS branch tags to Git branches, and agree which lines of development become protected branches.
  • Label and tag mapping: Convert labels, baselines and release marks into annotated Git tags, and check that tag names do not collide.
  • Large binaries: Identify binary files in history early, because they are the main driver of repository size and may need Git LFS rules for future work.
  • Verification: Compare commit counts per branch, sample file contents at several revisions and confirm blame output on known lines.

Confirm with your conversion tool which of these mappings it supports before you commit to a full conversion.",

How the migration sequence runs from inventory to decommission

The sequence moves history, identities and large binaries from Perforce, TFVC or CVS into self-managed GitLab in controlled stages, with a proven pilot, a parallel run and a read-only archive of the old system at the end.

  1. Inventory repositories, depots and large binaries

    Catalog every source unit in scope: Perforce depots, streams and client workspaces, TFVC team projects and mapped paths, or CVS modules and their branch and tag history. Record file counts, total history size, the binaries that sit in version control, and which projects have active build or deployment dependencies. Flag anything that Git cannot represent cleanly, such as locked files, shelved changes or heavy binary revisions, so the team can decide how each is handled. The output is a signed-off inventory with a named owner for each project and a list of items that need a history decision before the pilot begins.

  2. Pilot a conversion of one representative project

    Choose one project that has realistic history depth, branching, and binary content, then convert it with the tooling your team selects for the source system. Import the result into a non-production GitLab group and check that commit counts, tags, branch names and file contents match the source for a sampled set of revisions. Run the GitLab import and permissions checks on the imported project to confirm members, visibility and default branch behave as intended. The step closes when the team has a written list of conversion defects, each with a decision to fix, accept or exclude, and a repeatable conversion script.

  3. Map identities and branch policy

    Build a table that maps every source account to a GitLab user, service account or placeholder, and agree which contributors keep authorship on converted commits. Decide the target branch model, protected branches, merge request approval rules and how historical branches are retired or kept. Confirm with security and compliance owners how access is granted, including whether group membership comes from your identity provider through the configured authentication method. The step is done when the identity map has been reviewed for every active contributor and the branch policy is recorded in the project template.

  4. Run a parallel period in both systems

    Keep the source system readable for teams while new work lands in GitLab. Set a rule that each change is recorded in one place for each project during this period, and state which system is authoritative for review and release. Use incremental conversions to keep GitLab current with the source, and compare revision counts after each run. Issues, build triggers and documentation links are updated during this period so that the cutover does not depend on last-minute edits. The step closes when two consecutive incremental runs reconcile cleanly and teams have signed off on the authority rule.

  5. Freeze, run the final import and set up Git LFS

    Announce a freeze window in which no commits or check-ins are made to the source projects. Run the final incremental conversion, verify revision counts and tags against the frozen source, and move binary assets into Git LFS, confirming that LFS objects are present and downloadable after import. Update CI/CD variables, runner access and integrations to point to the new projects. Reopen GitLab to developers only after the import verification and permission review are complete, and keep a rollback path documented until those checks pass.

  6. Verify access, pipelines and integrations after cutover

    Confirm that every migrated project has the intended members, protected branches and approval rules, and that no former contributor retains access they should not have. Run a representative pipeline on each project type to confirm runners, secrets and artifacts work with the new repositories. Check that issue links, webhooks and downstream consumers resolve. The step is complete when the cutover checklist is signed off by the project owners and the security reviewer, and open defects are logged with owners.

  7. Decommission the old system with an archived read-only copy

    After an agreed stabilization period, set the source system to read-only for the migrated projects and retain a full, verifiable archive of its history, metadata and configuration. Store the archive in a location with access controls that match your records policy, and record its checksum and location in the change record. Remove build triggers and service accounts that no longer serve a purpose. The step closes when the archive has been restored or inspected successfully, the source is read-only, and the decommission record is filed with compliance evidence.

How Git LFS handles large binaries from Perforce and TFVC

Teams leaving Perforce or TFVC often hold large binary assets such as game art, CAD files, video and compiled third-party libraries, and the first decision is which of these belong in Git LFS at all. Git LFS stores the large file content on a separate object store and leaves a small pointer file in the Git repository, so clones and fetches stay light while history still references each version. Build a path inventory from the source system, sorted by file size and extension, and mark each class of file as either tracked by LFS, kept as ordinary Git content, or excluded because it is generated and should be rebuilt in CI. Confirm the tracking rules with the Git LFS documentation for the Git and LFS client versions your developers use.

The harder decision is whether to rewrite history so that existing binaries move into LFS. Rewriting makes clones lighter and avoids carrying large blobs in every future fetch, but it changes every commit hash that touches those files, breaks open merge requests and local branches, and must be coordinated with every team that has work in progress. Leaving history untouched keeps the migration simpler and preserves an exact record, at the cost of a heavier repository. Whichever path you choose, write it down before the first import, and test it on a copy of the largest repository, because the conversion is difficult to reverse once the history is published.

On self-managed GitLab, LFS content is stored in whatever storage you configure for the instance, so the storage and bandwidth plan belongs in the infrastructure design, not in the migration task list. Ask your platform team to size the object storage for the total binary volume after conversion, plus growth from the next several years of projects, and to plan backup and restore for that storage alongside the database and Git data. Clone and fetch traffic for large files flows through your network and any runner caches, so review how CI jobs fetch LFS objects. Ask DeployOpen or your GitLab administrator to check the LFS settings and storage options in the GitLab documentation for the version you will run, because these settings differ across releases.

What drives the cost and duration of the move

This table lists the factors that change how much work a Perforce, TFVC or CVS move takes, what makes each factor heavier or lighter, and what to request in a written quote. Read each row as a question to answer about your own estimate before comparing partner proposals. The main takeaway is that history volume, binary content and identity mapping usually decide scope more than the number of repositories.

DriverWhat increases effortWhat reduces effort and what to ask for in a quote
Total history size and revision countLong Perforce changelist history, deep CVS or TFVC changesets and many file revisions that must be converted and verifiedAgree whether full history or a recent cutoff is in scope; ask how conversion runs are timed and checked
Number of depots, streams or modulesSeveral Perforce depots, stream hierarchies or TFVC team projects that each need their own mapping to GitLab projectsConsolidate related areas before conversion; ask for the mapping of each depot or stream to a project
Branching complexityStream merges, branch copies, labels and tags that must be reproduced as Git branches and tags with correct ancestryLimit imported branches to active ones; ask how merge history and labels will be represented
Binary and large file volumeLarge binary assets in Perforce or TFVC that must move to Git LFS, with storage sizing and client setup for each teamSeparate binaries from source early; ask how Git LFS storage is sized and where it is hosted in your environment
Identity mappingMany committers, departed staff, shared accounts and inconsistent usernames that must be matched to GitLab usersExport and clean the author list before conversion; ask how unmatched authors are handled and who approves mappings
CI/CD rebuild scopePipelines, build agents and deployment scripts tied to Perforce, TFVC or CVS triggers that must be rewritten as GitLab CI/CD and runnersInventory existing jobs first; ask whether pipeline rewrites and runner setup are priced separately from the repository move
Work tracking integrationWork items in TFS, Azure DevOps or other trackers linked to changesets that must be re-linked or migratedDecide whether links are kept as references or rebuilt; ask what fields and links the quote covers
Compliance evidence and sign-offChange records, retention rules and audit trails that must show the conversion was complete and accurateDefine the evidence set up front; ask which records are produced and who reviews them before cutover
Number of approving teamsEach owning team, security function and compliance reviewer adding review cycles before cutoverName approvers and a single decision owner early; ask how review rounds are counted in the timeline

Questions buyers ask before moving Perforce, TFVC or CVS to GitLab

Does GitLab have a direct importer for Perforce, TFVC or CVS?

GitLab's import documentation does not list Perforce, TFVC or CVS as direct importers. The documented sources are GitHub, Bitbucket Cloud and Server, Gitea, other GitLab instances, Git repositories by URL or manifest, FogBugz and Jira issues. Subversion is the notable case that needs external conversion tools. For these three systems, the usual path is to export history with a conversion tool, rewrite it into Git, and push the result to GitLab. Confirm the current importer list with GitLab for your release before you commit to a plan, because supported sources can change between versions.

What does a Perforce, TFVC or CVS move to GitLab cost?

The migration cost is mostly one-time labor: building the conversion, mapping users, testing history, configuring Git LFS, rewiring CI/CD and running the cutover. Recurring cost comes from the GitLab subscription. GitLab's pricing page lists Free at 0 US dollars and Premium at 29 US dollars per user per month billed annually, with Ultimate custom priced, and all tiers are available for self-managed installations. The subscription is separate from the conversion work. Cost rises with repository count, history depth, binary volume, the number of identities to map and the number of pipelines to rebuild. Ask any provider, including DeployOpen, for a written scope with deliverables and a fixed quote before work starts.

How long does a move from Perforce, TFVC or CVS take?

The supplied sources do not give a standard duration for this kind of move, so treat any figure you hear as a hypothesis to test. Duration is driven by the number of repositories or modules, the depth and size of history, the amount of large binary content, the complexity of branching and labeling conventions, and how many CI/CD pipelines must be rebuilt. A pilot on a copy of one representative project is the most reliable way to measure your own timing. Ask for the pilot results, the cutover window and the rollback plan in the written scope.

What must our internal team provide for the migration?

Your team needs to supply access and knowledge that only your organization holds. The main items are:

  • Read access to the source server or depots, plus a complete inventory of projects, modules, branches and labels
  • A list of historical authors and their current GitLab accounts, so commits can be attributed correctly
  • Decisions on which branches, tags and history ranges must survive the move
  • A GitLab instance with administrator access, storage for Git LFS objects and a named owner for sign-off
  • Access to existing build and deployment systems, plus a change window agreed with the teams that depend on them

Missing identity mapping and unclear branch decisions are the most common sources of rework, so settle them before conversion starts.

Can our own engineers migrate from Perforce or TFVC without a partner?

Yes, a capable internal team can do this, especially for a small number of repositories with simple history. The work requires solid knowledge of Git history rewriting, Git LFS, your source system's branching model and GitLab administration. The risks sit in the details: incorrect author mapping, lost tags, oversized history and pipelines that fail after cutover. A partner adds value when the repository count is high, the history is complex, or the team lacks time for a dry run and a rehearsed cutover. DeployOpen plans, implements, integrates and documents self-hosted platforms in your infrastructure, and scopes this kind of work in writing before it starts. Weigh the internal effort against that scope, not against a general estimate.

How do Git LFS and history conversion choices change the outcome?

These two decisions shape storage, clone speed and what is searchable after the move. Converting full history keeps every past commit but can carry large binaries into Git. Moving large files into Git LFS keeps repositories lighter, but the LFS objects must be migrated and stored on the GitLab side, and the .gitattributes tracking rules must match the file types you want to track. Truncating history or starting from a snapshot is faster and smaller, but the old history stays only in the archived source system. Decide which trade-off applies per repository, document it, and confirm LFS storage and object handling with GitLab's documentation for your version.

Should we back up GitLab before importing history from a legacy system?

Yes. Take a full backup of the self-managed GitLab instance before the first import, and verify that you can restore it into a test environment. Imports write new projects, users and repositories into the instance, and reversing a bad import is far easier from a known-good backup than from memory. Include the secrets file and the Git repository data in your backup plan, and test the restore on the same GitLab version. GitLab's backup and restore documentation describes the procedure and the components it covers. Keep the legacy system read-only during the final cutover so no new commits land after the last conversion.

What usually goes wrong in Perforce, TFVC or CVS to GitLab moves

Most failed moves from Perforce, TFVC or CVS fail on history, identity, and process rather than on the Git hosting itself. The six risks below are the ones that tend to surface late, so each card names a prevention step to settle before the first import runs.

Lost or flattened history

Converting centralized history into Git can drop changelists, collapse commits, or lose file moves and renames if the conversion is run without checks. Before cutover, pick the conversion tool and confirm which metadata it preserves, then compare sample file histories, blame output, and tag counts between the source and the converted repository. Agree in writing which history is kept and which is archived read-only.

Mismatched author identities

Perforce users, TFVC identities, and CVS login names rarely match GitLab accounts, so commits can end up attributed to raw depot usernames or to nobody. Build an author map from the source user list before conversion, including retired staff and service accounts, and decide how unmatched identities appear. Review the map with HR or team leads so contribution history reads correctly.

Large binaries bloating the repository

Perforce depots often hold design files, compiled assets, and media that Git stores poorly if they are committed directly. Scan the source for large and binary files, decide which paths move to Git LFS and which stay in artifact or object storage, and test clone times on a realistic sample. Confirm your GitLab instance has LFS enabled and sized storage before the import, since large history rewrites are hard to undo once teams have cloned.

Branch models that do not fit Git

Perforce streams, TFVC branches, and CVS vendor branches have semantics that do not map one to one onto Git branches and tags. Document how each active codebase is branched and released today, then choose a target model such as trunk-based or release branches with protected default branches. Retire stale branches deliberately rather than importing every historical path, which keeps the GitLab project list navigable.

CI jobs that depend on depot paths

Build scripts, deployment jobs, and automation often reference depot paths such as //depot/ or TFVC $/Project locations. After the move these references break silently or point at the wrong tree. Inventory every script, build definition, and scheduled job that reads a source path, rewrite them against repository-relative paths in .gitlab-ci.yml, and run each pipeline on the new project before the old system is switched off.

Parallel runs that drift

Teams often keep committing to the legacy system while the GitLab copy is prepared, and the two histories diverge within days. Set a freeze window for each repository, define the final delta import, and state who can commit during the parallel period. Verify the cutover by comparing the last revision, file count, and tags, and keep the legacy server read-only with a documented retention period.

How DeployOpen works on this

DeployOpen plans, implements and documents self-hosted GitLab inside your own infrastructure, and migrations from Perforce, TFVC or CVS follow the same fixed-scope method. The work starts with a written scope that settles which history, branches and large files move, and what is retired.

How we scope

  • Inventory every Perforce depot, TFVC collection or CVS module with its size, branch count and binary content
  • Agree in writing which history depth, labels and users are converted and which are archived read-only
  • Confirm the Git LFS plan for large binaries, storage location and client configuration before any conversion runs
  • Engagements are fixed-scope, with deliverables and timelines agreed in writing before work starts

How we build

  • Run conversion dry runs on representative repositories and check commit counts, author mapping and tag integrity
  • Test conversion tooling against a copy of the source, then record the exact steps and versions used
  • Verify migrated repositories in a staging GitLab with branch protection, LFS objects and CI configuration checked
  • Document the cutover sequence, freeze window and rollback steps for your engineering and operations teams

How we hand over

  • Deliver a runbook covering conversion scripts, author mapping files and verification reports
  • Provide a record of which legacy history is in GitLab, which stays archived and where it lives
  • Walk your team through developer workflows for Git, LFS and the retired source control system
  • Document the self-managed GitLab backup and upgrade responsibilities that follow the migration

How we support

  • Support after cutover can be provided as a retainer covering fixes, questions and follow-up conversions
  • Agree in the written scope who answers developer questions during the first weeks after cutover
  • Confirm with your team how legacy checkouts, shelved changes and open reviews are closed out
  • Ongoing GitLab upgrades and continuous testing are available as retainers once the platform is live

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.