3. Creating repositories: init 创建仓库:init
Obviously, the first Git command in chronological and logical order is git init
, so we’ll begin by creating wyag init
. To achieve this, we’re going to first need some very basic repository abstraction.
显然,按照时间顺序和逻辑顺序,第一个 Git 命令是 git init
,所以我们将首先创建 wyag init
。为此,我们需要一些非常基础的仓库抽象。
3.1. The Repository object 仓库对象
Obviously, the first Git command in chronological and logical order is git init
, so we’ll begin by creating wyag init
. To achieve this, we’re going to first need some very basic repository abstraction.
显然,我们需要对仓库的抽象:几乎每次运行 Git 命令时,我们都是在尝试对某个仓库进行操作,创建、读取或修改。
A git repository is made of two things: a “work tree”, where the files meant to be in version control live, and a “git directory”, where Git stores its own data. In most cases, the worktree is a regular directory and the git directory is a child directory of the worktree, called .git
.
Git 仓库由两部分组成:一个是“工作区(work tree)”,其中存放要进行版本控制的文件,另一个是“Git 目录(git directory)”,Git 在这里存储自己的数据。在大多数情况下,工作区是一个常规目录,而 Git 目录是工作区的一个子目录,名为 .git
。
Git supports much more cases (bare repo, separated gitdir, etc) but we won’t need them: we’ll stick to the basic approach of worktree/.git
. Our repository object will then just hold two paths: the worktree and the gitdir.
Git 支持更多的情况(裸仓库、分离的 Git 目录 等),但我们不需要这些:我们将坚持使用基本的 worktree/.git
方法。我们的仓库对象将仅包含两个路径:工作区和 Git 目录。
To create a new Repository
object, we only need to make a few checks:
要创建一个新的 Repository
对象,我们只需进行一些检查:
- We must verify that the directory exists, and contains a subdirectory called
.git
. - 我们必须验证该目录是否存在,并且包含一个名为
.git
的子目录。 - We read its configuration in
.git/config
(it’s just an INI file) and control thatcore.repositoryformatversion
is 0. More on that field in a moment. - 我们读取
.git/config
中的配置(这只是一个 INI 文件),并确保core.repositoryformatversion
为 0。稍后我们会详细讨论这个字段。
The constructor takes an optional force
which disables all checks. That’s because the repo_create()
function which we’ll create later uses a Repository
object to create the repo. So we need a way to create repository even from (still) invalid filesystem locations.
构造函数接受一个可选的 force
参数,用于禁用所有检查。这是因为稍后我们将创建的 repo_create()
函数使用 Repository
对象来创建仓库。因此,我们需要一种方法,即使在(仍然)无效的文件系统位置,也能创建仓库。
class GitRepository (object):
"""A git repository"""
"""一个 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)
We’re going to be manipulating lots of paths in repositories. We may as well create a few utility functions to compute those paths and create missing directory structures if needed. First, just a general path building function:
我们将会在仓库中处理大量的路径。不妨创建一些工具函数来计算这些路径,并在需要时创建缺失的目录结构。首先,我们先写一个通用的路径构建函数:
def repo_path(repo, *path):
"""Compute path under repo's gitdir."""
return os.path.join(repo.gitdir, *path)
(A note on Python syntax: the star on the *path
makes the function variadic, so it can be called with multiple path components as separate arguments. For example, repo_path(repo, "objects", "df", "4ec9fc2ad990cb9da906a95a6eda6627d7b7b0")
is a valid call. The function receives path
as a list)
(关于 Python 语法的一点说明:*path
前的星号使得函数具有可变参数特性,因此可以将多个路径组件作为单独的参数调用。例如,repo_path(repo, "objects", "df", "4ec9fc2ad990cb9da906a95a6eda6627d7b7b0")
是一个有效的调用。函数接收到的 path
是一个列表。)
The next two functions, repo_file()
and repo_dir()
, return and optionally create a path to a file or a directory, respectively. The difference between them is that the file version only creates directories up to the last component.
接下来的两个函数,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
(Second and last note on syntax: because the star in *path
makes the functions variadic, the mkdir
argument must be passed explicitly by name. For example, repo_file(repo, "objects", mkdir=True)
.)
(关于语法的第二个也是最后一个说明:由于 *path
中的星号使得函数具有可变参数特性,因此 mkdir
参数必须通过名称显式传递。例如,repo_file(repo, "objects", mkdir=True)
。)
To create a new repository, we start with a directory (which we create if doesn’t already exist) and create the git directory inside (which must not exist already, or be empty). That directory is called .git
(the leading period makes it “hidden” on Unix systems), and contains:
要 创建 一个新的仓库,我们从一个目录开始(如果该目录尚不存在则创建),然后在其中创建 git 目录(该目录必须尚不存在,或者为空)。这个目录名为 .git
(前面的点使其在 Unix 系统上被视为“隐藏”),并包含:
.git/objects/
: the object store, which we’ll introduce in the next section..git/objects/
: 对象存储,我们将在 下一节 中介绍。.git/refs/
the reference store, which we’ll discuss a bit later. It contains two subdirectories,heads
andtags
..git/refs/
: 引用存储,我们稍后会讨论 更多内容。它包含两个子目录,heads
和tags
。.git/HEAD
, a reference to the current HEAD (more on that later!).git/HEAD
: 当前 HEAD 的引用(稍后会详细介绍!).git/config
, the repository’s configuration file..git/config
: 仓库的配置文件。.git/description
, holds a free-form description of this repository’s contents, for humans, and is rarely used..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
The configuration file is very simple, it’s a INI-like file with a single section ([core]
) and three fields:
配置文件非常简单,它是一个类似于 INI 的文件,包含一个部分([core]
)和三个字段:
repositoryformatversion = 0
: the version of the gitdir format. 0 means the initial format, 1 the same with extensions. If > 1, git will panic; wyag will only accept 0.repositoryformatversion = 0
:gitdir 格式的版本。0 表示初始格式,1 表示相同格式但带有扩展。如果大于 1,git 将会崩溃;wyag 只接受 0。filemode = false
: disable tracking of file modes (permissions) changes in the work tree.filemode = false
:禁用对工作区中文件模式(权限)更改的跟踪。bare = false
: indicates that this repository has a worktree. Git supports an optionalworktree
key which indicates the location of the worktree, if not..
; wyag doesn’t.bare = false
:表示该仓库有一个工作区。Git 支持一个可选的worktree
键,用于指示工作区的位置,如果不是..
;而 wyag 不支持这个。
We create this file using Python’s configparser
lib:
我们使用 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. The init command | init 命令
Now that we have code to read and create repositories, let’s make this code usable from the command line by creating the wyag init
command. wyag init
behaves just like git init
— with much less customizability, of course. The syntax of wyag init
is going to be:
现在我们有了读取和创建仓库的代码,让我们通过创建 wyag init
命令来使这些代码可以从命令行使用。wyag init
的行为与 git init
一样——当然,定制化程度要低得多。wyag init
的语法如下:
wyag init [path]
We already have the complete repository creation logic. To create the command, we’re only going to need two more things:
我们已经有了完整的仓库创建逻辑。要创建这个命令,我们只需要再添加两件事:
We need to create an argparse subparser to handle our command’s argument.
我们需要创建一个 argparse 子解析器来处理我们命令的参数。
pythonargsp = argsubparsers.add_parser("init", help="初始化一个新的空仓库。")
In the case of
init
, there’s a single, optional, positional argument: the path where to init the repo. It defaults to.
, the current directory:在
init
的情况下,有一个单独的可选位置参数:初始化仓库的路径。默认值为当前目录.
:pythonargsp.add_argument("path", metavar="directory", nargs="?", default=".", help="仓库创建的路径。")
We also need a “bridge” function that will read argument values from the object returned by argparse and call the actual function with correct values.
我们还需要一个“桥接”函数,该函数将从 argparse 返回的对象中读取参数值,并使用正确的值调用实际函数。
pythondef cmd_init(args): repo_create(args.path)
And we’re done! If you’ve followed these steps, you should now be able to wyag init
a git repository anywhere:
就这样完成了!如果你按照这些步骤操作,现在应该能够在任何地方执行 wyag init
来创建一个 Git 仓库:
$ wyag init test
(wyag
可执行文件通常不在你的 $PATH
中:你需要使用完整名称调用它,例如 ~/projects/wyag/wyag init .
)
3.3. The repo_find() function | repo_find() 函数
While we’re implementing repositories, we’re going to need a function to find the root of the current repository. We’ll use it a lot, since almost all Git functions work on an existing repository (except init
, of course!). Sometimes that root is the current directory, but it may also be a parent: your repository’s root may be in ~/Documents/MyProject
, but you may currently be working in ~/Documents/MyProject/src/tui/frames/mainview/
. The repo_find()
function we’ll now create will look for that root, starting at the current directory and recursing back to /
. To identify a path as a repository, it will check for the presence of a .git
directory.
在我们实现仓库的过程中,我们需要一个函数来找到当前仓库的根目录。我们会频繁使用这个函数,因为几乎所有的 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)
# If we haven't returned, recurse in parent, if w
parent = os.path.realpath(os.path.join(path, ".."))
if parent == path:
# Bottom case
# os.path.join("/", "..") == "/":
# If parent==path, then path is root.
if required:
raise Exception("No git directory.")
else:
return None
# Recursive case
return repo_find(parent, required)
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)
And we’re done with repositories!
仓库的部分就完成了!