Процесс миграции нередко представляет собой трудную задачу, особенно, когда объем информации, который необходимо перенести, настолько велик, что выгоднее становится его автоматизировать. Именно необходимость миграции с Gitolite на GitLab и побудила меня написать статью о моем опыте в данном вопросе.


Для миграции репозиториев я буду использовать компьютер с установленной операционной системой CentOS 7. На ней необходимо установить следующий набор приложений: git, ssh-agent, curl, jq, xargs.


Для начала работы нам необходимо получить ключ доступа к API GitLab. В веб-интерфейсе заходим в раздел настройки пользователя. Далее выбираем пункт меню «Access Tokens». В открывшейся форме необходимо указать наименование получаемого ключа, а также можно указать срок, до которого ключ будет активен. Ниже ставим галочку напротив пункта api и нажимаем кнопку «Create personal access token». Этот ключ будет применяться при отправке запросов в API GitLab.


  1. Подготовка рабочей среды


    Для того чтобы мы могли загружать код в репозиторий GitLab по SSH, необходимо сгенерировать пару приватного и публичного ключей, выполнить регистрацию публичного ключа в GitLab. Для регистрации публичного ключа в панели администратора GitLab «Admin area» заходим в раздел «Deploy keys» и нажимаем кнопку «New deploy key». В открывшейся форме необходимо указать наименование загружаемого ключа, а также сам публичный ключ. После добавления ключа, нам необходимо определить идентификатор данного ключа в системе GitLab. Чтобы определить его мы можем выполнить следующий команду в bash:


    curl -k -X GET --header "PRIVATE-TOKEN: <your private token>" <gitlab url>/api/v4/deploy_keys

    Примечание: Чтобы не вводить пароль от приватного SSH ключа при выполнении действий git с сервером, необходимо добавить данный ключ в ssh-agent. Для этого выполним следующие команды:


    eval `ssh-agent`

    ssh-add ~/.ssh/id_rsa

  2. Создание репозитория в GitLab с использованием API


    Чтобы создать репозиторий в GitLab, необходимо отправить POST запрос в API GitLab, передав параметры, согласно документации. Подробнее о Projects API GitLab читайте в документации на сайте GitLab.


    # $1 - gitlab remote url
    # $2 - api access token
    # $3 - remote repository name
    # $4 - remote repository namespace identifier
    initRemoteRepository() {
        local response=`curl -s -k -X POST --header "Private-Token: $2" --data "name=$3&namespace_id=$4&visibility=private&description=$3" $1/api/v4/projects`
        local id=`echo $response | jq -r ".id"`
        echo "$id"
    }

    Функция initRemoteRepository возвращает идентификатор созданного репозитория. В дальнейшем идентификатор репозитория будет использоваться для установки прав доступа.
    Примечание: ключ –k необходим, если серверный сертификат GitLab является самозаверенным.


  3. Предоставление доступа на запись и чтение репозитория с использованием API


    Для того, чтобы выполнить запись в созданный репозиторий, необходимо выдать соответствующее право. Подробнее о Deploy Keys API GitLab читайте в документации на сайте GitLab.


    # $1 - remote url
    # $2 - api token
    # $3 - repository identifier
    # $4 - deploy key identifier
    enableRepositoryDeployKey() {
        local response=`curl -s -k -X POST --header "Private-Token: $2" $1/api/v4/projects/$3/deploy_keys/$4/enable`
        echo "$response"
    }

    Функция enableRepositoryDeployKey позволяет добавить публичный ключ с соответствующим идентификатором в список ключей соответствующего репозитория.


    # $1 - remote url
    # $2 - api token
    # $3 - repository identifier
    # $4 - deploy key identifier
    setRepositoryWriteAccess() {
        local response=`curl -s -k -X PUT --header "PRIVATE-TOKEN: $2" --header "Content-Type: application/json" --data '{"can_push": true}' $1/api/v4/projects/$3/deploy_keys/$4`
        echo "$response"
    }

    Функция setRepositoryWriteAccess позволяет установить доступ на запись в конкретный репозиторий.


  4. Миграция репозиториев включая все ветки


    Основной скрипт проходит по предварительно составленному списку репозиториев, указанных в заданном файле. Создается соответствующий репозиторий в GitLab, а также выдаются привилегии для чтения и записи. После подготовительных действий выполняется запрос веток данного репозитория. Далее выполняется переключение на очередную ветку репозитория и отправка в GitLab.


    # $1 - branch
    # $2 – separator
    getCanonicalBranchName() {
        local branch=""
        IFS=$2 read -r -a array <<< "$1"
        length=${#array[@]}
        for index in "${!array[@]}"
        do
            if [ $index -gt 0 ]; then
                if [ $index -eq 1 ]; then
                    local branch="${array[index]}"
                else
                    local branch="$branch/${array[index]}"
                fi
            fi
        done
        echo "$branch"
    }
    PSWD="$(dirname "$0")"
    cd $PSWD
    while IFS= read -r REPOSITORY
    do
        echo "====================  <$REPOSITORY>  ===================="
        GITOLITE_REPO=$GITOLITE$REPOSITORY
        echo "Run cloning remote repository $GITOLITE_REPO"
        git clone $GITOLITE_REPO
        cd $REPOSITORY
        GITLAB_REPO=$GITLAB$REPOSITORY.git
        echo "Initialize remote gitgab repository $GITLAB_REPO"
        PROJECT=$(initRemoteRepository $GITLAB_URL $API_TOKEN $REPOSITORY)
        echo "Remote repository initialized identifier $PROJECT"
        echo "Add deploy key and enable write access to remote repository $REPOSITORY ($PROJECT)"
        response=$(enableRepositoryDeployKey $GITLAB_URL $API_TOKEN $PROJECT $DEPLOY_KEY_ID)
        response =$(setRepositoryWriteAccess $GITLAB_URL $API_TOKEN $PROJECT $DEPLOY_KEY_ID)
        echo "Add new remote url for repository $GITLAB_REPO"
        git remote add gitlab $GITLAB_REPO
        # push all branches
        IFS_BACK=$IFS
        IFS=$'\n'
        branches=$(git branch -r)
        for branchName in $branches; do
            trimBranchName=`echo $branchName | xargs`
            canonicalBranchName=$(getCanonicalBranchName $trimBranchName '/')
            echo "$trimBranchName ($canonicalBranchName) init push"
            git checkout -b $canonicalBranchName remotes/origin/$canonicalBranchName
            git push -f gitlab $canonicalBranchName
        done
        IFS=$IFS_BACK
        cd ..
        rm -r -f $REPOSITORY
        echo "====================  <$REPOSITORY>  ===================="
        echo ""
    done < "$REPOSITORIES"

    Сохраняем файл скрипта и запускаем, при необходимости изменив права доступа. Пример вывода работы данного скрипта можно посмотреть ниже.



Полный текст скрипта
REPOSITORIES="<repositories_file>"
GITOLITE="<gitolite_ssh_url>"
GITLAB_URL="<gitlab_api_url>"
GITLAB="<gitolite_ssh_url>"
API_TOKEN="<api_access_token>"
DEPLOY_KEY_ID="<deploy_key_identifier>"
# $1 - gitlab remote url
# $2 - api access token
# $3 - remote repository name
# $4 - remote repository namespace identifier
initRemoteRepository() {
        local response=`curl -s -k -X POST --header "Private-Token: $2" --data "name=$3&namespace_id=$4&visibility=private&description=$3" $1/api/v4/projects`
        local id=`echo $response | jq -r ".id"`
        echo "$id"
}
# $1 - remote url
# $2 - api token
# $3 - repository identifier
# $4 - deploy key identifier
setRepositoryWriteAccess() {
    local response=`curl -s -k -X PUT --header "PRIVATE-TOKEN: $2" --header "Content-Type: application/json" --data '{"can_push": true}' $1/api/v4/projects/$3/deploy_keys/$4`
    echo "$response"
}
# $1 - remote url
# $2 - api token
# $3 - repository identifier
# $4 - deploy key identifier
enableRepositoryDeployKey() {
    local response=`curl -s -k -X POST --header "Private-Token: $2" $1/api/v4/projects/$3/deploy_keys/$4/enable`
    echo "$response"
}
# $1 - branch
# $2 – separator
getCanonicalBranchName() {
    local branch=""
    IFS=$2 read -r -a array <<< "$1"
    length=${#array[@]}
    for index in "${!array[@]}"
    do
        if [ $index -gt 0 ]; then
            if [ $index -eq 1 ]; then
                local branch="${array[index]}"
            else
                local branch="$branch/${array[index]}"
            fi
        fi
    done
    echo "$branch"
}
PSWD="$(dirname "$0")"
cd $PSWD
while IFS= read -r REPOSITORY
do
    echo "====================  <$REPOSITORY>  ===================="
    GITOLITE_REPO=$GITOLITE$REPOSITORY
    echo "Run cloning remote repository $GITOLITE_REPO"
    git clone $GITOLITE_REPO
    cd $REPOSITORY
    GITLAB_REPO=$GITLAB$REPOSITORY.git
    echo "Initialize remote gitgab repository $GITLAB_REPO"
    PROJECT=$(initRemoteRepository $GITLAB_URL $API_TOKEN $REPOSITORY)
    echo "Remote repository initialized identifier $PROJECT"
    echo "Add deploy key and enable write access to remote repository $REPOSITORY ($PROJECT)"
    response=$(enableRepositoryDeployKey $GITLAB_URL $API_TOKEN $PROJECT $DEPLOY_KEY_ID)
    response=$(setRepositoryWriteAccess $GITLAB_URL $API_TOKEN $PROJECT $DEPLOY_KEY_ID)
    echo "Add new remote url for repository $GITLAB_REPO"
    git remote add gitlab $GITLAB_REPO
    # push all branches
    IFS_BACK=$IFS
    IFS=$'\n'
    branches=$(git branch -r)
    for branchName in $branches; do
        trimBranchName=`echo $branchName | xargs`
        canonicalBranchName=$(getCanonicalBranchName $trimBranchName '/')
        echo "$trimBranchName ($canonicalBranchName) init push"
        git checkout -b $canonicalBranchName remotes/origin/$canonicalBranchName
        git push -f gitlab $canonicalBranchName
    done
    IFS=$IFS_BACK
    cd ..
    rm -r -f $REPOSITORY
    echo "====================  <$REPOSITORY>  ===================="
    echo ""
done < "$REPOSITORIES"

Пример вывода в консоль
====================  <project>  ====================
Run cloning remote repository git@skynet-uro.bank.srv:project
Cloning into 'project'...
remote: Counting objects: 62, done.
remote: Compressing objects: 100% (61/61), done.
remote: Total 62 (delta 21), reused 0 (delta 0)
Receiving objects: 100% (62/62), 15.57 KiB | 0 bytes/s, done.
Resolving deltas: 100% (21/21), done.
Current folder path: /app/migration/project
Initialize remote gitgab repository git@127.0.0.1:project.git
Remote repository initialized identifier 222
Enable deploy key write access to remote repository project (222)
Set write access to remote repository project (222)
Add new remote url for repository git@127.0.0.1:project.git
Push to new remote gitlab repository git@127.0.0.1:project.git
Counting objects: 55, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (38/38), done.
Writing objects: 100% (55/55), 14.65 KiB | 0 bytes/s, done.
Total 55 (delta 18), reused 50 (delta 16)
To git@127.0.0.1:project.git
 * [new branch]      master -> master
<--- gitlab/master (master) init push
fatal: A branch named 'master' already exists.
Everything up-to-date
---> gitlab/master (master) success pushed
<--- origin/v0.0.0 (v0.0.0) init push
Branch v0.0.0 set up to track remote branch v0.0.0 from origin.
Switched to a new branch 'v0.0.0'
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: To create a merge request for v0.0.0, visit:
remote:   https://127.0.0.1:8082/project/merge_requests/new?merge_request%5Bsource_branch%5D=v0.0.0
remote:
To git@127.0.0.1:project.git
 * [new branch]      v0.0.0 -> v0.0.0
---> origin/v0.0.0 (v0.0.0) success pushed
<--- origin/v0.0.0-13 (v0.0.0-13) init push
Branch v0.0.0-13 set up to track remote branch v0.0.0-13 from origin.
Switched to a new branch 'v0.0.0-13'
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: To create a merge request for v0.0.0-13, visit:
remote:   https://127.0.0.1:8082/project/merge_requests/new?merge_request%5Bsource_branch%5D=v0.0.0-13
remote:
To git@127.0.0.1:project.git
 * [new branch]      v0.0.0-13 -> v0.0.0-13
---> origin/v0.0.0-13 (v0.0.0-13) success pushed
<--- origin/v0.0.0-15 (v0.0.0-15) init push
Branch v0.0.0-15 set up to track remote branch v0.0.0-15 from origin.
Switched to a new branch 'v0.0.0-15'
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: To create a merge request for v0.0.0-15, visit:
remote:   https://127.0.0.1:8082/project/merge_requests/new?merge_request%5Bsource_branch%5D=v0.0.0-15
remote:
To git@127.0.0.1:project.git
 * [new branch]      v0.0.0-15 -> v0.0.0-15
---> origin/v0.0.0-15 (v0.0.0-15) success pushed
<--- origin/v0.0.1 (v0.0.1) init push
Branch v0.0.1 set up to track remote branch v0.0.1 from origin.
Switched to a new branch 'v0.0.1'
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: To create a merge request for v0.0.1, visit:
remote:   https://127.0.0.1:8082/project/merge_requests/new?merge_request%5Bsource_branch%5D=v0.0.1
remote:
To git@127.0.0.1:project.git
 * [new branch]      v0.0.1 -> v0.0.1
---> origin/v0.0.1 (v0.0.1) success pushed
<--- origin/v0.0.1-11 (v0.0.1-11) init push
Branch v0.0.1-11 set up to track remote branch v0.0.1-11 from origin.
Switched to a new branch 'v0.0.1-11'
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: To create a merge request for v0.0.1-11, visit:
remote:   https://127.0.0.1:8082/project/merge_requests/new?merge_request%5Bsource_branch%5D=v0.0.1-11
remote:
To git@127.0.0.1:project.git
 * [new branch]      v0.0.1-11 -> v0.0.1-11
---> origin/v0.0.1-11 (v0.0.1-11) success pushed
<--- origin/v0.0.2 (v0.0.2) init push
Branch v0.0.2 set up to track remote branch v0.0.2 from origin.
Switched to a new branch 'v0.0.2'
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: To create a merge request for v0.0.2, visit:
remote:   https://127.0.0.1:8082/project/merge_requests/new?merge_request%5Bsource_branch%5D=v0.0.2
remote:
To git@127.0.0.1:project.git
 * [new branch]      v0.0.2 -> v0.0.2
---> origin/v0.0.2 (v0.0.2) success pushed
<--- origin/v0.0.3 (v0.0.3) init push
Branch v0.0.3 set up to track remote branch v0.0.3 from origin.
Switched to a new branch 'v0.0.3'
Counting objects: 7, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 592 bytes | 0 bytes/s, done.
Total 4 (delta 3), reused 1 (delta 0)
remote:
remote: To create a merge request for v0.0.3, visit:
remote:   https://127.0.0.1:8082/project/merge_requests/new?merge_request%5Bsource_branch%5D=v0.0.3
remote:
To git@127.0.0.1:project.git
 * [new branch]      v0.0.3 -> v0.0.3
---> origin/v0.0.3 (v0.0.3) success pushed
====================  <project>  ====================