Skip to content

Migrating an SVN project to Git with all branches, tags, and authors

This guide is intended for the standard SVN project structure:

project/
├── trunk/
├── branches/
│   ├── develop/
│   └── release-1.0/
└── tags/
    ├── v1.0.0/
    └── v1.1.0/

For the import, use the project root address:

https://svn.example.com/project

Do not specify only the main branch address:

https://svn.example.com/project/trunk

If trunk is specified, the other branches and tags will not be imported.

1. Start the sh shell

If the current shell is zsh/fish or another shell, start sh:

sh

After finishing the work, you can exit sh with the command:

exit

2. Set migration parameters

Specify the SVN project root address:

export SVN_URL="https://svn.example.com/project"

Specify the SVN username:

export SVN_USER="svn-user"

Specify the working directory:

export MIGRATION_DIR="$HOME/svn-migration"

Specify the name of the local project directory:

export PROJECT_NAME="project-full-migration"

Specify the path to the author mapping file:

export AUTHORS_FILE="$MIGRATION_DIR/authors.txt"

Specify the address of the target Git repository:

export GIT_URL="https://git.example.com/company/project.git"

Check the parameters:

printf 'SVN: %s\nGit: %s\nКаталог: %s\nАвторы: %s\n' "$SVN_URL" "$GIT_URL" "$MIGRATION_DIR/$PROJECT_NAME" "$AUTHORS_FILE"

3. Check the required tools

Check SVN:

svn --version --quiet

Check Git:

git --version

Check git svn:

git svn --version

Check Python:

python3 --version

Python will be used only to generate the authors.txt file.

4. Check the SVN project structure

Check the project root:

svn ls "$SVN_URL" --username "$SVN_USER"

Expected result:

branches/
tags/
trunk/

Check the branches:

svn ls "$SVN_URL/branches" --username "$SVN_USER"

Check the tags:

svn ls "$SVN_URL/tags" --username "$SVN_USER"

If the trunk, branches, and tags directories are missing from the project root, this guide is not suitable for the project structure.

5. Create the working directory

Create the migration directory:

mkdir -p "$MIGRATION_DIR"

Go to it:

cd "$MIGRATION_DIR"

Check the current directory:

pwd

6. Get the SVN log

Export the project log in XML format:

svn log --xml "$SVN_URL" --username "$SVN_USER" > svn-log.xml

Check that the file has been created:

ls -lh svn-log.xml

Check the beginning of the file:

head -n 10 svn-log.xml

The file must contain elements with author names:

<author>andrey</author>

7. Create the script for generating authors.txt

Create the file:

nano create-authors.sh

Add the following to it:

#!/bin/sh

set -eu

INPUT_FILE="${1:-svn-log.xml}"
OUTPUT_FILE="${2:-authors.txt}"

if [ ! -f "$INPUT_FILE" ]; then
    echo "Не найден XML-файл журнала SVN: $INPUT_FILE" >&2
    exit 1
fi

python3 - "$INPUT_FILE" "$OUTPUT_FILE" <<'PY'
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

input_file = Path(sys.argv[1])
output_file = Path(sys.argv[2])

try:
    tree = ET.parse(input_file)
except ET.ParseError as error:
    print(f"Не удалось разобрать XML-файл: {error}", file=sys.stderr)
    raise SystemExit(1)

authors = {
    element.text.strip()
    for element in tree.findall(".//author")
    if element.text and element.text.strip()
}

output_file.parent.mkdir(parents=True, exist_ok=True)

with output_file.open("w", encoding="utf-8", newline="\n") as output:
    for author in sorted(authors, key=str.casefold):
        output.write(f"{author} = {author} <{author}@svn.local>\n")

print(f"Найдено SVN-пользователей: {len(authors)}")
print(f"Создан файл: {output_file}")
PY

Make the script executable:

chmod +x create-authors.sh

Run it:

./create-authors.sh svn-log.xml "$AUTHORS_FILE"

8. Edit authors.txt

Check the created file:

cat "$AUTHORS_FILE"

Initially, it will look approximately as follows:

admin = admin <admin@svn.local>
andrey = andrey <andrey@svn.local>
ivanov = ivanov <ivanov@svn.local>

Open the file:

nano "$AUTHORS_FILE"

Specify real names and email addresses:

admin = SVN Administrator <svn-admin@example.com>
andrey = Андрей Иванов <andrey@example.com>
ivanov = Иван Иванов <ivanov@example.com>

Format of each line:

SVN_LOGIN = Имя пользователя <email@example.com>

The left part must exactly match the author name in SVN. Only the name and email address in the right part should be changed.

Save a backup copy:

cp "$AUTHORS_FILE" "$AUTHORS_FILE.backup"

9. Import the entire SVN project

Make sure the project directory does not exist yet:

test ! -e "$MIGRATION_DIR/$PROJECT_NAME" && echo "Каталог свободен" || echo "Каталог уже существует"

If the directory remains from a previous attempt, rename it:

mv "$MIGRATION_DIR/$PROJECT_NAME" "$MIGRATION_DIR/$PROJECT_NAME-old"

Go to the working directory:

cd "$MIGRATION_DIR"

Start the import:

git svn clone "$SVN_URL" --stdlayout --prefix=svn/ --authors-file="$AUTHORS_FILE" --username="$SVN_USER" --log-window-size=1000 "$PROJECT_NAME"

The command will automatically create a local Git repository and import:

  • trunk;
  • all directories from branches;
  • all directories from tags;
  • commit history;
  • commit dates and messages;
  • authors from authors.txt.

Initializing Git inside SVN is not required.

If the import stops because of an unknown author, add them to authors.txt:

printf '%s\n' 'unknown-user = Unknown User <unknown-user@example.com>' >> "$AUTHORS_FILE"

Go to the created repository:

cd "$MIGRATION_DIR/$PROJECT_NAME"

Continue the import:

git svn fetch

If the import is interrupted because of a network error:

git svn fetch --log-window-size=100

10. Check the imported repository

Go to the project directory:

cd "$MIGRATION_DIR/$PROJECT_NAME"

Check that this is a Git repository:

git rev-parse --is-inside-work-tree

Expected result:

true

Check the git svn settings:

git config --get-regexp '^svn-remote\.'

Check the imported SVN references:

git branch -r

The result should look approximately as follows:

svn/trunk
svn/develop
svn/release-1.0
svn/tags/v1.0.0
svn/tags/v1.1.0

11. Create the main branch main

Create the local main branch from SVN trunk:

git switch -C main refs/remotes/svn/trunk

Check the current branch:

git branch --show-current

Expected result:

main

Check the latest commits:

git log --oneline --decorate -10

12. Create the branch migration script

Create the file:

nano create-branches.sh

Add the following to it:

#!/bin/sh

set -eu

git for-each-ref --format='%(refname)' refs/remotes/svn/ |
while IFS= read -r ref
do
    branch="${ref#refs/remotes/svn/}"

    case "$branch" in
        trunk|tags/*|*@*)
            continue
            ;;
    esac

    if ! git check-ref-format "refs/heads/$branch"
    then
        echo "Недопустимое имя Git-ветки: $branch" >&2
        continue
    fi

    git update-ref "refs/heads/$branch" "$ref"
    echo "Создана или обновлена Git-ветка: $branch"
done

Make the script executable:

chmod +x create-branches.sh

Run it:

./create-branches.sh

The script skips:

  • trunk, because the main branch has already been created from it;
  • the tags directory;
  • technical references such as branch@123.

13. Check local branches

Show the created branches:

git branch --list

Expected result:

develop
* main
release-1.0

Show only short branch names:

git for-each-ref --format='%(refname:short)' refs/heads/

Compare them with the list of branches in SVN:

svn ls "$SVN_URL/branches" --username "$SVN_USER"

Git must additionally contain the main branch created from trunk.

14. Create the tag migration script

Create the file:

nano create-tags.sh

Add the following to it:

#!/bin/sh

set -eu

git for-each-ref --format='%(refname)' refs/remotes/svn/tags/ |
while IFS= read -r ref
do
    tag="${ref#refs/remotes/svn/tags/}"

    case "$tag" in
        *@*)
            continue
            ;;
    esac

    if ! git check-ref-format "refs/tags/$tag"
    then
        echo "Недопустимое имя Git-тега: $tag" >&2
        continue
    fi

    git tag -f "$tag" "$ref"
    echo "Создан или обновлён Git-тег: $tag"
done

Make the script executable:

chmod +x create-tags.sh

Run it:

./create-tags.sh

15. Check Git tags

Show the created tags:

git tag --list

Expected result:

v1.0.0
v1.1.0

Check a specific tag:

git show v1.0.0

Compare with the list of tags in SVN:

svn ls "$SVN_URL/tags" --username "$SVN_USER"

16. Check history and authors

Show the graph of all branches and tags:

git log --all --graph --decorate --oneline

Check authors:

git log --all --format='%an <%ae>' | sort -u

Compare them with the file:

cat "$AUTHORS_FILE"

Check for the connection with SVN revisions:

git log --all --grep='git-svn-id:' --oneline

Check repository integrity:

git fsck --full

17. Connect the remote Git repository

It is recommended to create the target Git repository empty:

  • without README;
  • without .gitignore;
  • without a license;
  • without an initial commit.

Check the current remotes:

git remote -v

If origin is missing, add it:

git remote add origin "$GIT_URL"

If origin already exists, replace its address:

git remote set-url origin "$GIT_URL"

Check the result:

git remote -v

18. Check push without publishing

Check the main branch:

git push --dry-run origin main

Check all local branches:

git push --dry-run origin --all

Check all tags:

git push --dry-run origin --tags

The command with the --dry-run parameter checks the push but does not change the remote repository.

19. Push all branches and tags

Push the main branch:

git push -u origin main

Push all local branches:

git push origin --all

Push all tags:

git push origin --tags

The git push origin --all command pushes only branches. Tags must be pushed with a separate command.

20. Check the remote Git repository

Show published branches:

git ls-remote --heads origin

Show published tags:

git ls-remote --tags origin

The remote Git project must contain:

main
develop
release-1.0
v1.0.0
v1.1.0

Also check the following in the Git platform interface:

  • main branch;
  • commit history;
  • authors;
  • branch list;
  • tag list;
  • file contents in each current branch.

21. Perform final synchronization

If new commits appeared in SVN after the initial import, put the SVN project into read-only mode before the final switch.

Go to the local repository:

cd "$MIGRATION_DIR/$PROJECT_NAME"

Fetch the latest SVN revisions:

git svn fetch

Update main to the latest state of SVN trunk:

git switch main
git reset --hard refs/remotes/svn/trunk

Update local branches again:

./create-branches.sh

Update tags again:

./create-tags.sh

Check the history:

git log --all --graph --decorate --oneline

Push updated branches:

git push origin --all

Push new tags:

git push origin --tags

Check the remote repository:

git ls-remote --heads origin
git ls-remote --tags origin

After checking the remote Git repository, SVN should remain in read-only mode for an agreed period. It is recommended to keep the SVN backup, authors.txt, svn-log.xml, and the local migration directory until the successful switch is finally confirmed.

Automated translation!

This page has been automatically translated. The text may contain inaccuracies.