Coverage for src/rhiza_tools/commands/bump/git.py: 100%

48 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-30 13:37 +0000

1"""Git operations specific to the bump command. 

2 

3This module wraps the git plumbing that ``bump_command`` uses: switching to a 

4target branch before bumping, pushing the resulting commit to the remote, and 

5restoring the original branch afterwards. Keeping these side-effecting git calls 

6separate from the pure orchestration in ``bump.py`` makes each piece easier to 

7test in isolation. 

8 

9All symbols defined here are re-exported by ``bump.py`` so the public import 

10surface is unchanged. 

11""" 

12 

13from __future__ import annotations 

14 

15import questionary as qs 

16import typer 

17from loguru import logger 

18 

19from rhiza_tools import console 

20from rhiza_tools.commands._shared import ( 

21 COOL_STYLE, 

22 NON_INTERACTIVE_ERRORS, 

23 run_git_command, 

24) 

25 

26 

27def _handle_branch_checkout(branch: str | None, dry_run: bool) -> str | None: 

28 """Handle branch checkout if specified. 

29 

30 Args: 

31 branch: Branch to checkout, or None. 

32 dry_run: If True, only simulate checkout. 

33 

34 Returns: 

35 Original branch name if we switched, None otherwise. 

36 

37 Raises: 

38 typer.Exit: If checkout fails. 

39 """ 

40 if not branch: 

41 return None 

42 

43 # Get current branch 

44 result = run_git_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], check=False) 

45 if result.returncode != 0: 

46 return None 

47 

48 current_branch = result.stdout.strip() 

49 if current_branch == branch: 

50 return None 

51 

52 console.info(f"Switching from {current_branch} to {branch}") 

53 if not dry_run: 

54 result = run_git_command(["git", "checkout", branch], check=False) 

55 if result.returncode != 0: 

56 console.error(f"Failed to checkout branch {branch}: {result.stderr}") 

57 console.error(f"Ensure the branch '{branch}' exists: git branch -a") 

58 raise typer.Exit(code=1) 

59 else: 

60 console.info(f"[DRY-RUN] Would checkout branch {branch}") 

61 

62 return current_branch 

63 

64 

65def _handle_push_to_remote(version: str | None) -> None: 

66 """Handle pushing changes to remote. 

67 

68 Args: 

69 version: Version argument (None means interactive mode). 

70 

71 Raises: 

72 typer.Exit: If push fails. 

73 """ 

74 # Interactive prompt if not in non-interactive mode and version was not specified 

75 if not version: 

76 try: 

77 if not qs.confirm("Push changes to remote?", default=False, style=COOL_STYLE).ask(): 

78 console.info("Push cancelled by user") 

79 return 

80 except NON_INTERACTIVE_ERRORS: 

81 # In testing or non-interactive environment, do not proceed with push 

82 console.info("Push cancelled - non-interactive environment detected") 

83 logger.debug("Running in non-interactive environment, skipping push") 

84 return 

85 

86 console.info("Pushing changes to remote...") 

87 result = run_git_command(["git", "push"], check=False) 

88 if result.returncode == 0: 

89 console.success("Changes pushed to remote successfully!") 

90 else: 

91 console.error(f"Failed to push changes: {result.stderr}") 

92 console.error("The version bump has been applied locally but could not be pushed.") 

93 console.error("To recover:") 

94 console.error(" Push manually: git push") 

95 console.error(" Or undo bump: git reset --hard HEAD~1") 

96 raise typer.Exit(code=1) 

97 

98 

99def _restore_original_branch(original_branch: str | None, dry_run: bool) -> None: 

100 """Restore original branch if we switched. 

101 

102 Args: 

103 original_branch: Original branch to restore, or None. 

104 dry_run: If True, don't actually restore (since we didn't actually switch). 

105 """ 

106 if original_branch and not dry_run: 

107 console.info(f"Returning to original branch {original_branch}") 

108 run_git_command(["git", "checkout", original_branch], check=False)