コンテンツにスキップ

バージョン管理(Git/GitHub)

バージョン管理とは、ファイルの更新履歴を記録する仕組みです。ソースコードのバックアップとして使えます。 ここではバージョン管理の基本の基本について紹介します。バージョン管理は現代のソフトウェア開発において必須の技術です。これから先、授業や研究でコーディングする際は、常にバージョン管理をすることを強くオススメします。バージョン管理は全ての学生が知っておいて良い概念ですが、比較的新しい技術なので、 これまでは必修の講義で教える機会がありませんでした。 なので、電気系学生必修のこのソフト1の講義で少し触れておくことになりました。

有益な資料:

  • ドットインストールのGitの回: 短い動画でGitを紹介しています。これを眺めて雰囲気を知るといいかもしれません。
  • Git/gitlabで共同作業をするための最小限の知識:田浦先生による3年生向けの選択実験の「大規模ソフトウェアを手探る」の講義資料です。特に多人数での協調作業について詳しく説明されており、非常に有益です。また、田浦先生の講義ではコードのホスト先としてGitLabという仕組みを使っています。一方、本講義(ソフトウェア1)ではGitHubを使います。このあたりの差が、本講義を読んだあと田浦先生の資料を読むと明らかになり、面白いと思います。
  • ソフトウェア工学 [08 バージョン管理], [13 GitHub]:名工大の玉木徹先生による講義資料です。本講義の内容をさらに一段深く理解したいという方におすすめです。また、バージョン管理に限らず、このソフトウェア工学の講義資料はコンテナやCI/CDなど現代的な開発の基礎が丁寧に解説されており、必読です。
  • GitHub演習: 慶應大学渡辺先生によるGitおよびGitHubに関する講義の資料です。Gitコマンドの裏側で何が起きているかを含めた非常にわかりやすく詳細で包括的な解説です。
  • The Missing Semester of Your CS Education, MIT EECS [英語版][日本語版]:MITの講義のビデオと資料です。今日説明するGitをはじめ、情報系学生が知っておくべき現代的なtoolの使い方がまとめられています。こういった知識は先輩から伝授されたりしてだんだん学んでいくものですが、体系的に知っていれば効率的に物事を進められると思います。卒論を開始するまでにこれを全部見ておくと、卒論以降が効率的になると思います。情報系以外の学生にもむしろオススメです。日本語版も存在します。日本語版の翻訳は松井が取りまとめました。2020年のソフ1講義をとった学生さんも翻訳に手伝ってくれました。

コラム

ここでは基礎の基礎だけ教えます。「個人で自分のコードのバックアップをとる」ことを目指します。

Gitの根幹を成す重要な概念として「ブランチ」がありますが、本講義では触りしか説明しません。個人でバックアップする分には、全て「mainブランチ」というデフォルト設定で行います。

最後に少しだけ、ペアプログラミング(一つのリポジトリを複数人で編集する)を説明します。その際、少しだけブランチについて触れます。

バージョン管理とは何か?

バージョン管理とは、一言でいえばテキストファイルの更新履歴を記録してくれる仕組みです。 普通、main.cを更新して上書き保存すると、その最新の状態しか残りません。過去のどの時点でどういう変更がされたかを記録するのがバージョン管理システムです。 歴史的にはsubversionやCVSなど色々あったのですが、近年はgitという仕組みがデファクトスタンダードになっています。 gitはコマンド名であり、様々なgitコマンドが準備されています。これにより、手元のファイルをバージョン管理できます。

そして、手元でgitでバージョン管理されたファイルに対して、それをウェブ上にアップロードしてウェブサーバ上に保存してくれるサービスが存在します。これがGitHubなどです。 このGit + GitHubの仕組みを図示したものが下記です。

バージョン管理(+GitHub保存)が無い場合は、パソコンが壊れたら全てのデータは失われてしまいます。 また、3日前のコードと4日前のコードの差を見る、といったこともできません。 そして、現在のコードはそのままに新機能を増やしたいとき、しばしばmain_最新版.cのようなファイルを作ってしまいます。これにより、ファイルがどんどん煩雑になります。

ここでバージョン管理をしたものが右図です。 ローカルの作業ディレクトリに対して、$ git initコマンドを用いて.gitというディレクトリを作ります。このディレクトリの中にはgitの作業データが入っています。 この中身は触らなくて大丈夫です。 この.gitがあるディレクトリが、gitの管理対象になります。このディレクトリ中のファイルに対して、$ git add$ git commitといったコマンド を用いて変更履歴を保存します。

そして、その内容をGitHubのウェブ上のリポジトリに同期させます。これにより、コードのマスター情報はGitHub上に保存されます。これは安全なバックアップにもなります。 そして今後、ローカル側で作業してファイルを更新したときは、それを$ git pushという操作でGitHubのリポジトリに反映させます。 逆に、GitHubリポジトリ上で更新があった場合は、それを$ git pullというコマンドで手元にもってこれます。

ここで重要なのはバージョン管理という概念を学ぶことです。 具体的なプログラム(Git/GitHub)の使い方を知ることは副次的なものです(とはいえ、今日の講義は実質ほとんどGit/GitHubの使い方の説明ですが)。 バージョン管理は情報の変更という概念をプログラマブルに扱う仕組みです。この考え方は非常に重要です。 近年の例だと、infrastructure as codeといって、 サーバなどの大規模システムの構成をGitのような「変更を追従出来る仕組み」で管理する概念が有効であることがわかってきました。 また、コードだけではなく、卒論などの文章を書くときに使うTeXファイルもバージョン管理します。 よって、バージョン管理は重要です。 これから先、授業や研究でコーディングする際は、常にバージョン管理をすることを強くオススメします。

また、よくある勘違いとして「最初はコードがグチャグチャだから、コーディングを進めて綺麗になってからバージョン管理しよう」というものがあります。 しかし、個人的には、最初のグチャグチャなときから始めることをオススメします。なぜなら、グチャグチャなときからバージョン管理をはじめても、デメリットがないからです。 そして、もし何かおかしいことになってしまったら、(1) ローカルの.gitを消す (2) GitHubのリポジトリを消す、とすれば全て綺麗サッパリなくなるので、 やり直せます。加えて、コードが綺麗になってから管理しようと思っても、どうせ永遠に綺麗になりません。恐れず初手からバージョン管理を始めましょう。

コラム

長年、GitHubはGitホストサービスで一番有名だったのですが、無料でプライベートリポジトリを作れないという問題がありました。 そのため、無料で作れるBitbucketというサービスをみんな使ったりしてました。近年はGitHubでも無料で作れるようになりました。 また、自分のサーバでGitホストの仕組みを運営できるGitLabというものもあります。これは上記の田浦先生が使っているものです。 これらのウェブホストサービスの違いによって、仕組みに違いもあります。例えばこれから「Pull Request」という単語を教えるのですが、 これはGitHubの言い方であり、同様の操作はGitLabでは「Merge Request」です。

注意

情報教育棟のマックを使っている人は、今日の説明すべての「ローカル」とか「My PC」というのはそのマックのことです。

Google Cloud Shell Editorを使っている人は、「ローカル」とか「My PC」は、Google Cloud Shell Editor上での話だと思ってください。Google Cloud Shell Editorもウェブサービスなのでややこしいですが。。。

注意

データやコードの権利などには十分注意してください。例えば医療データを扱うときなどに、データやコードをクラウドサービスにアップロードしてはいけない、 という規約になっているとします。 そのような場合は、もちろんデータやコードをGitHubに上げることは規約違反となるので、注意してください。 研究室の方針としてクラウドサービスを使っておらず、自前のサーバでGitLabを運営している、といったところも多く存在すると思います。

やってみよう(5分)

まず準備として、「Personal Access Token (Classic)」を発行します。これはパスワードのようなものです。 近年のGitHubでは適切な権限をもったトークンを最初に発行し、それを使ってリポジトリの操作を行います。ここではリポジトリの編集権限だけを持つトークンを作ります。 このページ に従い、トークンを発行してください。ここで、

  • トークンの名前はrepo_accessとしましょう。
  • Expirationというのは、トークンが有効な期限のことです。この期限を超えると、トークンが使えなくなります。ここではデフォルトの30日間としておきましょう。30日たつとこのトークンは使えなくなるので注意してください
  • スコープを選ぶときは、一番上のrepoにだけチェックをいれましょう。これによりrepo:status, repo_deployment, public_repo, repo:invite,security_eventsが自動的に選ばれます。
  • これにより、ghp_4qjmpor34prrwnrkuhfo8gupdsoirjtのようなランダム文字列が表示されます。これがトークンです。これを大事に保存し、人に見せないようにしましょう。

ちなみに、「SSH設定」と言う設定をすでに済ませている人は上記の手続きは必要ないです。将来的には上記のトークンを使うよりも「SSH」を使うほうが良いので、調べてみることをオススメします。

リポジトリを作ってclone

それでは早速リポジトリを作ってみましょう。始め方として、(A) GitHubリポジトリを作り、それを手元に持ってくる、 (B) $ git initコマンドで手元のディレクトリをgit管理下におき、それをGitHubに上げる、と二通りあるのですが、 ここでは簡単な(A)でいってみましょう。

  • まず上図の①のように、GitHubのウェブ画面からリポジトリを作ります。これはポチポチクリックして情報を記入するだけです。
  • 次に②のように、GitHubの内容を手元のPCにコピーします。この作業をcloneと言います。このcloneは最初に一回するだけです。

    • cloneの作業は、具体的には次のようにします。ここではsimple_calcというリポジトリを持ってきます。cloneすべき作業ディレクトリの位置に移動したうえで、次のようにgit clone [取得したURL]を実行します
      $ git clone https://github.com/hoge528/simple_calc.git
      
      すると下記のようにユーザ名とパスワードの入力を求められますので入力してください
      Cloning into 'simple_calc'...
      Username for 'https://github.com': hoge528    <- GitHubのユーザ名を打つ
      Password for 'https://hoge528@github.com':    <- 先ほど取得した個人アクセストークンをコピペする
      remote: Enumerating objects: 3, done.
      remote: Counting objects: 100% (3/3), done.
      remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
      Unpacking objects: 100% (3/3), done.
      
      これでローカルにリポジトリをcloneできました。確認してみましょう
      $ ls         <- 確認
      simple_calc  <- git管理したに置かれたディレクトリが出来てる
      
      $ cd simple_calc
      $ ls -al
      total 16
      drwxr-xr-x  3 matsui528 matsui528 4096 Nov 17 11:01 .
      drwxr-xr-x 12 matsui528 matsui528 4096 Nov 17 11:00 ..
      drwxr-xr-x  8 matsui528 matsui528 4096 Nov 17 11:01 .git       <- .gitディレクトリ。触らない
      -rw-r--r--  1 matsui528 matsui528   13 Nov 17 11:01 README.md  <- 初期ファイルのREADME.md
      
  • そして③のように、手元で好きなようにコードを編集します。ここではmain.cというファイルを足しています。この内容をGitHub側に同期するために、pushを行います

コラム

【2022/11/28更新】Google Cloud Shell Editorでコピペがうまく行かない人は、以下のようにやってみてください。

  • 前準備
    • Google Cloud Shell Editor上にただのテキストファイルhoge.txtを準備しておく。
    • GitHubの画面からトークンをコピーする
    • コピーした内容を、hoge.txt上にペーストする。これは可能なはず。画面上(Cloud Shell Editor上で開かれているhoge.txt上)にはトークンが表示される。これをAと呼ぶ。この「hoge.txt上にA」を維持しておく。
  • 実際のプログラミング
    • プログラミングを行い、Git操作が必要になったとする。
    • hoge.txtを開き、その中のAを、再度マウスで選択して、コピーする。
    • コピーしたものを、Aの次の行にでもペーストする。画面にはトークンが2つ(Aと、Aを再度張り付けたもの)見える
    • この状態で、Gitコマンドのパスワードのところにペーストする(windowsの場合Ctrl+V

上記だと出来る可能性が高いです。上記の手続きはナンセンスなのですが、どうやらコピーペーストの記憶領域の扱いがローカルとCloud Shell上で扱いが違い?ゴチャゴチャしている?ようで、Cloud Shell Editor上で実際にペーストを行った直後は少なくともコピー内容を記憶してくれているようです。

やってみよう(15分)

それでは早速上記をやってみましょう。GitHubにログインしましょう。

上図の緑色の「New」を押して、新しくリポジトリを作ります

上図のような画面になります。適当なリポジトリ名を打ちましょう。ここでは「simple_calc」としています。 今回は公開しないので「private」設定にします。公開する場合は「public」にします。publicにすると、世の中の人全てから見えるようになります。 皆さんは普段GitHubにアクセスして人のリポジトリを色々見ていると思いますが、それらはここでpublicに設定されたリポジトリです。 さらに、初期状態でREADME.mdを含んでくれるようにチェックします。そしてリポジトリを作ります。

さて、これで下記のようなリポジトリページが作られました。 ここが、ソースコードがおかれるマスター情報になります。パソコンが壊れても、ここが残っているので安心です。 また、ブラウザからビジュアルに中身を確認でき、履歴などもクリックを繰り返すことで確認できます。 さて、ここで右上の「Code」をクリックします。するとCloneのために必要なURLが表示されます。 ここでコピーマークをクリックすることで、URLがコピーされます。

あとは、上で述べたように、手元の作業ディレクトリでgit cloneを行ってください。手元でREADME.mdが見れればOKです。

ファイル変更履歴の記録:add, commit

さて、それでは手元のファイルに対して、gitコマンドを色々試してみましょう。 まずは更新の管理のための便利コマンドを紹介します。simple_calcディレクトリに移動したうえで、 git statusを実行してみましょう。これは、「現在のgit管理の状態を見せろ」という命令です。 実行すると以下のようになります。

$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

これは、「コードは何も編集されていないよ」という意味です。 git statusには副作用はないので、いつでも実行してOKです。

さて、それではコードを編集していきます。 ここでは、新しくmain.cというファイルを作って、そこに適当に書き込んでみましょう。 ディレクトリ内は以下のようになります。

.
├── .git
├── README.md
└── main.c

コラム

main.cを作ったあと、先ほど紹介した、更新管理の便利コマンドを実行してみましょう

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    main.c

nothing added to commit but untracked files present (use "git add" to track)
ここでUntracked files:main.cとなっている(端末上では赤字になっています)のは、 「main.cという新しいファイルがいるが、そいつはgit管理の対象になっていないよ」と言っています。

さて、このmain.cの更新をgit管理の対象にしましょう。 git add 更新を反映したいファイル を実行します。

$ git add main.c
これは、「main.cの更新をgit管理の対象にする」ことを意味します。

コラム

git statusで見てみましょう。

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   main.c
上からわかる通り、今度はmain.cChanges to be comittednew fileとなっています。 これはターミナルから見ると、字の色が緑色に変わっているのでわかりやすいです。 これにより、「main.cが新しく作られた。それはgit管理されている」を意味します。

次に、既存のファイルの更新もやってみましょう。README.mdの中身を適当に更新してください。 このとき、vscode / Google Cloud Shell Editor上ではmdファイルをクリックする表示モードになり、編集できないかもしれません。 そのときはREADME.mdを右クリックして「Open With」から「Code Editor」を選んでください。すると編集できます。

更新したあと、git addでこの変更を反映させます。

$ git add README.md
これで、今行ったREADMEの変更も管理対象になりました。

コラム

git statusで見てみましょう。

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README.md
    new file:   main.c
上からわかる通り、今度はREADME.mdmodifiedになっています。 これは「README.mdは更新されている。それはgit管理されている」を意味します。

次に、更新内容に「更新メッセージ」をつけて、「アップロード準備OK」の状態にします。 以下のコマンドを実行してください。

$ git commit -m "added main.c and updated readme"

ここで、-mオプションは「更新メッセージをつける」という意味です。その後の"added main.c and updated readme"は更新メッセージ (コミットメッセージ)になります。 ここでは、編集した内容を簡単な文章で説明しておきます。この内容は自由です。"updated"のように一言だけではなく、 行った変更をちゃんと説明する文章にしておくと良いです。 これにより、git addしたファイルが「アップロード準備OK」になりました。 以下のようなログが表示されるでしょう。

[main 07c2e59] added main.c and updated readme
 2 files changed, 8 insertions(+), 1 deletion(-)
 create mode 100644 main.c

注意として、メッセージの前後のダブルクオーテーションを忘れないようにしましょう。 つまり、次ではダメです。

# ダブルクオーテーションを忘れているのでダメ
$ git commit -m added main.c and updated readme       

コラム

ここでgit statusを行うと、main.cREADME.mdも、アップロード準備OKになったので消えています。

$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
ここでYour branch is ahead of 'origin/main' by 1 commit.となっているのは、 現在のディレクトリは、GitHubにある本体のリポジトリに比べ、 1コミットぶんだけ進んでいるという意味です。

ここまでが、ローカル側でのバージョン管理です。次にこれをGitHubのリポジトリにアップロードします

コラム

初めてgit commitを実行した場合は、Google Cloud Shellの場合は以下のようなエラーメッセージが出てくると思います。

*** Please tell me who you are.

Run

  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'matsui528@cs-961687541475-default-boost-sq5pd.(none)')

これは、「gitを使うときはメールアドレスと名前を教えろ」と言うことです。

マックの場合は、以下のようなメッセージが出ます。

Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 2 files changed, 4 insertions(+), 1 deletion(-)
 create mode 100644 main.c
これは、「gitを使うときはメールアドレスと名前を教えてほしい。今回のcommitに関しては自動的に設定した」という意味です。

いずれにせよ、下記のように入力して設定してください。

$ git config --global user.email "hoge528@gmail.com"
$ git config --global user.name "Yusuke Matsui"
ここで"hoge528@gmail.com"と"Yusuke Matsui"の部分には自分のものを入力してください。 ここのemailアドレスは、GitHubの自分のアカウントの登録に使ったものとそろえておくと便利です。 これらの情報はリポジトリを公開すると公開されるので注意してください。

Cloud Shell Editorの人は、そのうえで、git commitをやり直してください。

ちなみに、ここで設定した内容は単なるテキストファイルとして/home/ユーザ名/.gitconfigに保存されています。

更新のアップロード:push

さて、ここまでで、ローカル側での準備が整いました。この更新をGitHubのリポジトリに反映しましょう。 すなわち、git addおよびgit commitで「アップロード準備OK」にした更新を、git pushのコマンドで実際にGitHub上のリポジトリにアップロードします。

$ git push origin main
ここで、originとは、GitHubウェブサイト上にあるリポジトリそのものを指します。mainとは、通常使っているブランチの名前です。 ここではブランチの説明はしないので、とりあえず全てmainにしてください。 よって、このコマンドは、「手元の更新情報を、origin(ウェブ上のリポジトリ)のmainブランチ(デフォルト)にアップロード(git push)する」という意味です これを実行すると、GitHubのアカウント名と個人アクセストークンを聞かれるので入力してください。 最終的にログは以下のようになります。

Username for 'https://github.com': hoge528
Password for 'https://hoge528@github.com': 
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 354 bytes | 354.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To https://github.com/hoge528/simple_calc.git
   4cd03fc..2cd5212  main -> main

ここで、GitHubのリポジトリ画面にアクセスしてみると、更新が反映されていることがわかります。これでアップロードが出来ました。 手元でgit statusをしてみると最初と同様のクリーンな結果が返ります。

このgit add, git commit, git pushが更新の1サイクルです。 いくらかコーディングをすすめたら、それらをaddし、まとめてcommitしてメッセージをつけ、pushでアップロードします。 これを、少なくともと一日の最後には必ず行うようにしましょう。

問題が発生したら:

  • エラーメッセージをググって調べてみましょう
  • 調べてわからなかったら、誰か詳しい人に聞いてみましょう。
  • どうしてもおかしいことになってしまったら、編集中のファイル(今回でいうとmain.cREADME.md)を一旦どこかに退避させて、ディレクトリを完全に削除したうえで、cloneからやり直しましょう。そして、退避させたmain.cを置きなおせばいいです。

コラム

実は数年前までgitのデフォルトのブランチ名はmasterでした。なので、$ git push origin masterとしていました。 近年のBLM運動により、masterという名称は相応しくないという結論になり、業界全体としてデフォルトブランチ名がmasterからmainに変更されたのです。今新しくリポジトリを作ると全てデフォルトでmainが使われているので、これからはmainで大丈夫です。一方、過去のリポジトリに触る場合は、メインのブランチ名がmasterであることも多いと思いますので、注意してください。

また、GitHubにリポジトリを作るのではなく、手元のディレクトリに対してgit initコマンドを作ってgit管理を始めることもできるのですが、 その場合は未だにデフォルトブランチ名がmasterになってしまうので、注意が必要です。

更新のダウンロード: pull

さて、上記とは逆に、「リポジトリ上での更新を手元にもってくる」というコマンドがgit pullです。 手元は変わらずにGitHubリポジトリのみが変更されることってあるのか?と思うでしょう。 今回のワークフローではそれは発生しないです。ですがそれでも、 複数台のPCから同じリポジトリを編集しているときなど、GitHubリポジトリ側が手元よりも進むことはありえます。 今回はそれをシミュレートします。

GitHubリポジトリのブラウザの画面からREADME.mdをクリックし、右側の鉛筆アイコン(Edit this file) をクリックし、README.mdの中身を適当に編集してみましょう。そして、一番下の緑色のアイコンの「Commit changes」を押してください。

これにより、GitHubリポジトリの中のREADME.mdが更新されました。ブラウザから確認してみてください。この情報を手元にもってくるには、次のコマンドを実行します

$ git pull origin main

結果は次のようになります。ユーザ名と個人アクセストークンを打ってください。

Username for 'https://github.com': hoge528
Password for 'https://hoge528@github.com': 
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/hoge528/simple_calc
 * branch            main       -> FETCH_HEAD
   2cd5212..9ea0035  main       -> origin/main
Updating 2cd5212..9ea0035
Fast-forward
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
これによって、GitHubリポジトリ側での変更が手元に反映されたことがわかります。 この作業も何度も行うことができます。 このように新しい更新を反映することを忘れないために、コーディングを始めるときは、まずgit pullしてからスタートすることをオススメします。

コラム

pullの実行時に以下のような警告が出るかもしれません。

hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint:   git config pull.rebase false  # merge (the default strategy)
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
ここでは、「pullするときには色々な戦略があるが、どれを使うのか?次からはちゃんと指定してくれ」と言われています。今後も、pullするたびに上記のような警告が表示されます。

ここでは常にデフォルトの設定にしましょう。以下のコマンドを実行してください。【2022/11/28更新】 この設定は必ず行ってください。この設定を行わない場合、コミットの衝突がおきたときにエラーになってしまいます。

$ git config --global pull.rebase false
これにより、今後はデフォルト設定を使う、ということになりました。この設定内容は、繰り返しになりますが/home/ユーザ名/.gitconfigあるいは~/.gitconfigの中に書き込まれます。これを確認すると、例えば私の場合は以下になります。
$ cat ~/.gitconfig
[user]
        email = hoge528@gmail.com
        name = Yusuke Matsui
[pull]
        rebase = false

変更履歴の確認:log

さて、手元において、ファイルの変更履歴を確認するには次のコマンドを実行します。

$ git log

結果は例えば次のようになります。

commit 9ea0035492bf3b718a7b41f219a5dbea8c60331c (HEAD -> 
main, origin/main, origin/HEAD)
Author: hoge528 <70310899+hoge528@users.noreply.github.com>
Date:   Wed Nov 11 00:04:23 2020 +0900

    Update README.md

commit 2cd5212f9c9cf80814151a1f4ce6c194868c4195
Author: Yusuke Matsui <hoge528@gmail.com>
Date:   Tue Nov 10 14:56:39 2020 +0000

    added main.c and updated readme

commit 4cd03fc8923dc02a4ca1f83665223d3d419f6adf
Author: hoge528 <70310899+hoge528@users.noreply.github.com>
Date:   Tue Nov 10 13:56:37 2020 +0900

    Initial commit
これは、これまでに行ったコミットが表示されています。このように、gitを使うと過去を振り返ることができます。

コラム

履歴の確認はブラウザ上からリポジトリを見ても確認することができます。 上のように「X commits」となっているところをクリックします すると上のようにこれまでのコミット一覧が表示されます。これらをクリックすると、さらに詳しい履歴を確認できます。 みなさんは宿題の自動採点で既にこの部分によく触れているはずです。

使い方まとめ

さて、ここまでの使い方をまとめておきます。 なんらかのコーディングをしたいとします。

  • 初期化:
    • GitHub上でリポジトリを作り、それをcloneで手元に持ってくる
  • コーディング:以下を繰り返す
    • コーディングを開始するときに、まずpullでGitHubリポジトリの更新を取得
    • コーディングする
    • 区切りがいいところになったら、add, commit, pushで更新をGitHubリポジトリに反映
  • おかしなことが発生したら:
    • 全部消して、cloneからやり直す
  • 履歴を確認したい:
    • ウェブ上からGitHubリポジトリを見て確認
  • もっと複雑な操作をしたい:
    • ググるか詳しい人に聞く。おかしなことが発生したら全部消してcloneからやり直す

コラム

ここまではGitの操作をgitコマンドてCLIから行いました。一方で、GUIソフトウェアをインストールして、それらのコマンド操作をGUIで出来るようにする仕組みもあります。 そのようなソフトをGitクライアントといいます。Gitクライアントを使うと、addcommitといったgit操作をポチポチクリックするだけで出来ます。 有名どころの一覧はこんな感じです。 SourceTreeなどは昔ながらの有名どころです。GitHub公式のGitHub Desktopもあります。 個人的にはGitKrakenがビューア機能が豊富で好きです。

ですが、実は最近はvscodeに標準でgitクライアントが入っているので、それで十分かもしれません。下は、古いファイルと新しいファイルの差分を可視化している例です。 これはCloud Shell Editorでも見れます。左側のメニューのSource Control: Gitというところをクリックすると見えます。 このような可視化はGitHubにアクセスしてブラウザを見ればできるのですが、クライアントをインストールしておけばローカルでも見れます。

gitコマンドを直接使うかクライアントを使うかは意見が分かれるところです。松井自身は、gitコマンドは直接叩き、必要に応じてビューアとしてクライアントを使っています。

また、「CLIの」クライアントというものもありそれをtigといいます。こちらはGUIが使えないサーバ上などでもクライアント相当のことができるので、 根強い人気があったりします。 tigの日本語解説サイト およびtigの公式

また、上記とは別に、GitHubにアクセスしなければ実行できない様々な操作(例えばissueを読むとか)は、 実はブラウザを経由しなくても、GitHub公式のCLIツールであるGitHub CLIで行うこともできます。 GitHub CLIは高機能なうえに痒い所に手が届くモダンなツールなので、興味がある人は使ってみることをおすすめします。

やってみよう(20分)

それでは上記のadd, commit, push, pull, logをやってみましょう。

無視するファイル:.gitignore

gitを使う上で重要な点として、gitが管理するのはあくまでテキストファイル(ソースコード等)であり、バイナリファイル(a.out、画像など)は扱うべきではないということです。 gitはテキストの行レベルで更新を検知します。 例えばjpg画像データはほんのちょっと画像が変わっただけでも内部表現は全く変わるため、 gitで変更を追跡するとものすごい量になります。 よって、バイナリファイルや、使い捨てるファイル(毎回値が変わるログファイルとか)、巨大すぎるファイル、などはgitで管理しないほうがいいです。

そういったファイルを追加してしまうと具体的にどうなるかというと、.gitフォルダのサイズが肥大化します。 これによりgit操作が遅くなったり、エラーになったりします。 個人プロジェクトで、この.gitが数百MBぐらいになってしまう場合は、すべきでないファイルを管理してる可能性が高いです。

管理しないためには、git addしなければいいだけです。しかし、それではgit statusするたびに 毎回それらの管理対象でないファイルが表示されてしまい不便です。また、いつか間違ってaddしてしまうかもしれません。 それを防ぐため、.gitignoreというファイルが使われます。

これは中身はただのテキストファイルです。 この中に、git管理したくないファイルのファイル名を書いておきます。 そうすると、gitはそれらを無視してくれるようになり、丸く収まります。 ちなみに、言語ごとの.gitignoreの雛形は公開されているので、これをまずコピペしてくればいいです。 このなかには、言語固有の、典型的な「無視すべきファイル」が最初からまとめられています。注意として、.gitignoreそのものはちゃんとaddして追加してください。

コラム

じゃあ巨大なバイナリファイルのバックアップはどうすればいいのか?という疑問がわきます。実はこれに対する明確な答えは無いのです。 以下のような選択肢が考えられます

  • DropboxやOneDriveなどの自動同期システムで管理
  • ラボサーバや、AWS S3や、Google Cloud Storage (GCS)といったリモートストレージを用意してそこでバックアップ。必要に応じてそれらに直接アクセスしたり、ダウンロードしてくる
  • Git Large File Storage (LFS)といった特殊な仕組みを利用

個人レベルで現実的な回答はDropbox / OneDriveでしょう。研究を始めると、ラボサーバやS3など、ネットワーク越しに巨大データを置いたりします。 また、GitにはLFSという巨大ファイルを扱う仕組みがあるのですが、これはクセが強いので慣れるまで難しいです。

リモートストレージをラップする形で、まるでデータもバージョン管理しているかのように扱うData Version Controlといった仕組みもあります(個人的にオススメ)

ところで、Dropboxなどの自動同期システムを使うならソースコードもDropboxで十分にバックアップとれるんじゃないの?と思うかもしれませんが、それは正しくないです。 まず、DropboxではGitのように厳密な変更履歴を保持できません。 そして、Dropboxのような自動同期システムは、「あるときにうっかり自分でも気付かずにファイルを消してしまった」という状況に弱いです。 なのでGitの代わりにはなりません。Dropboxを使う場合は、Dropbox管理したうえでさらにGitでも管理すると堅牢です。

バージョン管理はバックアップ

さて、長々と説明してきましたが、バージョン管理が実用上強力なバックアップであることを示します。 上の図にあるように、手元のPCは卒論提出直前に高い確率で壊れます。ここでソースコードをGit/GitHubでしっかりバージョン管理をしていたなら、 別PCにcloneすることですぐに研究を再開できます。 バージョン管理を知っている人間からすると、バージョン管理をしないでプログラミングすることは、シートベルトをしないで運転しているように見えます。

この場面ではDropboxも有効な手段ですが、Gitであればより完璧な履歴が保持できます(例えばDropboxだとうっかり容量制限に達していて記録できていなかったとか、 大きいファイルの共有に失敗して止まっていて保存できていなかった、なんてこともあり得ます) また、普段から、新しい環境でcloneしたときにちゃんと動くようにリポジトリ内容を整備しておくクセをつけておくといいです (作業手順や依存ライブラリをREADME.mdに書くなど)

コラム

ちなみに、これまでのGitHub Classroomによる宿題提出は何をしていたのでしょうか? 上図のように松井が1人1人のための宿題リポジトリを作り、みなさんに編集権限を与えていました。 みなさんは上図のhongoutarouさんのように、それを編集していたのでした。 そして、Codespacesという便利機能により直接リポジトリを編集していましたが、それは裏側ではgitコマンドを使っていました。 なので、下のkomabahanakoさんのように、ローカルPCにcloneしたうえでpull/pushで編集してもOKです。

ペアプログラミング入門:1つのリポジトリを複数人で編集

さて、それでは最後に、1つのリポジトリを複数人で編集する方式のうち、簡単なものを勉強しましょう。

みなさんは3年生以降の課題や実験で、複数人でプログラミングをすることがあります。 もしGit/GitHubを知らなければ、場当たり的にソースファイルを共有して編集することになり、大変です。 Git/GitHubの真価は複数人でのコード編集にあります。それをやっていきましょう。

また、以下の「やってみよう」にはチームで取り組んでもらいます。

  • 知り合いとチームを組んだ場合はその知り合い同士で行ってください。チーム同士ではGitHubアカウント名がわかってしまうので注意してください。
  • TAチームでもやってもらうので、TAチームと一緒にやりたい人は松井かTAまでDMしてください。TAチームのメンバー内ではアカウント名が見えるので注意してください。
  • 松井も公開で同じことをやりますので、松井と同じチームでやりたければDMしてください(アカウント名はみんなに見えちゃいます)
  • アカウント名を秘密にしておきたい場合は、以下の「やってみよう」はやらなくて大丈夫です。

素朴リポジトリ共有

最も簡単な方式は、これまで行ってきたことを単に複数人でやるというものです。ここではAさん、Bさん、Cさんが共同で開発するとします。 ここでAさんを代表としましょう。Aさんが作ったリポジトリを、A, B, Cさんみんなで編集するとします。

  • Aがリポジトリを作る
  • そのリポジトリの編集権限を、B, Cに付与
  • A, B, Cともに、clone, add, commit, push, pullでリポジトリを編集

これが一番簡単な素朴リポジトリ共有方式であり、最初はこれでもOKです。しかし、このシンプルな方式の場合、衝突が発生することがあります。以下を考えましょう。

  • Aさんが自分の手元でmain.cを編集している
  • Bさんが自分の手元でmain.cを編集し、push
  • その後Aさんもpushしようとするが、Bさんが既にmain.cを更新してしまっており、その更新内容と衝突する

このような衝突が発生したときの対処方法は、田浦先生のGit/gitlabで共同作業をするための最小限の知識9. 衝突 (conflict) を理解するを参照してください。【2022/11/28更新】エラーになった場合は、エラーメッセージをよく読んでください。特に、わからなくなったら常にgit statusをしてみてください。赤いファイルがある場合はaddが出来ていません。緑色のファイルがある場合はcommitが出来ていません。

この簡単方式の別の弱点は、各自の更新が、「コミットレベル」でしかわからないことです。コミットというのは、とても小さい作業の単位であり、 コミットがたくさんあると一体最終的に何がおきたのかわかりません。例えば、Bさんが「calc関数に割り算機能を実装したい」としたときに、コミットは「割り算の出力部分を実装」「計算部分が間違っていたので修正」「さらに修正」「アルゴリズム部分を半分実装」・・といったように多岐になりえます。ここで、あとから振り返って知りたいのは、上記のコミットをたばねたものです。これを解決するために、Pull Requestという機能を紹介します。

また、この方式の場合は常にマスター情報である「AさんのGitHubのmainブランチ」を直接更新していく形になるので、不安という面があります。

また、本日は軽くしか述べませんが、Pull Requestを用いると、「レビュー」という、他のメンバーによるコード確認作業を行うこともできます。

やってみよう(15分)

それでは上記の素朴リポジトリ共有方式をやってみましょう。

  • グループの代表者を決めてください。
  • 代表者はsimple_printerというリポジトリをprivateで作ってください
  • リポジトリ画面で、右上のSettingsを押し、左側のCollaboratorsを押します。すると右側がManage accessという画面になります。ここでAdd peopleを押し、他のメンバーのgithubアカウントを入力してください。すると、招待状を飛ばすことができます。
  • 他のメンバーは、GitHubを登録したメールアドレスに、招待リンクが届いているはずです。そのメール中で「View invitation」を押し、「Accept invitation」を押してください。そうすると、代表者の作ったsimple_printerリポにアクセスできるようになります。
  • あとは、各自で自由にclone/add/commit/pushを実行してみてください。また、意図的に、上で説明したconflictを発生させてみてください。
  • リポジトリのコミット履歴をいろいろクリックしてみて、何が起こっているか確認してみてください。

素朴リポジトリ共有 + Pull Request

本講義で紹介するのは、上記の「素朴リポジトリ共有方式」に、Pull Request (PR)による更新を導入するというものです。 これは有名なGitHub Flowという方式の簡易版になっています。

ワークフローは以下になります。

  • Aがリポジトリを作る
  • そのリポジトリの編集権限を、B, Cに付与
  • A, B, Cともに、
    • clone, pull, add, commitを行い手元でコード編集
    • PRを用いてリポジトリを更新

ここでは、ブランチと言う概念を触りだけ導入します。ブランチとは、「自分の並行世界の分身」のようなものです。 これまでの話は全て、手元のローカル環境もGitHubのリモート環境も、デフォルトであるmainブランチ上の話でした。ここからは、自分の並行世界の分身であるブランチを作成し、その上で作業をします。 概要は下図になります。

【2022/11/28更新】以下、少し流れを修正

それでは、手元PC上で、前に作ったsimple_printerリポジトリの中に移動します。

そして、以下を実行してみましょう。

$ git branch

* main
git branchとだけ実行すると、現在のブランチの情報が見れます。ここではmainブランチしかなく、またそのブランチにいることがわかります。

まずはいつも通りpullをして手元のmainを最新にします。

$ git pull origin main
これは上図における(1)に相当します。これは必ずやってください。

それではsay_hogeブランチを作ってみましょう。【2022/11/28更新】ここで、ブランチ名は固有のものにしましょう。今回の運用では、ブランチ名は使いまわしません。なので、例えばBさんが足し算機能を追加するために、feat_additionブランチを作る、といった形です。Bさんがfeat_additionブランチを作っている間に、Cさんもfeat_additionブランチを作る、という運用はここではやめておきましょう。Bさんがfeat_additionブランチを作って色々やったあと、三日後にまたBさんがfeat_additionブランチを作るというのは大丈夫です。

$ git branch say_hoge
これで、分身であるsay_hogeブランチが出来ました。これは上図でいう(2)に相当します。このブランチに変身してみましょう。
$ git switch say_hoge

Switched to branch 'say_hoge'
ここではgit switchを使って変身ができます。確認しましょう。
$ git branch

  main
* say_hoge
ここでは、現在の手元にはmainブランチとsay_hogeブランチがあり、現在あなたはsay_hogeブランチです、ということがわかります。

コラム

少し前までは、git checkoutという便利コマンドがあり、それが色々な機能を担当していました。 ですが、同じコマンドで全然別の動作をするということで混乱を招いていました。 Git 2.23からは、git checkoutgit restoregit switchとうより直感的な名前の二つのコマンドに分割されました。 なので、皆さんはrestoreswitchを使っていくといいです。昔の記事ではcheckoutを使うものもたくさん残っていますので、注意してください。

ここでmain.cを適当に作って編集し、git add, git commitしましょう。ここで、何度もコミットしてみてください。 これは(3)の編集作業に相当します。

そしてpushします。(4)の部分です。これまではgit push origin mainとしてきました。繰り返しますが、これは「リモート(origin)の、mainブランチにプッシュする」という意味です。GitHubのリモート環境側にもブランチという概念があるのです。ここでは、リモート側のsay_hogeという同名ブランチを作りそこにプッシュします。以下を実行してください。

$ git push origin say_hoge

Username for 'https://github.com': hoge528
Password for 'https://hoge528@github.com':
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 280 bytes | 70.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: Create a pull request for 'say_hoge' on GitHub by visiting:
remote:      https://github.com/hoge528/simple_printer/pull/new/say_hoge
remote:
To https://github.com/hoge528/simple_printer.git
 * [new branch]      say_hoge -> say_hoge

そしてGitHubの画面に戻ると、下図のように「新しくsay_hogeブランチが出来ています。PRを作りますか?」という表示が出ています。 ここで「Compare & pull request」を押します。

すると次のようなPR作成画面になります。ここではタイトルと本文を適当につけてPRを作成しましょう。 PRタイトルはわかりやすいものにしましょう。

すると、次の画面のように、PRが出来ます。これは上図の(5)にあたる部分です。

本来であれば、この時点で、「コードに対する更新をまとめたPRを作りました。これをチェックしてください」という状況になります。ここで、経験があるエンジニアや、チームの別の人が、 このPRをチェックすることになります。これをレビューと呼びます。それは以下のように表されます。

ここで、レビュアからの指摘に応じて、コードを編集してpushし直すことができます。これにより、PR内容が自動的に更新されます。

普通は上記のようにレビューを交えてすすめていくのですが、ここでは単に自分自身でマージしてみましょう。PR画面で「Merge pull request」を押し、さらに「Confirm merge」を押すと、この変更がmainブランチにマージされます。

そして、その画面の中で、「Delete branch」も押しておきましょう。こうすると、リモート側に作ったsay_hogeブランチを消すことができます。 GitHubのブランチ画面に戻ると、リポジトリが更新されていることがわかります。

ローカル側では、現在say_hogeブランチになっていますので、mainに戻り最新更新をpullしたうえで、say_hogeブランチを消しておきましょう。

$ git branch
  main
* say_hoge      <- 現在say_hogeブランチにいる

$  git switch main       <- mainに戻る
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ git branch
* main          <- mainになった
  say_hoge

$ git pull origin main    <- リモートのmainをローカルのmainにもってくる
Username for 'https://github.com': hoge528
Password for 'https://hoge528@github.com':
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (1/1), 665 bytes | 95.00 KiB/s, done.
From https://github.com/hoge528/simple_printer
 * branch            main       -> FETCH_HEAD
   b28e440..6593ee4  main       -> origin/main
Updating b28e440..6593ee4
Fast-forward
 main.c | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 main.c

$ git branch --delete say_hoge     <- say_hogeブランチを消す
Deleted branch say_hoge (was 5cb130a).

$ git branch
* main        <- 分身は消えて初期状態に戻った
これで手元の分身も消え、スッキリして初期状態に戻りました。ここまでが1サイクルです。このブランチを消す部分を可視化したものが次になります。

ブランチの使い方はいろいろあるのですが、一番簡単なのは使い捨てることです。 上記のように、機能を追加するために作り、使い終わったら消すようにするとよいです。

さて、ゴチャゴチャと色々行いましたが、なぜこのPRが楽なのでしょうか?まずは「複数のコミットをまとめて、意味のある更新単位を作れる」ということがあります。 そして、PRに対するレビュー作業の様子なども記録として残るので、あとから見返したときに「この機能は誰がどういう議論をもとに追加されたんだろう」ということが あとからハッキリとわかります。これは非常に重要です。

また、衝突の回避も楽です。以下を考えましょう。

  • Aさんが手元で作業中に、Bさんがリポジトリを更新
  • AさんがPRを作成

この場合、下図のように「衝突がおきているよ」ということをPR画面で教えてくれます。 ここをクリックすると、「素朴リポジトリ共有」で行っていた衝突解消画面に移行します。 ここでは内容を編集し、右上の「Mark as resolved」を押しそして「Commit merge」を押すことで、 衝突を解消しつつマージが可能になります。

このように、「一つのPR」の中で衝突を解消したり複数のコミットを入れることができます。

また、リモートのメインブランチは常に最新かつちゃんと動く状態に保っておき、それに対する更新だけをPRという形で受付ける、という状態にしておくと、健康です。

コラム

有名なGitHub Flowは、今日紹介した方式にさらに「Fork」という仕組みを導入したものになりますが、概念としてはほとんど同じです。 今日の方式は最初にリポジトリに対するアクセス権限を各ユーザに与える必要があります。よって、GitHub上で今日の方式でオープンに開発を進めることはできません。 オープンというのは、例えば「バグを直したよ」とか「新機能を考えたんだけどどう?」とかいう新たな提案を、知らないユーザから受け付ける、ということです。 GitHubやオープンソースの世界の真髄はそういったオープンな開発にあります。 そのようなオープンな開発を可能にするためには、Forkというものを使います。 Forkについては、2021のソフ1で触れていたので、気になる方は見てみてください。

やってみよう(20分)

それでは上記の素朴リポジトリ共有+PR方式をやってみましょう。色々失敗しつつ、慣れてみてください。

【2022/11/28更新】ここで、Aさん、Bさん、Cさんがそれぞれブランチを作る場合、ブランチ名は別のものを使うようにしましょう。