65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Python 中执行 Git 操作

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2024 年 8 月 20 日

CPOL

11分钟阅读

viewsIcon

3676

本文将介绍如何使用 GitPython 库在 Python 中访问 Git 仓库。

我多年来一直从事面向对象语言的编程,我的大脑已经习惯了从类、对象、方法和属性的角度来思考问题。我不介意在命令行上执行简单的操作,但对于复杂的操作,我更喜欢编写代码。

就版本控制而言,我欣赏 Git 的灵活性和性能,但我从不认为它简单。因此,我喜欢使用 GitPython 库以编程方式与仓库进行交互。这减少了我必须在命令行上输入的文本量,并且允许我将 Git 操作集成到常规程序中。

GitPython 的另一个优点是它提供了对 Git 更深入的理解。该库的类和方法阐明了 Git 的实际工作原理。本文的目的是解释如何使用这些类和方法进行编码,第一部分介绍了基本类。第二部分讨论了子模块这一重要主题,它允许一个仓库包含其他仓库。

1. 基本类

根据这个 Github 页面,GitPython 库由 Sebastian Thiel 和 Harmon 开发。他们已经慷慨地在修改后的 BSD 3 条款许可下发布了他们的包,如果您安装了 Python,可以使用以下命令安装 GitPython

pip install gitpython

安装完包并在 Python 脚本中导入后,您就可以访问其数据结构了。本节介绍五个基本类

  1. Repo – 代表一个 Git 仓库
  2. Reference – 仓库引用的超类(HeadTagRemoteReference
  3. Remote – 代表一个远程仓库
  4. IndexFile– 代表仓库的暂存区
  5. Commit – 代表一个 Git 仓库

一旦您对这些类有了扎实的理解,您就能轻松地在 Python 中实现复杂的 Git 操作。

1.1 Repo 类

通常,GitPython 程序中使用的第一个类是 Repo 类,它代表一个仓库。创建 Repo 实例主要有两种方式

  1. 要克隆一个仓库,请调用 Repo.clone_from(...),并提供仓库的 URL 和用于存放克隆文件的本地目录名称。
  2. 要访问本地仓库,请调用 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 列出了十二个可以读取的属性

表 1:Repo 类的属性
属性 数据类型 描述
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 类还提供了几个访问和修改仓库的方法。其中许多方法都以 createdelete 开头

  • create_head/create_tag – 添加给定类型的新引用
  • delete_head/delete_tag – 删除给定类型的引用

Repo 类没有 create_branchdelete_branch 方法。正如下一节将清楚的那样,分支由 Head 类的实例表示。

1.2 Reference 类和子类

要理解 Reference 类,熟悉 Git 仓库的结构很有帮助。如果您查看仓库的 .git/refs 文件夹,会发现三个子文件夹

  • heads - 包含分支头引用
  • tags - 包含标签引用
  • remotes - 包含远程仓库引用

Reporefs 属性是 Reference 对象列表。Reference 的子类类似于 .git/refs 目录的子文件夹,因此每个 Reference 要么是 HeadTag,要么是 RemoteReference 对象。以下讨论将探讨这些子类的每一个。

1.2.1 Head 类

GitPython 没有 Branch 类,而是有一个 Head 类来表示仓库中的分支。您可以通过调用 Repo 实例的 heads 方法来获取 Head 对象列表,每个 Head 都有两个属性

  1. name – 标识分支名称的字符串
  2. commit – 分支的最新 Commit 对象

每个仓库都有一个称为活动分支的分支,您可以通过 Repoactive_branch 方法访问当前分支的 Head。为了演示,以下代码访问活动分支并打印其名称

print(repo.active_branch.name)

Head 类提供了四个有用的方法

  • checkout(force: bool=False) - 切换到分支
  • rename(str, force: bool=False) - 更改分支的名称
  • set_tracking_branch(Remote) - 设置分支以跟踪远程分支
  • tracking_branch() - 返回分支跟踪的 RemoteNone

创建和删除分支的最简单方法是调用 Repocreate_headdelete_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_branchtracking_branch 方法与 RemoteReference 实例相关,稍后将进行讨论。

1.2.2 HEAD 类

虽然可能令人困惑,但 GitPython 除了 Head 类之外还提供了一个 HEAD 类,两者都不是对方的子类。虽然可以通过 Repoactive_branch 属性访问当前 Head,但可以通过 Repohead 属性访问 HEAD。

Head 可以表示任何分支,但 HEAD 始终指向当前分支的最新提交。它有三个属性

  • abspath – HEAD 文件在 .git 目录中的位置
  • commit – HEAD 引用的 Commit 对象
  • is_detached – 一个布尔值,标识 HEAD 是否处于分离状态(指向提交而不是分支)

此外,HEAD 提供了一个名为 orig_head() 的方法,该方法指向 HEAD 的前一个值。

1.2.3 Tag 类

标签是分配给特定提交的名称,标签在分配版本号时特别有用。在代码中,可以使用 Repocreate_tag 方法创建标签,该方法接受一个字符串和一个 Commit。可以使用 Repodelete_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 列出了其中八种方法。

表 2:IndexFile 类的方法
方法 描述
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 类的实例表示。您可以通过调用 IndexFilecommit 方法并提供消息来创建 Commit。您还可以通过调用 Repo 实例的 commit 方法来访问 Commit

Commit 类有几个属性,表 3 列出了其中的十一个。

表 3:Commit 类的属性
属性 数据类型 描述
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 列出了所有参数。

表 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_namebranch_pathurlparent_commit。它还提供了许多有用的方法,包括 moveremoveupdatechildren 方法提供了一个子模块的子模块列表。

一个特别有用的方法是 module,它将 Submodule 转换为一个新的 Repo 实例。调用此方法后,应用程序可以使用常规的 Repo 方法对子模块执行 Git 操作。

3. 历史记录

本文最初于 2024 年 8 月 20 日提交。

© . All rights reserved.