Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions git_scratch/commands/porcelain_commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import typer

from git_scratch.utils.tree import create_root_tree_object
from git_scratch.utils.commit import build_commit_object
from git_scratch.utils.refs import get_head_commit_oid, update_head_to_commit, get_head_display, InvalidHeadError

def commit(
message: str = typer.Option(..., "-m", help="The commit message."),
verbose: bool = typer.Option(True, "--verbose/--quiet", help="Show commit summary."),
):
"""
Records changes to the repository.
"""
try:
tree_oid = create_root_tree_object()
parent_oid = get_head_commit_oid()

is_root_commit = parent_oid is None

new_commit_oid = build_commit_object(
tree_oid=tree_oid,
message=message,
parent_oid=parent_oid
)

update_head_to_commit(new_commit_oid)

if verbose:
head_desc = get_head_display(new_commit_oid)

commit_prefix = f"[{head_desc}"
if is_root_commit:
commit_prefix += " (root-commit)"
commit_prefix += f" {new_commit_oid[:7]}]"

typer.echo(f"{commit_prefix} {message}")
else:
typer.echo(new_commit_oid)


except InvalidHeadError as e:
typer.secho(f"HEAD Error: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)

except ValueError as e:
typer.secho(f"Error: {e}", fg=typer.colors.RED)
if "identity" in str(e).lower():
typer.secho('\nTo configure your user identity:\n'
' git config --global user.email "you@example.com"\n'
' git config --global user.name "Your Name"\n'
"Omit --global to set identity only for this repository.",
fg=typer.colors.RED)
raise typer.Exit(code=1)

except Exception as e:
typer.secho(f"An unexpected error occurred: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
20 changes: 10 additions & 10 deletions git_scratch/commands/write_tree.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@

import typer
from git_scratch.utils.index_utils import load_index
from git_scratch.utils.tree import build_tree
from git_scratch.utils.object import write_object
from git_scratch.utils.tree import create_root_tree_object

def write_tree():
"""
Writes a recursive Git tree from .git/index.json and displays its OID.
"""
index = load_index()
if not index:
typer.secho("Erreur : .git/index.json not found or empty.", fg=typer.colors.RED)
try:

oid = create_root_tree_object()
typer.echo(oid)
except ValueError as e:
typer.secho(f"Error: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
except Exception as e:
typer.secho(f"An unexpected error occurred while writing the tree: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)

tree_data = build_tree(index)
oid = write_object(tree_data, "tree")
typer.echo(oid)

6 changes: 6 additions & 0 deletions git_scratch/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from git_scratch.commands.rev_parse import rev_parse
from git_scratch.commands.ls_tree import ls_tree
from git_scratch.commands.commit_tree import commit_tree
from git_scratch.commands.porcelain_commit import commit


from git_scratch.commands.status import status
from git_scratch.commands.log import log
from git_scratch.commands.init import init
Expand All @@ -26,6 +29,9 @@
app.command("show-ref")(show_ref)
app.command("ls-tree")(ls_tree)
app.command("commit-tree")(commit_tree)
app.command("commit")(commit)


app.command("status")(status)
app.command("log")(log)

Expand Down
1 change: 1 addition & 0 deletions git_scratch/readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Salut l'homme au coeur blanc
15 changes: 4 additions & 11 deletions git_scratch/utils/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,18 @@ def build_commit_object(
) -> str:
"""
Construct and store a commit object.

Args:
tree_oid (str): OID of the associated tree.
message (str): Commit message.
parent_oid (Optional[str]): Parent commit OID, if any.

Returns:
str: The OID of the created commit object.
"""
author_name, author_email = get_author_identity()
timestamp, timezone = get_timestamp_info()
author_timestamp, author_timezone = get_timestamp_info(is_committer=False)
committer_timestamp, committer_timezone = get_timestamp_info(is_committer=True)

lines = [f"tree {tree_oid}"]

if parent_oid:
lines.append(f"parent {parent_oid}")

lines.append(f"author {author_name} <{author_email}> {timestamp} {timezone}")
lines.append(f"committer {author_name} <{author_email}> {timestamp} {timezone}")
lines.append(f"author {author_name} <{author_email}> {author_timestamp} {author_timezone}")
lines.append(f"committer {author_name} <{author_email}> {committer_timestamp} {committer_timezone}")
lines.append("")
lines.append(message)

Expand Down
37 changes: 28 additions & 9 deletions git_scratch/utils/identity.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# git_scratch/pit_identity.py
import os
import configparser
from datetime import datetime
from datetime import datetime, timezone
from typing import Tuple
import typer
from pathlib import Path

def get_author_identity() -> Tuple[str, str]:
Expand Down Expand Up @@ -44,14 +43,34 @@ def get_author_identity() -> Tuple[str, str]:
return name, email


def get_timestamp_info() -> Tuple[int, str]:
def get_timestamp_info(is_committer: bool = False) -> tuple[int, str]:
"""
Retrieve the current timestamp and timezone offset in Git's format.

Returns:
Tuple[int, str]: Timestamp (seconds since epoch) and timezone offset (e.g., "+0100").
Returns the timestamp and timezone for the commit.
"""
env_date_var = "GIT_COMMITTER_DATE" if is_committer else "GIT_AUTHOR_DATE"
date_str = os.environ.get(env_date_var)

if date_str:
parts = date_str.split(' ')
if len(parts) == 2:
try:
timestamp = int(parts[0])
tz_offset = parts[1]
if len(tz_offset) == 5 and (tz_offset[0] == '+' or tz_offset[0] == '-'):
return timestamp, tz_offset
print(f"[WARN] Format de fuseau horaire non standard pour '{date_str}'. Tentative d'un autre format.")
except ValueError:
print(f"[WARN] Impossible de parser '{parts[0]}' comme un timestamp. Tentative d'un autre format.")

try:
dt = datetime.strptime(date_str, "%a %b %d %H:%M:%S %Y %z")
timestamp = int(dt.timestamp())
tz_offset = date_str.strip().split()[-1]
return timestamp, tz_offset
except ValueError:
raise ValueError(f"Format de date/heure invalide dans GIT_AUTHOR_DATE/GIT_COMMITTER_DATE: '{date_str}'. Formats supportés: 'timestamp timezone_offset' ou 'Jour Mois Jour HH:MM:SS Année DécalageTimeZone'.")

now = datetime.now().astimezone()
timestamp = int(now.timestamp())
timezone = now.strftime('%z')
return timestamp, timezone
tz_offset = now.strftime("%z") # e.g. +0200
return timestamp, tz_offset
65 changes: 0 additions & 65 deletions git_scratch/utils/porcelain_commit.py

This file was deleted.

78 changes: 78 additions & 0 deletions git_scratch/utils/refs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from pathlib import Path
from typing import Optional


class GitError(Exception):
"""Base exception for Git errors."""


class InvalidHeadError(GitError):
"""Raised when HEAD points to an invalid ref or commit, or refers to a missing or invalid object."""


def _is_valid_oid(oid: str) -> bool:
return len(oid) == 40 and all(c in "0123456789abcdef" for c in oid.lower())


def get_head_commit_oid() -> Optional[str]:
head_path = Path(".git") / "HEAD"
if not head_path.exists():
return None

head_content = head_path.read_text().strip()

if head_content.startswith("ref: "):
ref_name = head_content[len("ref: "):]
ref_path = Path(".git") / ref_name

if not ref_path.exists():
return None

ref_oid = ref_path.read_text().strip()

if not _is_valid_oid(ref_oid):
raise InvalidHeadError(f"Ref '{ref_name}' contains an invalid or empty OID: '{ref_oid}'")

return ref_oid
else:
if _is_valid_oid(head_content):
return head_content
raise InvalidHeadError("HEAD contains an invalid commit OID format.")


def update_head_to_commit(new_commit_oid: str):
head_path = Path(".git") / "HEAD"

if not head_path.exists() or not head_path.read_text().strip().startswith("ref: "):
default_branch = "main"
branch_ref_path = Path(".git") / "refs" / "heads" / default_branch

branch_ref_path.parent.mkdir(parents=True, exist_ok=True)

branch_ref_path.write_text(new_commit_oid + "\n")

head_path.write_text(f"ref: refs/heads/{default_branch}\n")
return

head_content = head_path.read_text().strip()
ref_name = head_content[len("ref: "):]
ref_path = Path(".git") / ref_name

ref_path.parent.mkdir(parents=True, exist_ok=True)

ref_path.write_text(new_commit_oid + "\n")

def get_head_display(oid: str) -> str:
"""
Returns a description of the HEAD for display purposes
"""
head_path = Path(".git") / "HEAD"
content = head_path.read_text().strip()

if content.startswith("ref: "):
ref = content[len("ref: "):]
if ref.startswith("refs/heads/"):
return ref[len("refs/heads/"):]
return ref
else:
return f"HEAD detached at {oid[:7]}"
21 changes: 20 additions & 1 deletion git_scratch/utils/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
from typing import List, Dict, Tuple
from git_scratch.utils.object import write_object
from git_scratch.utils.index_utils import load_index


def build_tree(entries: List[Dict], base_path: str = "") -> bytes:
Expand Down Expand Up @@ -33,4 +34,22 @@ def build_tree(entries: List[Dict], base_path: str = "") -> bytes:
mode, oid = tree_entries[name]
result += f"{mode} {name}".encode() + b"\x00" + oid

return result
return result

def create_root_tree_object() -> str:
"""
Loads the index, recursively builds the root Git tree object,
stores it, and returns its OID.
This function orchestrates the creation of the complete tree.
"""
index_entries = load_index()
if not index_entries:
raise ValueError("Index is empty or not found. Nothing to commit.")

# Utilise la fonction build_tree existante pour obtenir le contenu binaire du tree racine pour l'arbre racine, base_path est vide
tree_content_bytes = build_tree(index_entries, base_path="")

# Stocke cet objet tree racine dans le dépôt Git
tree_oid = write_object(tree_content_bytes, "tree")

return tree_oid
Loading
Loading