3. 创建仓库:init
显然,按照时间顺序和逻辑顺序,第一个 Git 命令是 git init
,所以我们将首先创建 wyag init
。为此,我们需要一些非常基础的仓库抽象。
3.1. 仓库对象
显然,我们需要对仓库的抽象:几乎每次运行 Git 命令时,我们都是在尝试对某个仓库进行操作,创建、读取或修改。
Git 仓库由两部分组成:一个是“工作区(work tree)”,其中存放要进行版本控制的文件,另一个是“Git 目录(git directory)”,Git 在这里存储自己的数据。在大多数情况下,工作区是一个常规目录,而 Git 目录是工作区的一个子目录,名为 .git
。
Git 支持更多的情况(裸仓库、分离的 Git 目录 等),但我们不需要这些:我们将坚持使用基本的 worktree/.git
方法。我们的仓库对象将仅包含两个路径:工作区和 Git 目录。
要创建一个新的 Repository
对象,我们只需进行一些检查:
- 我们必须验证该目录是否存在,并且包含一个名为
.git
的子目录。 - 我们读取
.git/config
中的配置(这只是一个 INI 文件),并确保core.repositoryformatversion
为 0。稍后我们会详细讨论这个字段。
构造函数接受一个可选的 force
参数,用于禁用所有检查。这是因为稍后我们将创建的 repo_create()
函数使用 Repository
对象来创建仓库。因此,我们需要一种方法,即使在(仍然)无效的文件系统位置,也能创建仓库。
class GitRepository (object):
"""一个 Git 仓库"""
worktree = None
gitdir = None
conf = None
def __init__(self, path, force=False):
self.worktree = path
self.gitdir = os.path.join(path, ".git")
if not (force or os.path.isdir(self.gitdir)):
raise Exception("Not a Git repository %s" % path)
# 读取 .git/config 中的配置文件
self.conf = configparser.ConfigParser()
cf = repo_file(self, "config")
if cf and os.path.exists(cf):
self.conf.read([cf])
elif not force:
raise Exception("Configuration file missing")
if not force:
vers = int(self.conf.get("core", "repositoryformatversion"))
if vers != 0:
raise Exception("Unsupported repositoryformatversion %s" % vers)
我们将会在仓库中处理大量的路径。不妨创建一些工具函数来计算这些路径,并在需要时创建缺失的目录结构。首先,我们先写一个通用的路径构建函数:
def repo_path(repo, *path):
"""Compute path under repo's gitdir."""
return os.path.join(repo.gitdir, *path)
(关于 Python 语法的一点说明:*path
前的星号使得函数具有可变参数特性,因此可以将多个路径组件作为单独的参数调用。例如,repo_path(repo, "objects", "df", "4ec9fc2ad990cb9da906a95a6eda6627d7b7b0")
是一个有效的调用。函数接收到的 path
是一个列表。)
接下来的两个函数,repo_file()
和 repo_dir()
,分别返回并可选地创建指向文件或目录的路径。它们之间的区别在于,文件版本只会创建到最后一个组件的目录。
def repo_file(repo, *path, mkdir=False):
"""Same as repo_path, but create dirname(*path) if absent. For
example, repo_file(r, \"refs\", \"remotes\", \"origin\", \"HEAD\") will create
.git/refs/remotes/origin."""
if repo_dir(repo, *path[:-1], mkdir=mkdir):
return repo_path(repo, *path)
def repo_dir(repo, *path, mkdir=False):
"""Same as repo_path, but mkdir *path if absent if mkdir."""
path = repo_path(repo, *path)
if os.path.exists(path):
if (os.path.isdir(path)):
return path
else:
raise Exception("Not a directory %s" % path)
if mkdir:
os.makedirs(path)
return path
else:
return None
(关于语法的第二个也是最后一个说明:由于 *path
中的星号使得函数具有可变参数特性,因此 mkdir
参数必须通过名称显式传递。例如,repo_file(repo, "objects", mkdir=True)
。)
要 创建 一个新的仓库,我们从一个目录开始(如果该目录尚不存在则创建),然后在其中创建 git 目录(该目录必须尚不存在,或者为空)。这个目录名为 .git
(前面的点使其在 Unix 系统上被视为“隐藏”),并包含:
.git/objects/
: 对象存储,我们将在 下一节 中介绍。.git/refs/
: 引用存储,我们稍后会讨论 更多内容。它包含两个子目录,heads
和tags
。.git/HEAD
: 当前 HEAD 的引用(稍后会详细介绍!).git/config
: 仓库的配置文件。.git/description
: 包含该仓库内容的自由格式描述,供人类阅读,且很少使用。
def repo_create(path):
"""Create a new repository at path."""
repo = GitRepository(path, True)
# First, we make sure the path either doesn't exist or is an
# empty dir.
if os.path.exists(repo.worktree):
if not os.path.isdir(repo.worktree):
raise Exception ("%s is not a directory!" % path)
if os.path.exists(repo.gitdir) and os.listdir(repo.gitdir):
raise Exception("%s is not empty!" % path)
else:
os.makedirs(repo.worktree)
assert repo_dir(repo, "branches", mkdir=True)
assert repo_dir(repo, "objects", mkdir=True)
assert repo_dir(repo, "refs", "tags", mkdir=True)
assert repo_dir(repo, "refs", "heads", mkdir=True)
# .git/description
with open(repo_file(repo, "description"), "w") as f:
f.write("Unnamed repository; edit this file 'description' to name the repository.\n")
# .git/HEAD
with open(repo_file(repo, "HEAD"), "w") as f:
f.write("ref: refs/heads/master\n")
with open(repo_file(repo, "config"), "w") as f:
config = repo_default_config()
config.write(f)
return repo
配置文件非常简单,它是一个类似于 INI 的文件,包含一个部分([core]
)和三个字段:
repositoryformatversion = 0
:gitdir 格式的版本。0 表示初始格式,1 表示相同格式但带有扩展。如果大于 1,git 将会崩溃;wyag 只接受 0。filemode = false
:禁用对工作区中文件模式(权限)更改的跟踪。bare = false
:表示该仓库有一个工作区。Git 支持一个可选的worktree
键,用于指示工作区的位置,如果不是..
;而 wyag 不支持这个。
我们使用 Python 的 configparser
库来创建这个文件:
def repo_default_config():
ret = configparser.ConfigParser()
ret.add_section("core")
ret.set("core", "repositoryformatversion", "0")
ret.set("core", "filemode", "false")
ret.set("core", "bare", "false")
return ret
3.2. init 命令
现在我们有了读取和创建仓库的代码,让我们通过创建 wyag init
命令来使这些代码可以从命令行使用。wyag init
的行为与 git init
一样——当然,定制化程度要低得多。wyag init
的语法如下:
wyag init [path]
我们已经有了完整的仓库创建逻辑。要创建这个命令,我们只需要再添加两件事:
我们需要创建一个 argparse 子解析器来处理我们命令的参数。
pythonargsp = argsubparsers.add_parser("init", help="初始化一个新的空仓库。")
在
init
的情况下,有一个单独的可选位置参数:初始化仓库的路径。默认值为当前目录.
:pythonargsp.add_argument("path", metavar="directory", nargs="?", default=".", help="仓库创建的路径。")
我们还需要一个“桥接”函数,该函数将从 argparse 返回的对象中读取参数值,并使用正确的值调用实际函数。
pythondef cmd_init(args): repo_create(args.path)
就这样完成了!如果你按照这些步骤操作,现在应该能够在任何地方执行 wyag init
来创建一个 Git 仓库:
$ wyag init test
(wyag
可执行文件通常不在你的 $PATH
中:你需要使用完整名称调用它,例如 ~/projects/wyag/wyag init .
)
3.3. repo_find() 函数
在我们实现仓库的过程中,我们需要一个函数来找到当前仓库的根目录。我们会频繁使用这个函数,因为几乎所有的 Git 功能都在现有的仓库上工作(当然,init
除外!)。有时这个根目录是当前目录,但也可能是父目录:你的仓库根目录可能在 ~/Documents/MyProject
,而你当前可能在 ~/Documents/MyProject/src/tui/frames/mainview/
工作。我们现在要创建的 repo_find()
函数将从当前目录开始查找根目录,并递归向上直到 /
。为了识别一个路径是否为仓库,它将检查 .git
目录是否存在。
def repo_find(path=".", required=True):
path = os.path.realpath(path)
if os.path.isdir(os.path.join(path, ".git")):
return GitRepository(path)
# 如果没有返回,递归查找父目录
parent = os.path.realpath(os.path.join(path, ".."))
if parent == path:
# 底部情况
# os.path.join("/", "..") == "/":
# 如果 parent==path,那么 path 就是根目录。
if required:
raise Exception("没有 git 目录。")
else:
return None
# 递归情况
return repo_find(parent, required)
仓库的部分就完成了!