ソフトウェア設計論

まつ本

モダンSW開発

版管理は使えるべき

SW開発における「当たり前技術」

版管理しないプロジェクトはない
共同開発で必須 / 1人開発でも役立つ

主要機能:履歴の記録 / ブランチ / マージ

❌ 破壊的操作の前にファイルコピーを取ろう
⭕ 破壊的操作の前にgit-commitしよう

SW開発以外でも使える

  • 実験スクリプト
  • 論文LaTeX
  • 研究室のドキュメント管理
  • 講義資料

モダンSW開発 -版管理-


目次

・VCSって?
・分散型と集中型
・何のためにVCSを使う?

・Gitの内側
・オブジェクトDB
・演習
・branch/HEAD

・VCSのうまい使い方
・適切なコミットメッセージを付ける
・小さくコミットする
・VCSで管理すべきデータ

VCSって?

Version Control System; 版管理システム

SWの変更履歴を管理するシステム

  • 誰が / いつ / どのファイルを / どう書換えたか
$ cd lecture-sw-design  # この授業資料のリポジトリ
$ git log               # 変更履歴を確認
commit 5b1e665d40... (HEAD -> main, ...)
Author: shinsuke-mat <shinsuke-mat@users...>
Date:   Tue Jul 1 09:52:20 2025 +0900

    improve testing.md
...

リポジトリ

上記の変更履歴データのこと (一種のDB)

簡単な使い方

$ # リポジトリの取り込み
$ git clone https://github.com/kusumotolab/kGenProg.git
$ cd kGenProg

$ # 取り込んだ内容を確認
$ ls -a1
.git       ← これがリポジトリ
README.md
src/
...

$ vi README.md    # 適当なファイルを編集
...

$ git add README.md         # ステージへの追加
$ git commit -m 'fix typo'  # コミット
$ git push                  # push

ファイル名による版管理

雑談

👎

https://www.reddit.com/r/ProgrammerHumor/comments/oz4pfb/version_controlcan_anyone_relate/

コメントによる版管理

雑談
$ cat enshud/shidousho.tex
%       情報科学演習D  言語処理
%       指導書No.0 (演習の概要説明用)
%       作成日  1991年10月24日
%       修正日  1992年10月18日(年度変わりに伴って)
%       修正日  1993年10月05日(年度変わりに伴って)
%       修正日  1994年09月21日(年度変わりに伴って)
%       修正日  1995年09月21日(年度変わりに伴って)
%       修正日  1996年09月20日(年度変わりに伴って)
...
\begin{document}
\title{2024年度 情報科学演習D 指導書}
...

これはやめよう
プロダクトにプロセスの情報を書くべきではない

VCSはこのような管理を一般化したシステム

有名なVCS

CVS (Concurrent Versions System)

1970年頃・集中管理型
コミットやチェックアウトの概念はここから
ブランチもあり

$ cvs commit -m 'fix typo' main.py

SVN (Subversion)

2000年頃・集中管理型
CVSと互換性あり

Git

2005年頃・分散管理型
現在87%のシェア*















Linus B. Torvalds

分散型と集中型

集中型:リポジトリが世界に一つ

古いVCSで採用されていたアイデア

+ わかりやすい
- 対故障性が低い
- リポジトリへのアクセス環境が常に必須

分散型:リポジトリは世界に多数

Gitが採用するアイデア

- わかりにくい
+ 対故障性が高い
+ オフラインでも作業が可能

何のためにVCSを使う?

過去の状態の復元・確認

バグったらすぐにロールバックできる
破壊的な作業の前にはまずコミット (やスタッシュ)
ファイルコピーよりも建設的

変更意図の確認も可能
一種のドキュメンテーションの効果がある

効率的な共同開発

編集の競合を防ぐ強力な仕組みがある
ブランチとマージ

ブランチを使えば並行開発も可能
新機能はブランチ側で開発する

モダンSW開発 -版管理-


目次

・VCSって?
・分散型と集中型
・何のためにVCSを使う?

・Gitの内側
・オブジェクトDB
・演習
・branch/HEAD

・VCSのうまい使い方
・適切なコミットメッセージを付ける
・小さくコミットする
・VCSで管理すべきデータ

Gitを使いこなすのは難しい

Gitはコマンドとモデルの乖離が大きい

git add xx をどこに追加しているのか?
git commit → そもそもコミットって何?

非直感的だといえる

UNIX系コマンドはモデルとの乖離が小さい

cp x yxy に複製する
mkdir x → dir x を作る

Gitはコマンドを暗記するだけでは不十分

モデルの理解が伴わないため

都度ググっても解決はする

それは理解か?使いこなしているか?

経験的には内部を理解するのが手っ取り早い

内部構造の理解がモデルの理解に繋がる

抽象的な概念を具体的な挙動に落とし込める
git add = ステージングへの追加 = index への追加

優れたシステムの理解から得られる知見もある
ファイルシステムの上に特殊なFSを作る等

分解は楽しい

Linusの気持ちを読み取る

理解を伴う分解の楽しさ
魔法の箱を技術の箱にする楽しさ

オブジェクトDB

Gitでは全データをオブジェクトとして管理する

以下 の要素1つが1つのGitオブジェクト

ファイル中身:README main.py logo.png
ファイル構造:./README ./main.py ./assets/logo.png
コミット:04/25にshinsukeがREADMEを更新した

3種類のオブジェクトがあるという理解で良い

コンテンツアドレスシステム

コンテンツ(ファイルの中身)に基づいて
アドレス(ファイル名)が決まる

コンテンツが同じならファイル名も同じ

説明の流れ

低レベルコマンドを使って内部の挙動を追う

git-hash-object
配管コマンドとも呼ばれる (plumbing command)
普通は直接叩かない

例) git-hash-object = blobオブジェクトの生成

低レベルと高レベルコマンドの対応を考える

git-add
磁器コマンドとも呼ばれる (porcelain command)
我々が通常利用するコマンド

例) git-add = git-hash-object + git-update-index

初期セットアップ

まずは初期化

$ mkdir -p trial; cd trial

gitの初期化と確認

$ git init          # このフォルダを初期化
$ ls -a             # ファイル一覧を確認(隠しファイルも表示)
.  ..  .git         # .gitフォルダが生成されている
                    
$ find
.
./.git              # 様々なファイルが生成されているが,
./.git/config       # 現段階では理解しなくてOK
./.git/description
./.git/HEAD
...

適当なファイルを作ってオブジェクトに変換

配管コマンド git-hash-object を使う

$ echo 'hello' > README
$ git hash-object -w README  # オブジェクト生成
ce013625030ba8dba906f756967f9e9ca394464a  # Objのアドレス

生成オブジェクトを確認

$ find .git/objects/ -type f  # オブジェクト一覧を確認
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
$ cat .git/objects/ce/01*  # オブジェクトの中身を確認
xK▒▒OR0c▒H▒▒▒▒▒            # バイナリっぽい

生成オブジェクトの中身を確認

配管コマンド git-cat-file を使う

$ git cat-file -p ce01  # ce01の中身は?
hello

生成結果


ファイル名だけを変えてみる

$ cp README tmp
$ git hash-object -w tmp      # オブジェクト生成
ce01...                       # 先ほどと同じ名前

オブジェクトDBに変化はあるか?

$ find .git/objects/ -type f  # オブジェクト一覧を確認
.git/objects/ce/01...         # もちろん変化なし

コンテンツが同じなら同じオブジェクトを生成

少しでも中身を書き換えると?

$ echo '!' >> tmp             # 1文字追記
$ git hash-object -w tmp
a21c...                       # 別オブジェクト

これがコンテンツアドレスシステム
コンテンツのみに基づいてアドレスが決まる

先ほど作成したゴミは削除しておこう

$ rm tmp
$ rm -rf .git/objects/a21c*  # hello!を消しておく

blobオブジェクト

git-hash-object でファイルをオブジェクト化した
このオブジェクトはblobと呼ばれる

  • blobのファイル名 = sha1("hello") (ハッシュ・不可逆)
  • blobのコンテンツ = zlib("hello") (可逆)

版管理対象の全ファイルはblobに変換される
各ファイルの中身(コンテンツ)を記録する

ファイル名は保持されない

README という文字はblob自体には記録されない

ファイル名や構造は版管理では必須

$ ls
README  main.py  src/...

インデックスにREADMEを登録

配管コマンド git-update-index を使う

$ git update-index --add README
$ cat .git/index  # 登録内容はindexファイルに記録される
DIRCh▒9Gh▒9G▒m▒4ҋ▒▒▒  ▒▒README!▒3▒▒▒=o};▒▒▒▒-x

index にはファイル一覧がバイナリで記録される

インデックスからオブジェクトを生成

配管コマンド git-write-tree を使う

$ git write-tree              # .git/indexからツリーを作る
7d4a...

$ find .git/objects/ -type f  # オブジェクト一覧を確認
.git/objects/7d/4a...         # indexから生成したオブジェ
.git/objects/ce/01...         # READMEのblob

オブジェクトの中身を確認

$ git cat-file -p 7d4a         # 7d4aの中身は?
100644 blob ce01...    README  # ファイル名とアドレスが記録

生成結果


treeオブジェクト

git-update-indexでインデックスにファイル名を記録

git-write-tree でインデックスからオブジェを生成
これをtreeオブジェクトと呼ぶ

blobと同じくコンテンツのみから生成される

  • treeのファイル名 = sha1("README ce01...")
  • treeのコンテンツ = zlib("README ce01...")
    index というファイル名には根本的に興味がない

blobもtreeもオブジェクト

$ git cat-file -t ce01  # オブジェクトce01の種類は?
blob
$ git cat-file -t 7d4a  # オブジェクト7d4aの種類は?
tree

treeからのファイルの復元

まずはtreeを見る

$ git cat-file -p 7d4a
100644 blob ce01..    README

ファイル構造を復元

$ touch README     # 空ファイルの生成

blobからコンテンツを復元

$ git cat-file -p ce01 > README

結果の確認

$ cat README
hello

treeはファイル群のスナップショットである

もしコンテンツが変わると?

blobだけでなくtreeも巻き込まれて変化する
tree+blob = ファイル全体のスナップショット

※古いtreeとblobは残っているので復元可

コミット

コミットする

配管コマンド git-commit-tree を使う

$ # tree 7d4aを指定してコミット
$ echo '1st commit' | git commit-tree 7d4a
677e...    # やっぱりオブジェクトが出来上がる

オブジェクトの内容を確認

$ git cat-file -p 677e
tree 7d4a...
author shinsuke-mat <...> 1745986054 +0900
committer shinsuke-mat <...> 1745986054 +0900

1st commit

誰が / いつ / なぜ変更したかが記録される
まさに履歴の1点

いったんファイルを改変してtreeまで生成

$ echo '!' >> README             # 1文字追記
$ git hash-object -w README      # blob生成
a21c...
$ git update-index --add README  # indexに追加
$ git write-tree                 # tree生成
4423...

新たなコミットを生成

$ # 親コミットを前ページの677eとして新規コミット生成
$ echo '2nd commit' | git commit-tree 4423 -p 677e
330b...
$ git cat-file -p 330b
tree 4423...
parent 677e...
author shinsuke-mat <...> 1745987355 +0900
committer shinsuke-mat <...> 1745987355 +0900

2nd commit

生成結果(Gitのデータモデル)


コミットするたびにアドレスが変わる?

雑談
$ echo 'test' | git commit-tree 7d4a  # tree 7d4aでコミット
8cfb...
$ echo 'test' | git commit-tree 7d4a  # tree 7d4aでコミット
79ea...
$ echo 'test' | git commit-tree 7d4a  # tree 7d4aでコミット
bec8...

なぜだろう?

commitオブジェクト

git-commit-treeでオブジェクトを生成した
これをcommitオブジェクトと呼ぶ

以下を記録
誰が / いつ / どのファイルを / どう書換えたか

常に一つのtreeを持つ

ある単一のスナップショットに紐づく

0個以上の親commitを持つ

親commitが1 = 通常の開発スタイル (起点が1つ)
親commitが0 = 初期コミット or 浮いたコミット
親commitが2以上 = マージコミット

演習 (10m)

Gitオブジェクトの構造を確認せよ

まずは高レベルコマンドを使ってリポを作る

$ mkdir trial; cd trial; git init
$ echo 'hello' > README
$ echo 'print("hi")' > main.py
$ git add .                     # 高レベルコマンドでOK
$ git commit -m '1st commit'    # 高レベルコマンドでOK

生成したGitオブジェクトの参照関係を記述せよ

commit ff3a...
-> tree aaa...
   -> blob bbb...
   -> blob ccc...

※図のハッシュは例である

提出方法

参照関係のテキストをCLEに提出すること

ヒント

まずはcommitオブジェクトのアドレスを探す

$ git log
commit ff3a..
...

あとは ff3agit-cat-file で辿る
ff3a は環境によって異なるので注意

補足

git-commit でエラーが出る際は以下を実行

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

回答例

commit 9ef9      (変動する)
-> tree 56dd     (変動しない)
  -> blob ce01   (変動しない, README)
  -> blob b80e   (変動しない, main.py)

commitのアドレスが変動する要因;

  • 開発者の名前 / コミットした時刻

高レベルコマンドとの対応

git-init; リポの初期化

$ git init
$ # = mkdir .git
$ # + mkdir .git/objects
$ # + .git内へ各種設定ファイルを生成

git-add; ステージングへの追加

$ git add README
$ #  = git-hash-object  : blob生成
$ #  + git-update-index : index書換 (ステージングに追加)

git-commit; コミット

$ git commit -m '1st commit'
$ #  = git-write-tree  : tree生成
$ #  + git-commit-tree : commit生成

オブジェクトDB

再掲

Gitでは全データをオブジェクトとして管理する

以下 の要素1つが1つのGitオブジェクト

ファイル中身:README main.py logo.png
ファイル構造:./README ./main.py ./assets/logo.png
コミット:04/25にshinsukeがREADMEを更新した

3種類のオブジェクトがあるという理解で良い

コンテンツアドレスシステム

コンテンツ(ファイルの中身)に基づいて
アドレス(ファイル名)が決まる

コンテンツが同じならファイル名も同じ

gitオブジェクトのまとめ

3種類のオブジェクト

コンテンツ (中身) アドレス (ファイル名)
blob zlib(fileA) sha1(fileA)
tree zlib(index) sha1(index)
commit zlib(name(tree)+meta) sha1(name(tree)+meta)

commitの親子構造=改変履歴

コンテンツアドレスシステム

ファイル内容の変化に敏感なファイルシステム

オブジェクトは人が触るものではない

高レベルコマンドを通じて自動的に生成される

ブランチ

ブランチの正体

論理的:枝のこと
技術的:commitオブジェクトへの参照のこと

ブランチの作成

$ cat .git/refs/heads/main  # mainブランチの正体は?
330b...                     # ただの参照(テキストファイル)

330bって何?

少し前に作った"2nd commit"のこと

$ git cat-file -t 330b
commit

$ git cat-file -p 330b
tree 4423...
parent 677e...
author shinsuke-mat <...> 1745987355 +0900
committer shinsuke-mat <...> 1745987355 +0900

2nd commit

HEAD

HEADとは何なのか?

論理的:現在作業しているブランチのこと
技術的:ブランチへの参照のこと

HEADの確認

$ cat .git/HEAD  # HEADの中身は?
330b...          # ただの参照(テキストファイル)

履歴を確認

$ git log
commit 330b (HEAD -> main)
Author: shinsuke-mat <...>
Date:   Wed Apr 30 16:13:38 2025 +0900

    2nd commit

commit 677e...
Author: shinsuke-mat <...>
Date:   Wed Apr 30 16:13:28 2025 +0900

    1st commit

detached HEADとは?

よくあるトラブルの一つ

HEADがブランチを指し示していない状態

$ git checkout 677e  # HEADをbranchではなくcommitに
Note: switching to 677e.

You are in 'detached HEAD' state. You can ...

git-checkoutはやめよう

雑談

checkoutは汎用的な命令

様々な目的に使える

ブランチの切り替え (HEADの参照の書き換え)
現在の変更の取り消し
特定のファイルを過去の状態に戻す

git-switchを使うべき

ブランチの切り替えにしか使えない
目的特化のコマンド

$ git switch 677e
fatal: a branch is expected, got commit '677e'
hint: If you want to detach HEAD at the commit, ...

goto不要論からの学び

再掲

汎用的・使いやすいは必ずしも正義ではない

目的に特化している方が意図が明確

CMP JMP ではなく if()while()
 → 構造化プログラミング

var ではなく intstring,型なしよりも型あり

自前main()より制御の反転

できることを制限したほうが良い場合がある

意図が明確になる
物事が単純になる
例外がなくなる
秩序が生まれる

モダンSW開発 -版管理-


目次

・VCSって?
・分散型と集中型
・何のためにVCSを使う?

・Gitの内側
・オブジェクトDB
・演習
・branch/HEAD

・VCSのうまい使い方
・適切なコミットメッセージを付ける
・小さくコミットする
・VCSで管理すべきデータ

VCSのうまい使い方

VCSはただのツールである

強力なツールだが使えばOKというわけではない

プログラミングと同じ
「動く」の先に「良い」がある

VCSをどう使うかが重要

開発という作業をどう管理するか?

うまく管理できれば避けられる問題もある
変更の衝突が典型例

衝突自体は避けられない (gitは衝突を許容する)
衝突した後のマージを楽にする方法を考えるべき

適切なメッセージを付ける

悪いコミットメッセージ

何も書いていないのと同じ
bug fix more work minor changes oops wtf

コミットの区別がつかない
work on feature X work on feature X work on feature X

事実しか書いていない (意図が不明)
Change X constant to be 10

https://www.codelord.net/2015/03/16/bad-commit-messages-hall-of-shame/

良いコミットメッセージ

なぜその変更を行ったのか意図が明確
自己完結である / 過去形である

まつ本のコミット

雑談
$ git log --oneline  # この授業資料の履歴を確認
e588063 (HEAD -> main) writing vcs
7b889c8 writing vcs
fe9d82b writing vcs
df18f82 update firebase deploy
fdb72e1 cp fig folder
2f16539 rm public folder
9c2edc8 setup firebase5
e43c5be setup firebase5
a84693f setup firebase4
e4d3389 setup firebase3
f3e7ec1 setup firebase2
...

👎👎👎

小さくコミットする

コミットはatomicにすべき

一つの動詞で表現できる
refactor fix feat test 等はOK,change はNG

良い履歴の例

* feat: add claimUsername test helper
* test: ensure that username is at least 5 characters long
* test: ensure that in-use username cannot be claimed
* refactor: have registerNewTestUser use claimUsername
* refactor: have completeUserOnboarding use claimUsername

https://dev.to/gajus/what-makes-a-good-commit-3plh

コードを破壊しない

常にテストが通る状態を維持する

Linusのコミット

雑談
$ git log ac71fabf15679fc7
commit ac71fabf15679fc7bc56c51bc92bd4b626564c37
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date:   Sun Apr 20 11:30:11 2025 -0700

    gcc-15: work around sequence-point warning

    The C sequence points are complicated things,
    and gcc-15 has apparently added a warning for
    the case where an object is both used and modified
    multiple times within the same sequence point.

    That's a great warning.
    ...

単項演算のタイミングを変更しただけで
2k文字のメッセージを書いている

https://github.com/torvalds/linux/commit/ac71fabf15679fc7

Tangled commit

雑談

複数の意図を含む巨大なコミットのこと
開発中に自然に発生する

バグ修正の15%以上はtangled
tangledを自動でほどく手段が必要

K. Herzig and A. Zeller, "The impact of tangled code changes," MSR, 2013

VCSで管理すべきデータ

管理すべきデータ

ビルド・テストに必要な全てのファイル
ソースコード / テストコード / 設定ファイル

各種ドキュメント
README / LICENSE / CONTRIBUTING

管理すべきではないデータ

自動生成ファイル .o .class dist/ .log
外部から取得できるデータ .jar .dll

履歴のノイズになる

各種言語の .gitignore も公開されている
https://github.com/github/gitignore

研究でもGitを使おう

実験用スクリプト

間違いなく「開発」の一種でありGitで管理すべき
スクリプトの入力データもGit内で管理する

スクリプトの出力データは? (=自動生成ファイル)
スクリプトの実行方法を管理するほうがベター

実行が高コストなら実験結果は別方法で管理する

論文LaTeX

Gitと極めて相性が良い
複数人での作業が可能
一人の執筆でもやはりGitを使うべき

プレゼンはイマイチ (バイナリはGitの恩恵が少)

モダンSW開発 -版管理-


目次

・VCSって?
・分散型と集中型
・何のためにVCSを使う?

・Gitの内側
・オブジェクトDB
・演習
・branch/HEAD

・VCSのうまい使い方
・適切なコミットメッセージを付ける
・小さくコミットする
・VCSで管理すべきデータ

版管理は使えるべき

再掲

SW開発における「当たり前技術」

版管理しないプロジェクトはない
共同開発で必須 / 1人開発でも役立つ

主要機能:履歴の記録 / ブランチ / マージ

❌ 破壊的操作の前にファイルコピーを取ろう
⭕ 破壊的操作の前にgit-commitしよう

SW開発以外でも使える

  • 実験スクリプト
  • 論文LaTeX
  • 研究室のドキュメント管理
  • 講義資料

--- # 変更の差分はどこへ? <div class="corner-triangle"><div class="corner-triangle-text">雑談</div></div> ## Q. どのファイルをどのように変更したのか? commitが参照するtreeはスナップショット 差分ではない 変更の内容はどこに記録されるのか? 過去の復元:treeから全ファイルを復元すればOK 過去と現在の差分:??? ## A. gitは変更内容を記録していない 差分を調べる際は2つのスナップショットの差分を計算する 差分 = tree A - tree B

--- 研究でもGitを使おう 実験スクリプト + 実行方法 → ビルドツール 論文latex リポジトリは自動化の中心にある - 開発者がリポ内のソースを更新する - CIが 自動生成はいれるべきではない バイナリも 良いメッセージを付ける Linuxのgit-log GitHub Gitのホスティングサービス 小さくコミットする 意味のある範囲でまとめる トップダウンな作業をすべき.