Skip to content
Merged
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
14 changes: 14 additions & 0 deletions gcode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,22 @@ def main() -> None:
parser.add_argument("--session", default=DEFAULT_SESSION, help="Named session for history.")
parser.add_argument("--yes", action="store_true", help="Auto-approve bash commands (unsafe).")
parser.add_argument("--version", action="version", version=f"gcode {__version__}")
parser.add_argument(
"--cwd",
metavar="DIR",
help="Run as if started in DIR (like git -C), instead of the current directory.",
)
args = parser.parse_args()

# Before anything reads the working directory. .gcoderc discovery, the
# banner and every tool resolve paths against it, so changing it later
# would leave them disagreeing about which project this session is for.
if args.cwd is not None:
try:
os.chdir(args.cwd)
except OSError as exc:
parser.error(f"--cwd {args.cwd!r} is not usable: {exc.strerror}")

# Load ~/.gcode/.env first (setup module's config location)
load_env()

Expand Down
108 changes: 108 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest.mock import Mock, patch

import pytest
from gcode import __version__
from gcode.cli import _cmd_diff, _cmd_status, _cmd_version, _print_help

Expand Down Expand Up @@ -112,3 +113,110 @@ def test_report_no_models_neither_source():

assert "No model source is reachable" in ui.error.call_args.args[0]
assert "Ollama" in ui.info.call_args.args[0]


def test_help_lists_cwd_flag(capsys):

from gcode.cli import main

with patch("sys.argv", ["gcode", "--help"]), pytest.raises(SystemExit) as exc:
main()

assert exc.value.code == 0
assert "--cwd" in capsys.readouterr().out


class _Sentinel(Exception):
"""Raised from the first call after the chdir, to stop main() there."""


def test_cwd_changes_directory_before_config_is_loaded(tmp_path, monkeypatch):
"""The chdir must land before anything reads the working directory.

.gcoderc discovery, the banner and every tool resolve against it, so a
chdir performed later would leave them describing different projects.
load_env is the first call after it, so raising there proves the ordering
without running the interactive session.
"""
import os

from gcode.cli import main

start = os.getcwd()
seen = {}

def _record():
seen["cwd"] = os.getcwd()
raise _Sentinel

monkeypatch.setattr("gcode.cli.load_env", _record)
monkeypatch.setattr("sys.argv", ["gcode", "--cwd", str(tmp_path)])
try:
with pytest.raises(_Sentinel):
main()
finally:
os.chdir(start)

assert os.path.realpath(seen["cwd"]) == os.path.realpath(str(tmp_path))


def test_without_cwd_the_directory_is_untouched(monkeypatch):
import os

from gcode.cli import main

start = os.getcwd()
seen = {}

def _record():
seen["cwd"] = os.getcwd()
raise _Sentinel

monkeypatch.setattr("gcode.cli.load_env", _record)
monkeypatch.setattr("sys.argv", ["gcode"])
try:
with pytest.raises(_Sentinel):
main()
finally:
os.chdir(start)

assert seen["cwd"] == start


def test_cwd_that_does_not_exist_exits_two(tmp_path, capsys, monkeypatch):
"""A bad --cwd is user input: one line on stderr, exit 2, no traceback."""
import os

from gcode.cli import main

missing = tmp_path / "no-such-project"
start = os.getcwd()
monkeypatch.setattr("sys.argv", ["gcode", "--cwd", str(missing)])
try:
with pytest.raises(SystemExit) as exc:
main()
finally:
os.chdir(start)

assert exc.value.code == 2
stderr = capsys.readouterr().err
assert "--cwd" in stderr
assert "Traceback" not in stderr


def test_cwd_pointing_at_a_file_exits_two(tmp_path, capsys, monkeypatch):
import os

from gcode.cli import main

target = tmp_path / "notadir.txt"
target.write_text("x\n")
start = os.getcwd()
monkeypatch.setattr("sys.argv", ["gcode", "--cwd", str(target)])
try:
with pytest.raises(SystemExit) as exc:
main()
finally:
os.chdir(start)

assert exc.value.code == 2