Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Things on this page are fragmentary and immature notes/thoughts of the author. Please read with your own judgement!

Note: dulwich is not feature complete yet and the development of the project is extremely slow. It is suggested that you use other Python packages instead. For more discussions, please refer to Git Implementations and Bindings in Python .

Tips and Traps

  1. The git command (and thus Dulwich) accepts URLs both with and without the trailing .git.

!pip3 install dulwich
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: dulwich in /usr/local/lib/python3.10/dist-packages (0.21.5)
Requirement already satisfied: urllib3>=1.25 in /usr/local/lib/python3.10/dist-packages (from dulwich) (2.0.3)

[notice] A new release of pip is available: 23.2 -> 23.2.1
[notice] To update, run: python3 -m pip install --upgrade pip
from pathlib import Path
from dulwich import porcelain
from dulwich.repo import Repo
from dulwich.walk import WalkEntry
url = "https://github.com/dclong/test_dulwich"
dir_local = Path("/tmp/test_dulwich")
!rm -rf {dir_local}

git clone

repo = porcelain.clone(url, dir_local)
repo
<Repo at '/tmp/test_dulwich'>
!ls {dir_local}
abc  build.sh  readme.md  test1.txt

git fetch

porcelain.fetch(repo=dir_local)
FetchPackResult({b'HEAD': b'729bb376c018f068548c574a9fa05764432ef33a', b'refs/heads/dev': b'9f02363cbb049bdc6d0384c78d9b581dc2e31dba', b'refs/heads/feature': b'eab89ba3900c7483d97978f5038cea68997f6780', b'refs/heads/main': b'729bb376c018f068548c574a9fa05764432ef33a', b'refs/pull/1/head': b'9f02363cbb049bdc6d0384c78d9b581dc2e31dba', b'refs/tags/v1.0.0': b'6716bb0d016bd63ba543f3d9c67a65dadecd152e', b'refs/tags/v1.1.0': b'729bb376c018f068548c574a9fa05764432ef33a'}, {b'HEAD': b'refs/heads/main'}, b'git/github-60d715541676-Linux\n')

dulwich.repo.Repo

type(repo)
dulwich.repo.Repo

Create a repo from a local directory.

repo.path
'/tmp/test_dulwich'
repo2 = Repo("/tmp/test_dulwich")
repo2
<Repo at '/tmp/test_dulwich'>
repo3 = Repo("/workdir/archives/github_actions_scripts")
repo3
<Repo at '/workdir/archives/github_actions_scripts'>

git status

s = porcelain.status(repo3)
[
    m for m in dir(s) if not m.startswith("_")
]
['count', 'index', 'staged', 'unstaged', 'untracked']
s.staged
{'add': [b'update_version_dockerfile.py'], 'delete': [], 'modify': []}
s.unstaged
[b'update_version_dockerfile.py']
s.untracked
[]
!git -C {dir_local} status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   test1.txt

no changes added to commit (use "git add" and/or "git commit -a")
porcelain.get_tree_changes(repo3)
{'add': [b'update_version_dockerfile.py'], 'delete': [], 'modify': []}
!git -C {dir_local} diff
diff --git a/test1.txt b/test1.txt
index 9daeafb..a5f96b1 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1 +1,3 @@
 test
+add a new line
+
repo[b"head"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[64], line 1
----> 1 repo[b"head"]

File /usr/local/lib/python3.10/dist-packages/dulwich/repo.py:783, in BaseRepo.__getitem__(self, name)
    781         pass
    782 try:
--> 783     return self.object_store[self.refs[name]]
    784 except RefFormatError as exc:
    785     raise KeyError(name) from exc

File /usr/local/lib/python3.10/dist-packages/dulwich/refs.py:326, in RefsContainer.__getitem__(self, name)
    324 _, sha = self.follow(name)
    325 if sha is None:
--> 326     raise KeyError(name)
    327 return sha

KeyError: b'head'
repo.head
<bound method BaseRepo.head of <Repo at '/tmp/test_dulwich'>>
repo.head()
b'729bb376c018f068548c574a9fa05764432ef33a'
repo[repo.head()]
<Commit b'729bb376c018f068548c574a9fa05764432ef33a'>
repo.object_store
<DiskObjectStore('/tmp/test_dulwich/.git/objects')>
import sys

from dulwich.patch import write_tree_diff

outstream = getattr(sys.stdout, 'buffer', sys.stdout)
write_tree_diff(outstream, repo.object_store, repo[repo.head()], commit.tree)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[74], line 6
      3 from dulwich.patch import write_tree_diff
      5 outstream = getattr(sys.stdout, 'buffer', sys.stdout)
----> 6 write_tree_diff(outstream, repo.object_store, repo[repo.head()], commit.tree)

NameError: name 'commit' is not defined
import sys

from dulwich.patch import write_tree_diff
from dulwich.repo import Repo

repo_path = "."
commit_id = b"a6602654997420bcfd0bee2a0563d9416afe34b4"

r = Repo(repo_path)

commit = r[commit_id]
parent_commit = r[commit.parents[0]]
outstream = getattr(sys.stdout, 'buffer', sys.stdout)
write_tree_diff(outstream, r.object_store, parent_commit.tree, commit.tree)

git add

!touch /tmp/test_dulwich/abc2
!ls /tmp/test_dulwich/
abc  abc2  build.sh  Dockerfile  LICENSE  readme.md  scripts
!git -C /tmp/test_dulwich/ status
On branch dev
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   abc

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	abc2

dulwich.porcelain.add

This is the recommended way to add changes to stage. Repo.stage and Repo.unstage have been deprecated and will be removed in v0.26.0+. Notice that the original behavior of porcelain.add was to add files in the current working directory of Python instead of files from the specified repository (if different from the current working directory), which was confusing. This was later fixed. For more details, please refer to the issue that I filed.

porcelain.add("/tmp/test_dulwich", paths="/tmp/test_dulwich/abc")
(['abc'], set())
porcelain.add(repo3, paths="update_version_dockerfile.py")
(['update_version_dockerfile.py'], set())
porcelain.add(repo3)
(['nima'], set())

By default, dulwich adds all files in the current working directory, which is not the right behavior! I have submitted a ticket to fix the issue.

porcelain.add("/tmp/test_dulwich", paths="/tmp/test_dulwich/.")
(['./'], set())
status = porcelain.status("/tmp/test_dulwich")
status
GitStatus(staged={'add': [], 'delete': [], 'modify': []}, unstaged=[], untracked=['abc2'])
status.unstaged
[]
status.untracked
['abc2']

git commit

porcelain.commit(repo3, message="update python script")
b'b616e430f08642973571c51c9c5eeced0f0f17eb'
porcelain.commit("/tmp/test_dulwich/", message="add abc")
b'11fb9f18f9d211c93175e898faa731584b8be368'

ConfigFile

ConfigFile inherits ConfigDict which means that you operate on a ConfiFile like a dict.

config = repo.get_config()
config
ConfigFile(CaseInsensitiveDict([((b'core',), CaseInsensitiveDict([(b'repositoryformatversion', b'0'), (b'filemode', b'true'), (b'bare', b'false'), (b'logallrefupdates', b'true')])), ((b'remote', b'origin'), CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/origin/*')])), ((b'remote', b'nima'), CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/nima/*')]))]))
config.keys()
KeysView(ConfigFile(CaseInsensitiveDict([((b'core',), CaseInsensitiveDict([(b'repositoryformatversion', b'0'), (b'filemode', b'true'), (b'bare', b'false'), (b'logallrefupdates', b'true')])), ((b'remote', b'origin'), CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/origin/*')])), ((b'remote', b'nima'), CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/nima/*')]))])))
config.values()
ValuesView(ConfigFile(CaseInsensitiveDict([((b'core',), CaseInsensitiveDict([(b'repositoryformatversion', b'0'), (b'filemode', b'true'), (b'bare', b'false'), (b'logallrefupdates', b'true')])), ((b'remote', b'origin'), CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/origin/*')])), ((b'remote', b'nima'), CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/nima/*')]))])))
config[(b"remote", b"origin")]
CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/origin/*')])
config.get((b"remote", b"origin"), b"url")
b'https://github.com/dclong/docker-ubuntu_b'
dict(config)
{(b'core',): CaseInsensitiveDict([(b'repositoryformatversion', b'0'), (b'filemode', b'true'), (b'bare', b'false'), (b'logallrefupdates', b'true')]), (b'remote', b'origin'): CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/origin/*')]), (b'remote', b'nima'): CaseInsensitiveDict([(b'url', b'https://github.com/dclong/docker-ubuntu_b'), (b'fetch', b'+refs/heads/*:refs/remotes/nima/*')])}

git remote -v

[m for m in dir(dulwich.porcelain) if 'remote' in m]
['_import_remote_refs', 'get_branch_remote', 'get_remote_repo', 'ls_remote', 'remote_add', 'remote_remove']
porcelain.get_branch_remote(repo)
b'origin'
porcelain.get_remote_repo(repo)
('origin', 'https://github.com/dclong/docker-ubuntu_b')
porcelain.ls_remote(url)
{b'HEAD': b'4996e93a5f24c375b1d56deddda1cd9cfddd14f6', b'refs/heads/centos7': b'd7a6f672771be1fc33ddd1006b3318901c914b19', b'refs/heads/debian': b'c7fc15b52f4c76faa4ba487a113b5c987ac3b371', b'refs/heads/dev': b'4996e93a5f24c375b1d56deddda1cd9cfddd14f6', b'refs/heads/main': b'925dd68d39ea943f1c387e4906e72aebc765c4bf', b'refs/pull/1/head': b'b90bd029b4707a94640957cbfae73a631a9d83e0', b'refs/pull/1/merge': b'c767a6a929e599c245a4241477093554c1ab0d10', b'refs/pull/10/head': b'ea5b60d7fc34fce65ec4a503c20536d3e0d4587a', b'refs/pull/100/head': b'b514f4d74dd93b237cb72a91e992a0480393b546', b'refs/pull/101/head': b'b514f4d74dd93b237cb72a91e992a0480393b546', b'refs/pull/102/head': b'f2f28a5809657930caa51d2606dc62c3e74d2e27', b'refs/pull/103/head': b'f2f28a5809657930caa51d2606dc62c3e74d2e27', b'refs/pull/104/head': b'43863e528cbd069b8f095f8eade79ae990fbe916', b'refs/pull/105/head': b'43863e528cbd069b8f095f8eade79ae990fbe916', b'refs/pull/106/head': b'238b7db8c24f6ed43125696405f56cbd33302ad1', b'refs/pull/107/head': b'238b7db8c24f6ed43125696405f56cbd33302ad1', b'refs/pull/108/head': b'2e5bec99f5b0bfcb5e74ad8b9c0ff4c36827a295', b'refs/pull/109/head': b'4996e93a5f24c375b1d56deddda1cd9cfddd14f6', b'refs/pull/11/head': b'0d7e63476077d2ed51728823f3ce54b57a8287e6', b'refs/pull/110/head': b'4996e93a5f24c375b1d56deddda1cd9cfddd14f6', b'refs/pull/12/head': b'd2fcee067c4f003084950799f8cb408625d8c610', b'refs/pull/13/head': b'5d29d66bdadfb7e265b0f895ca1bf6f26f7bad39', b'refs/pull/14/head': b'114a4a2af63270ea69f08b5801cf1bdfa1c29222', b'refs/pull/15/head': b'ffe1f38e3a91ef69b784b867ad5d7729bf17499f', b'refs/pull/16/head': b'1b2ee6dbbe96435009ef96a80776ebc30e74b082', b'refs/pull/17/head': b'b1d52163d2170c1b7a8fe9a0d05bf15a7c4dfcb0', b'refs/pull/18/head': b'ac8a2c93c8c93985eb7a6e2742e2a3e5aa4786d8', b'refs/pull/19/head': b'2363a9a27b3223553f87ce2607a431b0b6ac9510', b'refs/pull/2/head': b'044c475cbccd47aad5dfbc3aa672ef37dc401fbc', b'refs/pull/20/head': b'163eadfc89d1e2a19ef87b979cd3f912b2606a31', b'refs/pull/21/head': b'd265b2309d471cb6edbcf349259837bd7401be10', b'refs/pull/22/head': b'4a7b4e9f3ab3b38abbf300c4c7ea216d804bf768', b'refs/pull/23/head': b'b3d3e249c98848e3f94cd266fcec31677cf51ba7', b'refs/pull/24/head': b'c6a5918b051d30688bf70f7113bc097d44f97353', b'refs/pull/25/head': b'cdc7a8a78d0344207a701370fe64452b8a32cbaa', b'refs/pull/26/head': b'cdc7a8a78d0344207a701370fe64452b8a32cbaa', b'refs/pull/27/head': b'40fee483349643993e1aa807b6bb92e70f8d0026', b'refs/pull/28/head': b'7fbbc778dbcf88f57ac70a1c8c47f51dcc7c044a', b'refs/pull/29/head': b'e3bd34e88a215a96d79dc362c3bda22966d1a159', b'refs/pull/3/head': b'f05bb4e34974fe56bde6329b65917eb6c13bb492', b'refs/pull/30/head': b'8f9f426f13d70b21f573f7c50bbe01e8ce38f158', b'refs/pull/31/head': b'8f9f426f13d70b21f573f7c50bbe01e8ce38f158', b'refs/pull/32/head': b'5e8e2a3d76050131a74e943045ce370cf05c8a5a', b'refs/pull/33/head': b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06', b'refs/pull/34/head': b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06', b'refs/pull/35/head': b'01394b26a91cbfdcdea7ceccf87b1d57dc8d954a', b'refs/pull/36/head': b'01394b26a91cbfdcdea7ceccf87b1d57dc8d954a', b'refs/pull/37/head': b'9329115eba6048db7972381ecb86bbc897d3f977', b'refs/pull/38/head': b'9329115eba6048db7972381ecb86bbc897d3f977', b'refs/pull/39/head': b'2922709aaef766e633d29ad29b559b3597ebce5f', b'refs/pull/4/head': b'4ced0ff539deaed8c0550d8b9375964cf4f301ea', b'refs/pull/40/head': b'2922709aaef766e633d29ad29b559b3597ebce5f', b'refs/pull/41/head': b'2922709aaef766e633d29ad29b559b3597ebce5f', b'refs/pull/42/head': b'a80543f6271e23effee307d9b1e43ae0346ed32c', b'refs/pull/43/head': b'a80543f6271e23effee307d9b1e43ae0346ed32c', b'refs/pull/44/head': b'2e6645a55e1a665b42880d10fa731909ca2cbc62', b'refs/pull/45/head': b'2e6645a55e1a665b42880d10fa731909ca2cbc62', b'refs/pull/46/head': b'2e6645a55e1a665b42880d10fa731909ca2cbc62', b'refs/pull/47/head': b'5701b39fa080dc72d44bb8ab3748272535a1f555', b'refs/pull/48/head': b'5701b39fa080dc72d44bb8ab3748272535a1f555', b'refs/pull/49/head': b'5701b39fa080dc72d44bb8ab3748272535a1f555', b'refs/pull/5/head': b'f1f7d2da22ebd871b40633480bbcf65c6e359183', b'refs/pull/50/head': b'abee9488b95fe174d8c70eaf81481ff75e6a0aeb', b'refs/pull/51/head': b'abee9488b95fe174d8c70eaf81481ff75e6a0aeb', b'refs/pull/52/head': b'abee9488b95fe174d8c70eaf81481ff75e6a0aeb', b'refs/pull/53/head': b'5c9060d676990842910378026a54f76159f14af1', b'refs/pull/54/head': b'5c9060d676990842910378026a54f76159f14af1', b'refs/pull/55/head': b'5c9060d676990842910378026a54f76159f14af1', b'refs/pull/56/head': b'4772475351cda80b6417c70faf574e2b129ae3ce', b'refs/pull/57/head': b'4772475351cda80b6417c70faf574e2b129ae3ce', b'refs/pull/58/head': b'4772475351cda80b6417c70faf574e2b129ae3ce', b'refs/pull/59/head': b'6c22d07d89bb18e3085ba95855c3e57f78b4efd9', b'refs/pull/6/head': b'09c70147a2697351b54ab4c6fa963674ebfaf762', b'refs/pull/60/head': b'6a35b22ac59b05e838bc6c2541f6a43fb0aad0f1', b'refs/pull/61/head': b'1ca56d99216ea8e12ee32e40131c54c85473531d', b'refs/pull/62/head': b'140948d0e0f9e93242643485667db698b91ca84b', b'refs/pull/63/head': b'e26ef132b990841aa0d5fcb4d1172519d7f47540', b'refs/pull/64/head': b'140948d0e0f9e93242643485667db698b91ca84b', b'refs/pull/65/head': b'feac3d41c796fce059cf555730ec274b8b850687', b'refs/pull/66/head': b'feac3d41c796fce059cf555730ec274b8b850687', b'refs/pull/67/head': b'feac3d41c796fce059cf555730ec274b8b850687', b'refs/pull/68/head': b'feac3d41c796fce059cf555730ec274b8b850687', b'refs/pull/69/head': b'7b952d878baa72b3f1a07b7ea5ee2836f7fdb4ba', b'refs/pull/7/head': b'204624e13a9e07770997f95bdf45af179f27e9c4', b'refs/pull/70/head': b'7b952d878baa72b3f1a07b7ea5ee2836f7fdb4ba', b'refs/pull/71/head': b'7b952d878baa72b3f1a07b7ea5ee2836f7fdb4ba', b'refs/pull/72/head': b'da4c00f7e53abceeb80efc9baae8d5a836d32f30', b'refs/pull/73/head': b'da4c00f7e53abceeb80efc9baae8d5a836d32f30', b'refs/pull/74/head': b'bd0515a6b4b6e6266013920694598ae113d48b2c', b'refs/pull/75/head': b'4372406c65f3e11d0138123d45aaa774c27fbe76', b'refs/pull/76/head': b'1f997c23eab00214e49b2def58b6a6e86bdf1cc2', b'refs/pull/77/head': b'4372406c65f3e11d0138123d45aaa774c27fbe76', b'refs/pull/78/head': b'bf346d6742f6558d1bc5c34cd71c63e4eadf22bd', b'refs/pull/79/head': b'6beaaa4db5fb1705f2468462bb40ff2c46341786', b'refs/pull/8/head': b'4546786737d692063b9c175b2e2b5035649e7aaa', b'refs/pull/80/head': b'4df4e6289e4644f21e400d8e148649e9881b4adc', b'refs/pull/81/head': b'2ba920d2d2c834f3fd9c570d2150a0f5dd9dc200', b'refs/pull/82/head': b'd3880bf360b7e36e8cd1ad2c0ee588ffd30611f4', b'refs/pull/83/head': b'f13f3bfd7e4ad3e7084b89b94d789e4e4a9c8e0b', b'refs/pull/84/head': b'7ddf22fdb933da4734fac522287c2c99105b8ced', b'refs/pull/85/head': b'4b7fd4e6cb22afc01020d2464be022ed4b055395', b'refs/pull/86/head': b'80fb1308d9d7576f2d04526f3ce72e197f9ae7de', b'refs/pull/87/head': b'11f32203327e26fd9bcd26c0d7af0c2ebe6266de', b'refs/pull/88/head': b'5961f169e5cd94319fc32ff3d4888469d4a9a293', b'refs/pull/89/head': b'4415bf6ee68993eca9ea6c81d978d5d9f541a8d4', b'refs/pull/9/head': b'24763e5554012f97af5e9926673bdf9cccdc79dc', b'refs/pull/90/head': b'4415bf6ee68993eca9ea6c81d978d5d9f541a8d4', b'refs/pull/91/head': b'cdd4da987285797630230d68d871395a3e59f563', b'refs/pull/92/head': b'c3f9f83ef09dfe6cf221302cc3cb5e601d419fe2', b'refs/pull/93/head': b'cdd4da987285797630230d68d871395a3e59f563', b'refs/pull/94/head': b'8df5667732c0040b0c5f472568e1c5ef2a514170', b'refs/pull/95/head': b'8df5667732c0040b0c5f472568e1c5ef2a514170', b'refs/pull/96/head': b'8ee75013f768869f5e7b15e48dc69937d49de1ec', b'refs/pull/97/head': b'8ee75013f768869f5e7b15e48dc69937d49de1ec', b'refs/pull/98/head': b'7d46e2e2574cbfb575f32206468e3d0c9ad04231', b'refs/pull/99/head': b'7d46e2e2574cbfb575f32206468e3d0c9ad04231'}
[key[1].decode() for key in config.keys() if key[0] == b"remote"]
['origin', 'nima']

dulwich.porcelain.ls_remote

porcelain.ls_remote(url)
{b'HEAD': b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06', b'refs/heads/debian': b'618389f8300615ba7d31c4ba7b75fb94770391e1', b'refs/heads/dev': b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06', b'refs/heads/main': b'71f2b1d96cfa8596319686a5a98514ad4ac85506', b'refs/pull/1/head': b'b90bd029b4707a94640957cbfae73a631a9d83e0', b'refs/pull/1/merge': b'c767a6a929e599c245a4241477093554c1ab0d10', b'refs/pull/10/head': b'ea5b60d7fc34fce65ec4a503c20536d3e0d4587a', b'refs/pull/11/head': b'0d7e63476077d2ed51728823f3ce54b57a8287e6', b'refs/pull/12/head': b'd2fcee067c4f003084950799f8cb408625d8c610', b'refs/pull/13/head': b'5d29d66bdadfb7e265b0f895ca1bf6f26f7bad39', b'refs/pull/14/head': b'114a4a2af63270ea69f08b5801cf1bdfa1c29222', b'refs/pull/15/head': b'ffe1f38e3a91ef69b784b867ad5d7729bf17499f', b'refs/pull/16/head': b'1b2ee6dbbe96435009ef96a80776ebc30e74b082', b'refs/pull/17/head': b'b1d52163d2170c1b7a8fe9a0d05bf15a7c4dfcb0', b'refs/pull/18/head': b'ac8a2c93c8c93985eb7a6e2742e2a3e5aa4786d8', b'refs/pull/19/head': b'2363a9a27b3223553f87ce2607a431b0b6ac9510', b'refs/pull/2/head': b'044c475cbccd47aad5dfbc3aa672ef37dc401fbc', b'refs/pull/20/head': b'163eadfc89d1e2a19ef87b979cd3f912b2606a31', b'refs/pull/21/head': b'd265b2309d471cb6edbcf349259837bd7401be10', b'refs/pull/22/head': b'4a7b4e9f3ab3b38abbf300c4c7ea216d804bf768', b'refs/pull/23/head': b'b3d3e249c98848e3f94cd266fcec31677cf51ba7', b'refs/pull/24/head': b'c6a5918b051d30688bf70f7113bc097d44f97353', b'refs/pull/25/head': b'cdc7a8a78d0344207a701370fe64452b8a32cbaa', b'refs/pull/26/head': b'cdc7a8a78d0344207a701370fe64452b8a32cbaa', b'refs/pull/27/head': b'40fee483349643993e1aa807b6bb92e70f8d0026', b'refs/pull/28/head': b'7fbbc778dbcf88f57ac70a1c8c47f51dcc7c044a', b'refs/pull/29/head': b'e3bd34e88a215a96d79dc362c3bda22966d1a159', b'refs/pull/3/head': b'f05bb4e34974fe56bde6329b65917eb6c13bb492', b'refs/pull/30/head': b'8f9f426f13d70b21f573f7c50bbe01e8ce38f158', b'refs/pull/31/head': b'8f9f426f13d70b21f573f7c50bbe01e8ce38f158', b'refs/pull/32/head': b'5e8e2a3d76050131a74e943045ce370cf05c8a5a', b'refs/pull/33/head': b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06', b'refs/pull/34/head': b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06', b'refs/pull/4/head': b'4ced0ff539deaed8c0550d8b9375964cf4f301ea', b'refs/pull/5/head': b'f1f7d2da22ebd871b40633480bbcf65c6e359183', b'refs/pull/6/head': b'09c70147a2697351b54ab4c6fa963674ebfaf762', b'refs/pull/7/head': b'204624e13a9e07770997f95bdf45af179f27e9c4', b'refs/pull/8/head': b'4546786737d692063b9c175b2e2b5035649e7aaa', b'refs/pull/9/head': b'24763e5554012f97af5e9926673bdf9cccdc79dc'}

dulwich.objects.Commit

repo.head()
b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06'
commit = repo[repo.head()]
commit
<Commit b'2fd55f0a653bf8ec2e7ffda16c7b2d601167da06'>
type(commit)
dulwich.objects.Commit
commit.message
b'Update etc.sh'
dir(commit)
['__bytes__', '__class__', '__cmp__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '_author', '_author_time', '_author_timezone', '_author_timezone_neg_utc', '_check_has_member', '_chunked_text', '_commit_time', '_commit_timezone', '_commit_timezone_neg_utc', '_committer', '_deserialize', '_encoding', '_extra', '_get_extra', '_get_parents', '_gpgsig', '_header', '_is_legacy_object', '_mergetag', '_message', '_needs_serialization', '_parents', '_parse_file', '_parse_legacy_object', '_parse_legacy_object_header', '_parse_object', '_parse_object_header', '_serialize', '_set_parents', '_sha', '_tree', 'as_legacy_object', 'as_legacy_object_chunks', 'as_pretty_string', 'as_raw_chunks', 'as_raw_string', 'author', 'author_time', 'author_timezone', 'check', 'commit_time', 'commit_timezone', 'committer', 'copy', 'encoding', 'extra', 'from_file', 'from_path', 'from_raw_chunks', 'from_raw_string', 'from_string', 'get_type', 'gpgsig', 'id', 'mergetag', 'message', 'parents', 'raw_length', 'set_raw_chunks', 'set_raw_string', 'set_type', 'sha', 'tree', 'type', 'type_name', 'type_num']
main = heads.main
main
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_300/3703271362.py in <module>
----> 1 main = heads.main
      2 main

NameError: name 'heads' is not defined

Get the commit pointed to by head called master.

main.commit
<git.Commit "95ed236bd715a06320ee85d519fb79a0adffe072">
main.rename("main2")
<git.Head "refs/heads/main2">

Verify that the main branch has been renamed to main2.

!cd {dir_local} && git branch
* main2

Get the Active Branch

[m for m in dir(porcelain) if "br" in m]
['_make_branch_ref', '_update_head_during_checkout_branch', 'active_branch', 'branch_create', 'branch_delete', 'branch_list', 'checkout_branch', 'find_unique_abbrev', 'get_branch_remote']
porcelain.active_branch(repo)
b'main'

Get All Branches

porcelain.branch_list(repo)
{b'main'}

Changed Files

Update a file.

!echo "# add a line of comment" >> {dir_local}/build.sh

Staged Files

The file build.sh is now staged.

Commit the change.

git push

porcelain.push(repo3)
SendPackResult({b'refs/heads/main': b'b616e430f08642973571c51c9c5eeced0f0f17eb'}, b'github/spokes-receive-pack-acac8763c60f636c44baaf5c3887895cf5f55c30')

git pull

!ls {dir_local}
abc  build.sh  readme.md
porcelain.pull(repo, refspecs="main")
!ls {dir_local}
abc  build.sh  readme.md  test1.txt

git checkout

[m for m in dir(porcelain) if 'checkout' in m]
['_update_head_during_checkout_branch', 'checkout_branch']
!git -C {dir_local} status
On branch dev
nothing to commit, working tree clean
!git -C {dir_local} branch
* dev
  main
porcelain.checkout_branch(repo, "main")
!git -C {dir_local} branch
  dev
* main

git tag

List all tags.

!git -C {dir_local} tag 
v1.0.0
[m for m in dir(porcelain) if "tag" in m]
['_make_tag_ref', 'get_unstaged_changes', 'print_tag', 'show_tag', 'tag_create', 'tag_delete', 'tag_list']
porcelain.tag_list(repo)
[b'v1.0.0', b'v2.0.0']

git tag tag_name

Create a new tag.

porcelain.tag_create(repo, "v2.0.0")
porcelain.push(repo, refspecs="v2.0.0")
---------------------------------------------------------------------------
HTTPUnauthorized                          Traceback (most recent call last)
Cell In[40], line 1
----> 1 dulwich.porcelain.push(repo, refspecs="v2.0.0")

File /usr/local/lib/python3.10/dist-packages/dulwich/porcelain.py:1181, in push(repo, remote_location, refspecs, outstream, errstream, force, **kwargs)
   1179 remote_location = client.get_url(path)
   1180 try:
-> 1181     result = client.send_pack(
   1182         path,
   1183         update_refs,
   1184         generate_pack_data=r.generate_pack_data,
   1185         progress=errstream.write,
   1186     )
   1187 except SendPackError as exc:
   1188     raise Error(
   1189         "Push to " + remote_location + " failed -> " + exc.args[0].decode(),
   1190     ) from exc

File /usr/local/lib/python3.10/dist-packages/dulwich/client.py:2015, in AbstractHttpGitClient.send_pack(self, path, update_refs, generate_pack_data, progress)
   1996 """Upload a pack to a remote repository.
   1997 
   1998 Args:
   (...)
   2012 
   2013 """
   2014 url = self._get_url(path)
-> 2015 old_refs, server_capabilities, url = self._discover_references(
   2016     b"git-receive-pack", url
   2017 )
   2018 (
   2019     negotiated_capabilities,
   2020     agent,
   2021 ) = self._negotiate_receive_pack_capabilities(server_capabilities)
   2022 negotiated_capabilities.add(capability_agent())

File /usr/local/lib/python3.10/dist-packages/dulwich/client.py:1940, in AbstractHttpGitClient._discover_references(self, service, base_url)
   1938     tail += "?service=%s" % service.decode("ascii")
   1939 url = urljoin(base_url, tail)
-> 1940 resp, read = self._http_request(url, headers)
   1942 if resp.redirect_location:
   1943     # Something changed (redirect!), so let's update the base URL
   1944     if not resp.redirect_location.endswith(tail):

File /usr/local/lib/python3.10/dist-packages/dulwich/client.py:2218, in Urllib3HttpGitClient._http_request(self, url, headers, data)
   2216     raise NotGitRepository()
   2217 if resp.status == 401:
-> 2218     raise HTTPUnauthorized(resp.headers.get("WWW-Authenticate"), url)
   2219 if resp.status == 407:
   2220     raise HTTPProxyUnauthorized(resp.headers.get("Proxy-Authenticate"), url)

HTTPUnauthorized: No valid credentials provided

git diff

help(repo.refs[4].commit.diff)
Help on method diff in module git.diff:

diff(other: Union[Type[git.diff.Diffable.Index], Type[ForwardRef('Tree')], object, NoneType, str] = <class 'git.diff.Diffable.Index'>, paths: Union[str, List[str], Tuple[str, ...], NoneType] = None, create_patch: bool = False, **kwargs: Any) -> 'DiffIndex' method of git.objects.commit.Commit instance
    Creates diffs between two items being trees, trees and index or an
    index and the working tree. It will detect renames automatically.
    
    :param other:
        Is the item to compare us with.
        If None, we will be compared to the working tree.
        If Treeish, it will be compared against the respective tree
        If Index ( type ), it will be compared against the index.
        If git.NULL_TREE, it will compare against the empty tree.
        It defaults to Index to assure the method will not by-default fail
        on bare repositories.
    
    :param paths:
        is a list of paths or a single path to limit the diff to.
        It will only include at least one of the given path or paths.
    
    :param create_patch:
        If True, the returned Diff contains a detailed patch that if applied
        makes the self to other. Patches are somewhat costly as blobs have to be read
        and diffed.
    
    :param kwargs:
        Additional arguments passed to git-diff, such as
        R=True to swap both sides of the diff.
    
    :return: git.DiffIndex
    
    :note:
        On a bare repository, 'other' needs to be provided as Index or as
        as Tree/Commit, or a git command error will occur

url = "https://github.com/dclong/docker-ubuntu_b.git"
dir_local = "/tmp/" + url[(url.rindex("/") + 1) :]
!rm -rf {dir_local}
repo = git.Repo.clone_from(url, dir_local, branch="main")
repo
<git.repo.base.Repo '/tmp/docker-ubuntu_b.git/.git'>
repo.refs
[<git.Head "refs/heads/debian">, <git.Head "refs/heads/dev">, <git.Head "refs/heads/main">, <git.RemoteReference "refs/remotes/origin/HEAD">, <git.RemoteReference "refs/remotes/origin/debian">, <git.RemoteReference "refs/remotes/origin/dev">, <git.RemoteReference "refs/remotes/origin/main">]
diffs = repo.refs[4].commit.diff(repo.refs[3].commit)
diffs
[]
diffs = repo.refs[4].commit.diff(repo.refs[2].commit)
diffs
[<git.diff.Diff at 0x7f0eb05d1a60>, <git.diff.Diff at 0x7f0eb05d1af0>]
str(diffs[0])
'Dockerfile\n=======================================================\nlhs: 100644 | 8ae5c7650a8c031a8e176d896a3665bbe7e2aae8\nrhs: 100644 | 9f2304d9a97aa1279ad1938b3bb74790172c9d8b'
repo.refs[5].name
'origin/main'
print(repo.git.status())
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
repo.git.checkout("debian", force=True)
"Your branch is up to date with 'origin/debian'."
repo.git.checkout(b="a_new_branch", force=True)
''
nima = repo.refs[4].checkout(force=True, b="nima")
nima
diffs = nima.commit.diff(repo.refs[-1].commit)
diffs[0].diff
''

Diff the dev and the main branch, which is equivalent to the Git command git diff dev..main.

repo.refs[2].commit.diff(repo.refs[1].commit)
[]
diffs = repo.refs[2].commit.diff(repo.refs[0].commit)
diffs
[<git.diff.Diff at 0x128ca3a60>]
diffs[0]
<git.diff.Diff at 0x128ca3a60>
diffs = repo.refs[6].commit.diff(repo.refs[7].commit)
diffs
[]
diffs = repo.refs[4].commit.diff(repo.refs[7].commit)
diffs
[<git.diff.Diff at 0x128ca3790>]
diffs[0].diff
''
diffs = repo.refs[7].commit.diff(repo.refs[4].commit)
diffs
[<git.diff.Diff at 0x128ca3820>]
diffs[0].diff
''
any(ele for ele in [""])
False
repo.branches[0].name
'dev'
for branch in repo.branches:
    branch.
commit = repo.head.commit
commit
<git.Commit "6716bb0d016bd63ba543f3d9c67a65dadecd152e">
type(repo.branches[0])
git.refs.head.Head
repo.refs[4].commit.diff(repo.refs[2].commit)
[]
repo.refs[4].commit.diff(repo.refs[3].commit)
[<git.diff.Diff at 0x127370310>]
help(repo.git.branch)
Help on function <lambda> in module git.cmd:

<lambda> lambda *args, **kwargs

repo.heads
[<git.Head "refs/heads/dev">, <git.Head "refs/heads/main">]

Diff the debian and the main branches but limit diff to specified paths (via the paths parameter).

diffs = repo.refs[4].commit.diff(repo.refs[2].commit, paths=["build.sh", "scripts"])
diffs
[]

Get Ahead Commits

def get_remote_refs(repo, remote: str = "origin") -> dict[bytes, bytes]:
    url = repo.get_config().get(("remote", remote), "url").decode("utf-8")
    return porcelain.ls_remote(url).refs


def get_ahead_commits(path: str, branch: str = "", remote: str = "origin") -> list[WalkEntry]:
    repo = Repo(path)
    if not branch:
        branch = porcelain.active_branch(repo)
    if isinstance(branch, str):
        branch = branch.encode()
    remote_branch_head = get_remote_refs(repo=repo, remote=remote)[b"refs/heads/" + branch]
    walker = repo.get_walker(
        include=[repo.head()],
        exclude=[remote_branch_head]
    )
    return list(walker)
commits = get_ahead_commits("test_dulwich")
commits
[<WalkEntry commit=1e000c9b6b2557d54f08623ca470241352b6fc2a, changes=[TreeChange(type='modify', old=TreeEntry(path=b'abc', mode=33188, sha=b'ed1293c7cab82424dcee8060829d7345a49e6108'), new=TreeEntry(path=b'abc', mode=33188, sha=b'a88d44100e656261166feab9f690f3b2f4b4d210'))]>]