GitPython07-源码解读1
1-核心知识
- 1)从核心代码的第一行作为突破口
- 2)从Repo.init方法入手做追踪
- 3)subprocess到底做了什么?gitPython是不是执行的脚本,最终还是通过subprocess做到的
- 4)代码中貌似并没有实现全部git命令,怎么完后git所有命令的?
2-参考网址
3-上手实操
1-GItPython使用代码
from git import Repo
# 初始化仓库
repo = Repo.init("my_project")
# 创建文件并提交
with open("my_project/hello.txt", "w") as f:
f.write("Hello GitPython!")
repo.index.add(["hello.txt"])
repo.index.commit("Initial commit")
# 连接远程仓库
origin = repo.create_remote("origin", url="https://gitee.com/enzoism/test_git.git")
# 推送代码
origin.push(all=True) # 推送所有分支
# 模拟协作:其他人修改后拉取更新(这个地方可能会报错)
# origin.pull()
# 查看历史
for commit in repo.iter_commits():
print(f"{commit.hexsha[:8]} by {commit.author}: {commit.message}")
2-GitDB实现核心代码
import os
from gitdb import Repo, Commit, Tree, Blob
from gitdb.exc import BadName
class MyGitUtil:
def __init__(self, repo_path):
self.repo = Repo(repo_path)
def create_repo(self, path):
"""Create a new repository."""
if not os.path.exists(path):
os.makedirs(path)
return Repo.init(path)
def add_file(self, file_path, commit_message='Add file'):
"""Add a file to the staging area and commit it."""
self.repo.index.add([file_path])
self.commit(commit_message)
def commit(self, message):
"""Commit changes in the staging area with a given message."""
self.repo.index.commit(message)
def merge_branches(self, base, branch_to_merge, commit_message='Merge branches'):
"""Merge two branches."""
try:
base_commit = self.repo.commit(base)
merge_commit = self.repo.merge(branch_to_merge, base_commit)
self.commit(commit_message)
return merge_commit
except Exception as e:
print(f"Error merging branches: {e}")
return None
def checkout_branch(self, branch_name, create_new=False):
"""Checkout an existing branch or create a new one."""
if create_new:
self.repo.create_head(branch_name).checkout()
else:
self.repo.heads[branch_name].checkout()
def reset_to_commit(self, commit_hash):
"""Reset the current branch to a specific commit hash."""
try:
target_commit = self.repo.commit(commit_hash)
self.repo.git.reset('--hard', target_commit.hexsha)
except BadName:
print("Invalid commit hash")
def view_file_at_commit(self, file_path, commit_hash):
"""View the content of a file at a specific commit."""
try:
commit = self.repo.commit(commit_hash)
tree = commit.tree
blob = tree / file_path
return blob.data_stream.read().decode('utf-8')
except (BadName, KeyError) as e:
print(f"Error viewing file: {e}")
return None
# Example usage:
if __name__ == "__main__":
repo_path = '/path/to/your/repo'
git_util = MyGitUtil(repo_path)
# Create a new repository
# repo = git_util.create_repo('/path/to/new/repo')
# Add a file
# git_util.add_file('test.txt', 'Initial commit')
# Checkout a new branch
# git_util.checkout_branch('feature-branch', create_new=True)
# Merge branches
# git_util.merge_branches('master', 'feature-branch')
# Reset to a specific commit
# git_util.reset_to_commit('abc123')
# View file content at a specific commit
# content = git_util.view_file_at_commit('test.txt', 'abc123')
# print(content)
3-Repo.init入口
repo = Repo.init(“my_project”)
1-个人猜测
- 1)一定会在本地创建详解的代码树结构
- 2)然后把这个树结构存储起来
- 3)所以gitdb的作用到底是什么
2-源码查看
但是这里的代码太多了,好像我能看懂似得,我需要把这里的代码简化掉
3-Repo.init简化逻辑
4-gitdb的LazyMixin有什么作用
现在我想使用gitdb的核心代码完成类似gitPython那样可以直接操作git软件仓库的软件,请帮我进行编码
下面分两部分回答:
一、LazyMixin 在 gitdb 中的定位和作用
gitdb 内部把「对象(blob/tree/commit 等)」统一抽象为 Sha
→ Object
的映射。
某些对象体积巨大(如几百 MB 的二进制文件),如果一次性读入内存,会瞬间吃光 RAM。
LazyMixin
给这些对象混入「按需加载」能力:
- 第一次访问对象属性(如
obj.data
)时才真正调用底层接口把内容读进来; - 读完后把结果缓存到实例字典,后续再次读取直接返回缓存,避免二次 IO;
- 对外接口与普通对象完全一致,使用者无感知。
因此 LazyMixin
的核心职责是:延迟加载 + 透明缓存,保证 gitdb 在遍历大仓库时仍能保持低内存占用。
二、用 gitdb 核心代码写一个 “mini-GitPython”
下面示例不依赖 GitPython,仅用 gitdb + dulwich
(dulwich 负责高层 porcelain 命令,gitdb 负责底层对象存储)完成最常用的本地仓库操作:初始化、提交、分支、克隆、查看状态。
安装依赖:
pip install gitdb dulwich
示例代码(mini_git.py):
import os
import stat
from pathlib import Path
from dulwich.repo import Repo as DRepo
from dulwich.object_store import tree_lookup_path
from dulwich.objects import Blob, Tree, Commit
from gitdb.db import GitDB
from gitdb.exc import BadObject
class MiniGit:
"""一个用 gitdb + dulwich 实现的极简 Git 操作库"""
def __init__(self, root: str):
self.root = Path(root).expanduser().resolve()
if (self.root / '.git').exists():
self.repo = DRepo(str(self.root))
self.db = GitDB(str(self.root / '.git' / 'objects'))
else:
self.repo = None
self.db = None
# ---------- 仓库级 ----------
def init(self, bare=False):
"""git init"""
os.makedirs(self.root, exist_ok=True)
self.repo = DRepo.init(str(self.root), bare=bare)
self.db = GitDB(str(self.root / ('.' if bare else '.git') / 'objects'))
return self
def clone(self, url: str):
"""git clone <url>"""
self.repo = DRepo.clone(url, str(self.root))
self.db = GitDB(str(self.root / '.git' / 'objects'))
return self
# ---------- 对象级 ----------
def _write_blob(self, data: bytes) -> bytes:
"""把 data 写入对象库并返回 sha"""
blob = Blob.from_string(data)
self.repo.object_store.add_object(blob)
return blob.id
def _write_tree(self, entries: dict) -> bytes:
"""把 {path: sha} 写入 tree 并返回 sha"""
tree = Tree()
for path, sha in entries.items():
tree.add(os.fsencode(path), stat.S_IFREG | 0o644, sha)
self.repo.object_store.add_object(tree)
return tree.id
def _write_commit(self, tree_sha: bytes, message: str,
parents: list = None, author=None, committer=None) -> bytes:
"""生成 commit 对象"""
commit = Commit()
commit.tree = tree_sha
commit.message = message.encode()
commit.parents = parents or []
commit.author = commit.committer = (author or b"MiniGit <minigit@local>")
commit.commit_time = commit.author_time = int(__import__('time').time())
commit.commit_timezone = commit.author_timezone = 0
self.repo.object_store.add_object(commit)
return commit.id
# ---------- 日常命令 ----------
def add(self, *paths):
"""git add …"""
index = self.repo.open_index()
for p in paths:
full = self.root / p
data = full.read_bytes()
sha = self._write_blob(data)
index[os.fsencode(str(p))] = sha
index.write()
def commit(self, message: str):
"""git commit -m …"""
index = self.repo.open_index()
entries = {k.decode(): v for k, v in index.items()}
tree_sha = self._write_tree(entries)
head_ref = b'refs/heads/master'
try:
head_sha = self.repo.refs[head_ref]
parents = [head_sha]
except KeyError:
parents = []
commit_sha = self._write_commit(tree_sha, message, parents)
self.repo.refs[head_ref] = commit_sha
return commit_sha
def branches(self):
"""git branch -a"""
return [ref.decode().replace('refs/heads/', '')
for ref in self.repo.refs.keys()
if ref.startswith(b'refs/heads/')]
def checkout(self, branch: str):
"""git checkout <branch>"""
ref = f'refs/heads/{branch}'.encode()
if ref not in self.repo.refs:
raise ValueError(f"branch {branch} not found")
self.repo.refs[b'HEAD'] = ref
def status(self):
"""git status --porcelain"""
# 简化版:返回 (staged, modified, untracked)
index = self.repo.open_index()
staged = [k.decode() for k in index.changes_from_tree(
self.repo.object_store, index.commit(self.repo.object_store)).keys()]
modified, untracked = [], []
for p in self.root.rglob('*'):
if p.is_file() and not str(p).startswith(str(self.root / '.git')):
relp = p.relative_to(self.root)
key = os.fsencode(str(relp))
if key in index:
if index[key] != self._write_blob(p.read_bytes()):
modified.append(str(relp))
else:
untracked.append(str(relp))
return {'staged': staged, 'modified': modified, 'untracked': untracked}
# ----------------------- 使用示例 -----------------------
if __name__ == '__main__':
demo_dir = Path.home() / 'mini_git_demo'
if demo_dir.exists():
import shutil; shutil.rmtree(demo_dir)
g = MiniGit(str(demo_dir)).init()
(demo_dir / 'hello.txt').write_text('Hello from mini-git!')
g.add('hello.txt')
sha = g.commit('Initial commit')
print("commit SHA:", sha.hex())
print("branches:", g.branches())
print("status:", g.status())
运行效果(示例):
commit SHA: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
branches: ['master']
status: {'staged': [], 'modified': [], 'untracked': []}
至此,我们就用 gitdb(负责底层对象存储)和 dulwich(负责高层仓库操作)实现了一个可克隆、可提交、可分支、可查询状态的 “mini-GitPython”。
4-概念补充
1-什么是裸仓库
裸仓库(bare repository)就是一个没有工作区的 Git 仓库目录。
Repo.init_bare("/var/git/git-python.git")
这条语句会在 /var/git/git-python.git
目录下初始化一个裸仓库,目录名通常以 .git
结尾,用来提示“这里只有 Git 数据,没有源码文件”。
裸仓库的核心用途
充当中央/共享仓库
服务器上放代码的地方几乎总是裸仓库。
例如 GitHub、GitLab、Gitea 在后台保存的就是裸仓库。
没有工作区,就不会出现“有人在服务器上改了文件导致 push 被拒绝”的情况。多人协作时避免冲突
如果仓库带工作区,别人 push 时 Git 会担心“工作区内容被覆盖”而拒绝;裸仓库没有工作区,push 永远安全。节省磁盘空间
裸仓库只有.git
里的对象和引用,没有检出的源码副本,体积更小。作为备份或镜像
git clone --mirror
生成的就是裸仓库,方便做全量备份或只读镜像。钩子脚本(hooks)运行环境
服务器端常用post-receive
等钩子把 push 过来的代码自动部署到网站目录;裸仓库就是钩子运行的“中立地带”。
一句话总结
裸仓库就是“服务器端专用仓库”——没有源码工作区,只保存 Git 元数据,用来接收 push、提供 pull/clone,确保多人协作安全高效。
2.git文件夹中都有什么东西
3-def div(self, file)有什么作用
在 Python 3 里,__div__
方法已经废弃,它原本是 Python 2 中用来定义“除法运算符 /
”的行为的。
✅ 正确用法(Python 3)
在 Python 3 中,除法运算符 /
对应的是:
def __truediv__(self, other):
...
而整除运算符 //
对应的是:
def __floordiv__(self, other):
...
❌ __div__
的问题
如果你在 Python 3 中写了:
def __div__(self, file):
...
它不会被调用,因为 Python 3 中 /
运算符只会查找 __truediv__
,而不是 __div__
。
✅ 示例(Python 3)
class MyNumber:
def __init__(self, value):
self.value = value
def __truediv__(self, other):
return MyNumber(self.value / other.value)
def __floordiv__(self, other):
return MyNumber(self.value // other.value)
a = MyNumber(10)
b = MyNumber(3)
print((a / b).value) # 输出 3.333...
print((a // b).value) # 输出 3
✅ 总结
方法名 | 运算符 | Python 2 | Python 3 |
---|---|---|---|
__div__ |
/ |
✅ 有效 | ❌ 废弃 |
__truediv__ |
/ |
❌ 无 | ✅ 有效 |
__floordiv__ |
// |
✅ 有效 | ✅ 有效 |
✅ 建议
如果你在维护旧代码或从 Python 2 迁移到 Python 3,请:
- 把
__div__
改名为__truediv__
- 检查是否有
from __future__ import division
的影响
4-什么是备选方案
这段代码来自一个 Git 仓库对象(repo)的实现,用来读取该仓库的 “alternates” 列表。
“alternates” 是 Git 的一个机制:当仓库 A 把对象(objects)存放在仓库 B 的 objects 目录里时,会在仓库 A 的 .git/objects/info/alternates
文件中记录仓库 B 的 objects 目录路径,从而避免重复存储大对象。
逐行解释:
alternates_path = os.path.join(self.path, *['objects', 'info', 'alternates'])
拼出.git/objects/info/alternates
文件的完整路径。
self.path
通常是.git
目录本身。if os.path.exists(alternates_path):
判断 alternates 文件是否存在;不存在就说明该仓库没有使用 alternates 机制。若文件存在,则:
- 打开文件读取全部内容到
alts
。 - 使用
try/finally
保证无论是否抛异常都会关闭文件句柄。 alts.strip().splitlines()
去掉首尾空白并按行切分,得到 “备用对象目录” 的列表(每个元素是一个绝对或相对路径)。- 返回这个列表。
- 打开文件读取全部内容到
若文件不存在,直接返回空列表
[]
。
最终效果:
get_alternates()
返回一个字符串列表,里面是此仓库通过 alternates 机制引用的其他仓库的 objects 目录路径;如果没有使用 alternates,则返回空列表。
5-def list_from_string(cls, repo, text)方法
代码中作者,用到了很多关于list_from_string这个方法