Ben Chuanlong Du's Blog

It is never too late to learn.

Hands on pygit2

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

In [2]:
!pip3 install pygit2
Defaulting to user installation because normal site-packages is not writeable
Collecting pygit2
  Downloading pygit2-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (3.3 kB)
Requirement already satisfied: cffi>=1.16.0 in /usr/local/lib/python3.10/dist-packages (from pygit2) (1.16.0)
Requirement already satisfied: pycparser in /usr/local/lib/python3.10/dist-packages (from cffi>=1.16.0->pygit2) (2.22)
Downloading pygit2-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.1 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.1/5.1 MB 20.5 MB/s eta 0:00:0000:0100:01
Installing collected packages: pygit2
Successfully installed pygit2-1.15.1

[notice] A new release of pip is available: 24.0 -> 24.2
[notice] To update, run: python3 -m pip install --upgrade pip
In [3]:
import pygit2
In [4]:
pygit2.InvalidSpecError
Out[4]:
_pygit2.InvalidSpecError

Clone a Repository

In [22]:
url = "https://github.com/dclong/docker-ubuntu_b.git"
dir_local = "/tmp/test_pygit2"
!ls {dir_local}
build.sh  Dockerfile  LICENSE  readme.md  scripts
In [25]:
!rm -rf {dir_local}
!ls {dir_local}
ls: cannot access '/tmp/test_pygit2': No such file or directory
In [29]:
repo = pygit2.clone_repository(url, dir_local)
In [30]:
!ls {dir_local}
build.sh  Dockerfile  LICENSE  readme.md  scripts
In [26]:
url2 = "https://github.com/dclong/docker-ubuntu_b"
dir_local2 = "/tmp/test_pygit2_2"
In [27]:
pygit2.clone_repository(url, dir_local2)
Out[27]:
pygit2.Repository('/tmp/test_pygit2_2/.git/')
In [28]:
ls /tmp/test_pygit2_2/
build.sh*  Dockerfile  LICENSE  readme.md  scripts/

Methods of pygit2.Repository

In [28]:
[m for m in dir(repo) if not m.startswith("_")]
Out[28]:
['TreeBuilder',
 'add_submodule',
 'add_worktree',
 'ahead_behind',
 'amend_commit',
 'applies',
 'apply',
 'blame',
 'branches',
 'checkout',
 'checkout_head',
 'checkout_index',
 'checkout_tree',
 'cherrypick',
 'compress_references',
 'config',
 'config_snapshot',
 'create_blob',
 'create_blob_fromdisk',
 'create_blob_fromiobase',
 'create_blob_fromworkdir',
 'create_branch',
 'create_commit',
 'create_note',
 'create_reference',
 'create_reference_direct',
 'create_reference_symbolic',
 'create_remote',
 'create_tag',
 'default_signature',
 'descendant_of',
 'describe',
 'diff',
 'expand_id',
 'free',
 'get',
 'get_attr',
 'git_object_lookup_prefix',
 'head',
 'head_is_detached',
 'head_is_unborn',
 'ident',
 'index',
 'init_submodules',
 'is_bare',
 'is_empty',
 'is_shallow',
 'list_worktrees',
 'listall_branches',
 'listall_reference_objects',
 'listall_references',
 'listall_stashes',
 'listall_submodules',
 'lookup_branch',
 'lookup_note',
 'lookup_reference',
 'lookup_reference_dwim',
 'lookup_submodule',
 'lookup_worktree',
 'merge',
 'merge_analysis',
 'merge_base',
 'merge_base_many',
 'merge_base_octopus',
 'merge_commits',
 'merge_file_from_index',
 'merge_trees',
 'notes',
 'odb',
 'pack',
 'path',
 'path_is_ignored',
 'raw_listall_branches',
 'raw_listall_references',
 'read',
 'refdb',
 'references',
 'remotes',
 'reset',
 'resolve_refish',
 'revert_commit',
 'revparse',
 'revparse_ext',
 'revparse_single',
 'set_head',
 'set_ident',
 'set_odb',
 'set_refdb',
 'stash',
 'stash_apply',
 'stash_drop',
 'stash_pop',
 'state_cleanup',
 'status',
 'status_file',
 'update_submodules',
 'walk',
 'workdir',
 'write',
 'write_archive']

Branches

In [31]:
repo.listall_branches()
Out[31]:
['dev']
In [104]:
set(repo.branches)
Out[104]:
{'dev',
 'main',
 'origin/22.04',
 'origin/HEAD',
 'origin/centos7',
 'origin/debian',
 'origin/dev',
 'origin/main'}
In [103]:
repo.branches
Out[103]:
<pygit2.repository.Branches at 0x7f91e5e8b340>
In [106]:
[m for m in dir(repo.branches) if not m.startswith("_")]
Out[106]:
['create', 'delete', 'get', 'local', 'remote', 'with_commit']
In [110]:
repo.branches.get("non_exist_branch") is None
Out[110]:
True
In [112]:
br = repo.branches.get("dev")
br
Out[112]:
<_pygit2.Branch at 0x7f9223593bf0>
In [50]:
repo.branches.get("origin/main")
Out[50]:
<_pygit2.Branch at 0x7f9223679490>

Create a Branch

  1. Unlike the --force parameter of git checkout -b, repo.create_branch(..., force=True) does not throw away changes in the current directory. Notice that it does overwrite an existing branch.
In [129]:
repo.create_branch(
    "centos7", repo.references["refs/remotes/origin/centos7"].peel(), True
)
Out[129]:
<_pygit2.Branch at 0x7f92236e8830>

References

In [51]:
repo.references
Out[51]:
<pygit2.repository.References at 0x7f91e60d76a0>
In [141]:
repo.listall_references()
Out[141]:
['refs/heads/centos7',
 'refs/heads/dev',
 'refs/heads/main',
 'refs/remotes/origin/22.04',
 'refs/remotes/origin/HEAD',
 'refs/remotes/origin/centos7',
 'refs/remotes/origin/debian',
 'refs/remotes/origin/dev',
 'refs/remotes/origin/main',
 'refs/stash']
In [142]:
set(repo.references)
Out[142]:
{'refs/heads/centos7',
 'refs/heads/dev',
 'refs/heads/main',
 'refs/remotes/origin/22.04',
 'refs/remotes/origin/HEAD',
 'refs/remotes/origin/centos7',
 'refs/remotes/origin/debian',
 'refs/remotes/origin/dev',
 'refs/remotes/origin/main',
 'refs/stash'}
In [72]:
repo.lookup_reference("centos7")
---------------------------------------------------------------------------
InvalidSpecError                          Traceback (most recent call last)
Input In [72], in <cell line: 1>()
----> 1 repo.lookup_reference("centos7")

InvalidSpecError: centos7: the given reference name 'centos7' is not valid
In [145]:
repo.lookup_reference("refs/remotes/origin/centos7")
Out[145]:
<_pygit2.Reference at 0x7f9223679b50>
In [146]:
repo.references.get("refs/remotes/origin/centos7")
Out[146]:
<_pygit2.Reference at 0x7f92234c0910>

Checkout a Reference

In [36]:
!git -C {dir_local} status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean
In [71]:
repo.checkout("nima")
---------------------------------------------------------------------------
InvalidSpecError                          Traceback (most recent call last)
Input In [71], in <cell line: 1>()
----> 1 repo.checkout("nima")

File ~/.local/lib/python3.8/site-packages/pygit2/repository.py:422, in BaseRepository.checkout(self, refname, **kwargs)
    420     refname = refname.name
    421 else:
--> 422     reference = self.lookup_reference(refname)
    424 oid = reference.resolve().target
    425 treeish = self[oid]

InvalidSpecError: nima: the given reference name 'nima' is not valid
In [77]:
repo.checkout("refs/heads/dev")
In [74]:
repo.checkout("refs/heads/main")
In [75]:
!git -C {dir_local} status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
In [53]:
repo.checkout("refs/remotes/origin/main")
In [54]:
!git -C {dir_local} status
HEAD detached at origin/main
nothing to commit, working tree clean
In [55]:
!git -C {dir_local} branch
* (HEAD detached at origin/main)
  dev
In [135]:
repo.listall_references()
Out[135]:
['refs/heads/centos7',
 'refs/heads/dev',
 'refs/heads/main',
 'refs/remotes/origin/22.04',
 'refs/remotes/origin/HEAD',
 'refs/remotes/origin/centos7',
 'refs/remotes/origin/debian',
 'refs/remotes/origin/dev',
 'refs/remotes/origin/main',
 'refs/stash']
In [136]:
!git -C {dir_local} branch
  centos7
* dev
  main
In [138]:
repo.checkout(repo.references["refs/heads/centos7"])
In [139]:
!git -C {dir_local} branch
* centos7
  dev
  main

Status of the Repository

In [78]:
repo.status()
Out[78]:
{'Dockerfile': 256}

Stash Changes

In [86]:
repo.default_signature
Out[86]:
pygit2.Signature('Benjamin Du', 'longendu@yahoo.com', 1651127956, 4294966876, 'utf-8')
In [87]:
repo.stash(repo.default_signature)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [87], in <cell line: 1>()
----> 1 repo.stash(repo.default_signature)

File ~/.local/lib/python3.8/site-packages/pygit2/repository.py:1091, in BaseRepository.stash(self, stasher, message, keep_index, include_untracked, include_ignored)
   1089 coid = ffi.new('git_oid *')
   1090 err = C.git_stash_save(coid, self._repo, stasher_cptr[0], stash_msg, flags)
-> 1091 check_error(err)
   1093 return Oid(raw=bytes(ffi.buffer(coid)[:]))

File ~/.local/lib/python3.8/site-packages/pygit2/errors.py:56, in check_error(err, io)
     53     if io:
     54         raise IOError(message)
---> 56     raise KeyError(message)
     58 if err == C.GIT_EINVALIDSPEC:
     59     raise ValueError(message)

KeyError: 'cannot stash changes - there is nothing to stash.'

Create a Commit

In [27]:
?repo.create_commit
Docstring:
create_commit(reference_name: str, author: Signature, committer: Signature, message: bytes | str, tree: Oid, parents: list[Oid][, encoding: str]) -> Oid

Create a new commit object, return its oid.
Type:      builtin_function_or_method

Index

In [150]:
index = repo.index
index
Out[150]:
<pygit2.index.Index at 0x7f92234a9b50>
In [158]:
[m for m in dir(index) if not m.startswith("_")]
Out[158]:
['add',
 'add_all',
 'clear',
 'conflicts',
 'diff_to_tree',
 'diff_to_workdir',
 'from_c',
 'read',
 'read_tree',
 'remove',
 'remove_all',
 'write',
 'write_tree']

git reset / Repository.reset

Reset current HEAD to the specified state.

In [37]:
!git -C {dir_local} status
On branch dev
Your branch is up to date with 'origin/dev'.

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:   Dockerfile

no changes added to commit (use "git add" and/or "git commit -a")
In [180]:
repo.reset(repo.head.peel().oid, pygit2.GIT_RESET_HARD)
In [38]:
repo.reset(repo.head.peel().id, pygit2.GIT_RESET_HARD)
In [39]:
!git -C {dir_local} status
On branch dev
Your branch is up to date with 'origin/dev'.

nothing to commit, working tree clean

git diff / Repository.diff

In [13]:
repo.listall_references()
Out[13]:
['refs/heads/dev',
 'refs/heads/main',
 'refs/remotes/origin/22.04',
 'refs/remotes/origin/HEAD',
 'refs/remotes/origin/centos7',
 'refs/remotes/origin/debian',
 'refs/remotes/origin/dev',
 'refs/remotes/origin/main']
In [15]:
diff = repo.diff("refs/heads/dev", "refs/heads/main")
diff
Out[15]:
<_pygit2.Diff at 0x7f581bc368d0>
In [36]:
not any(True for _ in diff.deltas)
Out[36]:
True
In [38]:
not any(True for _ in repo.diff("refs/heads/main", "refs/heads/dev").deltas)
Out[38]:
True
In [40]:
diff = repo.diff("refs/heads/dev", "refs/heads/centos7")
diff
Out[40]:
<_pygit2.Diff at 0x7f581954a4b0>
In [43]:
deltas = list(diff.deltas)
deltas
Out[43]:
[<_pygit2.DiffDelta at 0x7f58195d4d50>,
 <_pygit2.DiffDelta at 0x7f58195d4510>,
 <_pygit2.DiffDelta at 0x7f58195d4f30>,
 <_pygit2.DiffDelta at 0x7f58195d4180>]
In [59]:
deltas[0].old_file.path
Out[59]:
'.github/workflows/create_pull_request.yml'
In [60]:
deltas[0].new_file.path
Out[60]:
'.github/workflows/create_pull_request.yml'
In [61]:
deltas[1].old_file.path
Out[61]:
'Dockerfile'
In [62]:
deltas[1].new_file.path
Out[62]:
'Dockerfile'

Comments