GitPythonチュートリアル

AI

GitPythonは、Gitリポジトリへのオブジェクトモデルアクセスを提供するライブラリです。このチュートリアルでは、実際の使用例を通してGitPythonの主要な機能を説明します。


GitHubのOAuthアプリのスコープ解説:必要な権限を正確に指定しよう
GitHubのOAuthアプリを開発する際、適切なスコープを設定することが重要です。スコープによって、アプリがアクセスできる範囲が制限されるため、必要最小限の権限のみを要求することが推奨されています。本記事では、GitHubのOAuthアプ...

Repoクラスを理解する

まず初めに、git.Repoオブジェクトを作成してリポジトリを表現します。

from git import Repo

# rorepoは、git-pythonリポジトリを指すRepoインスタンス
# 第1引数には操作対象のリポジトリのパスを指定
repo = Repo("/Users/mtrier/Development/git-python")
assert not repo.bare

上記の例では、/Users/mtrier/Development/git-pythonディレクトリが作業リポジトリであり、.gitディレクトリが含まれています。
ベアリポジトリ(作業ツリーのないリポジトリ)で初期化することもできます。

bare_repo = Repo.init("path/to/bare-repo", bare=True) 
assert bare_repo.bare

Repoオブジェクトは、リポジトリデータへのハイレベルなアクセスを提供し、ヘッドやタグ、リモートの作成・削除、リポジトリの設定変更などが行えます。

repo.config_reader() # 読み取り専用の設定の取得
with repo.config_writer(): # 設定を変更するwriter
    pass 

アクティブブランチの確認、未追跡ファイルの確認、リポジトリデータの変更状態のチェックなども簡単です。

assert not bare_repo.is_dirty() # 変更状態の確認
repo.untracked_files # 未追跡ファイルのリストを取得
# ['my_untracked_file'] 

既存のリポジトリからクローンしたり、新しい空のリポジトリを初期化することもできます。

cloned_repo = repo.clone("path/to/cloned/repo")
assert cloned_repo.__class__ is Repo # 既存リポジトリのクローン
assert Repo.init("path/for/new/repo").__class__ is Repo

リポジトリの内容をtarファイルにアーカイブすることも可能です。

with open("repo.tar", "wb") as fp:
    repo.archive(fp)

発展的なRepoの使い方

もちろん、Repoクラスにはもっと多くの機能があります。以下の例は、Gitの内部動作について深い理解が必要な場合もありますが、すぐには理解できなくても大丈夫です。

関連するリポジトリパスの確認:

assert os.path.isdir(cloned_repo.working_tree_dir) # 作業ファイルのあるディレクトリ
assert cloned_repo.git_dir.startswith(cloned_repo.working_tree_dir) # Gitリポジトリのあるディレクトリ  
assert bare_repo.working_tree_dir is None # ベアリポジトリには作業ツリーがない

HeadはGitにおけるブランチのこと。Referenceは特定のコミットや他のリファレンスを指すポインタ。HeadTagは参照の一種。GitPythonでは直感的に参照を操作できます。

repo.head.ref == repo.heads.master # headはmasterを指すsym-ref
repo.tags["0.3.5"] == repo.tag("refs/tags/0.3.5") # タグへのアクセス方法もいろいろ
repo.refs.master == repo.heads["master"] # .refsですべての参照(ヘッド、リモート、タグ)にアクセス可能

新しいヘッドの作成:

new_branch = cloned_repo.create_head("feature") # 新しいブランチを作成
assert cloned_repo.active_branch != new_branch # まだチェックアウトされていない
new_branch.commit == cloned_repo.active_branch.commit # チェックアウト中のコミットを指している  

タグの作成:

past = cloned_repo.create_tag("past", ref=new_branch, message="Tagging the past")
past.commit == new_branch.commit # タグは指定したコミットを指す 
assert past.tag.message.startswith("Tagging") # タグオブジェクトにメッセージを持たせられる

参照やオブジェクトを通してgitオブジェクトまで掘り下げられます。Commitなどのオブジェクトには追加のメタデータがあります。

past.commit.message != cloned_repo.head.commit.message
past.commit.tree["VERSION"].data_stream.read().startswith(b"3") # オブジェクトからバイナリデータを直接読み込める(作業ツリー不要)

Remoteを使うとフェッチ、プル、プッシュ操作が行え、進捗情報をデリゲートに渡すこともできます。

assert len(cloned_repo.remotes) == 1 # クローンしたのでリモートが1つあるはず
origin = bare_repo.create_remote("origin", url=cloned_repo.working_tree_dir)  
assert origin.exists()
origin.fetch() # フェッチしてリモートの最新データを取得
bare_repo.create_head("master", origin.refs.master) # 最新のmasterからローカルブランチを作成

index(ステージとも呼ばれる)は、次のコミットに書き込まれる変更や、マージ結果を格納する場所です。

index = repo.index 
index.add(["new-file"]) # indexに新しいファイルを追加
index.remove(["LICENSE"]) # 既存ファイルを削除(作業ツリーには影響なし)
index.commit("commit message") # 変更をコミット

他のツリーやマージ結果から新しいインデックスを作成したり、インデックスの内容を新しいファイルに書き出すこともできます。

from git import IndexFile

# ツリーからインメモリの一時インデックスに読み込む
IndexFile.from_tree(repo, "HEAD~1") 

# 2つのツリーを3-wayマージしてインデックスに書き込む
merge_index = IndexFile.from_tree(repo, "HEAD~10", repo.merge_base("HEAD~10", "HEAD"))

# マージ結果のインデックスをファイルに永続化  
merge_index.write("merged_index")

リファレンスを調べる

Referenceは、コミットグラフの先端であり、プロジェクトの履歴を簡単に調べるための起点となります。

master = repo.heads.master # masterブランチの先頭コミットへの参照
master.commit # masterが指すコミット
master.rename("new_name") # ブランチのリネーム

Tagは通常、不変のコミットを指す参照です。

tags = repo.tags
tag_0_8_1 = repo.tag("refs/tags/0.8.1") 
tag_0_8_1.tag # タグにはタグオブジェクトを伴うことがある
tag_0_8_1.commit # タグは常にコミットを指す
repo.delete_tag(tag_0_8_1) # タグの削除
repo.create_tag("0.8.1") # タグの作成

SymbolicReferenceは、他の参照を指す特殊な参照です。

head = repo.head # HEADはアクティブなブランチを指す 
master = head.reference # HEADが指している参照を取得

コミットの履歴を辿ることもできます。

master.commit.parents[0] # 一つ前のコミット
repo.merge_base("master", "stable") # masterとstableの共通祖先

参照を変更する

Referenceの作成、削除、参照先の変更は簡単に行えます。

new_branch = repo.create_head("new_branch") # 新しいブランチを作成
new_branch.commit = "HEAD~5" # ブランチの指すコミットを変更(インデックスと作業ツリーには影響なし)
repo.delete_head(new_branch) # ブランチの削除(チェックアウトされていないことが条件)

タグも同様に作成・削除できますが、参照先の変更はできません。

new_tag = repo.create_tag("0.8.1", message="Release 0.8.1")
# タグの指すコミットは変更不可。再作成が必要。
repo.delete_tag(new_tag)

Symbolic Referenceを変更することで、インデックスや作業ツリーに影響を与えずにブランチを切り替えられます。

repo.head.reference = new_branch # HEADをnew_branchに変更

オブジェクトを理解する

オブジェクトは、Gitのオブジェクトデータベースに格納できるあらゆるもののことです。オブジェクトにはタイプ、非圧縮サイズ、実際のデータに関する情報が含まれます。それぞれのオブジェクトは、20バイト(40文字の16進数)のSHA1ハッシュで一意に識別されます。

Gitが扱うオブジェクトは、BlobTreeCommitTagの4種類だけです。

GitPythonでは、すべてのオブジェクトに共通の基底クラスからアクセスでき、比較やハッシュ化が可能です。通常は直接インスタンス化せず、参照やリポジトリの特定の関数を通して取得します。

hc = repo.head.commit
hct = hc.tree
assert hc != hct # コミットとツリーを比較
assert hc != repo.tags[0] # コミットとタグを比較

オブジェクトの共通フィールド:

hct.type # オブジェクトのタイプ文字列 
hct.size # サイズ(バイト)
hct.hexsha # SHA1ハッシュ(40文字の16進数)
hct.binsha # SHA1ハッシュ(20バイトのバイナリ)

IndexObject は、Gitのインデックスに追加できるオブジェクト(ツリー、ブロブ、サブモジュール)のことで、ファイルシステム上のパスとモード情報を持っています。

hct.path # ルートツリーはパスを持たない
hct.trees[0].path # サブツリーはパスを持つ 
hct.mode # ツリーはLinuxのディレクトリと同じモード
hct.blobs[0].mode # ブロブは通常のLinuxファイルと同様のモードを持つ

blobのデータ(またはオブジェクトのデータ)には、ストリームを通してアクセスします。

hct.blobs[0].data_stream.read() # データを読み込むストリーム
hct.blobs[0].stream_data(open("blob", "wb")) # ストリームにデータを書き込む

Commitオブジェクト

Commitオブジェクトは、特定のコミットに関する情報を保持しています。コミットは、Referenceを通して取得するのが一般的ですが、以下のような方法でも取得できます。

指定したリビジョンのコミットを取得:

repo.commit("master") 
repo.commit("v1.0.0")
repo.commit("HEAD~10") 

コミットを反復処理。ページングが必要な場合はスキップ数を指定。

# masterブランチの最新50コミット
commits = list(repo.iter_commits("master", max_count=50))

# masterブランチの先頭から21〜30番目のコミット 
commits_21_30 = list(repo.iter_commits("master", max_count=10, skip=20))

コミットオブジェクトにはさまざまなメタデータがあります。

commit = repo.head.commit
commit.hexsha # SHA1ハッシュ 
commit.parents # 親コミットのリスト
commit.tree # コミットのツリー
commit.author.name # 作者名
commit.authored_date # 作成日時(エポック秒)
commit.committer.name # コミッター名
commit.committed_date # コミット日時(エポック秒)
commit.message # コミットメッセージ

日時はエポック秒形式で表現されます。人間が読める形式に変換するには、timeモジュールの関数を使います。

import time

time.strftime("%Y-%m-%d", time.gmtime(commit.committed_date))

親コミットをたどることで、コミットの系譜をたどれます。

commit.parents[0] # 一世代前のコミット
commit.parents[0].parents[0] # 二世代前のコミット
repo.commit("master~3") # 三世代前のコミット

Treeオブジェクト

Tree は、ディレクトリの内容へのポインタを記録します。masterブランチの最新コミットのルートツリーを取得してみましょう。

tree = repo.heads.master.commit.tree

了解です。チュートリアルの続きを説明していきます。

ツリーからコンテンツを取得できます。

len(tree.trees) # ツリー(サブディレクトリ)の数
len(tree.blobs) # ブロブ(ファイル)の数
len(tree) == len(tree.trees) + len(tree.blobs) # ツリーの全エントリ数 

ツリーはリストのように振る舞い、名前で要素を検索することもできます。

tree[0] == tree["some/path"] # インデックスとパスでのアクセス
for entry in tree: # ツリーの反復処理
    print(entry)

blob = tree[1]["some/file"] # サブツリー内のブロブの取得
blob.name
blob.path # 相対パス 
tree[blob.path] == blob # ツリーにパスでアクセス可能

パス指定でサブオブジェクトを取得する便利メソッドもあります。

tree / "some/path" == tree["some/path"] 
tree / blob.path == blob

リポジトリから直接ルートツリーを取得することもできます。

repo.tree() == repo.head.commit.tree
repo.tree("v1.0.0") # タグを指定
repo.tree("some_branch") # ブランチを指定

ツリーから再帰的に要素を取得するには traverse メソッドを使います。

len(tree) < len(list(tree.traverse())) # traverse()ですべての子孫を取得

差分情報の取得

差分は通常、Diffableのサブクラスのdiffメソッドで取得します。これは、パスごとの差分情報にアクセスしやすいDiffIndexオブジェクトを返します。

差分は、インデックスとツリー、インデックスとワークツリー、ツリー間、ツリーとワークツリー間で取得できます。コミットが関係する場合は、そのツリーが暗黙的に使われます。

commit.diff() # コミットのツリーとインデックスの差分
commit.diff('HEAD~1') # 一つ前のコミットとの差分
commit.diff(None) # ワークツリーとの差分

index = repo.index
index.diff() # インデックス自身との差分(空になる)
index.diff(None) # ワークツリーとの差分
index.diff('HEAD') # HEADのツリーとの差分

戻り値のDiffIndex は基本的に Diff オブジェクトのリストですが、必要な情報を見つけやすいようにフィルタリング機能を備えています。

# 追加されたパスの差分のみ処理
for diff_added in commit.diff('HEAD~1').iter_change_type('A'):
    print(diff_added)

git status のような機能を実装したい場合は、この差分フレームワークを活用します。

  • インデックスとHEADコミットの差分
    • repo.index.diff(repo.head.commit)
  • インデックスとワークツリーの差分
    • repo.index.diff(None)
  • 未追跡ファイルのリスト
    • repo.untracked_files

ブランチの切り替え

git checkout のようにブランチを切り替えるには、HEADのシンボリック参照を新しいブランチに向け、インデックスとワークツリーを合わせる必要があります。
シンプルな方法は以下の通りです。

# 現在のブランチから10コミット過去に巻き戻す
past_branch = repo.create_head('past_branch', 'HEAD~10') 
repo.head.reference = past_branch
repo.head.reset(index=True, working_tree=True)

# HEADの分離
repo.head.reference = repo.commit('HEAD~5')  

ただし上記の方法では、強制的にインデックスとワークツリーの内容が上書きされるため、作業中の変更が失われる危険があります。
git checkout のように、作業中の内容を保護するには、以下のような方法を取ります。

# git checkoutでブランチ切り替え
repo.heads.past_branch.checkout()

リポジトリの初期化

この例では、空のリポジトリを作成し、空のファイルをインデックスに追加してコミットします。

import os
from git import Repo

repo_dir = "my_new_repo"
file_name = "first_file"

os.mkdir(repo_dir)
repo = Repo.init(repo_dir)

open(os.path.join(repo_dir,file_name), 'wb').close() 
repo.index.add([file_name])
repo.index.commit("initial commit")

各メソッドには、動作をカスタマイズするための多数の引数が用意されているので、必要に応じて確認してください。

Git コマンドを直接使う

ラッパーメソッドがない場合でも、Repoインスタンスの git プロパティを通して、Git コマンドを直接実行できます。

git = repo.git

git.checkout('HEAD', b='new_branch') # 新しいブランチを作成
git.branch('another_branch') # ブランチを作成
git.branch('-D', 'another_branch') # ブランチを削除

デフォルトでは、コマンドの標準出力が文字列として返されます。

キーワード引数は、コマンドラインのオプションに変換されます。 flag=True と指定すると、値のないフラグオプションを表します。
None が引数に含まれていれば、無視されます。リストやタプルは、再帰的に展開されて個別の引数になります。オブジェクトは str() 関数で文字列に変換されます。

オブジェクトデータベース

git.Repo インスタンスは、内部のオブジェクトデータベースインスタンスの機能に基づいています。これは、データの取得や新しいオブジェクトの書き込みに使われます。

データベースの種類によって、一定時間にアクセスできるオブジェクト数、大きなデータの読み込み時のリソース使用量、メモリ使用量などの特性が決まります。

GitDB

GitDBは、GitオブジェクトデータベースのピュアPython実装です。GitPython 0.3のデフォルトのデータベースです。
大きなファイルを扱う際のメモリ使用量は少ないですが、大量の小さなオブジェクトを密集したリポジトリから取得する場合、GitCmdObjectDBの2〜5倍程度低速です。

repo = Repo("path/to/repo", odbt=GitDB)

GitCmdObjectDB

Gitコマンドデータベースは、永続的な git cat-file プロセスを使ってリポジトリ情報を読み込みます。あらゆる条件下で高速に動作しますが、プロセス自体の分メモリを余分に消費します。
大きなファイルを取得する際のメモリ使用量は、GitDBよりもかなり多くなります。

repo = Repo("path/to/repo", odbt=GitCmdObjectDB)  

Gitコマンドのデバッグとカスタマイズ

環境変数を使うことで、Gitコマンドの動作をさらに調整できます。

  • GIT_PYTHON_TRACE
    • 0以外を設定すると、実行されるすべてのGitコマンドが表示される
    • "full"を設定すると、コマンドとその出力がすべて表示される
  • GIT_PYTHON_GIT_EXECUTABLE
    • Gitの実行ファイルのフルパスを設定する

その他の機能

リポジトリのアーカイブ、統計情報、ログ、ブレイムなど、本チュートリアルで触れられなかった機能がまだまだあります。
単体テストのコードを見ると、それぞれの機能の詳しい使用例を知ることができるでしょう。

以上がGitPythonチュートリアルの全体像です。実際のユースケースに合わせて応用してみてください。

コメント

タイトルとURLをコピーしました