Gitのリポジトリ移行を Selenium(Node.js) +シェルスクリプトで全自動化した話

先日、業務で『今までGitのリポジトリはGitLabで管理していたが、今後はGitHubを使用する (色々と便利な機能が多いため。GitHub Actionsとか)。そのためのリポジトリ移行の取りまとめ役』 というタスクを任されました。

会社には1,000近いリポジトリがあり、これを全て手作業で移行しようとすると膨大な時間がかかります(ストレスめっちゃかかりそう…)。
その移行作業を Selenium(Node.js) +シェルスクリプトで全自動化する事に成功しました。


---------------------------------------
#### 前提:下記の変数の箇所は、あなたの環境に合わせて逐一書き換えてください
<dl>
  <dt>init.sh</dt>
  <dd>${あなたのsshパスワード}</dd>
  <dd>${移行元 Gitアカウント}</dd>
  <dd>${移行先 Gitアカウント}</dd>
  <dt>create_repo.js</dt>
  <dd>${あなたのGitHubアカウント名}</dd>
  <dd>${あなたのGitHubパスワード}</dd>
  <dt>ほか、使用する変数名(@init.sh)：</dt>
  <dd>①$new_repository_name:　新規に作るリポジトリ名</dd>
  <dd>②$current_repo_name: 現在使用しているリポジトリ名</dd>
  <dd>③$current_repo_path_with_namespace：　現在使用しているリポジトリのパス</dd>
</dl>

(※)GitLab上では名前空間によってリポジトリが区切られているため、②と③を分ける必要があります。
しかし、もし『GitHubからGitHubへの以降』であれば、②③が無くとも①のみで充当できるかと思います。

---------------------------------------
まず、1件ずつ、手動で移行する際には、下記の通りにやれば移行出来ます(GitHub上でのリポジトリは既に作られてるものとする)
GitHubでも、GitLabでも同じです。

```打つコマンド.
git clone --mirror git@${移行元 Gitアカウント}:$current_repo_path_with_namespace.git
cd  $current_repo_path_with_namespace
git remote rm origin
git remote add origin git@${移行先 Gitアカウント}/$new_repo_path.git
cd ..
rm -rf ".git"

```


全自動化すると下記の通りになります。init.shを見て頂ければ全容は掴めると思います。

```ディレクトリ全容.
      +- node_modules # Seleniumを使うためにnpm installしたもの
      +- init.sh # メインの処理を①-⑦の順序で実行
      |
      +- create_repo.js  # ①でSeleniumでリポジトリを自動で作成
      |
      +- repo_list.csv  # 移行するリポジトリの一覧
      |
      +- cd.sh  # ③で、cdコマンドでディレクトリを移動したい。実際に移動してしまうと、init.sh自体を読み込めなく
なるためセッション上で移動するためにファイルを別出し
```


```init.sh
while read row; do  #csvをループする
  new_repository_name=`echo ${row} | cut -d , -f 1`
  current_repo_name=`echo ${row} | cut -d , -f 2`
  current_repo_path_with_namespace==`echo ${row} | cut -d , -f 3`

  # ① create_repo.jsを起動。Seleniumを使ってリポジトリを作成
  node create_repo.js ${new_repository_name}

  # ② sshの対話処理を使って以降元のリポジトリをgit clone
  expect -c "
    set timeout 10
    spawn git clone --mirror git@${移行元 Gitアカウント}:$current_repo_path_with_namespace.git
    expect \"Enter passphrase for key '/Users/glengoyne/.ssh/id_rsa': \"
    send \"${あなたのsshパスワード}\n\"
    interact
  " 0<&9-

  # ③ 以降してきたリポジトリのディレクトリに移る
  source cd.sh $current_repo_name
  
  # ④ 移行元のリポジトリとの紐付きを解除 
  git remote rm origin

  # ⑤ 移行先のリポジトリと紐付ける
  git remote add origin git@${移行先 Gitアカウント}/$new_repo_path.git
  
  # ⑥  sshの対話処理を使って以降先のリポジトリへgit pull
  expect -c "
    set timeout 10
    spawn git push -u origin --all
    expect \"Enter passphrase for key '/Users/glengoyne/.ssh/github/id_github_rsa': \"
    send \"${あなたのsshパスワード}\n\"
    interact
  " 0<&9-

  # ⑦ ②でcloneしてきたリポジトリのディレクトリを削除
  rm -rf ".git"
  
done  9<&0- 0<repo_list.csv
```


```create_repo.js
const {Builder, By, until} = require('selenium-webdriver');

async function CreateRepo(repo_name) {
  const driver = new Builder().forBrowser('chrome').build();
  try {
    await driver.get('https://github.com/new');
    await driver.findElement(By.id('login_field')).sendKeys(`${あなたのGitHubアカウント名}`) 
    await driver.findElement(By.id('password')).sendKeys(`${あなたのGitHubパスワード}`)
    await driver.findElement(By.name('commit')).click();
    await driver.wait(until.titleIs('Create a New Repository'), 10000);
    console.log("ログイン完了")

    await driver.findElement(By.className('js-owner-container')).click()
    await driver.findElement(By.css(".select-menu-list > label:nth-child(2)")).click();
    
    await driver.findElement(By.id('repository_name')).sendKeys(repo_name)
    await (await driver.findElement(By.id('repository_visibility_private'))).click();
    await driver.sleep(1000);
    await driver.findElement(By.className('first-in-line')).click();
    await driver.wait(until.urlContains(`${移行先のGitアカウント}`), 15000); // 『Create an Repository』ボタンを押してサーバー側がちゃんと処理してくれるまで、15秒待つ
    console.log("リポジトリが完成！", repo_name)
  } finally {
    await driver.quit();
    console.log("finished!!!")
  }
};


const repo_name = process.argv.slice(2)[0]
console.log("　repo_name", repo_name)
CreateRepo(repo_name)
```

```repo_list.csv
new_repository_name1, current_repo_name1, current_repo_path_with_namespace1
new_repository_name2, current_repo_name2, current_repo_path_with_namespace2
new_repository_name3, current_repo_name3, current_repo_path_with_namespace3
new_repository_name4, current_repo_name4, current_repo_path_with_namespace4
...その下にもずーっと続く
```
``` cd.sh
cd ${current_repo_name}
```





---------------------------------------
#### リポジトリの移行方法は↑ここまで。今回の件でシェルスクリプトを書くにあたり学びとなった事を下記に記載します。


##### ①shファイルでcsvを読み込み、行(${row})毎に処理を実行する：

``` csv読み込み.sh
while read row; do 
  new_repository_name=`echo ${row} | cut -d , -f 1`
  current_repo_name=`echo ${row} | cut -d , -f 2`
  current_repo_path_with_namespace==`echo ${row} | cut -d , -f 3`
done  9<&0- 0<repo_list.csv
```

##### ②シェルスクリプト内でディレクトリを移動したい：
-実際にcdコマンドでディレクトリを移動してしまうと、init.shの後続スクリプトを読み込めなくなるので、cd.shのようにファイルを別出しする事で『セッション上のディレクトリ移動』が出来る。こうする事でinit.shの後続スクリプトを継続実行できる。

##### ③対話処理(パスワード入力など)の自動化；
expect構文を使えば実行可能。下記のスクリプトでは、set timeoutで、10秒間サーバーからの応答を待つ。

```  expect.sh
  expect -c "
    set timeout 10
    spawn git clone --mirror git@${移行元 Gitアカウント}:$current_repo_path_with_namespace.git
    expect \"Enter passphrase for key '/Users/glengoyne/.ssh/id_rsa': \"
    send \"${あなたのsshパスワード}\n\"
    interact
  "
```

##### ④ファイルディスクリプタを使用する事で、while文の中でexpectを併用する。
シェル上では、ディスクリプタ"<0"を標準入力として使っている。
while文の処理を"<0"で使っていると、expect処理が失敗してしまうため、whileの処理を0-9以外のどこかのディスクリプタに移すのが↓この部分。

```
done  9<&0- 0<repo_list.csv
``` 
expectの処理を"0から9のどこかのディスクリプタ"でやらせるのが、下記の部分

```  expect.sh
  expect -c "
  " 0<&9-
```
自分も、詳しく人に説明できるほど理解出来ていないので、何か間違いがあればコメントなど頂ければと思います。


---------------------------------------


普段フロントエンドエンジニアとして仕事している事もあり、シェルスクリプトなど今までまともに書いたことが無く、色々と学びになる事がありました。
シェルスクリプトで書ける事が増えてくると、本当に応用が効きそうです。
いつか、腰を据えて学んでみたいなと思います。