├── .gitignore ├── MANIFEST ├── Makefile ├── README.md ├── gin ├── setup.py └── test ├── 01.index ├── 01.json ├── 01.txt └── run /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | dist 4 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | gin 3 | setup.py 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/sh 2 | 3 | .PHONY: publish 4 | publish: 5 | python3 setup.py sdist --formats=bztar upload 6 | rm -rf dist/ 7 | git add -A 8 | git commit -m "Published `egrep -m1 '^version' gin | cut -b12-18`" 9 | git tag `egrep -m1 '^version' gin | cut -b12-18` 10 | git push 11 | git push --tags 12 | 13 | .PHONY: test 14 | test: 15 | test/run 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gin - a Git index file parser 2 | 3 | The `gin` script parses the databases that live at `.git/index` in any Git repository, and shows the contents in a readable form, or as a JSON dump. These databases store the current state of the stage area, sometimes called the index or cache. 4 | 5 | ## Install 6 | 7 | pip3 install gin 8 | 9 | Or clone this repo and use the `gin` script. 10 | 11 | Or download the script directly. 12 | 13 | The script requires Python 3. 14 | 15 | ## Use 16 | 17 | ``` 18 | usage: gin [-h] [-j] [-v] [path] 19 | 20 | parse a Git index file 21 | 22 | positional arguments: 23 | path path to a Git repository or index file 24 | 25 | optional arguments: 26 | -h, --help show this help message and exit 27 | -j, --json output JSON 28 | -v, --version show script version number 29 | ``` 30 | 31 | ### Examples 32 | 33 | * Show the Git index file in the current repository, if in the repository root: 34 | 35 | gin 36 | 37 | * Show the Git index file in the `~/git-repo` repository: 38 | 39 | gin ~/git-repo 40 | 41 | * Show the Git index file `~/git-repo/.git/index`: 42 | 43 | gin ~/git-repo/.git/index 44 | 45 | The script supports index file versions 2 and 3, and will skip over extensions. 46 | 47 | Use the `-j` or `--json` flags to dump JSON. 48 | 49 | ### Advanced 50 | 51 | To use the script as a module, rename it to `gin.py`. 52 | 53 | ## Report issues 54 | 55 | [Submit issues](https://github.com/sbp/gin/issues) on Github. 56 | 57 | Tweet [@sbp](https://twitter.com/sbp) with short comments or enquiries. 58 | 59 | ## Example 60 | 61 | ### Pretty print an index 62 | 63 | $ gin test/01.index 64 | 65 | Output: 66 | 67 | ```ini 68 | [header] 69 | signature = DIRC 70 | version = 3 71 | entries = 5 72 | 73 | [entry] 74 | entry = 1 75 | ctime = 1363549359.0 76 | mtime = 1363549359.0 77 | dev = 16777217 78 | ino = 1154043 79 | mode = 100644 80 | uid = 501 81 | gid = 20 82 | size = 6 83 | sha1 = d5f7fc3f74f7dec08280f370a975b112e8f60818 84 | flags = 9 85 | assume-valid = False 86 | extended = False 87 | stage = (False, False) 88 | name = added.txt 89 | 90 | [...] 91 | 92 | [checksum] 93 | checksum = True 94 | sha1 = 1ef0972eb948e6229240668effcb9c600fe5888d 95 | ``` 96 | 97 | ### Get name fields from an index 98 | 99 | $ gin | egrep '^ name =' 100 | 101 | Output: 102 | 103 | ``` 104 | name = .gitignore 105 | name = MANIFEST 106 | name = Makefile 107 | name = README.md 108 | name = gin 109 | name = setup.py 110 | name = test/01.index 111 | name = test/01.json 112 | name = test/01.txt 113 | name = test/run 114 | ``` 115 | 116 | Which should be equivalent to `git ls-files`. 117 | -------------------------------------------------------------------------------- /gin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | "gin - a Git index file parser" 3 | version = "0.1.006" 4 | 5 | # https://github.com/git/git/blob/master/Documentation/technical/index-format.txt 6 | 7 | import binascii 8 | import collections 9 | import json 10 | import mmap 11 | import struct 12 | 13 | def check(boolean, message): 14 | if not boolean: 15 | import sys 16 | print("error: " + message, file=sys.stderr) 17 | sys.exit(1) 18 | 19 | def parse(filename, pretty=True): 20 | with open(filename, "rb") as o: 21 | f = mmap.mmap(o.fileno(), 0, prot=mmap.PROT_READ) 22 | 23 | def read(format): 24 | # "All binary numbers are in network byte order." 25 | # Hence "!" = network order, big endian 26 | format = "! " + format 27 | bytes = f.read(struct.calcsize(format)) 28 | return struct.unpack(format, bytes)[0] 29 | 30 | index = collections.OrderedDict() 31 | 32 | # 4-byte signature, b"DIRC" 33 | index["signature"] = f.read(4).decode("ascii") 34 | check(index["signature"] == "DIRC", "Not a Git index file") 35 | 36 | # 4-byte version number 37 | index["version"] = read("I") 38 | check(index["version"] in {2, 3}, 39 | "Unsupported version: %s" % index["version"]) 40 | 41 | # 32-bit number of index entries, i.e. 4-byte 42 | index["entries"] = read("I") 43 | 44 | yield index 45 | 46 | for n in range(index["entries"]): 47 | entry = collections.OrderedDict() 48 | 49 | entry["entry"] = n + 1 50 | 51 | entry["ctime_seconds"] = read("I") 52 | entry["ctime_nanoseconds"] = read("I") 53 | if pretty: 54 | entry["ctime"] = entry["ctime_seconds"] 55 | entry["ctime"] += entry["ctime_nanoseconds"] / 1000000000 56 | del entry["ctime_seconds"] 57 | del entry["ctime_nanoseconds"] 58 | 59 | entry["mtime_seconds"] = read("I") 60 | entry["mtime_nanoseconds"] = read("I") 61 | if pretty: 62 | entry["mtime"] = entry["mtime_seconds"] 63 | entry["mtime"] += entry["mtime_nanoseconds"] / 1000000000 64 | del entry["mtime_seconds"] 65 | del entry["mtime_nanoseconds"] 66 | 67 | entry["dev"] = read("I") 68 | entry["ino"] = read("I") 69 | 70 | # 4-bit object type, 3-bit unused, 9-bit unix permission 71 | entry["mode"] = read("I") 72 | if pretty: 73 | entry["mode"] = "%06o" % entry["mode"] 74 | 75 | entry["uid"] = read("I") 76 | entry["gid"] = read("I") 77 | entry["size"] = read("I") 78 | 79 | entry["sha1"] = binascii.hexlify(f.read(20)).decode("ascii") 80 | entry["flags"] = read("H") 81 | 82 | # 1-bit assume-valid 83 | entry["assume-valid"] = bool(entry["flags"] & (0b10000000 << 8)) 84 | # 1-bit extended, must be 0 in version 2 85 | entry["extended"] = bool(entry["flags"] & (0b01000000 << 8)) 86 | # 2-bit stage (?) 87 | stage_one = bool(entry["flags"] & (0b00100000 << 8)) 88 | stage_two = bool(entry["flags"] & (0b00010000 << 8)) 89 | entry["stage"] = stage_one, stage_two 90 | # 12-bit name length, if the length is less than 0xFFF (else, 0xFFF) 91 | namelen = entry["flags"] & 0xFFF 92 | 93 | # 62 bytes so far 94 | entrylen = 62 95 | 96 | if entry["extended"] and (index["version"] == 3): 97 | entry["extra-flags"] = read("H") 98 | # 1-bit reserved 99 | entry["reserved"] = bool(entry["extra-flags"] & (0b10000000 << 8)) 100 | # 1-bit skip-worktree 101 | entry["skip-worktree"] = bool(entry["extra-flags"] & (0b01000000 << 8)) 102 | # 1-bit intent-to-add 103 | entry["intent-to-add"] = bool(entry["extra-flags"] & (0b00100000 << 8)) 104 | # 13-bits unused 105 | # used = entry["extra-flags"] & (0b11100000 << 8) 106 | # check(not used, "Expected unused bits in extra-flags") 107 | entrylen += 2 108 | 109 | if namelen < 0xFFF: 110 | entry["name"] = f.read(namelen).decode("utf-8", "replace") 111 | entrylen += namelen 112 | else: 113 | # Do it the hard way 114 | name = [] 115 | while True: 116 | byte = f.read(1) 117 | if byte == "\x00": 118 | break 119 | name.append(byte) 120 | entry["name"] = b"".join(name).decode("utf-8", "replace") 121 | entrylen += 1 122 | 123 | padlen = (8 - (entrylen % 8)) or 8 124 | nuls = f.read(padlen) 125 | check(set(nuls) == {0}, "padding contained non-NUL") 126 | 127 | yield entry 128 | 129 | indexlen = len(f) 130 | extnumber = 1 131 | 132 | while f.tell() < (indexlen - 20): 133 | extension = collections.OrderedDict() 134 | extension["extension"] = extnumber 135 | extension["signature"] = f.read(4).decode("ascii") 136 | extension["size"] = read("I") 137 | 138 | # Seems to exclude the above: 139 | # "src_offset += 8; src_offset += extsize;" 140 | extension["data"] = f.read(extension["size"]) 141 | extension["data"] = extension["data"].decode("iso-8859-1") 142 | if pretty: 143 | extension["data"] = json.dumps(extension["data"]) 144 | 145 | yield extension 146 | extnumber += 1 147 | 148 | checksum = collections.OrderedDict() 149 | checksum["checksum"] = True 150 | checksum["sha1"] = binascii.hexlify(f.read(20)).decode("ascii") 151 | yield checksum 152 | 153 | f.close() 154 | 155 | def parse_file(arg, pretty=True): 156 | if pretty: 157 | properties = { 158 | "version": "[header]", 159 | "entry": "[entry]", 160 | "extension": "[extension]", 161 | "checksum": "[checksum]" 162 | } 163 | else: 164 | print("[") 165 | 166 | for item in parse(arg, pretty=pretty): 167 | if pretty: 168 | for key, value in properties.items(): 169 | if key in item: 170 | print(value) 171 | break 172 | else: 173 | print("[?]") 174 | 175 | if pretty: 176 | for key, value in item.items(): 177 | print(" ", key, "=", value) 178 | else: 179 | print(json.dumps(item)) 180 | 181 | last = "checksum" in item 182 | if not last: 183 | if pretty: 184 | print() 185 | else: 186 | print(",") 187 | 188 | if not pretty: 189 | print("]") 190 | 191 | def main(): 192 | import argparse 193 | import os.path 194 | import sys 195 | 196 | parser = argparse.ArgumentParser(description="parse a Git index file") 197 | parser.add_argument("-j", "--json", action="store_true", 198 | help="output JSON") 199 | parser.add_argument("-v", "--version", action="store_true", 200 | help="show script version number") 201 | parser.add_argument("path", nargs="?", default=".", 202 | help="path to a Git repository or index file") 203 | args = parser.parse_args() 204 | 205 | if args.version: 206 | print("gin " + version) 207 | sys.exit() 208 | 209 | if os.path.isdir(args.path): 210 | path = os.path.join(args.path, ".git", "index") 211 | if os.path.isfile(path): 212 | args.path = path 213 | else: 214 | print("error: couldn't find a .git/index file to use", file=sys.stderr) 215 | print("use -h or --help for some documentation", file=sys.stderr) 216 | sys.exit(1) 217 | 218 | if not args.path: 219 | parser.print_usage() 220 | sys.exit(2) 221 | 222 | parse_file(args.path, pretty=not args.json) 223 | 224 | if __name__ == "__main__": 225 | main() 226 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import distutils.core 4 | import os.path 5 | import sys 6 | 7 | if __name__ == "__main__": 8 | README = "https://github.com/sbp/gin/blob/master/README.md" 9 | 10 | if os.path.isfile("gin"): 11 | with open("gin", "r+", encoding="ascii") as f: 12 | f.seek(66) 13 | version = f.read(7) 14 | 15 | if os.path.isdir(".git") and ("sdist" in sys.argv): 16 | patch = int(version[-3:]) + 1 17 | if patch > 999: 18 | raise ValueError("Update major/minor version") 19 | version = version[:-3] + "%03i" % patch 20 | 21 | f.seek(66) 22 | f.write(version) 23 | else: 24 | print("Unable to find gin script: refusing to install") 25 | sys.exit(1) 26 | 27 | distutils.core.setup( 28 | name="gin", 29 | version=version, 30 | author="Sean B. Palmer", 31 | url="https://github.com/sbp/gin", 32 | description="Git index file parser", 33 | long_description="Documented in `@sbp/gin/README.md <%s>`_" % README, 34 | scripts=["gin"], 35 | platforms="Linux and OS X", 36 | classifiers=[ 37 | "Operating System :: MacOS :: MacOS X", 38 | "Operating System :: POSIX", 39 | "Programming Language :: Python :: 3" 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /test/01.index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbp/gin/1b12d8463f3261d57ef5ea565bd644ea731d9f1a/test/01.index -------------------------------------------------------------------------------- /test/01.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"signature": "DIRC", "version": 3, "entries": 5} 3 | , 4 | {"entry": 1, "ctime_seconds": 1363549359, "ctime_nanoseconds": 0, "mtime_seconds": 1363549359, "mtime_nanoseconds": 0, "dev": 16777217, "ino": 1154043, "mode": 33188, "uid": 501, "gid": 20, "size": 6, "sha1": "d5f7fc3f74f7dec08280f370a975b112e8f60818", "flags": 9, "assume-valid": false, "extended": false, "stage": [false, false], "name": "added.txt"} 5 | , 6 | {"entry": 2, "ctime_seconds": 0, "ctime_nanoseconds": 0, "mtime_seconds": 0, "mtime_nanoseconds": 0, "dev": 0, "ino": 0, "mode": 33188, "uid": 0, "gid": 0, "size": 0, "sha1": "71779d2cab258b810b2f567c9a619f6e0105f44e", "flags": 11, "assume-valid": false, "extended": false, "stage": [false, false], "name": "deleted.txt"} 7 | , 8 | {"entry": 3, "ctime_seconds": 1363549318, "ctime_nanoseconds": 0, "mtime_seconds": 1363549318, "mtime_nanoseconds": 0, "dev": 16777217, "ino": 1154007, "mode": 33188, "uid": 501, "gid": 20, "size": 7, "sha1": "79c53955ef856f16f2107446bc721c8879a1bd2e", "flags": 20, "assume-valid": false, "extended": false, "stage": [false, false], "name": "directory/nested.txt"} 9 | , 10 | {"entry": 4, "ctime_seconds": 0, "ctime_nanoseconds": 0, "mtime_seconds": 0, "mtime_nanoseconds": 0, "dev": 0, "ino": 0, "mode": 33188, "uid": 0, "gid": 0, "size": 0, "sha1": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "flags": 16394, "assume-valid": false, "extended": true, "stage": [false, false], "extra-flags": 8192, "reserved": false, "skip-worktree": false, "intent-to-add": true, "name": "intent.txt"} 11 | , 12 | {"entry": 5, "ctime_seconds": 1363549294, "ctime_nanoseconds": 0, "mtime_seconds": 1363549294, "mtime_nanoseconds": 0, "dev": 16777217, "ino": 1154004, "mode": 33188, "uid": 501, "gid": 20, "size": 8, "sha1": "e79c5e8f964493290a409888d5413a737e8e5dd5", "flags": 12, "assume-valid": false, "extended": false, "stage": [false, false], "name": "modified.txt"} 13 | , 14 | {"extension": 1, "signature": "TREE", "size": 40, "data": "\u0000-1 1\ndirectory\u00001 0\n\u009d\u00fd}\b\u00ce\u00f45\u00bc\u00cf\u00c5p\u001b[T|7@\u00a6t\u0004"} 15 | , 16 | {"checksum": true, "sha1": "1ef0972eb948e6229240668effcb9c600fe5888d"} 17 | ] 18 | -------------------------------------------------------------------------------- /test/01.txt: -------------------------------------------------------------------------------- 1 | [header] 2 | signature = DIRC 3 | version = 3 4 | entries = 5 5 | 6 | [entry] 7 | entry = 1 8 | ctime = 1363549359.0 9 | mtime = 1363549359.0 10 | dev = 16777217 11 | ino = 1154043 12 | mode = 100644 13 | uid = 501 14 | gid = 20 15 | size = 6 16 | sha1 = d5f7fc3f74f7dec08280f370a975b112e8f60818 17 | flags = 9 18 | assume-valid = False 19 | extended = False 20 | stage = (False, False) 21 | name = added.txt 22 | 23 | [entry] 24 | entry = 2 25 | ctime = 0.0 26 | mtime = 0.0 27 | dev = 0 28 | ino = 0 29 | mode = 100644 30 | uid = 0 31 | gid = 0 32 | size = 0 33 | sha1 = 71779d2cab258b810b2f567c9a619f6e0105f44e 34 | flags = 11 35 | assume-valid = False 36 | extended = False 37 | stage = (False, False) 38 | name = deleted.txt 39 | 40 | [entry] 41 | entry = 3 42 | ctime = 1363549318.0 43 | mtime = 1363549318.0 44 | dev = 16777217 45 | ino = 1154007 46 | mode = 100644 47 | uid = 501 48 | gid = 20 49 | size = 7 50 | sha1 = 79c53955ef856f16f2107446bc721c8879a1bd2e 51 | flags = 20 52 | assume-valid = False 53 | extended = False 54 | stage = (False, False) 55 | name = directory/nested.txt 56 | 57 | [entry] 58 | entry = 4 59 | ctime = 0.0 60 | mtime = 0.0 61 | dev = 0 62 | ino = 0 63 | mode = 100644 64 | uid = 0 65 | gid = 0 66 | size = 0 67 | sha1 = e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 68 | flags = 16394 69 | assume-valid = False 70 | extended = True 71 | stage = (False, False) 72 | extra-flags = 8192 73 | reserved = False 74 | skip-worktree = False 75 | intent-to-add = True 76 | name = intent.txt 77 | 78 | [entry] 79 | entry = 5 80 | ctime = 1363549294.0 81 | mtime = 1363549294.0 82 | dev = 16777217 83 | ino = 1154004 84 | mode = 100644 85 | uid = 501 86 | gid = 20 87 | size = 8 88 | sha1 = e79c5e8f964493290a409888d5413a737e8e5dd5 89 | flags = 12 90 | assume-valid = False 91 | extended = False 92 | stage = (False, False) 93 | name = modified.txt 94 | 95 | [extension] 96 | extension = 1 97 | signature = TREE 98 | size = 40 99 | data = "\u0000-1 1\ndirectory\u00001 0\n\u009d\u00fd}\b\u00ce\u00f45\u00bc\u00cf\u00c5p\u001b[T|7@\u00a6t\u0004" 100 | 101 | [checksum] 102 | checksum = True 103 | sha1 = 1ef0972eb948e6229240668effcb9c600fe5888d 104 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | cd .. 5 | 6 | ./gin test/01.index > test/01.test.txt 7 | ./gin -j test/01.index > test/01.test.json 8 | 9 | diff -u test/01.txt test/01.test.txt 10 | diff -u test/01.json test/01.test.json 11 | 12 | rm test/01.test.* 13 | --------------------------------------------------------------------------------