Colabからfastbookのサンプルコード・リポジトリをGoogleドライブにclone/pullする

Googleドライブに[fastbookのサンプルコード](https://github.com/fastai/fastbook)をアップロードする方法は他にもありますが、ここはかなり頻繁に更新されており、PCやGoogleドライブとColabとを行き来するのは面倒なので、Colabに居ながらGitを使ってGoogleドライブにcloneとpullできるコードを書いてみました。

**今回の更新では、gitリポジトリのGoogleドライブへのcloneとpullを関数化したコードを追加しました。**

**※** Googleドライブのマウントに関しては、k_uekadoさんの記事に「[ColaboratoryでのGoogle Driveへのマウントが簡単になっていたお話](https://qiita.com/k_uekado/items/45b76f9a6f920bf0f786)」というのがありました。この記事にあるように、**現在ではColabのGUIでマウントできるようになっています**。
**※** GUIを使うGoogleドライブのマウントに関しては、GGCSごたごた気流調査所さんの「[Google Colab/Drive の連携 [1/2]: Magic Commands 篇](https://ggcs.io/2020/12/04/google-colab-drive-01/)」というのもありました。画面のイメージを使って分かり易い説明があります。**これを見ていただければ、下記の「Googleドライブのマウント」の部分は読み飛ばしても結構です。**
## Googleドライブのマウント
Googleドライブにアクセスしないといけないので、まずはGoogleドライブをマウントしておきます。

```python:
from google.colab import drive
drive.mount('/content/drive')
```
これを実行すると認証コードの入力が求められます。詳細は私の記事「[fastbookのサンプルコード(02_production.ipynb)をGoogle Colabで試してみた Part 2](https://qiita.com/kumagorou/items/dbc63e9706f6729051be)」を参照してください。実行が正常に終了すると「Mounted at /content/drive」と表示されます。
### driveディレクトリの中身を確認する
次の画像は実行結果の例です。ドライブの内容は左側のペーンに表示されています。~~何故かカレントディレクトリを`!ls`だけで見ても、マウントした「drive」が表示されていないこともありましたが、`!ls drive`としてdriveを直接指定して確認するとちゃんと中身が確認できました。~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407748/93fa1c60-b9c9-74fe-ed1e-abe503c783e0.png)
driveディレクトリの直下には「MyDrive」というディレクトリがあります。実は、そこがGoogleドライブに対応しているようなので、Googleドライブのファイルにアクセスする場合は注意してください。
## ローカルリポジトリの作成(clone)と更新(pull)
fast.aiが提供しているフリーコースのマテリアルである[fastbookのサンプルコード](https://github.com/fastai/fastbook)は、コースに合わせてかなり頻繁に更新されているので、最新のものをアクセスするには必要に応じてGoogleドライブに持っているコピーを更新する必要があります。

そのためのツールとして、Googleドライブの所定のディレクトリにサンプルコードのローカルリポジトリを作成したり更新したりするコードを書いてみました。このコードではローカルリポジトリの作成と更新の両方ができるようにしてあります。

```python:
import os

# fast.aiのfastbookリポジトリを参照するためのURI文字列
git_remote = 'https://github.com/fastai/fastbook.git'
git_local  = 'drive/MyDrive/Colab\ Notebooks/fastai' # gitコマンド用のURI文字列
git_local_ = 'drive/MyDrive/Colab Notebooks/fastai'  # python用のURI文字列

# 既存のローカルリポジトリを削除するか否か？
do_force_remove = False #@param {type: "boolean"}
if do_force_remove:
  !rm -rf $git_local

# ローカルリポジトリを更新する
if not do_force_remove and os.path.exists(git_local_):
  print('git pull')
  !git -C $git_local pull
# 新規にローカルリポジトリを作る
else:
  print('git clone')
  !git clone $git_remote $git_local
```
このコードにあるように、ローカルリポジトリを置くディレクトリは「drive/MyDrive/Colab Notebooks/fastai」にしてありますが、場所は自分の好きなところにしてOKです。
### 作成(clone)か更新(pull)かの判定
作成か更新かの判定は既存のローカルリポジトリが存在するか否かで判定します。
また、既存のローカルリポジトリを強制削除することで、作成(clone)にすることも可能にしてあります。それにはGoogle colabのForm機能を使って、コードを直接書き換えなくて済むようにしています。

```python:
do_force_remove = False #@param {type: "boolean"}
if do_force_remove:
  !rm -rf $git_local
```
Form機能についてはstakemuraさんの「[コメント一行でUI生成。Form機能を使った一歩先のGoogle Colab活用術](https://qiita.com/stakemura/items/4b3a16c4f59590db067a)」を参照ください。

また、コードを非表示にしたい場合は、コードセルの上でマウスボタンを右クリックして「フォーム」→「コードを非表示」を選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407748/9cd917f7-fd2a-87a1-4053-88abd14b5aab.png)
コードを非表示にして既存のローカルリポジトリを強制削除指定で実行した結果は次のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407748/edcc97c5-eede-b617-994d-97dbeeee4f62.png)
同じく、強制削除Offで実行するとこのようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407748/606bd430-8175-31b2-802c-83d00266b4eb.png)
### gitコマンドの引数についての注意
rmやgitはシステムコマンドなので引数には注意が必要です。今回の場合、ローカルリポジトリの場所のURIには、次のように途中に空白スペースが混じっています。```
/drive/MyDrive/Colab Notebooks/fastai
```
そのため、システムコマンド用のURI文字列とpython関数用のURI文字列をそれぞれ用意する必要がありました。システムコマンドの場合、空白スペースはバックスラッシュ(円記号)でクォーテーションしないとエラーになりました。(環境により異なるかもしれませんが)

```python:
# fast.aiのfastbookリポジトリを参照するためのURI文字列
git_remote = 'https://github.com/fastai/fastbook.git'
git_local  = 'drive/MyDrive/Colab\ Notebooks/fastai' # gitコマンド用のURI文字列
git_local_ = 'drive/MyDrive/Colab Notebooks/fastai'  # python用のURI文字列
```
pythonの変数を使ってシステムコマンドに引数を与えるには、'$変数名'というように変数名の前に`$`を加えます。なお、この設定では、カレントディレクトリが「/content」である前提ですが、そうでない場合は'/content/drive/MyDrive/Colab Notebooks/fastai'としておくべきでしょう。

```python:
!rm -rf $git_local
!git -C $git_local pull
!git clone $git_remote $git_local
```
また、更新(git pull)の場合は、カレントディレクトリを移動させずに使うために'-C'オプションでターゲットのディレクトリを指定しました。そうすれば実行の前後でカレントディレクトリが変わってしまうことを防げます。

※ gitコマンドの使い方については、株式会社クレイのサイトに掲載されている「[git pull と git pull –rebase の違いって？図を交えて説明します！](https://kray.jp/blog/git-pull-rebase/)」を参考にしました。
※ [GitPython](https://gitpython.readthedocs.io/en/stable/tutorial.html)なんてのがあったんですね！　ディレクトリの再帰的削除も`shutil.rmtree()`というのがあるので、そもそもシステムコマンド直呼びせず、すべてPythonで書けそうですね。
## 関数化してみた
上記のことは、ややこしいし綺麗じゃないので、いっそノートブックのセルに書くことを関数化してみました。そうすればURIの文字列はPythonの書き方だけで済みます。それが下記のコードです。URIは引数で渡すことになるので、自由度も高くなります。(結果の表示がちょっとサッパリし過ぎかもです！)
※ シェルの呼び出しの実装には`subprocess`を使いました。

```python:
import os
import subprocess

def git_clone_or_pull(git_remote, git_local, do_force_remove=False):
    # 既存のローカルリポジトリを削除するか否か？
    if do_force_remove:
        print(f'Removing directory:{git_local}')
        cp = subprocess.run( ["rm", "-rf", git_local], capture_output=True, text=True )
        print(f'Args:{cp.args}\nReturn code:{cp.returncode}\nOutput:{cp.stdout} ')

    # ローカルリポジトリがすでに存在するなら更新する
    if not do_force_remove and os.path.exists(git_local):
        print('git pull')
        cp = subprocess.run( ["git", "-C", git_local, "pull"], capture_output=True, text=True )
        print(f'Args:{cp.args}\nReturn code:{cp.returncode}\nOutput:{cp.stdout} ')
    # 新規にローカルリポジトリを作る
    else:
        print('git clone')
        cp = subprocess.run( ["git", "clone", git_remote, git_local], capture_output=True, text=True )
        print(f'Args:{cp.args}\nReturn code:{cp.returncode}\nOutput:{cp.stdout} ')
```
次のコードは、この関数の利用例です。やってることは、セルに書いた先ほどのものと同じです。ただし、Pythonのバージョンが3.7以上でないと`subprocess.run`の引数の指定方法を変えないといけないかもしれません。

```python:
import sys

sys.path.append('/content/drive/MyDrive/Colab Notebooks/FastAI')
import Part2.git_utils as git_utils

# fast.aiのfastbookリポジトリを参照するためのURI文字列
git_remote = 'https://github.com/fastai/fastbook.git'
git_local = '/content/drive/MyDrive/Colab Notebooks/fastai'

# git_localへのCloneまたはPull
git_utils.git_clone_or_pull(git_remote, git_local)

!ls '$git_local'
```
ちょっと面倒なのは、定義した関数を含むモジュールをインポートするときのパスの指定ですが、この点の詳細は下記の参考サイトを見てください。

ちなみに、Drive 上のディレクトリやファイルのパスは、**わざわざ手打ちしなくても、GUIベースで左側のペーンでディレクトリ/ファイル名を右クリックしてコピペできます。**
## 参考にしたサイト
[Google Colab/Drive の連携 [1/2]: Magic Commands 篇](https://ggcs.io/2020/12/04/google-colab-drive-01/)
[Google Colab/Drive の連携 [2/2]: subprocess 篇](https://ggcs.io/2020/12/06/google-colab-drive-02/)
[note.nkmk.me:Pythonでimportの対象ディレクトリのパスを確認・追加（sys.pathなど）](https://note.nkmk.me/python-import-module-search-path/)