20200216のGitに関する記事は13件です。

Git概要

引用

https://www.atmarkit.co.jp/ait/series/3190/
https://www.ricksoft.jp/blog/archives/9483/

3つの場所

  • Gitリポジトリ
    • ファイルやディレクトリの状態を保存する領域
    • 実態は作業ディレクトリの中の「.gitディレクトリ」
  • 作業ディレクトリ
    • ファイルの編集作業を行うディレクトリ
  • ステージングエリア
    • 作業ディレクトリとGitリポジトリの中間に存在する領域。
    • コミットする準備ができたファイルはここに追加

ファイルの状態

  • tracked:追跡している
    • unmodified
      • スナップショットがリポジトリに保存されているファイル
    • modified
      • Gitが変更を追跡しているが変更がリポジトリに保存されていないファイル
    • staged
      • ステージングエリアに追加され、次回コミットの対象となっているファイル
  • untracked:追跡していない

コミット

  • コミットはその時点の全てのファイルの情報を持つ
  • Gitはコミット単位で変更履歴を管理し、以下の情報を登録する
    • コミット前のリビジョンID
    • 1つ前のリビジョンID
    • 全ファイルのツリー情報のスナップショット
  • 差分は比較対象のコミット同士だけで可能

ブランチ

  • ブランチの実態は「一連のコミット履歴」の最新のコミットを指すポインタである。
  • HEADは基本的にカレントブランチを指す
Gitブランチの操作 イメージ
作成 最新コミット情報に目印をつける
コミット コミット情報を作成して目印を移動する
マージ 新しいコミット情報を造りそこに目印を移動する
削除 コミット情報から目印を失くすこと(コミット情報は削除しない)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bundle install --path vendor/bundle で余計なコードをgit管理から除外するために。

はじめに

$ bundle install --path vendor/bundle

私は普段、Railsアプリを作成する際に、gem のインストール先が Ruby 環境でなくプロジェクト毎に gem を管理できるようにvendor/bundleフォルダをインストールパスに指定してこのコマンドを実行しています。

ですが、vendor/bundle を .gitignore に設定していないと、すべての gem が git 管理されてしまい、 bundle install 後の git push がけっこう重くなったりしていました。それに、必要な gem は git 管理対象の Gemfile と Gemfile.lock に書いてあるので、vendor/bundle 配下にある gem 自体を git 管理する必要がそもそもありませんでした。

対処法

vendor/bundle の git 管理から外すため、.gitignore に vendor/bundle を追記します。

.gitignore
vendor/bundle

本来はアプリを作成して bundle install する前にこれをやっておけばよかったのですが、私の場合はこれをやらなかったので、もうひと手間必要になりました。

1.ローカルで vendor/bundle を削除

$ rm -rf vendor

ローカル上の vendor/bundle 配下のディレクトリが全て消去されます。リモートとの差分を確認すると、削除した vendor/bundle 配下のディレクトリ等が赤字で表示されます。

$ git status
deleted:    vendor/bundle/~~~~~~~~~~~~
・
・
・
・

2.変更した内容をリモート(GitHub)にpush

$ git add .
$ git commit -m "vendor/bundleの削除"
$ git push origin master

ローカルとリモートのコードの差分がなくなります。

3.再度bundle installの実行

$ bundle install --path vendor/bundle

ローカル側に再び vendor/bundle が復活します。

4.Git管理からの除外確認

$ git status
On branch master
nothing to commit, working tree clean

.gitignore 編集前は、 bundle install 後の git status で vendor/bundle 配下のディレクトリ等が大量に差分変更として表示していましたが、 git 管理から除外したためworking tree cleanの表示になりました。

今後はこのように gem を管理していこうという自分への戒めです。今回は以上となります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gitについて

はじめに

今回、gitに関して体系的にインプットする機会があったので、備忘録も兼ねてまとめてみました。

この辺りが対象に書いてます。

・gitって何それ?って人
・仕事でgit使ってるけど、詳しくは分かんないなーって人

gitとは

ソースコードを管理する仕組み
分散管理システムにあたる。

中央管理システムと分散管理システム

分散管理システムを理解するために、まずは対義語にあたる中央管理システムについて。

中央管理システムは簡単に言うと1つのサーバーで全体を管理しているもの。
例:googleドライブ

デメリットとしてエンジニアによる同時に編集厳しい事。
サーバーが1つである為、それが壊れたら積んでしまう事。

この中央管理システムのデメリットに対応する為に作られたのが、分散管理システム。

分散管理システムは中央管理システムとは違い、本体だけでなく完璧な複製をユーザー各々の手元における。

それゆえに本体のサーバー壊れても死なない。
複数人での共同開発がしやすい。

git-flow

gitの実際の運用に関しては実際の開発現場ごとの作法に則るべきです。
その上で、一般的に言われているgitの運用の流れについてです。

gitコマンド

add gitで管理するファイルを追加
commit pushするための準備
push サーバーに反映
pull サーバーから差分を持ってくる
status 差分確認
branch ブランチ確認
checkout  ブランチ切り替え/作成
clone githubからクローンしてくる

git pushまでの流れ

0: git cloneでプロジェクトを手元に持ってくる
1:手元で開発する
2:githubに追加したい分をgit add ファイル名で追加する
3:git commitする。
4:git pullで他の人の差分を手元に反映させる
5:git pushでgithubにプッシュする
6:必要であれば、githubのpushしてあるブランチから、プルリクエストしたいブランチにプルリクエストを送る。

branchの運用

masterブランチ 

大元のブランチ。
開発時はユーザーの目に触れているサービスはmasterブランチの状態になっている。

developブランチ

開発時のメインブランチ。
開発が進んでいく度にpushしていく。
ユーザーの目に触れる前に試したい機能などはこのブランチを通じて行う。

featureブランチ

機能ごとの作成など、同時並行で開発を行う際にdevelopブランチから切るブランチ。
目的の開発が終わったらdevelopブランチにプルリクを送る。

おまけ gitとgithubの違い

git共有用のwebサービス
gitは技術そのもの
githubはそれらを提供しているサービス
プルリクなどの機能が充実している。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

globalなgit-hooksを設定して、すべてのリポジトリで共有のhooksを使う

皆さん、git-hooksは使っているでしょうか。

${REPOSITORY_ROOT}/.git/hooks/ 以下に決められた名前でスクリプトを作成すると、対応した操作に合わせて、その名前のスクリプトが自動で実行されます。

例えば、${REPOSITORY_ROOT}/.git/hooks/pre-commit ならコミット前にそのスクリプトを実行して、スクリプトの終了コードが0でないとき、commit前に処理が停止します。

よく使われるのは、pre-commitを使用して、コミット直前にCIと同様の構文チェックや、rubocop等のテストをすることではないでしょうか。

基本的には、上記のようにRepositoryごとにhooksは書くのがデフォルトですが、
最近、私はすべてのRepositoryで共通するような、設定や除外ファイルの記載をしたgitconfigやgitignoreをその他設定系ファイルとまとめてgit管理しています。

そんな中、git-hooksもすべてのリポジトリで共通化できないかと思い、調べてみました。

(この記事は基本的にMac環境での動作を前提に書いています。ほとんどの処理は他環境でも動くと思いますが、一部Homebrewに依存した処理があるので、その部分は読み替えてください。)

先行事例の調査

「git hook global」でGoogle検索するとそれっぽい情報がいくつか出てきます。

しかしこれらを読んでいくと、あくまでinitやclone時のテンプレートに含めて、最初にテンプレートからコピー=実質的にすべてのリポジトリで共通化という感じでした。

この方法だと以下のような欠点が考えられます。

  • 一度init, cloneしたリポジトリに一律で新しい変更を反映できない
  • すでにinit, clone済みのリポジトリには自動で反映されない

ただ記事自体の最終更新日も古く、gitは2系になっていろいろ新しい機能・設定も追加されています。
そのため、 gitconfigをグローバルで設定するときに使われる ~/.gitconfig のようなやり方があるのではないかと、公式ドキュメントを確認しました。

ありました。

Git - githooks Documentation

このドキュメントには、以下のように記載されています。

By default the hooks directory is $GIT_DIR/hooks, but that can be changed via the core.hooksPath configuration variable (see git-config[1]).

すなわち、 core.hooksPath にhooksディレクトリのパスを指定すれば、そちらを優先的に読み込んでくれるようです。

なので、私は管理しやすいように core.hooksPath を .config/git/hooks に指定しました。
(最近のgitでは、 .config/git/ にグローバルなデフォルト設定を書き込むことが多いのでそれに倣った形です)

core.hooksPath に書き込むには以下のコマンドを実行する、あるいは ~/.gitconfig or ~/.config/git/config に直接書き込む方法があります。

git config --global core.hooksPath .config/git/hooks

設定ファイルに直接書き込む場合は、以下の設定をconfigファイルに追記します。

~/.gitconfig
 [core]
    hooksPath = ~/.config/git/hooks

これで、pre-commitなどのgit-hooks時に ${GIT_ROOT}/.git/hooks ではなく、設定したディレクトリのファイルを読み込んでくれるようになりました。

gitリポジトリ内のgit-hooksも実行するようにする

上記の方法ですべてのgitリポジトリにおいて、git-hooksでは .config/git/hooks のスクリプトを実行するようになりました。

しかし、もしもリポジトリごとに個別のgit-hooksを設定したくなったとき、このままでは ${GIT_ROOT}/.git/hooks にファイルを置いても実行されません。

そのため、リポジトリ側においたスクリプトも実行されるように .config/git/hooks においたスクリプト側で設定します。

この記事のスクリプトこの記事のgitリポジトリのルートを取得するコマンドを参考に、以下のようなスクリプトを作成しました。

_local-hook-exec
#!/bin/bash

GIT_ROOT=`git rev-parse --show-superproject-working-tree --show-toplevel | head -1`
HOOK_NAME=`basename $0`
LOCAL_HOOK="${GIT_ROOT}/.git/hooks/${HOOK_NAME}"

if [ -e $LOCAL_HOOK ]; then
  source $LOCAL_HOOK
fi

このスクリプトでは、そのリポジトリ内に同名のgit-hooks用のスクリプトが存在している場合、そのスクリプトを実行するようになっています。

このファイルをすべてのgit-hooksスクリプトで実行されるようにします。
すべてのgit-hooksスクリプトを .config/git/hooks に一括で作成するため、gitに用意されているsampleファイルからスクリプト名を取得して作成するコマンドを用意しました。

ここでは↑に書いたスクリプトを _local-hook-exec というファイル名で作成済と仮定します。
(_ を先頭につけているのは、git-hooks用のコマンドか否かをわかりやすくするためです)

また、sampleのPATHを取得するため、6行目でbrewのコマンドでgitがインストールされた場所を取得しています。
Mac以外、あるいはHomebrew以外でインストールした場合は、適切に読み替えてください。

GIT_HOOKS_DIR="${HOME}/.config/git/hooks"

echo '#!/bin/bash' >> hooks_template
echo '' >> hooks_template
echo 'source `dirname ${0}`/_local-hook-exec' >> hooks_template
ls `brew --prefix git`/share/git-core/templates/hooks | sed s/.sample//g | xargs -I{} cp -n hooks_template ${GIT_HOOKS_DIR}/{}
chmod 700 ${GIT_HOOKS_DIR}/*
rm hooks_template

このコマンドでは git側にあるhooksのテンプレートからhooksのファイル名一覧を取得→取得したファイルに対して、以下のスクリプトをコピーしています。

#!/bin/bash

source `dirname ${0}`/_local-hook-exec

これにより、すべてのコマンド実行時に _local-hook-exec が呼ばれる=gitリポジトリ側に設置したgit-hooksスクリプトが実行されるようになりました。

また、今後git側で対応するhooksが増えても同様のスクリプトを実行するだけで、同様の状態にすることができます。

まとめ

この手順を踏むことで、すべてのRepository共通のhooks、および各Repository専用のhooksをそれぞれ実行できる様になりました。

これで、言語共通で同じ処理をしたいときは、ファイルの拡張子ごとにその処理をするように、プロジェクト特有の処理はそのRepository内で定義して実行できるようになりました。

例えば、すべてのRepositoryで共通してやりたいこととしては、AWSアクセスキーをgitに誤って登録しないようにすることなど、セキュリティ的に含まないようにする処理などいろいろ考えられます。

私自身もどこまでglobalにするかは考えている最中ですが、私のglobalなgit-hooksはここで公開していますので、興味がある方は参考にしてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gemfileでインストールするgemを間違えた時の対処法

本記事の対象者

「インストールしようと思っていたgemを間違えてしまった」
「Gemfileでインストールしたgemを削除したい」
そんな方に向けてこの記事を書いています。

ステップ1:Gemfileから削除する

はじめに、Gemfileから間違ってインストールしたgemの記載を削除します。

ステップ2:Gemfile.lockから削除する

次に、Gemfile.lockの記載を更新します。Gemfile.lockは手動で修正せずに、以下のコマンドを実行して、自動で修正するようにしてください。

$ bundle install

もしくは

$ bundle install --path vendor/bundle

まとめ

Gemfileでインストールするgemを間違えた際には、以上のたった2ステップを実行することで対処できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gemfileでインストールするgemを間違えた時の取り消し方

本記事の対象者

「インストールしようと思っていたgemを間違えてしまった」
「Gemfileでインストールしたgemを削除したい」
そんな方に向けてこの記事を書いています。

ステップ1:間違ってインストールしたgemを削除する

まずは、以下のコマンドを実行して、間違ってインストールしたgemを削除しましょう。このとき削除するgemによっては、他のgemやコードに影響を及ぼす可能性があるので、注意してください。

$ bundle exec gem uninstall <gem名>

ステップ2:Gemfileから削除する

続いて、Gemfileから間違ってインストールしたgemの記載を削除します。

ステップ3:Gemfile.lockから削除する

最後に、Gemfile.lockの記載を更新します。Gemfile.lockは手動で修正せずに、以下のコマンドを実行して、自動で修正するようにしてください。

$ bundle install

もしくは

$ bundle install --path vendor/bundle

まとめ

間違ってインストールしたgemは、①gemのアンインストール、②Gemfileの書き換え、③Gemfile.lockの書き換えの3ステップで行えました。
ただし、gemをアンインストールする前に、他のgemやコードへの影響がないか確認をしてから行うようにしましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gitを深く理解するためにメンタルイメージその1:コミット

Gitコミットを正しく理解していますか?

誤解1:コミットはレポジトリのあるひとつの変更のみを表している

コミットが「点」として認識されているケースです。図で解説するときに「点」で表すことが多いので、そのまま「コミット=点」と考えがちですがそれは不十分な認識です。

コミットは常に親コミットの情報も併せ持っています。ですから、その情報を使って辿ることが出来る全ての先祖コミットを含むコミットの集合を代表しています。それらのコミット群はグラフもしくはツリーの特徴を持っていて、コミットグラフやコミットツリーとも呼ばれます。(ここではコミットツリーという呼び方に統一します)

下図で、3つのコミットX,Y,Zと、それぞれのコミットツリーの例を示します。例えばコミットXの話をしている時は、実はそこまでに至るすべてのコミット(黄色部分)も含めての話をしているのです。この場合、4つのコミットを含みます。

image.png

次に同じ3つのコミットをGitkで見てみましょう。
コミットの位置が分かりやすいようにブランチX,Y,Zを作っておきました。

この記事では便宜上、コミットのIDであるハッシュ値を使わずにブランチ名を使用しますが、ブランチ名の代わりにそのブランチが指すコミットのハッシュ値を使っても同じ結果になります

ところで、Gitkのグラフをパッとみて理解する必要はありません。大切なのは、ある特定のコミットからコミットツリーをイメージできるかどうかです。

image.png

この状態のレポジトリをGitHubに上げておきます https://github.com/yoshiwatanabe/learngit

練習として、コミットXの位置から、順次親コミットを辿って「先祖コミットのコミットツリー全体」をイメージしてみましょう。下図左側のようになります。ところでコミットXの場合、ツリー構造ではなくリスト構造になっていますが、それはたまたま途中にマージコミットが無かっただけです(マージコミットに関しては別記事で解説する予定です)。コミットYとコミットZについても同様に、それぞれのコミットツリーをイメージしてみます。

image.png

このように「コミット」を考える時は「コミットの点」ではなく「コミットツリー」として考えるようにしましょう。そうすることで何となくわかったようで実はよく分からなかったGit操作が明瞭に理解できるようになるでしょう。例えばgit diffコマンドは2つのコミットの点ではなく2つのコミットツリー間での差分を調べていると理解できます。またgit checkoutコマンドは指定されたコミットのコミットツリーに含まれるすべてのコミットを反映した状態を作業ディレクトリに展開している、と正しくより直感的に理解できるようになるでしょう。

この「コミットツリーに含まれるすべてのコミットを反映した状態」は次章で解説する2つ目の重要なイメージに関係があります。

コミットはコミットツリーに含まれるすべてのコミットを反映したレポジトリの状態を表す

よくある誤解が「コミットは差分の記録」という認識です。これは2つの意味で誤解を含んでいて、混乱の原因のひとつになっています。ひとつひとつ見ていきましょう。

コミットはあくまでもレポジトリの状態を表す

まずレポジトリが含む「どのコミット」を選んだとしても、そのコミットツリーの変更が反映されたレポジトリの状態を示します。繰り返しますがレポジトリの状態です。「差分」というイメージではなく「全体」というイメージ、「個別のコミット」に着目するのではなく、「全体としてのレポジトリ」に着目するのが大切です。

コミットはレポジトリの、とあるバージョン(リビジョン)を表しているに過ぎません。バージョンと言うと、つい直線的な成長をイメージしてしまいますが、Gitレポジトリではバージョンはタコの足のようにいろんな方向に延びていきます。それぞれの成長は直線的にコミットを積んで行き、なおかつ別の成長に合流したりもします(その際にはマージコミットという特別なコミットを、合流した両方の成長の上に積みます)。

レポジトリの状態とはすなわちファイルとディレクトリとファイルの内容の話になるので、実際にファイルを使って見ていきましょう(ただし、話を単純にするために、ディレクトリは作りませんし、ファイルの内容も加えません)。下図は、それぞれのコミットで新規にレポジトリに加えたテキストファイルを示しています。一番最初のコミットではa.txtが加えられ、その次のコミットでb.txtが加えられた、といった具合で開発を進めてきたと想定しましょう。

image.png

ところで、黒点の付いたコミットは、複数の親コミットを持つ特別なコミットでマージコミットと呼ばれるものです。マージコミットは、場合によってはマージコミットそのものがレポジトリに変更を加えることがありますが、今回は出来るだけシンプルにするためそういう状態が起こらないようにしています。

さて、先に上げたコミットX,Y,Zを例に使って「コミットツリーに含まれるすべてのコミットを反映したレポジトリの状態」をイメージ出来るように練習してみます。

image.png

コミットXは3つのコミット(Initial commitを除いて)を含んでいて、全部合わせてa.txt b.txt c.txt の3つのファイルをレポジトリに加えました。ですから、コミットXをcheckoutした作業ディレクトリは、その3つのファイルがある状態になるはずです。

image.png

コミットYはほとんど全部のファイルを含みますが、m.txt k.txt l.txt n.txtを含みません。

image.png

最後にコミットZは、コミットYには含まれていなかったk.txt l.txt n.txtを含みます。

image.png

これらの例が示す通り、コミットはレポジトリのあるバージョンの状態と同義です。ですから、あるコミットをチェックアウトすることはつまり、ある状態のレポジトリを作業ディレクトリに反映せよ、と言っているわけです。

Gitは「差分」を記録しない

差分というと、定義としては「変化した部分」だけですが、Gitコミットは決して「変化した部分」だけを記録するものではありません。正しくは「変化した部分を含んだ新たな状態」を記録する、です。別の言い方で「スナップショットを記録する」と言い、そちらの方がGitのイメージとしては正しいです。

・・・と言っておきながら、実は最適化の一部として圧縮の際に差分を考慮するそうです。ですが、話がややこしくなるので、概念上は「差分」を記録しない、あくまでも「スナップショット(状態)」を記録するとイメージします。

例えば下のビフォーアフターで考えてみましょう

ビフォー

a.txt
abc
123
xyz

アフター

a.txt
abc
123
xyz
456

何が変わったかというとアフターで、1行あらたに加えられて、その行の内容が456であったということです。

「差分」ではなく「ファイル全体のスナップショット」がコミットの一部として記録されたという状況を実際に検証してみます。

まず、コミットされた時点での a.txt ファイルの「内容」のSHA-1ハッシュ値を見てみます。ファイルをgit hash-object コマンドに渡すとファイルの内容のハッシュ値を戻します。このハッシュ値が記録されていれば、ファイルの内容全体が記録されたことになります。

image.png

コミットが実際にどのような構造になっているかはここでは省略します(commit, tree, blobの3つのオブジェクトで構成される木構造になっています。Qiitaに良記事があります。)

あと「Gitの中身」というタイトルで、3つに分けで4年前になりますが、動画をアップしてます。(スピード感が無くて結構ダルいです、すみません)

https://youtu.be/KknQgXfH_uM
https://youtu.be/mpqblZVNof0
https://youtu.be/1buKbzimhcI

Gitオブジェクトデータベースの理解は大切ですが、どうしても理解しておかなくてはならない、という性質のものではありません。

下はa.txtを含むディレクトリ(treeオブジェクト)の内容です。2e5e94a8b9e0feaf546dcff39348089054499b81というハッシュ値が記録されているのが分かります。この値は、先にa.txtの内容のハッシュ値を求めた時と同じ値です。ということで、コミットに記録されるのは「差分」ではなく「変更を含んだスナップショット」だと確認できました。

image.png

この例ではファイルひとつだけを見ていきましたが、複数のファイルに対する操作ともまったく同じように「状態のスナップショット」が最終的にひとつのコミットの元に集約されて、そのコミットで表されます。

例えば、ファイル名を変えると、treeオブジェクトが変わりますから、treeオブジェクトのハッシュ値が更新されます。ファイルを削除すると、やはりtreeオブジェクトの内容が変わるのでそのハッシュ値も変わります。同じファイルを別のディレクトリに移動した場合も同様です(ですが、ファイルの内容は変わらないので、同じblobが使われます)。

Gitレポジトリはブロックチェーンの原理とよく似ています。どちらも親を含んだハッシュ値でそのノードを表すグラフになってます。なので、ブロックチェーンでも、あるブロックチェーンはそれまでの全て先祖ブロックチェーンが改竄されていないことを保証します(その検証を可能にします)。またブロックそのものを元に集約されるトランザクションのどれもが改竄されていないことを保証します(検証可能にします)。

レポジトリの全体像のイメージ

ここまでで2つのデータ構造がイメージできるようになったでしょうか?

  • 「レポジトリの状態を表すコミット(コミットツリー)構造」のイメージ
  • 「ひとつのコミットの元に集約されているツリー構造(構成要素はtreeオブジェクトやblobオブジェクト)」のイメージ

この2つのデータ構造を頭の中にイメージ出来れば成功です。まず最初に、コミットツリーという2次元構造をイメージして、そのそれぞれのコミットの点について、さらにGitオブジェクト(treeやblob)からなるツリー構造がぶら下がっている3次元イメージです。

このメンタルイメージを基礎にして、他の概念、例えばブランチ、リモート、fetch push pull などのコマンド、ワーキングディレクトリやステージングの理解を深めて行くと、混乱が少なくて済むと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gitを深く理解するためのメンタルイメージその1:コミット

Gitコミットを正しく理解していますか?

誤解1:コミットはレポジトリのあるひとつの変更のみを表している

コミットが「点」として認識されているケースです。図で解説するときに「点」で表すことが多いので、そのまま「コミット=点」と考えがちですがそれは不十分な認識です。

コミットは常に親コミットの情報も併せ持っています。ですから、その情報を使って辿ることが出来る全ての先祖コミットを含むコミットの集合を代表しています。それらのコミット群はグラフもしくはツリーの特徴を持っていて、コミットグラフやコミットツリーとも呼ばれます。(ここではコミットツリーという呼び方に統一します)

下図で、3つのコミットX,Y,Zと、それぞれのコミットツリーの例を示します。例えばコミットXの話をしている時は、実はそこまでに至るすべてのコミット(黄色部分)も含めての話をしているのです。この場合、4つのコミットを含みます。

image.png

次に同じ3つのコミットをGitkで見てみましょう。
コミットの位置が分かりやすいようにブランチX,Y,Zを作っておきました。

この記事では便宜上、コミットのIDであるハッシュ値を使わずにブランチ名を使用しますが、ブランチ名の代わりにそのブランチが指すコミットのハッシュ値を使っても同じ結果になります

ところで、Gitkのグラフをパッとみて理解する必要はありません。大切なのは、ある特定のコミットからコミットツリーをイメージできるかどうかです。

image.png

この状態のレポジトリをGitHubに上げておきます https://github.com/yoshiwatanabe/learngit

練習として、コミットXの位置から、順次親コミットを辿って「先祖コミットのコミットツリー全体」をイメージしてみましょう。下図左側のようになります。ところでコミットXの場合、ツリー構造ではなくリスト構造になっていますが、それはたまたま途中にマージコミットが無かっただけです(マージコミットに関しては別記事で解説する予定です)。コミットYとコミットZについても同様に、それぞれのコミットツリーをイメージしてみます。

image.png

このように「コミット」を考える時は「コミットの点」ではなく「コミットツリー」として考えるようにしましょう。そうすることで何となくわかったようで実はよく分からなかったGit操作が明瞭に理解できるようになるでしょう。例えばgit diffコマンドは2つのコミットの点ではなく2つのコミットツリー間での差分を調べていると理解できます。またgit checkoutコマンドは指定されたコミットのコミットツリーに含まれるすべてのコミットを反映した状態を作業ディレクトリに展開している、と正しくより直感的に理解できるようになるでしょう。

この「コミットツリーに含まれるすべてのコミットを反映した状態」は次章で解説する2つ目の重要なイメージに関係があります。

コミットはコミットツリーに含まれるすべてのコミットを反映したレポジトリの状態を表す

よくある誤解が「コミットは差分の記録」という認識です。これは2つの意味で誤解を含んでいて、混乱の原因のひとつになっています。ひとつひとつ見ていきましょう。

コミットはあくまでもレポジトリの状態を表す

まずレポジトリが含む「どのコミット」を選んだとしても、そのコミットツリーの変更が反映されたレポジトリの状態を示します。繰り返しますがレポジトリの状態です。「差分」というイメージではなく「全体」というイメージ、「個別のコミット」に着目するのではなく、「全体としてのレポジトリ」に着目するのが大切です。

コミットはレポジトリの、とあるバージョン(リビジョン)を表しているに過ぎません。バージョンと言うと、つい直線的な成長をイメージしてしまいますが、Gitレポジトリではバージョンはタコの足のようにいろんな方向に延びていきます。それぞれの成長は直線的にコミットを積んで行き、なおかつ別の成長に合流したりもします(その際にはマージコミットという特別なコミットを、合流した両方の成長の上に積みます)。

レポジトリの状態とはすなわちファイルとディレクトリとファイルの内容の話になるので、実際にファイルを使って見ていきましょう(ただし、話を単純にするために、ディレクトリは作りませんし、ファイルの内容も加えません)。下図は、それぞれのコミットで新規にレポジトリに加えたテキストファイルを示しています。一番最初のコミットではa.txtが加えられ、その次のコミットでb.txtが加えられた、といった具合で開発を進めてきたと想定しましょう。

image.png

ところで、黒点の付いたコミットは、複数の親コミットを持つ特別なコミットでマージコミットと呼ばれるものです。マージコミットは、場合によってはマージコミットそのものがレポジトリに変更を加えることがありますが、今回は出来るだけシンプルにするためそういう状態が起こらないようにしています。

さて、先に上げたコミットX,Y,Zを例に使って「コミットツリーに含まれるすべてのコミットを反映したレポジトリの状態」をイメージ出来るように練習してみます。

image.png

コミットXは3つのコミット(Initial commitを除いて)を含んでいて、全部合わせてa.txt b.txt c.txt の3つのファイルをレポジトリに加えました。ですから、コミットXをcheckoutした作業ディレクトリは、その3つのファイルがある状態になるはずです。

image.png

コミットYはほとんど全部のファイルを含みますが、m.txt k.txt l.txt n.txtを含みません。

image.png

最後にコミットZは、コミットYには含まれていなかったk.txt l.txt n.txtを含みます。

image.png

これらの例が示す通り、コミットはレポジトリのあるバージョンの状態と同義です。ですから、あるコミットをチェックアウトすることはつまり、ある状態のレポジトリを作業ディレクトリに反映せよ、と言っているわけです。

Gitは「差分」を記録しない

差分というと、定義としては「変化した部分」だけですが、Gitコミットは決して「変化した部分」だけを記録するものではありません。正しくは「変化した部分を含んだ新たな状態」を記録する、です。別の言い方で「スナップショットを記録する」と言い、そちらの方がGitのイメージとしては正しいです。

・・・と言っておきながら、実は最適化の一部として圧縮の際に差分を考慮するそうです。ですが、話がややこしくなるので、概念上は「差分」を記録しない、あくまでも「スナップショット(状態)」を記録するとイメージします。

例えば下のビフォーアフターで考えてみましょう

ビフォー

a.txt
abc
123
xyz

アフター

a.txt
abc
123
xyz
456

何が変わったかというとアフターで、1行あらたに加えられて、その行の内容が456であったということです。

「差分」ではなく「ファイル全体のスナップショット」がコミットの一部として記録されたという状況を実際に検証してみます。

まず、コミットされた時点での a.txt ファイルの「内容」のSHA-1ハッシュ値を見てみます。ファイルをgit hash-object コマンドに渡すとファイルの内容のハッシュ値を戻します。このハッシュ値が記録されていれば、ファイルの内容全体が記録されたことになります。

image.png

コミットが実際にどのような構造になっているかはここでは省略します(commit, tree, blobの3つのオブジェクトで構成される木構造になっています。Qiitaに良記事があります。)

あと「Gitの中身」というタイトルで、3つに分けで4年前になりますが、動画をアップしてます。(スピード感が無くて結構ダルいです、すみません)

https://youtu.be/KknQgXfH_uM
https://youtu.be/mpqblZVNof0
https://youtu.be/1buKbzimhcI

Gitオブジェクトデータベースの理解は大切ですが、どうしても理解しておかなくてはならない、という性質のものではありません。

下はa.txtを含むディレクトリ(treeオブジェクト)の内容です。2e5e94a8b9e0feaf546dcff39348089054499b81というハッシュ値が記録されているのが分かります。この値は、先にa.txtの内容のハッシュ値を求めた時と同じ値です。ということで、コミットに記録されるのは「差分」ではなく「変更を含んだスナップショット」だと確認できました。

image.png

この例ではファイルひとつだけを見ていきましたが、複数のファイルに対する操作ともまったく同じように「状態のスナップショット」が最終的にひとつのコミットの元に集約されて、そのコミットで表されます。

例えば、ファイル名を変えると、treeオブジェクトが変わりますから、treeオブジェクトのハッシュ値が更新されます。ファイルを削除すると、やはりtreeオブジェクトの内容が変わるのでそのハッシュ値も変わります。同じファイルを別のディレクトリに移動した場合も同様です(ですが、ファイルの内容は変わらないので、同じblobが使われます)。

Gitレポジトリはブロックチェーンの原理とよく似ています。どちらも親を含んだハッシュ値でそのノードを表すグラフになってます。なので、ブロックチェーンでも、あるブロックチェーンはそれまでの全て先祖ブロックチェーンが改竄されていないことを保証します(その検証を可能にします)。またブロックそのものを元に集約されるトランザクションのどれもが改竄されていないことを保証します(検証可能にします)。

レポジトリの全体像のイメージ

ここまでで2つのデータ構造がイメージできるようになったでしょうか?

  • 「レポジトリの状態を表すコミット(コミットツリー)構造」のイメージ
  • 「ひとつのコミットの元に集約されているツリー構造(構成要素はtreeオブジェクトやblobオブジェクト)」のイメージ

この2つのデータ構造を頭の中にイメージ出来れば成功です。まず最初に、コミットツリーという2次元構造をイメージして、そのそれぞれのコミットの点について、さらにGitオブジェクト(treeやblob)からなるツリー構造がぶら下がっている3次元イメージです。

このメンタルイメージを基礎にして、他の概念、例えばブランチ、リモート、fetch push pull などのコマンド、ワーキングディレクトリやステージングの理解を深めて行くと、混乱が少なくて済むと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitにpushしたファイルをGit管理下から外す方法

はじめに

Gitにすでにpushしてしまったファイルを管理下から外す方法を備忘録としてまとめました。

ローカルにファイルを残してGit管理下から削除したい

$ git rm --cached [削除したいファイル名]

これでファイルを残したまま管理から外せます。
最後に問題がなければコミットをして終了です。

ファイルごとGit管理下から削除したい

$ git rm [削除したいファイル名]

こちらはファイルを残さずGit管理下から削除できます。

fatalエラーが出る

fatal: pathspec did not match any filesが出る場合。
これはGitに登録していないファイルがあると出るそうです。

$ git rm --ignore-unmatch [削除したいファイル名]

これで削除できました。

--ignore-unmatchは、Gitに登録していないファイルを無視して
git rmを実行できるようにするものらしいです。

参考記事

参考にさせていただいた記事です。
http://qiita.com/ytkt/items/a2afd6be8e4f06c1ea25
https://qiita.com/pugiemonn/items/2f6af4467b33ed3f41b5

最後に

最後まで読んでいただきありがとうございました。間違いがありましたらご指摘ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【git】git pushを取り消す方法

本記事の対象者

「git pushした後に間違いに気が付いてしまった!」
「git pushする前に戻りたい!」

そんな方に向けてこの記事を書いています。

主な対処法

  • git resetコマンド(履歴が残らずに修正できる)
  • git revertコマンド(履歴を残して修正する)

対処法1.git reset

git resetを実行すると、前のコミットの状態に戻ることができます。主にhardとsoftの2つのオプションを利用します。

$ git reset --hard HEAD^

このコマンドを実行するとcommit,add,ソースの変更を取り消すことができます。

しかし、ローカルでの変更等は残して、git pushだけ取り消したいという場合があると思います。その場合は以下のコマンドを実行してください。

$ git reset --soft HEAD^

このコマンドを実行するとコミットのみを取り消すことができます。

注意点1

git resetの後に、修正した内容をpushしようとすると、以前のコミットとコンフリクトしてエラーとなり、pushできません。

そのため、「-f」オプションをつけて強制的にpushする必要があります。

$ git push -f origin HEAD

これで何事もなかったように修正することができます。

注意点2

過去の履歴を書き換えてしまうので、チーム開発をしているとコンフリクトが起きる可能性があります。

注意点3

git resetの際に、直前のコミットのみ取り消すのであれば「HEAD^」で良いですが、直前のn個のコミットを取り消すのであれば「HEAD~n」と指定する必要があります。

対処法2.git revert

変更を取り消すのではなく、打ち消すことができます。(履歴は残る)
git resetとは異なり、過去の履歴を書き換えることがないため、安全だと言えます。

$ git revert HEAD

このコマンドを実行すると、過去のコミットを打ち消す、新しいコミットができるので、

$ git push origin HEAD

を実行すると再度pushすることができます。

注意点

git revertは安全な取り消し方法ですが、不要な履歴が増えてしまうので、個人開発などではgit resetの方が良いかもしれません。

まとめ

誤ってgit pushしてしまった時の対処法は2つあります。

  • git resetコマンド(履歴が残らずに修正できる)
  • git revertコマンド(履歴を残して修正する)

それぞれメリット・デメリットはあるので、その場に応じたコマンドを選択しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

git pushを取り消す方法

本記事の対象者

「git pushした後に間違いに気が付いてしまった!」
「git pushする前に戻りたい!」

そんな方に向けてこの記事を書いています。

主な対処法

  • git resetコマンド(履歴が残らずに修正できる)
  • git revertコマンド(履歴を残して修正する)

対処法1.git reset

git resetを実行すると、前のコミットの状態に戻ることができます。主にhardとsoftの2つのオプションを利用します。

$ git reset --hard HEAD^

このコマンドを実行するとcommit,add,ソースの変更を取り消すことができます。

しかし、ローカルでの変更等は残して、git pushだけ取り消したいという場合があると思います。その場合は以下のコマンドを実行してください。

$ git reset --soft HEAD^

このコマンドを実行するとコミットのみを取り消すことができます。

注意点1

git resetの後に、修正した内容をpushしようとすると、以前のコミットとコンフリクトしてエラーとなり、pushできません。

そのため、「-f」オプションをつけて強制的にpushする必要があります。

$ git push -f origin HEAD

これで何事もなかったように修正することができます。

注意点2

過去の履歴を書き換えてしまうので、チーム開発をしているとコンフリクトが起きる可能性があります。

注意点3

git resetの際に、直前のコミットのみ取り消すのであれば「HEAD^」で良いですが、直前のn個のコミットを取り消すのであれば「HEAD~n」と指定する必要があります。

対処法2.git revert

変更を取り消すのではなく、打ち消すことができます。(履歴は残る)
git resetとは異なり、過去の履歴を書き換えることがないため、安全だと言えます。

$ git revert HEAD

このコマンドを実行すると、過去のコミットを打ち消す、新しいコミットができるので、

$ git push origin HEAD

を実行すると再度pushすることができます。

注意点

git revertは安全な取り消し方法ですが、不要な履歴が増えてしまうので、個人開発などではgit resetの方が良いかもしれません。

まとめ

誤ってgit pushしてしまった時の対処法は2つあります。

  • git resetコマンド(履歴が残らずに修正できる)
  • git revertコマンド(履歴を残して修正する)

それぞれメリット・デメリットはあるので、その場に応じたコマンドを選択しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitのコミットコメントとGithubのissueを紐づけてタスク管理する

個人開発する際に,タスク管理のため行っているissueとbranchの紐付け方法のメモ.

Gitでコミットする時にissueの番号をコミットコメントにする

例を挙げると

git commit -m "#8"

とすると,codeのコメント欄ににissue番号#8に対してのリンクが付与されていることが確認できる.

コミットコメントに番号が含まれてさえいればいいので,下記のような形でもOK.

git commit -m "#8 削除機能実装"

これを利用して,1日から数時間程度で終わるぐらいの分量でissueを作成し,各issueごとにbranchを切って作業するとよい.

タスク管理の流れ

  1. issue作る
  2. issueひとつに対しひとつブランチ切る(ブランチ名もissue番号にしてしまうのがおすすめ)
  3. 作業する
  4. コミットする(右のコマンドでissue閉じながらコミットできる) git commit -m "hoge create close #8"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MATLABでGitを操作しよう (第3回 : チェックアウト "特定のコミットに戻る")

引き続きMATLAB GUIによるGit操作の第3回目

作成環境 MATLAB R2019b

第1回 : プロジェクト作成とGUIの説明
第2回 : git add/git commit
第3回 : チェックアウト "特定のコミットに戻る"
第4回 : ブランチ作成
第5回 : リモートリポジトリとの連携 (git push/git pull)
第6回 : クローンを作成する
第7回 : 応用1
第8回 : 応用2

よく見たら、チェックアウトが抜けていたので、今回はチェックアウト。次回ブランチ作成とさせて頂きます。また、次回はMATLABのAPIを使ってGitとどの様な連携が取れるのか?も紹介予定です。

チェックアウト

① : 特定のコミットに戻る
② : ブランチの切り替え
前者は、「覆水盆に返す」とでも言いましょうか、今回紹介します。
簡単に図示すると次の様なことをMATLAB GUIで実施してみます。
git3_0.png

1.ファイルを更新してコミットを繰り返す

準備として先ず、foo.mとgoofy.mがリポジトリにコミットされている所から話を始めます。
git3_1.png

ここでgoofy.mのファイルを開いてみます。(単なるコメント文のみです)
git3_2.png

次にgoofy.mを編集します。
git3_3.png

Gitのステータスアイコンが青い四角に変わったので、トラッキングしているファイルに変更があったことを意味しています。
git3_4.png

コミットコメントをシンプルに書いて、コミットを実行します。
git3_5.png

再度、goofy.mを編集し、コミットを行います。
git3_6.png

ここまでで、goofy.mは2回変更があったことが、コミットされているはずです。
そこで、「ブランチ」アイコンをクリックしてコミット履歴を確認してみます。
git3_7.png

2.チェックアウト(過去のコミットに戻る)

ブランチブラウザを閉じて、プロジェクトウィンドウで対象となるgoofy.mを右クリックします。
コンテキストメニューから「ソース管理」> 「Gitを使用して元に戻す」をクリックします。
git3_8.png

「ファイルを元に戻す」というウィンドウが起動します。
過去のコミットポイントが一覧で表示されるので、戻したいコミットポイントを選択して、「元に戻す」をクリックします。今回は一番最初の状態に戻してみます。
git3_9.png

goofy.mを開くと、無事に最初の状態に戻っていました。
git3_10.png

3.差分確認

最後に、MATLABの差分ツールを使って、テキストの差分を見てみます。
goofy.mを右クリックし「比較」> 「先祖と比較」をクリックします。

git3_11.png

確かに、チェックアウトしている状態のファイルと最新の状態のファイルのテキスト差分をちゃんと認識していることを確認出来ました。
git3_12.png

誤解の無いように補足すると、Gitはコミット毎にスナップショットを保存しており、ファイル差分を保存しておりません。スナップショット内のファイルからテキストデータを抽出してテキスト差分を表示しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む