2024-03-20 19:43:58 +01:00
|
|
|
import itertools
|
|
|
|
import operator
|
2024-03-07 19:44:28 +01:00
|
|
|
import os
|
2024-04-23 17:36:41 +02:00
|
|
|
import string
|
2024-02-26 18:59:58 +01:00
|
|
|
|
2024-03-20 19:43:58 +01:00
|
|
|
from collections import namedtuple
|
|
|
|
from pathlib import Path, PurePath
|
|
|
|
|
2024-02-21 20:46:59 +01:00
|
|
|
from . import data
|
2024-02-26 18:59:58 +01:00
|
|
|
|
|
|
|
|
|
|
|
def write_tree(directory="."):
|
2024-03-02 16:02:42 +01:00
|
|
|
entries = []
|
2024-02-26 18:59:58 +01:00
|
|
|
with Path.iterdir(directory) as it:
|
|
|
|
for entry in it:
|
|
|
|
full = f"{directory}/{entry.name}"
|
2024-02-28 19:45:34 +01:00
|
|
|
if is_ignored(full):
|
|
|
|
continue
|
2024-02-26 18:59:58 +01:00
|
|
|
if entry.is_file(follow_symlinks=False):
|
2024-03-02 16:02:42 +01:00
|
|
|
type_ = "blob"
|
2024-02-28 19:51:24 +01:00
|
|
|
with open(full, "rb") as f:
|
2024-03-02 16:02:42 +01:00
|
|
|
oid = data.hash_object(f.read())
|
2024-02-26 18:59:58 +01:00
|
|
|
elif entry.is_dir(follow_symlinks=False):
|
2024-03-02 16:02:42 +01:00
|
|
|
type_ = "tree"
|
|
|
|
oid = write_tree(full)
|
|
|
|
entries.append((entry.name, oid, type_))
|
|
|
|
|
|
|
|
tree = "".join(f"{type_} {oid} {name}\n" for name, oid, type_ in sorted(entries))
|
|
|
|
|
|
|
|
return data.hash_object(tree.encode(), "tree")
|
2024-02-28 19:45:34 +01:00
|
|
|
|
|
|
|
|
2024-03-02 16:18:48 +01:00
|
|
|
def _iter_tree_entries(oid):
|
|
|
|
if not oid:
|
|
|
|
return
|
|
|
|
tree = data.get_object(oid, "tree")
|
|
|
|
for entry in tree.decode().splitlines():
|
|
|
|
type_, oid, name = entry.split(" ", 2)
|
|
|
|
yield type_, oid, name
|
|
|
|
|
|
|
|
|
|
|
|
def get_tree(oid, base_path=""):
|
|
|
|
result = {}
|
|
|
|
for type_, oid, name in _iter_tree_entries(oid):
|
|
|
|
assert "/" not in name
|
|
|
|
assert name not in ("..", ".")
|
|
|
|
path = base_path + name
|
|
|
|
if type_ == "blob":
|
|
|
|
result[path] = oid
|
|
|
|
elif type_ == "tree":
|
|
|
|
result.update(get_tree(oid, f"{path}/"))
|
|
|
|
else:
|
|
|
|
assert False, f"Unknown tree entry {type_}"
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2024-03-07 19:44:28 +01:00
|
|
|
def _empty_current_directory():
|
|
|
|
for root, dirnames, filenames in os.walk(".", topdown=False):
|
|
|
|
for filename in filenames:
|
|
|
|
path = PurePath.relative_to(f"{root}/{filename}")
|
|
|
|
if is_ignored(path) or not Path.is_file(path):
|
|
|
|
continue
|
|
|
|
Path.unlink(path)
|
|
|
|
for dirname in dirnames:
|
|
|
|
path = PurePath.relative_to(f"{root}/{dirname}")
|
|
|
|
if is_ignored(path):
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
Path.rmdir(path)
|
|
|
|
except (FileNotFoundError, OSError):
|
|
|
|
# Deletion might fail if the directory contains ignored files,
|
|
|
|
# so it's OK
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2024-03-02 16:18:48 +01:00
|
|
|
def read_tree(tree_oid):
|
2024-03-07 19:44:28 +01:00
|
|
|
_empty_current_directory()
|
2024-03-02 16:18:48 +01:00
|
|
|
for path, oid in get_tree(tree_oid, base_path="./").items():
|
|
|
|
Path.mkdir(PurePath.parent(path), exist_ok=True)
|
|
|
|
with open(path, "wb") as f:
|
|
|
|
f.write(data.get_object(oid))
|
|
|
|
|
|
|
|
|
2024-03-11 19:29:15 +01:00
|
|
|
def commit(message):
|
|
|
|
commit = f"tree {write_tree()}\n"
|
2024-03-18 19:01:53 +01:00
|
|
|
|
2024-04-12 17:19:14 +02:00
|
|
|
HEAD = data.get_ref("HEAD")
|
2024-03-18 19:01:53 +01:00
|
|
|
if HEAD:
|
|
|
|
commit += f"parent {HEAD}\n"
|
|
|
|
|
2024-03-11 19:29:15 +01:00
|
|
|
commit += "\n"
|
|
|
|
commit += f"{message}\n"
|
|
|
|
|
2024-03-13 19:38:35 +01:00
|
|
|
oid = data.hash_object(commit.encode(), "commit")
|
|
|
|
|
2024-04-12 17:19:14 +02:00
|
|
|
data.update_ref("HEAD", oid)
|
2024-03-13 19:38:35 +01:00
|
|
|
|
|
|
|
return oid
|
2024-03-11 19:29:15 +01:00
|
|
|
|
2024-04-20 21:38:04 +02:00
|
|
|
|
2024-04-17 19:33:54 +02:00
|
|
|
def create_tag(name, oid):
|
|
|
|
data.update_ref(f"refs/tags/{name}", oid)
|
2024-03-11 19:29:15 +01:00
|
|
|
|
2024-04-20 21:38:04 +02:00
|
|
|
|
2024-03-29 18:27:10 +01:00
|
|
|
def checkout(oid):
|
|
|
|
commit = get_commit(oid)
|
|
|
|
read_tree(commit.tree)
|
2024-04-12 17:19:14 +02:00
|
|
|
data.update_ref("HEAD", oid)
|
2024-03-29 18:27:10 +01:00
|
|
|
|
|
|
|
|
2024-03-20 19:43:58 +01:00
|
|
|
Commit = namedtuple("Commit", ["tree", "parent", "message"])
|
|
|
|
|
|
|
|
|
|
|
|
def get_commit(oid):
|
|
|
|
parent = None
|
|
|
|
|
|
|
|
commit = data.get_object(oid, "commit").decode()
|
|
|
|
lines = iter(commit.splitlines())
|
|
|
|
for line in itertools.takewhile(operator.truth, lines):
|
|
|
|
key, value = line.split(" ", 1)
|
|
|
|
if key == "tree":
|
|
|
|
tree = value
|
|
|
|
elif key == "parent":
|
|
|
|
parent = value
|
|
|
|
else:
|
|
|
|
assert False, f"Unknown field {key}"
|
|
|
|
|
|
|
|
message = "\n".join(lines)
|
|
|
|
return Commit(tree=tree, parent=parent, message=message)
|
|
|
|
|
|
|
|
|
2024-05-16 12:01:30 +02:00
|
|
|
def iter_commits_and_parents(oids):
|
|
|
|
oids = set(oids)
|
|
|
|
visited = set()
|
|
|
|
|
|
|
|
while oids:
|
|
|
|
oid = oids.pop()
|
|
|
|
if not oid or oid in visited:
|
|
|
|
continue
|
|
|
|
visited.add(oid)
|
|
|
|
yield oid
|
|
|
|
|
|
|
|
commit = get_commit(oid)
|
|
|
|
oids.add(commit.parent)
|
|
|
|
|
|
|
|
|
2024-04-20 21:38:04 +02:00
|
|
|
def get_oid(name):
|
2024-05-05 21:04:28 +02:00
|
|
|
if name == "@":
|
|
|
|
name = "HEAD"
|
|
|
|
|
2024-04-23 17:36:41 +02:00
|
|
|
# Name is ref
|
|
|
|
refs_to_try = [
|
|
|
|
f"{name}",
|
|
|
|
f"refs/{name}",
|
|
|
|
f"refs/tags/{name}",
|
|
|
|
f"refs/heads/{name}",
|
|
|
|
]
|
|
|
|
for ref in refs_to_try:
|
|
|
|
if data.get_ref(ref):
|
|
|
|
return data.get_ref(ref)
|
|
|
|
|
|
|
|
# Name is SHA1
|
|
|
|
is_hex = all(c in string.hexdigits for c in name)
|
|
|
|
if len(name) == 40 and is_hex:
|
|
|
|
return name
|
|
|
|
|
|
|
|
assert False, f"Unknown name {name}"
|
2024-04-20 21:38:04 +02:00
|
|
|
|
|
|
|
|
2024-02-28 19:45:34 +01:00
|
|
|
def is_ignored(path):
|
|
|
|
return ".ugit" in path.split("/")
|