diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4377c4b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(source .venv/bin/activate)", + "Bash(pytest:*)", + "Bash(gh pr checks:*)", + "Bash(gh issue:*)", + "Bash(git checkout:*)", + "Bash(git push:*)", + "Bash(uvx:*)", + "Bash(gh run view:*)", + "Bash(gh pr ready:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/CHANGES.md b/CHANGES.md index c54e1dc..080d459 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ ## 4.1.2 (unreleased) +- Fix #34: The `offline` configuration setting and `--offline` CLI flag are now properly respected to prevent VCS fetch/update operations. Previously, setting `offline = true` in mx.ini or using the `--offline` CLI flag was ignored, and VCS operations still occurred. + [jensens] + - Fix #46: Git tags in branch option are now correctly detected and handled during updates. Previously, updating from one tag to another failed because tags were incorrectly treated as branches. [jensens] diff --git a/CLAUDE.md b/CLAUDE.md index bd99fca..96a5347 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -519,6 +519,70 @@ Quick summary: **CRITICAL: Always follow these steps before pushing code:** +### Bug Fix Workflow (Test-Driven Development - MANDATORY) + +**IMPORTANT**: All bug fixes MUST follow the TDD (Test-Driven Development) approach: + +1. **Analysis** + - Investigate and understand the root cause + - Identify the exact location and nature of the bug + - Document your findings + +2. **Comment on Issue with Analysis Only** + ```bash + gh issue comment --body "Root cause analysis..." + ``` + - Post detailed root cause analysis to the GitHub issue + - Do NOT include the solution or plan in the comment + - Include code references, line numbers, and explanation + +3. **Create Failing Test** + - Create branch: `git checkout -b fix/-description` + - Write a test that reproduces the bug + - Verify the test fails with the current code + - Commit: `git commit -m "Add failing test for issue #XX"` + +4. **Push and Create Draft PR** + ```bash + git push -u origin fix/-description + gh pr create --draft --title "..." --body "..." + ``` + - PR body should explain the bug, show the failing test, and describe next steps + +5. **Implement the Fix** + - Write the minimal code needed to make the test pass + - Verify the test now passes + - Run all related tests to ensure no regressions + +6. **Commit and Push Fix** + ```bash + git add + git commit -m "Fix issue #XX: description" + git push + ``` + - Include issue reference in commit message + - Update CHANGES.md in the same commit or separately + +7. **Verify CI is Green** + ```bash + gh pr checks + ``` + - Wait for all CI checks to pass + - Address any failures + +8. **Mark PR Ready for Review** + ```bash + gh pr ready + ``` + - Only mark ready when all checks are green + - Update PR description if needed + +**Why TDD for Bug Fixes?** +- Ensures the bug is actually fixed +- Prevents regressions in the future +- Documents the expected behavior +- Provides confidence in the solution + ### Pre-Push Checklist 1. **Always run linting before push** diff --git a/src/mxdev/main.py b/src/mxdev/main.py index fd682a5..bf82040 100644 --- a/src/mxdev/main.py +++ b/src/mxdev/main.py @@ -1,4 +1,5 @@ from .config import Configuration +from .config import to_bool from .hooks import load_hooks from .hooks import read_hooks from .hooks import write_hooks @@ -89,7 +90,9 @@ def main() -> None: read(state) if not args.fetch_only: read_hooks(state, hooks) - if not args.no_fetch: + # Skip fetch if --no-fetch flag is set OR if offline mode is enabled + offline = to_bool(state.configuration.settings.get("offline", False)) + if not args.no_fetch and not offline: fetch(state) if args.fetch_only: return diff --git a/src/mxdev/processing.py b/src/mxdev/processing.py index fcc5f43..e01ac38 100644 --- a/src/mxdev/processing.py +++ b/src/mxdev/processing.py @@ -182,6 +182,8 @@ def read(state: State) -> None: def fetch(state: State) -> None: """Fetch all configured sources from a VCS.""" + from .config import to_bool + packages = state.configuration.packages logger.info("#" * 79) if not packages: @@ -192,13 +194,15 @@ def fetch(state: State) -> None: workingcopies = WorkingCopies( packages, threads=int(state.configuration.settings["threads"]) ) + # Pass offline setting from configuration instead of hardcoding False + offline = to_bool(state.configuration.settings.get("offline", False)) workingcopies.checkout( sorted(packages), verbose=False, update=True, submodules="always", always_accept_server_certificate=True, - offline=False, + offline=offline, ) diff --git a/src/mxdev/vcs/common.py b/src/mxdev/vcs/common.py index c5aeb2d..7f9bebd 100644 --- a/src/mxdev/vcs/common.py +++ b/src/mxdev/vcs/common.py @@ -288,6 +288,11 @@ def status( def update(self, packages: typing.Iterable[str], **kwargs) -> None: the_queue: queue.Queue = queue.Queue() + # Check for offline mode early - skip all updates if offline + offline = kwargs.get("offline", False) + if offline: + logger.info("Skipped updates (offline mode)") + return for name in packages: kw = kwargs.copy() if name not in self.sources: diff --git a/tests/test_git.py b/tests/test_git.py index fd76bcc..d43ede3 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -259,3 +259,52 @@ def test_update_git_tag_to_new_tag(mkgitrepo, src): result = repository.process.check_call(f"git -C {path} describe --tags", echo=False) current_tag = result[0].decode("utf8").strip() assert current_tag == "2.0.0" + + +def test_offline_prevents_vcs_operations(mkgitrepo, src): + """Test that offline mode prevents VCS fetch/update operations. + + This test reproduces issue #34: offline setting should prevent VCS operations + but is currently being ignored. + + When offline=True is set (either in config or via CLI --offline flag), + mxdev should NOT perform any VCS operations (no fetch, no update). + """ + repository = mkgitrepo("repository") + path = src / "egg" + + # Create initial content + repository.add_file("foo", msg="Initial") + + sources = { + "egg": dict( + vcs="git", + name="egg", + url=str(repository.base), + path=str(path), + ) + } + packages = ["egg"] + verbose = False + + # Initial checkout (not offline) + vcs_checkout(sources, packages, verbose, offline=False) + assert {x for x in path.iterdir()} == {path / ".git", path / "foo"} + + # Add new content to remote repository + repository.add_file("bar", msg="Second") + + # Try to update with offline=True + # BUG: This should NOT fetch/update anything, but currently it does + # because offline parameter is ignored + vcs_update(sources, packages, verbose, offline=True) + + # After offline update, should still have only initial content (foo) + # The "bar" file should NOT be present because offline prevented the update + assert {x for x in path.iterdir()} == {path / ".git", path / "foo"} + + # Now update with offline=False to verify update works when not offline + vcs_update(sources, packages, verbose, offline=False) + + # After normal update, should have both files + assert {x for x in path.iterdir()} == {path / ".git", path / "foo", path / "bar"}