Gitと変更差分|コマンドライン・Git・変更管理

このページはプロモーション(広告)を含みます。

コマンドライン・Git・変更管理(初級・読む30分+演習35分)

この授業のゴール

仕事でどう使うか

「報告書_最終.xlsx」「報告書_最終_修正2.xlsx」「報告書_本当に最終.xlsx」――ファイル名で版を管理すると、どこが変わったのか、なぜ変えたのかが誰にもわからなくなります。Git(ギット)は、ファイルの変更を行単位で記録し、いつ・誰が・何を・なぜ変えたかを後から確かめられるようにするバージョン管理システムです。

路線 この授業の使いどころ
DS(データサイエンティスト) 集計スクリプトやSQLの変更差分を見て、「数字が変わった原因はこの1行」と特定する
ML(機械学習エンジニア) 前処理・特徴量・設定ファイルの差分を確認し、精度が変わった実験と変更を対応づける
DX(DX推進・AI企画) 業務手順書や設定の変更を差分で承認する。誰が何を変えたかの説明責任を果たす
RS(研究者) 論文原稿や解析コードの版を差分で比べ、共同研究者に「前回からの変更点」を示す

この授業では、Gitの中心にある「変更を3つの場所に分けて扱う」考え方と、差分(diff)の読み方を身につけます。

前提

前提:

2種類の練習環境を区別してください。

環境 何か 何ができるか
模擬Git(ブラウザ内) この教材が用意した小さなPythonのモデル MiniGit。本物のGitではありません 3領域の動きを安全に、何度でも確かめる
実Git(Macのターミナル) あなたのMacに入っている本物のGit 本物のリポジトリを作る。操作は作業用フォルダ内だけで行う

模擬Gitは、本物の動きを単純化しています(例:ステージはファイル全体か、指定した内容で行う。IDは c1, c2 …の連番)。本物のGitのcommit IDは40桁の16進数(例:3f9a2c1…)です。

身近な状況:どこを直したのか説明できない

ハナさんは月次レポートの原稿 report.md を、次のように直しました。

行 直す前 直した後
1 # 月次レポート # 月次レポート
2 対象月: 2026-03 対象月: 2026-04 ← A:今月分に更新
3 売上: 1,200千円 売上: 1,200千円
4 担当: 佐藤 担当: 鈴木 ← B:担当交代(まだ上司の確認待ち)

上司から「対象月の更新だけ先に記録して。担当の変更は確認が取れてから」と言われました。Aは記録したい、Bはまだ記録したくない。ファイルは1つです。

ファイルを丸ごと保存するだけの方法(別名で保存)では、「Aだけ記録」はできません。Gitには、作業中の変更と、次の記録に入れる変更を分けて置く場所があるので、これができます。

直感的な説明:机の上・提出トレー・綴じた記録簿

Gitの3つの場所を、事務作業にたとえます。

たとえ Gitの用語 何が置かれるか
机の上 作業ツリー(working tree) いま編集しているファイルそのもの。Finderで見えるもの
提出トレー ステージ(staging area、インデックス index とも呼ぶ) 「次の記録にはこの内容を入れる」と選んだもの
綴じた記録簿 リポジトリ(repository)の commit 確定した記録。1回の記録が1つの commit(コミット)

提出トレーがあるおかげで、机の上でAとBを両方直していても、Aだけをトレーに置いて綴じることができます。Bは机の上に残り、あとで記録できます。

たとえと実際の違い:トレーに置いたものは「その時点の写し」です。トレーに置いた後に机の上で書き直しても、トレーの中身は自動では変わりません。この性質が、失敗1の原因になります。

正確な定義:status と diff が比べているもの

リポジトリは、プロジェクトのフォルダの中に作られる隠しフォルダ .git に保存される「全履歴とステージの情報」です。git init で作られます。

HEAD(ヘッド)は「いま基準にしている最新のcommit」を指す名前です。Gitの表示は、次の3つの組み合わせの比較です。

コマンド 比べるもの 表示される意味
git status の Changes to be committed HEAD と ステージ 次のcommitに入る変更(staged)
git status の Changes not staged for commit ステージ と 作業ツリー 編集したが、まだステージしていない変更(unstaged)
git status の Untracked files ステージに無いファイル Gitがまだ管理していないファイル(untracked)
git diff ステージ → 作業ツリー unstaged の中身
git diff --staged HEAD → ステージ staged の中身(--cached も同じ意味)

一度でもステージ(またはcommit)されたファイルを追跡済み(tracked)と呼びます。同じファイルが staged と unstaged の両方に出ることがあります。「一部をステージした」「ステージした後にまた編集した」ときです。

差分(diff)の読み方:Gitは行単位で比べ、統一差分形式(unified diff)で表示します。

diff --git a/report.md b/report.md
--- a/report.md
+++ b/report.md
@@ -2 +2 @@
-対象月: 2026-03
+対象月: 2026-04
記号 意味
--- a/… / +++ b/… 比較の古い側(a)と新しい側(b)
@@ -2 +2 @@ 古い側の2行目から1行、新しい側の2行目から1行が対象(-2,3 なら2行目から3行)
- で始まる行 古い側にだけある行(削除)
+ で始まる行 新しい側にだけある行(追加)
記号なし(空白)の行 前後の文脈。変わっていない

Gitには「行を書き換えた」という表現はありません。書き換え=古い行の削除+新しい行の追加として表示されます。また、Gitは文字単位ではなく行単位で比べるので、1文字の変更でも1行まるごと - と + になります。

段階付き例題:A行だけをステージする(3領域を追跡)

「身近な状況」の例題を、3領域の中身を追いながら解きます。最初の状態は「report.md の旧版(対象月 2026-03、担当 佐藤)がcommit済み」です。

手順 操作 作業ツリー ステージ HEAD(最新commit) git status
0 (開始) 03・佐藤 03・佐藤 03・佐藤 変更なし
1 A・Bを編集して保存 04・鈴木 03・佐藤 03・佐藤 unstaged: report.md
2 Aだけステージ(git add -p) 04・鈴木 04・佐藤 03・佐藤 staged と unstaged の両方に report.md
3 commit 04・鈴木 04・佐藤 04・佐藤 unstaged: report.md(Bだけ残る)

手順2の意味:ステージには「Aだけ反映した中間の内容」が置かれます。だから git diff --staged(HEAD→ステージ)にはAだけ、git diff(ステージ→作業ツリー)にはBだけが出ます。

手順3の意味:commitに入るのはステージの内容です。作業ツリーの内容ではありません。Bが未ステージで残るのは、ステージに置かなかったからで、正常な状態です。上司の確認後に git add report.md してcommitすれば記録できます。

Macのターミナルで試す(実Git)

git --version

初めてのとき、「コマンドラインデベロッパツールをインストールしますか?」と表示されたら、案内に従ってインストールします(Apple公式のツールです)。git version 2.x.x のように表示されればOKです。次に、記録に残る名前とメールアドレスを設定します(未設定のときだけ)。

git config --global user.name
git config --global user.name "Hana Sato"
git config --global user.email "hana@example.com"

1行目で何も表示されなければ未設定です。メールアドレスはcommitに残り、後でGitHub等に公開すると見える点に注意してください(秘密情報と共同作業 で扱います)。続いて練習用リポジトリを作ります。

mkdir -p ~/se01-practice/git-demo
cd ~/se01-practice/git-demo
git init -b main
printf '# 月次レポート\n対象月: 2026-03\n売上: 1,200千円\n前月比: +3%%\n前々月比: +1%%\n前年比: +8%%\n目標比: 96%%\n備考: なし\n担当: 佐藤\n' > report.md
git status
git add report.md
git commit -m "月次レポートの雛形を追加"

git init -b main は、最初のブランチ名を main にして空のリポジトリを作ります(古いGitで -b が使えないときは git init のあと git branch -m main)。printf の %% は % 1文字を書く書き方です。AとBの間に変わらない行を何行か入れたのは、git add -p で s(分割)を押したときに、AとBを別々の塊に分けられるようにするためです。

次に、テキストエディタ(open -e report.md でテキストエディットが開きます)で「対象月: 2026-04」「担当: 鈴木」に書き換えて保存し、次を実行します。

git diff
git add -p report.md

git add -p は変更の塊(hunk:ハンク)ごとに Stage this hunk [y,n,q,a,d,s,e,?]? のように聞きます(括弧内の文字の並びはGitの版によって少し違います。y=ステージする、n=しない)。この例では、AとBの間の変わらない行が6行しかないため、最初は2つの変更が1つの塊にまとまって表示されます(Gitは前後3行ずつの文脈が接する変更を1つの塊にまとめます)。まず s(split:分割)を押して2つの塊に分け、対象月の塊で y、担当の塊で n と答えます。最後に確かめます。

git status
git diff --staged
git diff

--staged には対象月だけ、git diff には担当だけが出れば成功です。

操作して理解する:模擬Gitで3領域を観察する

次のコードは模擬Git(ブラウザ内)の最小モデルです。3つの領域を、ファイル名→内容の辞書 head(最新commit)・index(ステージ)・work(作業ツリー)で表します。

実Gitの操作 最小モデルでの書き方
ファイルを編集して保存 work['report.md'] = 新しい内容
git add report.md index['report.md'] = work['report.md']
git add -p(一部だけ) index['report.md'] = 選んだ変更だけを反映した内容
git commit head = dict(index)(ステージの写しを記録)
git status status():3分類の辞書を返す
git diff / git diff --staged diff(index, work) / diff(head, index):差分の行のリストを返す

演習では、同じ動きを MiniGit というクラスにまとめたもの(g.write()・g.add()・g.add_part()・g.commit()・g.status()・g.diff())を使います。

# ===== 模擬Gitの最小モデル(ブラウザ内だけ。実際のGitではありません) =====
import difflib
head, index, work = {}, {}, {}      # 最新commit / ステージ / 作業ツリー(ファイル名 -> 内容)
def status():                        # git status の3分類
    return {'staged':    sorted(p for p in set(head) | set(index) if head.get(p) != index.get(p)),
            'unstaged':  sorted(p for p in index if work.get(p) != index[p]),
            'untracked': sorted(p for p in work if p not in index)}
def diff(old, new):                  # 行単位の差分(- 削除 / + 追加)
    out = []
    for p in sorted(set(old) | set(new)):
        for d in difflib.unified_diff(old.get(p, '').splitlines(), new.get(p, '').splitlines(), n=0, lineterm=''):
            if d[:1] in '+-' and d[:3] not in ('---', '+++'): out.append(p + ': ' + d)
    return out

# ----- ここから操作 -----
work['report.md'] = '# 月次レポート\n対象月: 2026-03\n売上: 1,200千円\n担当: 佐藤\n'
index['report.md'] = work['report.md']   # git add report.md
head = dict(index)                       # git commit(ステージの内容を記録。詳しくは次の授業)

# A行(対象月)とB行(担当)を編集して保存
work['report.md'] = '# 月次レポート\n対象月: 2026-04\n売上: 1,200千円\n担当: 鈴木\n'
print('status      :', status())
print('git diff    :', diff(index, work))

# A行だけを選んでステージ(git add -p で A の塊だけ y と答えた状態)
index['report.md'] = '# 月次レポート\n対象月: 2026-04\n売上: 1,200千円\n担当: 佐藤\n'
print('status      :', status())
print('diff staged :', diff(head, index))
print('diff        :', diff(index, work))

出力:

status      : {'staged': [], 'unstaged': ['report.md'], 'untracked': []}
git diff    : ['report.md: -対象月: 2026-03', 'report.md: +対象月: 2026-04', 'report.md: -担当: 佐藤', 'report.md: +担当: 鈴木']
status      : {'staged': ['report.md'], 'unstaged': ['report.md'], 'untracked': []}
diff staged : ['report.md: -対象月: 2026-03', 'report.md: +対象月: 2026-04']
diff        : ['report.md: -担当: 佐藤', 'report.md: +担当: 鈴木']

試してみよう:①最後に head = dict(index)(commit)を加え、status() で unstaged に report.md が残ることを確かめる。②work['memo.txt'] = 'メモ\n' を加えると untracked に出ることを確かめる。③A行だけの内容をステージする代わりに index['report.md'] = work['report.md'](git add report.md)にすると、diff(index, work) が空になる理由を説明する。

発展:差分が読めないファイルと、Gitが保存しているもの

バイナリファイル(Excel・画像・PDF・学習済みモデルなど、文字ではないファイル)では、Gitは Binary files differ としか表示できず、何が変わったか読めません。実務では、レビューしたい内容はCSV・Markdown・Pythonのようなテキスト形式で管理し、Excelはテキストに書き出した版も残す、といった工夫をします。巨大なデータを入れない理由は 秘密情報と共同作業 で扱います。

行末と空白:WindowsとMacでは行末の文字(CRLF と LF)が違い、見た目が同じでも全行が変更扱いになることがあります。git diff --word-diff は単語単位で差分を示すので、文章の推敲を見るときに便利です。

Gitはバックアップではありません:履歴は手元の .git フォルダの中にあるだけです。commitの中身とバックアップとの違いは commitと履歴 で扱います。

よくある失敗

失敗1:add した後に編集し、そのまま commit する

誤り:閾値を20にして git add、その後25に直したのに、add し直さずにcommitした。

# ===== 模擬Gitの最小モデル(ブラウザ内だけ。実際のGitではありません) =====
import difflib
head, index, work = {}, {}, {}      # 最新commit / ステージ / 作業ツリー(ファイル名 -> 内容)
def status():                        # git status の3分類
    return {'staged':    sorted(p for p in set(head) | set(index) if head.get(p) != index.get(p)),
            'unstaged':  sorted(p for p in index if work.get(p) != index[p]),
            'untracked': sorted(p for p in work if p not in index)}
def diff(old, new):                  # 行単位の差分(- 削除 / + 追加)
    out = []
    for p in sorted(set(old) | set(new)):
        for d in difflib.unified_diff(old.get(p, '').splitlines(), new.get(p, '').splitlines(), n=0, lineterm=''):
            if d[:1] in '+-' and d[:3] not in ('---', '+++'): out.append(p + ': ' + d)
    return out

work['clean.py'] = 'THRESHOLD = 10\n'; index['clean.py'] = work['clean.py']; head = dict(index)   # 初版
work['clean.py'] = 'THRESHOLD = 20\n'; index['clean.py'] = work['clean.py']   # 20 にして add
work['clean.py'] = 'THRESHOLD = 25\n'                                          # その後さらに 25 に直した
head = dict(index)                                                             # add し直さずに commit
print('commitされた内容:', head['clean.py'].strip())
print('作業ツリーの内容:', work['clean.py'].strip())
print('status:', status())

出力:

commitされた内容: THRESHOLD = 20
作業ツリーの内容: THRESHOLD = 25
status: {'staged': [], 'unstaged': ['clean.py'], 'untracked': []}

なぜ誤りか:ステージは add した瞬間の写しです。commitメッセージは「25に変更」なのに、記録されたのは20です。履歴を信じた人が誤った結論を出します。

直し方:commitの直前に必ず git status と git diff --staged を見て、入る内容を確かめる。編集したら add し直す。

失敗2:git diff が空だから「変更はない」と判断する

誤り:add した後に git diff を実行し、何も出ないので「変更なし」と報告した。

# ===== 模擬Gitの最小モデル(ブラウザ内だけ。実際のGitではありません) =====
import difflib
head, index, work = {}, {}, {}      # 最新commit / ステージ / 作業ツリー(ファイル名 -> 内容)
def status():                        # git status の3分類
    return {'staged':    sorted(p for p in set(head) | set(index) if head.get(p) != index.get(p)),
            'unstaged':  sorted(p for p in index if work.get(p) != index[p]),
            'untracked': sorted(p for p in work if p not in index)}
def diff(old, new):                  # 行単位の差分(- 削除 / + 追加)
    out = []
    for p in sorted(set(old) | set(new)):
        for d in difflib.unified_diff(old.get(p, '').splitlines(), new.get(p, '').splitlines(), n=0, lineterm=''):
            if d[:1] in '+-' and d[:3] not in ('---', '+++'): out.append(p + ': ' + d)
    return out

work['a.py'] = 'x = 1\n'; index['a.py'] = work['a.py']; head = dict(index)   # 初版をcommit
work['a.py'] = 'x = 2\n'; index['a.py'] = work['a.py']                       # 編集して add
print('git diff         :', diff(index, work))   # ステージ → 作業ツリー の差 → 空
print('git diff --staged:', diff(head, index))   # 最新commit → ステージ の差

出力:

git diff         : []
git diff --staged: ['a.py: -x = 1', 'a.py: +x = 2']

なぜ誤りか:git diff は「ステージ→作業ツリー」の差だけを見ます。すでにステージした変更は --staged にしか出ません。

直し方:状況把握は git status から始める。staged があれば git diff --staged、unstaged があれば git diff を見る。

失敗3:ホームフォルダで git init してしまう

誤り:cd だけ実行した直後(ホームにいる状態)で git init すると、ホーム全体(書類・写真・ダウンロード…)がリポジトリになります。git status に大量の untracked が並び、うっかり git add . すると個人ファイルまで記録対象になります。

なぜ誤りか:リポジトリは「そのフォルダとその下すべて」が範囲です。記録したいプロジェクトだけを含むフォルダで作る必要があります。

直し方:git init の前に必ず pwd を見て、プロジェクトのフォルダ(例:/Users/hana/se01-practice/git-demo)であることを確かめる。git rev-parse --show-toplevel は、いまいる場所がどのリポジトリに属するか(リポジトリの最上位フォルダ)を表示します。もしホームで作ってしまったら、コマンドで消そうとせず(rm -rf は使わない)、Finderで Command+Shift+. を押して隠しファイルを表示し、ホーム直下の .git フォルダだけをゴミ箱に入れます。不安なら詳しい人に画面を見てもらってください。

まとめ

用語

Git(Git)

ファイルの変更履歴を記録・比較・共有するためのバージョン管理システム。

作業ツリー(working tree)

いま編集しているファイルそのもの。Finderやエディタで見える状態。

ステージ(インデックス)(staging area / index)

次のcommitに入れる内容を置く場所。git add で更新する。

HEAD(HEAD)

現在基準にしている最新のcommitを指す名前。

差分(diff)(diff)

2つの版の違いを行単位で示したもの。- は削除、+ は追加。

hunk(ハンク)(hunk)

差分の中のひとまとまりの変更箇所。git add -p はhunkごとにステージするか尋ねる。

よくある質問

なぜステージという中間の場所が必要なのですか?

1回の作業で複数の目的の変更が混ざるのは普通だからです。ステージがあれば、『バグ修正』と『見た目の調整』を別々のcommitに分けられます。分けておくと、後で片方だけ取り消したり、レビューで理由を説明したりしやすくなります。

保存(Command+S)とcommitの違いは何ですか?

保存は作業ツリーのファイルを更新するだけで、前の内容は残りません。commitは、ステージの内容を履歴の1点として名前(ID)とメッセージ付きで残し、後から比較・復元できるようにします。

差分が大量に出て読めません。どこで失敗していますか?

多いのは、①Excelや画像などのバイナリ、②自動整形で全行の空白や改行が変わった、③行末文字(CRLF/LF)の違い、④関係ない複数の変更を一度にした、のどれかです。目的ごとに小さく変更し、整形だけの変更は別のcommitに分けましょう。

git add . と git add ファイル名 はどう使い分けますか?

git add . は現在のフォルダ以下の変更と新規ファイルをすべてステージします。便利ですが、秘密情報やデータまで入る危険があります。初めのうちはファイル名を指定し、git status で確かめる習慣をつけましょう(秘密情報と共同作業)。

条件が変わって、ファイルを削除した場合はどう扱われますか?

削除も変更です。作業ツリーで消すと unstaged に deleted と出て、git add ファイル名(または git rm ファイル名)でステージすると staged に deleted と出ます。commitすれば最新版からは消えますが、過去のcommitには残っています。

仕事では、どのタイミングで diff を見るべきですか?

最低でも『add の前(git diff)』と『commit の前(git diff --staged)』の2回です。意図しない変更(デバッグ用のprint、パスワード、巨大な出力)が混ざっていないかを、記録する前に止めるのが目的です。

確認問題

問1 git commit で記録される内容はどれか。

  1. ① 作業ツリーにある最新の内容
  2. ② untracked のファイルすべて
  3. ③ 前回のcommitとまったく同じ内容
  4. ④ ステージ(インデックス)にある内容
解答と解説

正解:④

commitはステージの内容を記録します。add した後に編集した部分は、add し直さない限り入りません。

問2 ファイルを add した直後に git diff を実行したら何も表示されなかった。正しい解釈はどれか。

  1. ① 変更はどこにも無い
  2. ② ステージと作業ツリーは同じだが、ステージした変更は git diff --staged で見る必要がある
  3. ③ Gitが壊れている
  4. ④ add が失敗した
解答と解説

正解:②

git diff はステージ→作業ツリーの差です。ステージした変更は HEAD→ステージの差なので --staged に出ます。

問3 次の差分で、実際に起きた変更として正しいものはどれか。

@@ -2 +2 @@
-売上: 1,200千円
+売上: 1,250千円
  1. ① 2行目が削除され、何も追加されていない
  2. ② 1,250千円 の行が新しく増え、行数が1つ増えた
  3. ③ 2行目の売上が 1,200千円 から 1,250千円 に書き換えられた
  4. ④ 1行目と2行目が入れ替わった
解答と解説

正解:③

書き換えは「古い行の削除(-)+新しい行の追加(+)」として表示されます。@@ -2 +2 @@ は両側とも2行目の1行が対象という意味です。

問4 report.md の2か所(A・B)を編集し、Aだけを git add -p でステージした。git status の表示として正しいものはどれか。

  1. ① report.md は staged と unstaged の両方に表示される
  2. ② report.md は unstaged のみに表示される
  3. ③ report.md は staged のみに表示される
  4. ④ report.md は untracked に表示される
解答と解説

正解:①

HEAD→ステージにはA、ステージ→作業ツリーにはBの差があるため、両方に表示されます。

問5 次のうち、ブラウザ内の模擬Git(MiniGit)について正しい説明はどれか。

  1. ① あなたのMacのリポジトリを直接操作している
  2. ② 模擬Gitのcommit IDは本物と同じ40桁になる
  3. ③ GitHubにも自動で記録が送られる
  4. ④ 本物のGitの3領域の考え方を単純化して再現した練習用のモデルである
解答と解説

正解:④

模擬Gitはブラウザ内だけのPythonモデルです。Macのファイルには触れず、IDも c1, c2 … の連番です。

問6 新しく作った notes.txt を一度も add していない。この状態の説明として正しいものはどれか。

  1. ① 自動で staged になる
  2. ② untracked で、commit しても記録されない
  3. ③ unstaged に表示される
  4. ④ すでにHEADに含まれている
解答と解説

正解:②

一度もステージされていないファイルは untracked(未追跡)です。add するまでcommitに入りません。

次に学ぶこと

次は commitと履歴 で、ステージした内容を commit として履歴に残し、良いcommitの単位とメッセージ、git log での履歴の読み方を学びます。今日の ~/se01-practice/git-demo には担当の変更(B)が未ステージで残っているはずなので、そのまま次の授業で使います。

パスやコマンドに不安があれば パスとコマンド に戻りましょう。

キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。

アプリを無料で開く有料版と無料版の違い