Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
12 changes: 8 additions & 4 deletions minigit/cli.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,9 +11,11 @@
import sys
from collections.abc import Sequence

from minigit import __version__
from minigit import __version__, commits
from minigit.errors import MiniGitError
from minigit.objects import register_subcommands
from minigit.index import register_index_commands
from minigit.objects import register_subcommands as register_object_commands
from minigit.remote import register_subcommands as register_remote_commands


def _register_commands(subparsers) -> None:
Expand All@@ -27,8 +29,10 @@ def register_subcommands(subparsers):
parser.add_argument("path")
parser.set_defaults(handler=cmd_add)
"""

register_subcommands(subparsers)
register_object_commands(subparsers)
register_index_commands(subparsers)
commits.register_subcommands(subparsers)
register_remote_commands(subparsers)


def build_parser() -> argparse.ArgumentParser:
Expand Down
107 changes: 107 additions & 0 deletions minigit/commits.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,3 +8,110 @@

Build the `CommitManager` class here, per the interface contract.
"""

import os
import time

from minigit.errors import RefNotFoundError
from minigit.index import WorkingTree
from minigit.objects import ObjectStore


def _cmd_commit(args) -> int:
"""Handle the 'minigit commit -m <message>' command"""
manager = CommitManager()
stub_tree = "0" * 40
manager.create_commit(
stub_tree,
[],
"Daniel <daniel@example.com>",
args.message,
)
return 0


def _cmd_branch(args) -> int:
"""Handle 'minigit branch <name>' command"""
manager = CommitManager()
if args.name:
manager.create_branch(args.name, "0" * 40)
else:
for branch in manager.list_branches():
if branch == manager._head:
marker = "* "
else:
marker = " "
print(f"{marker}{branch}")
return 0


def _cmd_checkout(args) -> int:
"""
Handle 'minigit checkout <name> command'
"""
manager = CommitManager()
manager.switch_branch(args.name)
print(f"Switched to branch '{args.name}'")
return 0


def register_subcommands(subparsers) -> None:
"""
Register the three commands: commit, branch, checkout
"""
commit_parser = subparsers.add_parser("commit", help="record changes to the repository")
commit_parser.add_argument("-m", dest="message", required=True, help="commit message")
commit_parser.set_defaults(handler=_cmd_commit)

branch_parser = subparsers.add_parser("branch", help="create or list branches")
branch_parser.add_argument("name", nargs="?", help="branch name to create")
branch_parser.set_defaults(handler=_cmd_branch)

checkout_parser = subparsers.add_parser("checkout", help="switch branches")
checkout_parser.add_argument("name", help="branch name to switch to")
checkout_parser.set_defaults(handler=_cmd_checkout)


class CommitManager:
def __init__(self, repo_path=".", store=None, tree=None):
self.root = os.path.abspath(repo_path)
self._refs = {}
self._head = "main"
self.store = store if store is not None else ObjectStore(repo_path)
self.tree = tree if tree is not None else WorkingTree(repo_path)

def _format_commit(self, tree_hash, parents, author, message) -> str:
"""Format commit object as a string"""
lines = []
timestamp = int(time.time())
lines.append(f"tree {tree_hash}")
for parent in parents:
lines.append(f"parent {parent}")
lines.append(f"author {author} {timestamp}")
lines.append(f"committer {author} {timestamp}")
lines.append("")
lines.append(message)
return "\n".join(lines)

def create_commit(self, tree_hash, parents: list[str], author, message) -> str:
"""
Create a new commit object and write it to the object store
"""
body = self._format_commit(tree_hash, parents, author, message)
return self.store.write_object(body.encode(), "commit")

def create_branch(self, name, commit_hash) -> None:
"""Create a new branch that points at commit_hash"""
self._refs[name] = commit_hash

def switch_branch(self, name) -> None:
"""Switch to a branch"""
if name not in self._refs:
raise RefNotFoundError(name)
self._head = name

def list_branches(self) -> list[str]:
return sorted(self._refs)

def merge(self, branch_name) -> str | None:
return None
78 changes: 78 additions & 0 deletions minigit/index.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,3 +8,81 @@

Build the `WorkingTree` class here, per the interface contract.
"""

from dataclasses import dataclass
from typing import NamedTuple


class IndexEntry(NamedTuple):
mode: str
hash: str
path: str


@dataclass
class DiffResult:
added: list
deleted: list
modified: list


class WorkingTree:
def __init__(self, repo_path=".", store=None):
self.root = repo_path
self.index_path = f"{repo_path}/.minigit/index"

if store is None:
from minigit.objects import ObjectStore

store = ObjectStore(repo_path)

self.store = store
self._entries: list[IndexEntry] = []

def read_index(self) -> list[IndexEntry]:
return sorted(self._entries, key=lambda e: e.path)

def write_index(self, entries) -> None:
self._entries = sorted(entries, key=lambda e: e.path)

def stage_file(self, path) -> None:
with open(f"{self.root}/{path}", "rb") as f:
data = f.read()

blob_hash = self.store.write_object(data, "blob")

self._entries = [e for e in self._entries if e.path != path]

self._entries.append(IndexEntry("100644", blob_hash, path))
self._entries.sort(key=lambda e: e.path)

def build_tree_from_index(self) -> str:
return self.store.write_object(b"", "tree")

def diff_working_tree_vs(self, tree_hash) -> DiffResult:
return DiffResult([], [], [])

def checkout(self, tree_hash) -> None:
pass


def cmd_add(args) -> int:
wt = WorkingTree()
wt.stage_file(args.path)
return 0


def cmd_status(args) -> int:
wt = WorkingTree()
for entry in wt.read_index():
print(entry.path)
return 0


def register_index_commands(subparsers) -> None:
add_parser = subparsers.add_parser("add", help="stage a file")
add_parser.add_argument("path")
add_parser.set_defaults(handler=cmd_add)

status_parser = subparsers.add_parser("status", help="show staged files")
status_parser.set_defaults(handler=cmd_status)
105 changes: 105 additions & 0 deletions minigit/remote.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,3 +8,108 @@

Build the `RemoteClient` class here, per the interface contract.
"""

# from .objects import ObjectStore (Module 1 & 3)
# from .commits import CommitManager
import os

from minigit.errors import NetworkProtocolError


class RemoteClient:
"""Push and pull commits between two minigit repos over a TCP connection."""

def __init__(self, repo_path=".", store=None, commits=None):

self.repo_path = repo_path
self.config_path = os.path.join(self.repo_path, ".minigit", "config")
self.store = store
self.commits = commits
# Below is correct but need name of function within module 1 & 3
# self.store = store if store is not None else ObjectStore(self.repo_path)
# self.commits = commits if commits is not None else CommitManager(self.repo_path)

def _parse_address(self, address: str) -> tuple[str, int]:
"""split a string address by host part(string) and the port part(integer)"""

parts = address.rsplit(":", 1)
if len(parts) != 2:
raise NetworkProtocolError(f"address must be host:port, got {address!r}")

host, port = parts
if host and port:
if port.isnumeric():
int_port = int(port)
if 1 <= int_port <= 65535:
return host, int_port
else:
raise NetworkProtocolError(f"port out of range 1-65535: {port}")
else:
raise NetworkProtocolError(f"port is not a number: {port!r}")
else:
raise NetworkProtocolError(f"address must be host:port, got {address!r}")

def push(self, remote_address: str, branch: str, token: str) -> None:
"""Send local commits on `branch` to the remote, rejecting if it has diverged."""

host, port = self._parse_address(remote_address)
if len(token) == 0:
raise NetworkProtocolError("push needs a token: pass --token")
print(f"push: would push {branch} to {host}:{port}")

# connect over TCP
# ask remote for its current hash for <branch>
# remote hash not an ancestor of local -> someone else pushed first -> NetworkProtocolError
# walk local commit graph from remote's hash up to local -> collect reachable objects
# send only the missing objects
# move the remote ref LAST, only after every object arrived

def pull(self, remote_address: str, branch: str, token: str) -> None:
"""Fetch `branch` from the remote and update the matching local ref."""

host, port = self._parse_address(remote_address)
if len(token) == 0:
raise NetworkProtocolError("pull needs a token: pass --token")
print(f"pull: would pull {branch} from {host}:{port}")
# Week 6 - same exchange in reverse


# Wire protocol (draft only - Week 2 makes this real):
# One message per line, UTF-8 encoded, terminated with "\n".
#
# AUTH <token> - client authenticates the connection with its token
# REF <branch> - ask for / report the commit hash a branch currently points to
# WANT <hash> - request the object with this hash
# OBJ <type> <len> - announces an object is coming next: its type and byte length
# DONE - no more messages from this side
# ERR - something went wrong


def register_subcommands(subparsers) -> None:
"""Register the `push` and `pull` subcommands with the CLI parser."""

push_parser = subparsers.add_parser("push", help="push a branch to a remote")
push_parser.add_argument("address")
push_parser.add_argument("branch")
push_parser.add_argument("--token", default="")
push_parser.set_defaults(handler=cmd_push)

pull_parser = subparsers.add_parser("pull", help="pull a branch to a local")
pull_parser.add_argument("address")
pull_parser.add_argument("branch")
pull_parser.add_argument("--token", default="")
pull_parser.set_defaults(handler=cmd_pull)


def cmd_push(args) -> int:
"""Handle `minigit push` from the CLI."""

RemoteClient().push(args.address, args.branch, args.token)
return 0


def cmd_pull(args) -> int:
"""Handle `minigit pull` from the CLI."""

RemoteClient().pull(args.address, args.branch, args.token)
return 0
Loading
Loading