From 2e7956e06b8ec7bc6018ae9c0fafd876836d1ea5 Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Thu, 10 Sep 2026 17:09:29 -0400 Subject: [PATCH 01/11] branch & add recieve and send helper function --- minigit/.remote.py.swp | Bin 0 -> 16384 bytes minigit/remote.py | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 minigit/.remote.py.swp diff --git a/minigit/.remote.py.swp b/minigit/.remote.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..d9a134615a7ad414f85eb900271d4ef1236c8139 GIT binary patch literal 16384 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 BM None: + """Send one line of the wire protocol,""" + + sock.sendall((text + "\n").encode()) + + +def receive_line(sock, buf: bytearray) -> str: + """Read one `\\n`-terminated line from `sock`. + + `buf` is the connection's leftover-bytes buffer, owned by the caller and + reused across calls on the same socket: a `recv()` can return two lines + at once (the second one waits here for the next call) or half a line + (we keep reading until the rest arrives). + """ + + while b"\n" not in buf: + chunk = sock.recv(4096) + if not chunk: + raise NetworkProtocolError("connection closed mid-line") + buf.extend(chunk) + + line, _, rest = buf.partition(b"\n") + buf[:] = rest + return line.decode() + + class RemoteClient: """Push and pull commits between two minigit repos over a TCP connection.""" From 375e23012c1671be33142137b65b5f3addd0edba Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Thu, 10 Sep 2026 17:31:59 -0400 Subject: [PATCH 02/11] add init --- minigit/remote.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/minigit/remote.py b/minigit/remote.py index c6652cc..ad1b1d2 100644 --- a/minigit/remote.py +++ b/minigit/remote.py @@ -12,6 +12,7 @@ # from .objects import ObjectStore (Module 1 & 3) # from .commits import CommitManager import os +import socket from minigit.errors import NetworkProtocolError @@ -100,6 +101,21 @@ def pull(self, remote_address: str, branch: str, token: str) -> None: # Week 6 - same exchange in reverse +class RemoteServer: + """Accepts a RemoteClient's AUTH + REF handshake over TCP, one client at a time.""" + + def __init__(self, repo_path=".", token="", host="127.0.0.1", port=0): + self.repo_path = repo_path + self.token = token + self.host = host + + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._sock.bind((host, port)) + self._sock.listen() + self.port = self._sock.getsockname()[1] + + # Wire protocol (draft only - Week 2 makes this real): # One message per line, UTF-8 encoded, terminated with "\n". # From abf7af9c32d30e8ddd08efc44ff12b9897d2734e Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Thu, 10 Sep 2026 23:50:55 -0400 Subject: [PATCH 03/11] add push --- minigit/remote.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/minigit/remote.py b/minigit/remote.py index ad1b1d2..c3b2e0b 100644 --- a/minigit/remote.py +++ b/minigit/remote.py @@ -82,10 +82,27 @@ def push(self, remote_address: str, branch: str, token: str) -> None: 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 + try: + sock = socket.create_connection((host, port), timeout=5) + except OSError as exc: + raise NetworkProtocolError(f"could not connect to {host}:{port}: {exc}") from exc + + try: + buf = bytearray() + send_line(sock, f"AUTH {token}") + reply = receive_line(sock, buf) + if reply != "OK": + raise NetworkProtocolError(f"auth failed: {reply}") + + send_line(sock, f"REF {branch}") + reply = receive_line(sock, buf) + remote_hash = reply.rsplit(" ", 1)[1] + print(f"remote {branch} is at {remote_hash}") + print("# Week 6 - send missing objects, move the ref last") + finally: + sock.close() + # 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 From b5f015c91135f564e7181e064b56bda5505d2771 Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Thu, 10 Sep 2026 23:54:49 -0400 Subject: [PATCH 04/11] serve_forever --- minigit/remote.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/minigit/remote.py b/minigit/remote.py index c3b2e0b..8d063b8 100644 --- a/minigit/remote.py +++ b/minigit/remote.py @@ -132,6 +132,27 @@ def __init__(self, repo_path=".", token="", host="127.0.0.1", port=0): self._sock.listen() self.port = self._sock.getsockname()[1] + def close(self) -> None: + """Stop accepting connections. Unblocks a `serve_forever()` running on another thread.""" + + self._sock.close() + + def serve_forever(self) -> None: + """Accept connections and handle them one at a time until `close()` is called.""" + + while True: + try: + conn, _ = self._sock.accept() + except OSError: + return # listening socket was closed - shut down + + try: + self._handle_client(conn) + except (NetworkProtocolError, OSError): + pass # a bad client must not take down the server + finally: + conn.close() + # Wire protocol (draft only - Week 2 makes this real): # One message per line, UTF-8 encoded, terminated with "\n". From bf87501e98680a19819d065f83db6e97b8df5e10 Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Thu, 10 Sep 2026 23:58:20 -0400 Subject: [PATCH 05/11] finish RemoteServer --- minigit/remote.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/minigit/remote.py b/minigit/remote.py index 8d063b8..fc8d5fc 100644 --- a/minigit/remote.py +++ b/minigit/remote.py @@ -153,6 +153,32 @@ def serve_forever(self) -> None: finally: conn.close() + def _handle_client(self, conn) -> None: + """Run one client's AUTH + REF handshake.""" + + buf = bytearray() + + line = receive_line(conn, buf) + command, _, value = line.partition(" ") + if command != "AUTH" or value != self.token: + send_line(conn, "ERR bad auth") + return + send_line(conn, "OK") + + line = receive_line(conn, buf) + command, _, branch = line.partition(" ") + if command != "REF": + send_line(conn, "ERR expected REF") + return + + ref_path = os.path.join(self.repo_path, ".minigit", "refs", "heads", branch) + if os.path.exists(ref_path): + with open(ref_path) as f: + commit_hash = f.read().strip() + else: + commit_hash = "-" + send_line(conn, f"REF {branch} {commit_hash}") + # Wire protocol (draft only - Week 2 makes this real): # One message per line, UTF-8 encoded, terminated with "\n". From e49c64f285110ede97592f194458694a6e0c5ea8 Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Thu, 10 Sep 2026 23:59:40 -0400 Subject: [PATCH 06/11] add pull --- minigit/remote.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/minigit/remote.py b/minigit/remote.py index fc8d5fc..9407371 100644 --- a/minigit/remote.py +++ b/minigit/remote.py @@ -114,8 +114,26 @@ def pull(self, remote_address: str, branch: str, token: str) -> None: 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 + + try: + sock = socket.create_connection((host, port), timeout=5) + except OSError as exc: + raise NetworkProtocolError(f"could not connect to {host}:{port}: {exc}") from exc + + try: + buf = bytearray() + send_line(sock, f"AUTH {token}") + reply = receive_line(sock, buf) + if reply != "OK": + raise NetworkProtocolError(f"auth failed: {reply}") + + send_line(sock, f"REF {branch}") + reply = receive_line(sock, buf) + remote_hash = reply.rsplit(" ", 1)[1] + print(f"remote {branch} is at {remote_hash}") + print("# Week 6 - same exchange in reverse") + finally: + sock.close() class RemoteServer: From 43388f64decfe7947808cf04000ceb225e4459eb Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Fri, 11 Sep 2026 00:02:47 -0400 Subject: [PATCH 07/11] add serve --- minigit/remote.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/minigit/remote.py b/minigit/remote.py index 9407371..d723ee1 100644 --- a/minigit/remote.py +++ b/minigit/remote.py @@ -210,7 +210,7 @@ def _handle_client(self, conn) -> None: def register_subcommands(subparsers) -> None: - """Register the `push` and `pull` subcommands with the CLI parser.""" + """Register the `push`, `pull`, and `serve` subcommands with the CLI parser.""" push_parser = subparsers.add_parser("push", help="push a branch to a remote") push_parser.add_argument("address") @@ -224,6 +224,11 @@ def register_subcommands(subparsers) -> None: pull_parser.add_argument("--token", default="") pull_parser.set_defaults(handler=cmd_pull) + serve_parser = subparsers.add_parser("serve", help="serve this repo to push/pull clients") + serve_parser.add_argument("--port", type=int, required=True) + serve_parser.add_argument("--token", default="") + serve_parser.set_defaults(handler=cmd_serve) + def cmd_push(args) -> int: """Handle `minigit push` from the CLI.""" @@ -237,3 +242,12 @@ def cmd_pull(args) -> int: RemoteClient().pull(args.address, args.branch, args.token) return 0 + + +def cmd_serve(args) -> int: + """Handle `minigit serve` from the CLI.""" + + server = RemoteServer(port=args.port, token=args.token) + print(f"listening on {server.host}:{server.port}") + server.serve_forever() + return 0 From 4a945d2f931d72c053f3948f4b70f9f5a680e1c5 Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Fri, 11 Sep 2026 00:06:19 -0400 Subject: [PATCH 08/11] add test --- tests/test_remote.py | 52 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/tests/test_remote.py b/tests/test_remote.py index 3b21e1c..1861e81 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,7 +1,11 @@ +import threading + import pytest from minigit.errors import NetworkProtocolError -from minigit.remote import RemoteClient +from minigit.remote import RemoteClient, RemoteServer + +KNOWN_HASH = "a" * 40 class FakeObjectStore: @@ -16,6 +20,22 @@ def make_client(): return RemoteClient(store=FakeObjectStore(), commits=FakeCommitManager()) +@pytest.fixture +def remote_server(tmp_path): + refs_dir = tmp_path / ".minigit" / "refs" / "heads" + refs_dir.mkdir(parents=True) + (refs_dir / "main").write_text(KNOWN_HASH + "\n") + + server = RemoteServer(repo_path=str(tmp_path), token="tok", port=0) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + + yield server + + server.close() + thread.join(timeout=2) + + def test_parse_address_valid(): client = make_client() assert client._parse_address("127.0.0.1:9418") == ("127.0.0.1", 9418) @@ -51,6 +71,32 @@ def test_push_empty_token_raises(): client.push("127.0.0.1:9418", "main", "") -def test_push_valid_address_and_token_does_not_raise(): +def test_push_correct_token_does_not_raise(remote_server): client = make_client() - client.push("127.0.0.1:9418", "main", "sometoken") + client.push(f"127.0.0.1:{remote_server.port}", "main", "tok") + + +def test_push_wrong_token_raises(remote_server): + client = make_client() + with pytest.raises(NetworkProtocolError): + client.push(f"127.0.0.1:{remote_server.port}", "main", "wrong") + + +def test_push_existing_branch_prints_matching_hash(remote_server, capsys): + client = make_client() + client.push(f"127.0.0.1:{remote_server.port}", "main", "tok") + assert KNOWN_HASH in capsys.readouterr().out + + +def test_push_missing_branch_prints_dash(remote_server, capsys): + client = make_client() + client.push(f"127.0.0.1:{remote_server.port}", "nope", "tok") + assert "is at -" in capsys.readouterr().out + + +def test_push_closed_port_raises_network_protocol_error(remote_server): + port = remote_server.port + remote_server.close() + client = make_client() + with pytest.raises(NetworkProtocolError): + client.push(f"127.0.0.1:{port}", "main", "tok") From 4514414767128f9a16bf18d6c4da928b9b7db0d8 Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Fri, 11 Sep 2026 00:10:11 -0400 Subject: [PATCH 09/11] last few tweaks --- tests/test_remote.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_remote.py b/tests/test_remote.py index 1861e81..673042a 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -3,7 +3,7 @@ import pytest from minigit.errors import NetworkProtocolError -from minigit.remote import RemoteClient, RemoteServer +from minigit.remote import RemoteClient, RemoteServer, receive_line KNOWN_HASH = "a" * 40 @@ -100,3 +100,21 @@ def test_push_closed_port_raises_network_protocol_error(remote_server): client = make_client() with pytest.raises(NetworkProtocolError): client.push(f"127.0.0.1:{port}", "main", "tok") + + +class FakeSocket: + """Hands back pre-scripted chunks instead of reading a real socket.""" + + def __init__(self, chunks): + self._chunks = list(chunks) + + def recv(self, size): + return self._chunks.pop(0) if self._chunks else b"" + + +def test_receive_line_splits_two_lines_from_one_packet(): + sock = FakeSocket([b"AUTH tok\nREF main\n"]) + buf = bytearray() + + assert receive_line(sock, buf) == "AUTH tok" + assert receive_line(sock, buf) == "REF main" From e7f8dc143530026171508af8f8b23d1600a1846f Mon Sep 17 00:00:00 2001 From: Shuhan6017 Date: Fri, 11 Sep 2026 10:07:35 -0400 Subject: [PATCH 10/11] Fix flaky server shutdown --- minigit/remote.py | 12 ++++++++++-- tests/test_remote.py | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/minigit/remote.py b/minigit/remote.py index d723ee1..ad80ff6 100644 --- a/minigit/remote.py +++ b/minigit/remote.py @@ -148,19 +148,27 @@ def __init__(self, repo_path=".", token="", host="127.0.0.1", port=0): self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._sock.bind((host, port)) self._sock.listen() + # accept() polls this instead of blocking forever: closing the socket + # from another thread isn't guaranteed to unblock a pending accept() + # on every platform (it does on macOS, not reliably on Linux). + self._sock.settimeout(0.5) self.port = self._sock.getsockname()[1] + self._closed = False def close(self) -> None: - """Stop accepting connections. Unblocks a `serve_forever()` running on another thread.""" + """Stop accepting connections. `serve_forever()` notices within one poll interval.""" + self._closed = True self._sock.close() def serve_forever(self) -> None: """Accept connections and handle them one at a time until `close()` is called.""" - while True: + while not self._closed: try: conn, _ = self._sock.accept() + except TimeoutError: + continue # no connection yet - check self._closed and try again except OSError: return # listening socket was closed - shut down diff --git a/tests/test_remote.py b/tests/test_remote.py index 673042a..227aa98 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -29,6 +29,7 @@ def remote_server(tmp_path): server = RemoteServer(repo_path=str(tmp_path), token="tok", port=0) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() + server._thread = thread # test-only: lets a test wait for a manual close() to land yield server @@ -97,6 +98,7 @@ def test_push_missing_branch_prints_dash(remote_server, capsys): def test_push_closed_port_raises_network_protocol_error(remote_server): port = remote_server.port remote_server.close() + remote_server._thread.join(timeout=2) # wait for serve_forever() to actually exit client = make_client() with pytest.raises(NetworkProtocolError): client.push(f"127.0.0.1:{port}", "main", "tok") From 5336a9fffe501948d0e3e9a307cc5529ccad7f18 Mon Sep 17 00:00:00 2001 From: aman shah Date: Fri, 11 Sep 2026 10:45:56 -0400 Subject: [PATCH 11/11] Remove editor swap file --- minigit/.remote.py.swp | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 minigit/.remote.py.swp diff --git a/minigit/.remote.py.swp b/minigit/.remote.py.swp deleted file mode 100644 index d9a134615a7ad414f85eb900271d4ef1236c8139..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 BM