def commit(author: str, message: str) -> str: """https://benhoyt.com/writings/pygit/""" index = read_index() if len(index) == 0: raise ValueError("nothing to commit") repo = repo_find() assert repo is not None try: with open(str(repo_file(repo, "refs", "heads", "master")), "r") as f: parent: Optional[str] = f.read().strip() except FileNotFoundError: _LOG.debug("No prior commits") parent = None # git write-tree sha_of_tree = tree_write(repo, index) # git commit-tree lines = [f"tree {sha_of_tree}"] if parent: lines.append(f"parent {parent}") author_time = author_timestamp() lines = lines + [ f"author {author} {author_time}", f"committer {author} {author_time}", '', message, '' ] data = '\n'.join(lines).encode() sha1 = generic_object_hash(io.BytesIO(data), b"commit", repo) # git update-ref master_path = repo_file(repo, "refs", "heads", "master", write=True) write_file(str(master_path), (sha1 + "\n").encode()) return sha1
def object_write(obj: GitObject, actually_write: bool = True) -> str: # Serialize object data data = obj.serialize() # Add header result = obj.fmt + b' ' + str(len(data)).encode() + b'\x00' + data # Compute hash sha = hashlib.sha1(result).hexdigest() if actually_write: if obj.repo is None: raise ValueError("repo is None on actually_write in object_write") # Compute path path = repo_file(obj.repo, "objects", sha[0:2], sha[2:], mkdir=True, write=True) with open(str(path), "wb") as f: # Compress and write f.write(zlib.compress(result)) return sha
def generic_object_read(repo: GitRepository, sha: Sha) -> GitObject: """Read object object_id from Git repository repo. Return a GitObject whose exact type depends on the object""" path = repo_file(repo, "objects", sha[0:2], sha[2:]) assert path is not None, f"Path {path} for object {sha} could not be found" raw = zlib_read(path) # Read object type x = raw.find(b' ') fmt = raw[0:x] # Read and validate object size y = raw.find(b'\x00', x) size = int(raw[x:y].decode("ascii")) if size != len(raw) - y - 1: raise ValueError(f"Malformed object {sha}: bad length") # Pick constructor if fmt == b'commit': return GitCommit(repo, raw[y + 1:]) elif fmt == b'tree': return GitTree(repo, raw[y + 1:]) elif fmt == b'tag': return GitTag(repo, raw[y + 1:]) elif fmt == b'blob': return GitBlob(repo, raw[y + 1:]) else: raise ValueError( f"Unknown type {fmt.decode('ascii')} for object {sha}")
def ref_resolve(repo: GitRepository, ref: str) -> str: with open(str(repo_file(repo, ref)), 'r') as fp: data = fp.read()[:-1] # .trim() if data.startswith("ref: "): return ref_resolve(repo, data[5:]) return data
def object_get_type(repo: GitRepository, sha: Sha) -> bytes: """Read object object_id from Git repository repo. Return a GitObject whose exact type depends on the object""" path = repo_file(repo, "objects", sha[0:2], sha[2:]) assert path is not None, f"Path {path} for object {sha} could not be found" raw = zlib_read(path) # Read object type x = raw.find(b' ') return raw[0:x]
def object_resolve(repo: GitRepository, name: str) -> List[str]: """Resolve name to an object hash in repo. This function is aware of: - the HEAD literal - short and long hashes - tags - branches - remote branches""" candidates = [] hashRE = re.compile(r"^[0-9A-Fa-f]{40}$") smallHashRE = re.compile(r"^[0-9A-Fa-f]{4,40}$") # Empty string? Abort. if not name.strip(): return [] if name == "HEAD": return [ref_resolve(repo, "HEAD")] if hashRE.match(name): # This is a complete hash return [name.lower()] elif smallHashRE.match(name): # This is a small hash. 4 seems to be the minimal length for git to # consider something a short hash. This limit is documented in man # git-rev-parse name = name.lower() prefix = name[:2] path = repo_dir(repo, "objects", prefix, mkdir=False) if path: rem = name[2:] for f in path.iterdir(): if str(f).startswith(rem): candidates.append(prefix + str(f)) # search for branches and tags (with or without "refs" and "heads" or "tags" prefixes) for ref_path in [ f'refs/heads/{name}', f'refs/tags/{name}', f'refs/{name}', name ]: ref = repo_file(repo, ref_path) assert ref is not None if ref.exists(): candidates.append(ref_resolve(repo, ref_path)) return candidates
def write_index(entries: List[GitIndexEntry]) -> None: """Write list of index entries objects to git index file. https://github.com/benhoyt/pygit/blob/master/pygit.py""" packed_entries = [] for entry in entries: entry_head = struct.pack(ENTRY_FORMAT, entry.ctime[0], entry.ctime[1], entry.mtime[0], entry.mtime[1], entry.dev, entry.ino, entry.mode, entry.uid, entry.gid, entry.size, entry.obj, entry.flags) path = entry.name.encode() length = ((62 + len(path) + 8) // 8) * 8 packed_entry = entry_head + path + b'\x00' * (length - 62 - len(path)) packed_entries.append(packed_entry) header = struct.pack(HEADER_FORMAT, b'DIRC', 2, len(entries)) all_data = header + b''.join(packed_entries) digest = hashlib.sha1(all_data).digest() repo = repo_find() assert repo is not None with open(str(repo_file(repo, 'index', write=True)), "wb") as f: f.write(all_data + digest)
def ref_create(repo: GitRepository, ref_name: str, sha: str) -> None: with open(str(repo_file(repo, "refs/" + ref_name, write=True)), "w") as fp: fp.write(sha + '\n')