在 Python 中执行 Git 操作





0/5 (0投票)
本文将介绍如何使用 GitPython 库在 Python 中访问 Git 仓库。
我多年来一直从事面向对象语言的编程,我的大脑已经习惯了从类、对象、方法和属性的角度来思考问题。我不介意在命令行上执行简单的操作,但对于复杂的操作,我更喜欢编写代码。
就版本控制而言,我欣赏 Git 的灵活性和性能,但我从不认为它简单。因此,我喜欢使用 GitPython 库以编程方式与仓库进行交互。这减少了我必须在命令行上输入的文本量,并且允许我将 Git 操作集成到常规程序中。
GitPython 的另一个优点是它提供了对 Git 更深入的理解。该库的类和方法阐明了 Git 的实际工作原理。本文的目的是解释如何使用这些类和方法进行编码,第一部分介绍了基本类。第二部分讨论了子模块这一重要主题,它允许一个仓库包含其他仓库。
1. 基本类
根据这个 Github 页面,GitPython 库由 Sebastian Thiel 和 Harmon 开发。他们已经慷慨地在修改后的 BSD 3 条款许可下发布了他们的包,如果您安装了 Python,可以使用以下命令安装 GitPython
pip install gitpython
安装完包并在 Python 脚本中导入后,您就可以访问其数据结构了。本节介绍五个基本类
Repo
– 代表一个 Git 仓库Reference
– 仓库引用的超类(Head
、Tag
、RemoteReference
)Remote
– 代表一个远程仓库- IndexFile– 代表仓库的暂存区
Commit
– 代表一个 Git 仓库
一旦您对这些类有了扎实的理解,您就能轻松地在 Python 中实现复杂的 Git 操作。
1.1 Repo 类
通常,GitPython 程序中使用的第一个类是 Repo
类,它代表一个仓库。创建 Repo
实例主要有两种方式
- 要克隆一个仓库,请调用
Repo.clone_from(...)
,并提供仓库的 URL 和用于存放克隆文件的本地目录名称。 - 要访问本地仓库,请调用
Repo(...)
,并提供本地仓库的路径。
以下代码演示了如何创建这两种类型的 Repo
对象。它检查名为 local_dir
的目录是否存在,如果不存在,则克隆 remote_url
提供的 URL 中的仓库。如果 local_dir
文件夹存在,则从该目录创建 Repo
。
if not os.path.isdir(local_dir):
repo = Repo.clone_from(remote_url, local_dir)
else:
repo = Repo(local_dir)
创建 Repo
对象后,就可以在代码中访问其成员了。表 1 列出了十二个可以读取的属性
属性 | 数据类型 | 描述 |
---|---|---|
working_tree_dir | str | 仓库工作目录的路径 |
common_dir | str | 仓库的 Git (.git) 文件夹的路径 |
bare | bool | 标识仓库是否为裸仓库 |
untracked_files | List[str] | 工作目录中的未暂存文件 |
refs | List[Reference] | Reference 对象列表 |
heads | List[Head] | 仓库的分支头 |
head | HEAD | 指向当前 HEAD 引用的指针 |
branches | List[Head] | 代表分支头的 Head 对象列表 |
active_branch | Head | 当前分支的名称 |
标签 | List[Tag] | 仓库标签列表 |
remotes | List[Remote] | Remote 对象列表 |
index | IndexFile | 代表仓库的暂存区 |
前四个属性很容易理解。working_tree_dir
标识工作目录的路径,common_dir
标识 Git 数据库(.git 文件夹)的路径。bare
属性标识仓库是否为裸仓库(缺少工作目录)。如果仓库有包含未暂存文件的工作目录,则可以通过 untracked_files
属性访问文件的路径。
第五个属性 refs
非常重要,因为它提供了对仓库中所有引用的访问。我稍后将讨论重要的 Reference
类及其子类。
除了上面列出的属性之外,Repo
类还提供了几个访问和修改仓库的方法。其中许多方法都以 create
或 delete
开头
create_head
/create_tag
– 添加给定类型的新引用delete_head
/delete_tag
– 删除给定类型的引用
Repo
类没有 create_branch
或 delete_branch
方法。正如下一节将清楚的那样,分支由 Head
类的实例表示。
1.2 Reference 类和子类
要理解 Reference
类,熟悉 Git 仓库的结构很有帮助。如果您查看仓库的 .git/refs 文件夹,会发现三个子文件夹
- heads - 包含分支头引用
- tags - 包含标签引用
- remotes - 包含远程仓库引用
Repo
的 refs
属性是 Reference
对象列表。Reference
的子类类似于 .git/refs 目录的子文件夹,因此每个 Reference
要么是 Head
、Tag
,要么是 RemoteReference
对象。以下讨论将探讨这些子类的每一个。
1.2.1 Head 类
GitPython 没有 Branch
类,而是有一个 Head
类来表示仓库中的分支。您可以通过调用 Repo
实例的 heads
方法来获取 Head
对象列表,每个 Head
都有两个属性
name
– 标识分支名称的字符串commit
– 分支的最新Commit
对象
每个仓库都有一个称为活动分支的分支,您可以通过 Repo
的 active_branch
方法访问当前分支的 Head
。为了演示,以下代码访问活动分支并打印其名称
print(repo.active_branch.name)
Head
类提供了四个有用的方法
checkout(force: bool=False)
- 切换到分支rename(str, force: bool=False)
- 更改分支的名称set_tracking_branch(Remote)
- 设置分支以跟踪远程分支tracking_branch()
- 返回分支跟踪的Remote
或None
创建和删除分支的最简单方法是调用 Repo
的 create_head
和 delete_head
方法。为了演示它们的使用方法,以下代码创建了一个分支,检出它,然后将其删除。
# Access the active branch
old_branch = repo.active_branch
# Create a new branch and check it out
new_branch = repo.create_head('new_branch')
new_branch.checkout()
print('The active branch is ' + repo.active_branch.name)
# Check out the old branch
old_branch.checkout()
print('Now the active branch is ' + repo.active_branch.name)
# Delete the new branch
repo.delete_head(new_branch)
set_tracking_branch
和 tracking_branch
方法与 RemoteReference
实例相关,稍后将进行讨论。
1.2.2 HEAD 类
虽然可能令人困惑,但 GitPython 除了 Head
类之外还提供了一个 HEAD
类,两者都不是对方的子类。虽然可以通过 Repo
的 active_branch
属性访问当前 Head
,但可以通过 Repo
的 head
属性访问 HEAD。
Head
可以表示任何分支,但 HEAD
始终指向当前分支的最新提交。它有三个属性
abspath
– HEAD 文件在 .git 目录中的位置commit
– HEAD 引用的Commit
对象is_detached
– 一个布尔值,标识 HEAD 是否处于分离状态(指向提交而不是分支)
此外,HEAD
提供了一个名为 orig_head()
的方法,该方法指向 HEAD
的前一个值。
1.2.3 Tag 类
标签是分配给特定提交的名称,标签在分配版本号时特别有用。在代码中,可以使用 Repo
的 create_tag
方法创建标签,该方法接受一个字符串和一个 Commit
。可以使用 Repo
的 delete_tag
方法删除标签。
1.2.4 RemoteReference 类
尽管名称如此,RemoteReference
代表的是远程仓库的分支,而不是仓库本身。通过调用 set_tracking_branch
方法并传入 RemoteReference
,可以将本地分支设置为跟踪远程分支。据我所知,这是 RemoteReference
的唯一用途。
无法直接创建 RemoteReference
。而是需要通过 Remote
类来访问远程仓库。下一节将详细讨论该类。
1.3 Remote 类
远程仓库(或简称为 remote)是存储在服务器上的代码库版本。克隆仓库时,Git 会自动创建一个名为 origin
的远程,并将其与仓库的 URL 相关联。就像 git branch
命令列出本地仓库的分支一样,git remote
列出可用于跟踪的远程。
在 GitPython 中,远程仓库由 Remote
类的实例表示,它不是 Reference
的子类。可以通过调用 Repo
类的 create_remote
方法来创建 Remote
,并通过调用 delete_remote
来删除它。remote
方法返回具有给定名称的 Remote
,而 remotes
返回 Remote
实例的列表。
为了演示这一点,以下代码创建了一个分支和一个远程。然后,它通过调用 Remote
实例的 refs
方法来获取远程分支的列表。如果此列表不为空,则最后一行将新分支配置为跟踪第一个远程分支。
# Create a new branch
new_branch = repo.create_head('new_branch')
# Create a new remote
new_remote = repo.create_remote('new_remote',
'https://github.com/mattscar/opencl_book.git')
# Set the branch to track the first remote branch
if new_remote.refs:
new_branch.set_tracking_branch(new_remote.refs[0])
Remote
类提供了三个特别重要的方法,用于在本地分支和远程仓库之间传输数据
fetch(...)
– 从远程仓库下载更新到本地仓库,而不影响工作目录pull(...)
– 从远程仓库下载更新到本地仓库,并将其合并到当前分支push(...)
– 将提交从本地仓库上传到远程仓库
这些方法中的每一种都接受可选参数,并返回提供操作信息的 DTO。您可以在 GitPython 文档站点上找到更详细的信息。
1.4 IndexFile 类
在执行提交之前,Git 会将更新存储在它的*暂存区*中。在 .git 文件夹中,索引文件充当暂存区。因此,GitPython 提供了 IndexFile
类来表示仓库的暂存区。可以通过 Repo
实例的 index
属性在代码中访问它。
IndexFile
类有一个名为 entries
的属性,它将 IndexEntry
映射到仓库中每个被跟踪文件的索引数据。您可以通过更新此字典来更改暂存区。
IndexFile
还提供了几种修改暂存区或在工作目录、暂存区和本地仓库之间移动数据的方法。表 2 列出了其中八种方法。
方法 | 描述 |
---|---|
add(...) | 将文件从工作树添加到索引 |
checkout(...) | 将文件/路径检出到工作树 |
commit(...) | 提交索引文件并创建 Commit 实例 |
diff(...) | 将索引与工作副本或 Commit 进行比较 |
move(...) | 重命名/移动指定项 |
remove(...) | 从索引中移除项,并可选地从工作树中移除 |
reset(...) | 重置索引以反映给定提交的树 |
update() | 重新读取索引文件并丢弃缓存信息 |
为了演示这些方法的使用,以下代码在工作目录中创建一个文本文件,将其添加到暂存区,提交索引文件,并将其推送到远程仓库。
# Access the index file and remote repository
index = repo.index
remote = repo.remote()
# Create text file and add it to the staging area
file_name = os.path.join(repo.working_tree_dir, 'example.txt')
with open(file_name, 'w') as example_file:
example_file.write('example')
# Update the staging area
index.add(file_name)
# Commit the update
index.commit(message='Example commit')
# Push the commit
remote.push()
IndexFile
还提供了与 blob 和 tree 交互的方法。您可以在 GitPython 文档中找到该类的完整描述。
1.5 Commit 类
Git 仓库存储已提交的更改,并将每个更改与安全哈希(SHA-1)值关联起来。在 GitPython 中,每个提交都由 Commit
类的实例表示。您可以通过调用 IndexFile
的 commit
方法并提供消息来创建 Commit
。您还可以通过调用 Repo
实例的 commit
方法来访问 Commit
。
Commit
类有几个属性,表 3 列出了其中的十一个。
属性 | 数据类型 | 描述 |
---|---|---|
name_rev | str | 提交的 SHA-1 哈希标识符 |
message | str | 提交消息 |
encoding | str | 消息的编码(默认为 UTF-8) |
summary | str | 提交消息的第一行 |
stats | 统计信息 | 有关提交的信息 |
author | str | 提交的作者 |
authored_date | int | 作者的时间 |
author_tz_offset | int | 作者的时区偏移量 |
committer | str | 提交者字符串 |
committed_date | int | 提交日期 |
committer_tz_offset | int | 提交者的时区偏移量 |
stats
属性有一个名为 total
的字段,该字段有四个字段
insertions
– 插入的行数deletions
– 删除的行数lines
– 更改的行数files
– 更改的文件数
为了演示此属性的使用方法,以下代码访问仓库的最新提交并打印其统计信息
# Access the latest commit
commit = repo.commit()
# Access and print the commit statistics
st = commit.stats
print(st.total)
最后一行代码显示了插入行数、删除行数、更改行数和更改文件数。
2. 子模块
子模块使得可以在主仓库中将外部仓库作为目录来访问。许多开发人员不使用子模块,但它们可以大大提高模块化、协作和代码的可重用性。
在 GitPython 中,子模块由 Submodule
类的实例表示。您可以通过调用 Repo
实例的 create_submodule
方法来为仓库创建子模块。该方法接受多个参数,表 4 列出了所有参数。
参数 | 类型 | 描述 |
---|---|---|
名称 | str | 子模块的标识符 |
路径 | str | 子模块应存储的相对/绝对路径 |
url | URL | 子模块仓库的 URL |
branch | str | 要检出的子模块仓库分支的名称 |
no_checkout | bool | 是否应检出子模块的仓库分支 |
depth | int | 要下载的提交次数 |
env | dict | 子模块的环境变量字典 |
clone_multi_options | 列表 | 在克隆操作期间使用的选项 |
allow_unsafe_protocols | bool | 是否允许使用不安全协议 |
allow_unsafe_options | bool | 是否允许使用不安全选项 |
举例说明子模块的创建。以下代码从 http://github.com/submod.git
处的仓库的 main
分支创建了一个名为 submod
的子模块。该仓库将从名为 submod
的目录访问。
# Set path for the submodule
path = os.path.join(repo.working_tree_dir, 'submod')
# Create submodule
submod = repo.create_submodule('submod', path, 'https://github.com/submod.git', 'main')
在此代码中,submod
变量是 Submodule
类的实例。它提供了许多有用的属性,包括 branch_name
、branch_path
、url
和 parent_commit
。它还提供了许多有用的方法,包括 move
、remove
和 update
。children
方法提供了一个子模块的子模块列表。
一个特别有用的方法是 module
,它将 Submodule
转换为一个新的 Repo
实例。调用此方法后,应用程序可以使用常规的 Repo
方法对子模块执行 Git 操作。
3. 历史记录
本文最初于 2024 年 8 月 20 日提交。