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. 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"