├── gitinfo ├── __init__.py ├── helpers.py ├── gitinfo.py └── pack_reader.py ├── .gitignore ├── .github └── workflows │ └── pypi.yml ├── LICENSE ├── setup.py ├── test.py └── README.md /gitinfo/__init__.py: -------------------------------------------------------------------------------- 1 | from .gitinfo import get_git_info # NOQA 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | *.sqlite3 65 | *.db 66 | 67 | tags* 68 | .vscode* -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: 3 | # https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 4 | 5 | name: PyPI 6 | 7 | on: 8 | workflow_dispatch: ~ 9 | release: 10 | types: [published] 11 | push: 12 | tags: 13 | - '*.*.*' 14 | 15 | jobs: 16 | pypi: 17 | name: Publish to PyPI 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.x' 27 | architecture: 'x64' 28 | 29 | - name: Install dependencies and build 30 | run: | 31 | python -m pip install --upgrade build 32 | python -m build 33 | 34 | - name: Publish to PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | user: __token__ 38 | password: ${{ secrets.PYPI_TOKEN }} 39 | 40 | # vim:ts=2:sw=2:et -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Serafeim Papastefanos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import find_packages, setup 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | name="python-git-info", 9 | version="0.8.3", 10 | description="Get git information repository, directly from .git", 11 | author="Serafeim Papastefanos", 12 | author_email="spapas@gmail.com", 13 | license="MIT", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/spapas/python-git-info/", 17 | zip_safe=False, 18 | include_package_data=False, 19 | packages=find_packages( 20 | exclude=[ 21 | "tests.*", 22 | "tests", 23 | "sample", 24 | ] 25 | ), 26 | classifiers=[ 27 | "Environment :: Web Environment", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 2", 30 | "Programming Language :: Python :: 2.7", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.6", 33 | "Intended Audience :: Developers", 34 | "License :: OSI Approved :: MIT License", 35 | "Operating System :: OS Independent", 36 | "Programming Language :: Python", 37 | "Topic :: Software Development :: Version Control :: Git", 38 | "Topic :: Software Development :: Libraries", 39 | "Topic :: Software Development :: Libraries :: Python Modules", 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /gitinfo/helpers.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def parse_commiter_line(line): 5 | # print(line) 6 | # print("~") 7 | "Parse the commiter/author line which also contains a datetime" 8 | parts = line.split() 9 | # TODO: I'll ignore tz for now It is parts[:-1] 10 | unix_time = float(parts[-2]) 11 | commiter = " ".join(parts[1:-2]) 12 | commit_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(unix_time)) 13 | return commiter, commit_time 14 | 15 | 16 | def parse_git_message(data, gi): 17 | lines = data.decode("utf-8").split("\n") 18 | reading_pgp = False 19 | reading_msg = False 20 | for idx, l in enumerate(lines): 21 | if l == "" and not reading_msg: 22 | reading_pgp = False 23 | reading_msg = True 24 | continue 25 | 26 | if reading_pgp == True: 27 | continue 28 | 29 | if reading_msg == True: 30 | gi["message"] += l 31 | if not l and idx < len(lines) - 1: 32 | gi["message"] += "\n" 33 | 34 | if l.startswith("tree"): 35 | gi["tree"] = l.split()[1] 36 | elif l.startswith("parent"): 37 | gi["parent"] = l.split()[1] 38 | elif l.startswith("gpgsig"): 39 | reading_pgp = True 40 | elif l.startswith("commiter"): 41 | commiter, commit_time = parse_commiter_line(l) 42 | gi["commiter"] = commiter 43 | gi["commit_date"] = commit_time 44 | elif l.startswith("author"): 45 | author, author_time = parse_commiter_line(l) 46 | gi["author"] = author 47 | gi["author_date"] = author_time 48 | 49 | return gi 50 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from gitinfo import get_git_info 4 | 5 | 6 | class TestMethods(unittest.TestCase): 7 | def test_has_git(self): 8 | ret = get_git_info() 9 | self.assertTrue("commit" in ret) 10 | self.assertTrue("message" in ret) 11 | self.assertTrue("refs" in ret) 12 | 13 | def test_message_not_empty(self): 14 | ret = get_git_info() 15 | self.assertTrue(len(ret["message"]) > 0) 16 | 17 | def test_has_gitdir(self): 18 | ret = get_git_info() 19 | self.assertTrue("gitdir" in ret) 20 | self.assertTrue(os.path.dirname(ret["gitdir"]) == os.getcwd()) 21 | 22 | def test_doesnot_have_git(self): 23 | # The parent directory should *not* have a git file 24 | with self.assertLogs("gitinfo.gitinfo", level="WARNING") as cm: 25 | ret = get_git_info(os.path.dirname(os.getcwd())) 26 | self.assertEqual(ret, None) 27 | self.assertIn("No git dir found", cm.output[0]) 28 | 29 | def test_should_work_with_relative_paths(self): 30 | # The parent directory should *not* have a git file 31 | # Should work with .. 32 | with self.assertLogs("gitinfo.gitinfo", level="WARNING") as cm: 33 | ret = get_git_info("..") 34 | self.assertEqual(ret, None) 35 | self.assertIn("No git dir found", cm.output[0]) 36 | 37 | def test_should_not_crash_with_emtpy_git_dir(self): 38 | # Create a dir named named empty_git and run git init there to test this 39 | if os.path.isdir("empty_git"): 40 | ret = get_git_info("empty_git") 41 | self.assertEqual(ret, None) 42 | 43 | def test_packed(self): 44 | ret = get_git_info("../../c/git") 45 | self.assertTrue("commit" in ret) 46 | self.assertTrue("message" in ret) 47 | self.assertTrue("refs" in ret) 48 | 49 | 50 | if __name__ == "__main__": 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /gitinfo/gitinfo.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import zlib 4 | import struct 5 | import zlib 6 | 7 | 8 | from .pack_reader import get_pack_info 9 | from .helpers import parse_git_message 10 | 11 | import logging 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def find_git_dir(directory): 17 | "Find the correct git dir; move upwards if .git folder is not found here" 18 | absdir = os.path.abspath(directory) 19 | gitdir = os.path.join(absdir, ".git") 20 | if os.path.isdir(gitdir): 21 | return gitdir 22 | parentdir = os.path.dirname(absdir) 23 | if absdir == parentdir: 24 | # We reached root and found no gitdir 25 | logger.warning("No git dir found") 26 | 27 | return None 28 | return find_git_dir(parentdir) 29 | 30 | 31 | def get_head_commit(directory): 32 | "Retrieve the HEAD commit of this repo had" 33 | head_file = os.path.join(directory, "HEAD") 34 | refs = None 35 | 36 | head_parts = None 37 | 38 | if not os.path.isfile(head_file): 39 | logger.warning("Git repository is broken (wrong head file)") 40 | return None, None 41 | 42 | with open(head_file, "r") as fh: 43 | data = fh.read().strip() 44 | refs = data 45 | try: 46 | head_parts = data.split(" ")[1].split("/") 47 | except IndexError: 48 | # The head may contain just a commit so let's return it in that case: 49 | return data, refs 50 | if not head_parts: 51 | logger.warning("Git repository is broken (no head parts)") 52 | return None, None 53 | 54 | head_ref_file = os.path.join(directory, *head_parts) 55 | 56 | if not os.path.isfile(head_ref_file): 57 | logger.warning("Git repository is broken (no head ref file)") 58 | # Try to find it on the remotes ? 59 | bname = head_parts[-1] 60 | remotes_dir = os.path.join(directory, "refs", "remotes") 61 | remotes = os.listdir(remotes_dir) 62 | for r in remotes: 63 | remote_branch_file = os.path.join(remotes_dir, r, bname) 64 | if os.path.isfile(remote_branch_file): 65 | with open(remote_branch_file, "r") as fl: 66 | head_commit = fl.read().strip() 67 | return head_commit, refs 68 | 69 | return None, None 70 | head_commit = None 71 | with open(head_ref_file, "r") as fl: 72 | head_commit = fl.read().strip() 73 | return head_commit, refs 74 | 75 | 76 | def get_git_info_dir(directory): 77 | head_commit, refs = get_head_commit(directory) 78 | 79 | if not head_commit: 80 | logger.warning("Git repository is broken (no head commit)") 81 | return 82 | 83 | head_message_folder = head_commit[:2] 84 | head_message_filename = head_commit[2:] 85 | head_message_file = os.path.join( 86 | directory, "objects", head_message_folder, head_message_filename 87 | ) 88 | 89 | if refs.startswith("ref: refs/heads/"): 90 | refs = refs[len("ref: refs/heads/") :] 91 | 92 | gi = {"commit": head_commit, "gitdir": directory, "message": "", "refs": refs} 93 | 94 | if not os.path.isfile(head_message_file): 95 | # Here we open the snake bucket of idx+pack 96 | object_path = os.path.join(directory, "objects", "pack") 97 | for idx_file in glob.glob(object_path + "/*.idx"): 98 | r = get_pack_info(idx_file, gi) 99 | if not r: 100 | continue 101 | return r 102 | 103 | else: 104 | with open(head_message_file, "rb") as fl: 105 | data = zlib.decompress(fl.read()) 106 | if not data[:6] == b"commit": 107 | # Not a commit object for some reason... 108 | return 109 | # Retrieve the null_byte_idx and start from there 110 | null_byte_idx = data.index(b"\x00") + 1 111 | data = data[null_byte_idx:] 112 | 113 | return parse_git_message(data, gi) 114 | 115 | 116 | def get_git_info(dir=os.getcwd()): 117 | gitdir = find_git_dir(dir) 118 | if not gitdir: 119 | return None 120 | return get_git_info_dir(gitdir) 121 | 122 | 123 | if __name__ == "__main__": 124 | print(get_git_info()) 125 | -------------------------------------------------------------------------------- /gitinfo/pack_reader.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import binascii 3 | import zlib 4 | import codecs 5 | import os 6 | from .helpers import parse_git_message 7 | 8 | # Very useful info for pack and index files: https://codewords.recurse.com/issues/three/unpacking-git-packfiles 9 | 10 | # Taken from here https://stackoverflow.com/a/312464/119071 11 | def chunks(l, n): 12 | for i in range(0, len(l), n): 13 | yield l[i : i + n] 14 | 15 | 16 | def convert_commit_to_bytes(commit): 17 | c = map(lambda z: int(z, 16), chunks(commit, 2)) 18 | # Works with both py 2 and 3 19 | bc = bytes(bytearray(c)) 20 | return bc 21 | 22 | 23 | def get_pack_idx(idx_file, commit): 24 | head_commit_bytes = convert_commit_to_bytes(commit) 25 | pack_idx = -1 26 | with open(idx_file, "rb") as fin: 27 | # Check header 28 | if fin.read(4) != b"\xff\x74\x4f\x63": 29 | return 30 | 31 | # Read number of objects: 32 | # 8 bytes header + 1023 bytes that contain the number of objects that start with <= fe 33 | # so we'll reach number of objects that start with <= ff (all objects) 34 | fin.seek(1028, 0) 35 | 36 | tot_obj = struct.unpack("!I", fin.read(4))[0] 37 | # print("Total objects is {0}".format(tot_obj)) 38 | 39 | found = False 40 | idx = 0 41 | # TODO: This can be improved using binary search instead of seq seach... 42 | while idx <= tot_obj: 43 | inp = fin.read(20) 44 | if inp == head_commit_bytes: 45 | found = True 46 | break 47 | idx += 1 48 | 49 | if not found: 50 | return None, None 51 | 52 | # Ok here we've found the index of our commit in the file. Let's get 53 | # its index in the pack file: The index file is something like: 54 | # 1032 (header (8) + objects number (4*256)) bytes 55 | # total_objects * 20 bytes each 56 | # total_objects * 4 bytes crc 57 | # and finally the pack index starts 58 | pack_idx_idx = 1032 + tot_obj * 20 + tot_obj * 4 + 4 * idx 59 | # So let's go directly to that position in the file and read the pack index 60 | fin.seek(pack_idx_idx, 0) 61 | pack_idx = struct.unpack("!I", fin.read(4))[0] 62 | # print("PACK IDX IS {0}".format(pack_idx)) 63 | return pack_idx, tot_obj 64 | 65 | 66 | def read_len(fin, byte0): 67 | 68 | len_barr = bytearray() 69 | len_barr.append(byte0 & 0x0F) 70 | 71 | # read the rest of the bytes of the length 72 | while True: 73 | byt = struct.unpack("B", fin.read(1))[0] 74 | 75 | if byt & 0x80: # MSB is 1 we need to reread 76 | 77 | len_barr.append(byt & 0x7F) 78 | else: 79 | len_barr.append(byt & 0x7F) 80 | break 81 | 82 | return int(codecs.encode(bytes(reversed(len_barr)), "hex"), 16) 83 | 84 | 85 | def read_len_from_bytes(array): 86 | len_barr = bytearray() 87 | # read the rest of the bytes of the length 88 | c = 0 89 | while True: 90 | byt = array[c] 91 | c += 1 92 | if byt & 0x80: # MSB is 1 we need to reread 93 | len_barr.append(byt & 0x7F) 94 | else: 95 | len_barr.append(byt) 96 | break 97 | return array[c:], int(codecs.encode(bytes(len_barr), "hex"), 16) 98 | 99 | 100 | def decode_delta(fin, data, r, pack_idx, offset): 101 | d0 = data[0] 102 | if d0 & 0x80: 103 | # Copy data from other object 104 | oo = get_object(fin, pack_idx - offset) 105 | data = data[1:] 106 | 107 | offset = data[0] 108 | size = data[1] 109 | 110 | r += oo[offset : offset + size] 111 | 112 | data = data[2:] 113 | else: 114 | # Insert raw data 115 | l = d0 & 0x7F 116 | dti = data[1 : 1 + l] 117 | r += dti 118 | data = data[1 + l :] 119 | 120 | return data, r 121 | 122 | 123 | def get_object(fin, pack_idx): 124 | fin.seek(pack_idx, 0) 125 | 126 | # Read the 1st byte 127 | byte0 = struct.unpack("B", fin.read(1))[0] 128 | # Make sure this is a commit object or an ofs delta 129 | if not (byte0 & 0x70) >> 4 in [1, 6]: 130 | return 131 | 132 | if (byte0 & 0x70) >> 4 == 1: # OBJ_COMMIT 133 | obj_len = read_len(fin, byte0) 134 | data = zlib.decompress(fin.read(obj_len)) 135 | return data 136 | elif (byte0 & 0x70) >> 4 == 6: # OBJ_OFS_DELTA 137 | obj_len = read_len(fin, byte0) 138 | byte0 = struct.unpack("B", fin.read(1))[0] 139 | offset = read_len(fin, byte0) 140 | data = zlib.decompress(fin.read(obj_len)) 141 | 142 | # Read lengths (won't be used for now) 143 | data, l1 = read_len_from_bytes(data) 144 | data, l2 = read_len_from_bytes(data) 145 | 146 | # Decode the delta into the delta_res variable 147 | delta_res = b"" 148 | while len(data): 149 | data, r = decode_delta(fin, data, delta_res, pack_idx, offset) 150 | 151 | return delta_res 152 | else: 153 | raise NotImplementedError("Not implemented yet!") 154 | 155 | 156 | def get_pack_info(idx_file, gi): 157 | # Retrieve idx of our commit info in the pack file 158 | 159 | pack_idx, index_tot_objects = get_pack_idx(idx_file, gi["commit"]) 160 | if not pack_idx: 161 | return 162 | 163 | # Pack file has the same name as the index file 164 | pack_file = idx_file[0:-3] + "pack" 165 | 166 | with open(pack_file, "rb") as fin: 167 | # Check header 168 | if fin.read(4) != b"PACK": 169 | return 170 | fin.read(4) # ignore version 171 | 172 | # Make sure that the number of objects is the same as in index 173 | pack_total_objects = struct.unpack("!I", fin.read(4))[0] 174 | if index_tot_objects != pack_total_objects: 175 | return 176 | data = get_object(fin, pack_idx) 177 | 178 | return parse_git_message(data, gi) 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-git-info 2 | 3 | [![PyPi version](https://badge.fury.io/py/python-git-info.svg)](https://badge.fury.io/py/python-git-info) 4 | 5 | A very simple project to get information from the git repository of your project. 6 | This package does not have any dependencies; it reads directly the data from the 7 | .git repository. 8 | 9 | ## Installation 10 | 11 | Just do a `pip install python-git-info`. This project should work with both python 2.7 and 3.x. 12 | 13 | ## Usage 14 | 15 | This app will search the current directory for a `.git` directory (which is 16 | always contained inside the root directory of a project). If one is found 17 | it will be used; else it will search the parent directory recursively until a 18 | `.git` is found. 19 | 20 | There's a single function name `get_git_info()` with an optional `dir` parameter. 21 | If you leave it empty it will start the `.git` directory search from the current directory, 22 | if you provide a value for `dir` it will start from that directory. The `get_git_info` 23 | will return a dictionary with the following structure if everything works ok or 24 | `None` if something fishy happend or no `.git` folder was found: 25 | 26 | ``` 27 | 28 | >> import gitinfo 29 | >> gitinfo.get_git_info() 30 | 31 | { 32 | 'parent_commit': 'd54743b6e7cf9dc36354fe2907f2f415b9988198', 33 | 'message': 'commit: Small restructuring\n', 34 | 'commiter': 'Serafeim ', 35 | 'commit_date': '2018-11-14 13:52:34', 36 | 'author': 'Serafeim ', 37 | 'author_date': '2018-11-14 13:52:34', 38 | 'commit': '9e1eec364ad24df153ca36d1da8405bb6379e03b', 39 | 'refs': 'master' 40 | } 41 | 42 | ``` 43 | 44 | You can also use it directly from the command line, f.e to get the info from the current directory: `python -c "import gitinfo; print(gitinfo.get_git_info())"`. 45 | 46 | You can even do some funny stuff with jq if you convert that struct to json: 47 | 48 | ``` 49 | 50 | python -c "import gitinfo, json; print(json.dumps(gitinfo.get_git_info()))" | jq .commit 51 | "92c76134aa108de6fcd39462ed2c9bc72fad4d01" 52 | 53 | ``` 54 | 55 | Notice that `refs` is the current branch. 56 | 57 | ## How it works 58 | 59 | This project will return the info from the latest commit of your *current* branch. To do this, it will read the `.git/HEAD` file which contains your current branch (i.e something like `ref: refs/heads/master`). It will then read the file it found there (i.e `.git/refs/heads/master`) to retrieve the actual sha of the latest commit, something like `8f6223c849d4bba75f037aeeb8660d9e6e306862`. 60 | 61 | This object is located in`.git/objects/8f/6223c849d4bba75f037aeeb8660d9e6e306862` (notice 62 | the first two characters are a directory name and the rest is the actual filename). This 63 | is a zlib compressed folder. After it is uncompressed it has a simple format; I'm 64 | copying from the git internals manual: 65 | 66 | > The format for a commit object is simple: it specifies the top-level tree for the snapshot of the project at that point; the parent commits if any (the commit object described above does not have any parents); the author/committer information (which uses your user.name and user.email configuration settings and a timestamp); a blank line, and then the commit message. 67 | 68 | So a sample commit message file would be something like this: 69 | 70 | ``` 71 | tree fa077d18fe3309aa12791dad2f733bfbb50fdee6 72 | parent 943f6e8e3641ea38a9d9db3256944b46bcfc1f77 73 | author Serafeim Papastefanos 1562836041 +0300 74 | committer Serafeim Papastefanos 1562836041 +0300 75 | 76 | prep new ver 77 | ``` 78 | 79 | ## The "pack" of snakes 80 | 81 | Until now everything seems like sunshine; unfortunately there's a can of snakes in this process or better a `pack` of snakes: Non trivial git repositories will 82 | compress the contents of their `.git/objects` folder to save network (and disk) space to a file ending with `.pack`. This file is a dump of all the (zlib compressed) 83 | object your repository contains (including the commit messages of course) and is accompanied by a `.idx` object which says where each object can be found in the 84 | pack file. You can find these files in `.git/objects/pack` folder (if your repository has them of course). 85 | 86 | In any case, the process of reading the `.idx` file to find the index of your commit and then reading that from the `.pack` file is not trivial; if you want 87 | to learn more about it you can check out this excellent resource: https://codewords.recurse.com/issues/three/unpacking-git-packfiles 88 | 89 | Or you can take a look at my code at the `pack_reader` module which I tried to heavily comment to improve understanding. 90 | 91 | ## Rationale 92 | 93 | This project may seem useless or very useful, depending on the way you deploy to your servers. If you, like me, push every changeset to your VCS *before* deploying and then pull the changes from the remote server to actually deploy then you'll find this project priceless: You can easily add the latest commit information to somewhere in your web application so you'll be able to see immediately which changeset is deployed to each server without the need to actually login to the server and do a `git log`. 94 | 95 | Also it is important to add here that this project is pure python and does not have 96 | any external dependencies (not even `git`); making it very easy to install and 97 | use in any project. 98 | 99 | ## Changes 100 | 101 | 0.8.3 102 | 103 | * Make it work with empty head refs 104 | 105 | 0.8.2 106 | 107 | * Try the pypi github action 108 | 109 | 0.8.1 110 | 111 | * Fix bug in decode_delta 112 | 113 | 0.8.0 114 | 115 | * Add logging output for errors 116 | * Use black to format files 117 | 118 | 0.7.6 119 | 120 | * Fix bug with get_head_commit 121 | 122 | 0.7.5 123 | 124 | * Add current branch as `refs` 125 | 126 | 0.7.4 127 | 128 | * Add gh action to deploy with tags 129 | 130 | 0.7.3 131 | 132 | * Remove debug 133 | 134 | 0.7.2 135 | 136 | * Fix endianess bug 137 | 138 | 0.7.1 139 | 140 | * Improve parsing of multi-line messages 141 | 142 | 0.7 143 | 144 | * Various fixes to support more git repositories 145 | 146 | 0.6.1 147 | 148 | * Remove non-needed print stmts 149 | 150 | 0.6 151 | 152 | * It now parses the pack file to retrieve the commit object if it is packed! 153 | 154 | 0.5 155 | 156 | * Change the parsing algorithm from using `.git/logs` to parse the real commit object inside the `.git/objects` folder. 157 | 158 | 159 | 0.4 160 | 161 | * Add more error checks 162 | 163 | 0.3 164 | 165 | * Make it work with '..' 166 | 167 | 0.2 168 | 169 | * Initial 170 | 171 | --------------------------------------------------------------------------------