From 9fa8e3c6764529b3d870cd65a70976a1fab370fa Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Wed, 22 Oct 2025 12:34:53 +0200 Subject: [PATCH 1/2] Add failing test for issue #46: Git tag update regression This test demonstrates that updating from one Git tag to another fails in v4.x (worked in v2.x). Steps the test follows: 1. Create a repo with content at tag 1.0.0 2. Add more content and create tag 2.0.0 3. Initial checkout with branch=1.0.0 (works) 4. Update config to branch=2.0.0 5. Run update (FAILS - exits with 'No such branch 2.0.0') The test currently fails because git_update() treats tags as branches, trying to find them in 'git branch -a' output where they don't appear. Related to #46 --- tests/test_git.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_git.py b/tests/test_git.py index 1526a73..fd76bcc 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -202,3 +202,60 @@ def test_update_verbose(mkgitrepo, src, capsys): assert "Already up to date" in captured.out.replace("-", " ") status = vcs_status(sources, verbose=True) assert status == {"egg": ("clean", "## master...origin/master\n")} + + +def test_update_git_tag_to_new_tag(mkgitrepo, src): + """Test that updating from one git tag to another works correctly. + + This test reproduces issue #46: changing the branch option from one tag + to another tag should update the checkout to the new tag. + + Regression in v4.x - worked in v2.x + """ + repository = mkgitrepo("repository") + # Create initial content and tag it as 1.0.0 + repository.add_file("foo", msg="Initial") + repository("git tag 1.0.0", echo=False) + + # Create more content and tag as 2.0.0 + repository.add_file("bar", msg="Second") + repository("git tag 2.0.0", echo=False) + + path = src / "egg" + + # Initial checkout with tag 1.0.0 + sources = { + "egg": dict( + vcs="git", + name="egg", + branch="1.0.0", # This is actually a tag, not a branch + url=str(repository.base), + path=str(path), + ) + } + packages = ["egg"] + verbose = False + + # Checkout at tag 1.0.0 + vcs_checkout(sources, packages, verbose) + assert {x for x in path.iterdir()} == {path / ".git", path / "foo"} + + # Verify we're at tag 1.0.0 + result = repository.process.check_call(f"git -C {path} describe --tags", echo=False) + current_tag = result[0].decode("utf8").strip() + assert current_tag == "1.0.0" + + # Now update the sources to use tag 2.0.0 + sources["egg"]["branch"] = "2.0.0" + + # Update should switch to tag 2.0.0 + # BUG: This will fail because the code treats tags as branches + vcs_update(sources, packages, verbose) + + # After update, we should have both foo and bar (tag 2.0.0) + assert {x for x in path.iterdir()} == {path / ".git", path / "foo", path / "bar"} + + # Verify we're now at tag 2.0.0 + 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" From 7895ce17aa4c7971a9e3dcda871003c9c6b0b711 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Wed, 22 Oct 2025 12:38:59 +0200 Subject: [PATCH 2/2] Fix issue #46: Add tag detection for Git updates Add git_is_tag() method to detect if a branch value is actually a tag, and handle tags correctly during git_update(): - Tags are checked out directly with 'git checkout ' - Branches use the existing switch + merge logic - Explicitly fetch tags with 'git fetch --tags' This fixes the regression where updating from one tag to another stopped working in v4.x (worked in v2.x). The test now passes, demonstrating that tag updates work correctly. Fixes #46 --- CHANGES.md | 3 +++ src/mxdev/vcs/git.py | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4af5799..c54e1dc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ ## 4.1.2 (unreleased) +- 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] + - Fix #53: Per-package target setting now correctly overrides default-target when constructing checkout paths. [jensens] diff --git a/src/mxdev/vcs/git.py b/src/mxdev/vcs/git.py index 8a2fc97..72c9703 100644 --- a/src/mxdev/vcs/git.py +++ b/src/mxdev/vcs/git.py @@ -231,12 +231,25 @@ def git_switch_branch( raise GitError(f"git checkout of branch '{branch}' failed.\n{stderr}") return (stdout_in + stdout, stderr_in + stderr) + def git_is_tag(self, tag_name: str) -> bool: + """Check if the given name is a git tag. + + Returns True if tag_name exists as a tag in the repository. + """ + path = self.source["path"] + cmd = self.run_git(["tag", "-l", tag_name], cwd=path) + stdout, stderr = cmd.communicate() + if cmd.returncode != 0: + return False + # git tag -l returns the tag name if it exists, empty if not + return tag_name in stdout.strip().split("\n") + def git_update(self, **kwargs) -> typing.Union[str, None]: name = self.source["name"] path = self.source["path"] self.output((logger.info, "Updated '%s' with git." % name)) # First we fetch. This should always be possible. - argv = ["fetch"] + argv = ["fetch", "--tags"] # Also fetch tags explicitly update_git_submodules = self.source.get("submodules", kwargs["submodules"]) if update_git_submodules == "recursive": argv.append("--recurse-submodules") @@ -247,8 +260,23 @@ def git_update(self, **kwargs) -> typing.Union[str, None]: if "rev" in self.source: stdout, stderr = self.git_switch_branch(stdout, stderr) elif "branch" in self.source: - stdout, stderr = self.git_switch_branch(stdout, stderr) - stdout, stderr = self.git_merge_rbranch(stdout, stderr) + # Check if 'branch' is actually a tag (#46) + branch_value = self.source["branch"] + if self.git_is_tag(branch_value): + # It's a tag - checkout directly without merge + cmd = self.run_git(["checkout", branch_value], cwd=path) + tag_stdout, tag_stderr = cmd.communicate() + if cmd.returncode != 0: + raise GitError( + f"git checkout of tag '{branch_value}' failed.\n{tag_stderr}" + ) + stdout += tag_stdout + stderr += tag_stderr + self.output((logger.info, f"Switched to tag '{branch_value}'.")) + else: + # It's a branch - use normal branch switch + merge + stdout, stderr = self.git_switch_branch(stdout, stderr) + stdout, stderr = self.git_merge_rbranch(stdout, stderr) else: # We may have specified a branch previously but not # anymore. In that case, we want to revert to master.