From 7d80c2ddc6a12382a525c3ceb76db7231ad061bc Mon Sep 17 00:00:00 2001 From: Erez Date: Wed, 17 Dec 2025 07:56:01 +0700 Subject: [PATCH 1/2] v2.1.1 --- README.md | 5 +- README.rst | 3 +- docs/index.rst | 3 +- gitlabber/cli.py | 12 +- gitlabber/config.py | 1 + gitlabber/git.py | 28 +++- gitlabber/gitlab_tree.py | 6 +- pyproject.toml | 2 +- tests/test_git.py | 282 ++++++++++++++++++++++++++++++++++++++- tests/test_helpers.py | 10 +- 10 files changed, 335 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b0c7646..1c89da4 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,8 @@ options: number of concurrent git operations (default: 1) --api-concurrency N number of concurrent API calls for tree building (default: 5) -r, --recursive clone/pull git submodules recursively --F, --use-fetch clone/fetch git repository (mirrored repositories) +-F, --use-fetch use git fetch instead of pull for updates (normal repositories with working tree) +--mirror create bare mirror repositories (for backups, automatically uses fetch) -s, --include-shared include shared projects in the results -g term, --group-search term only include groups matching the search term, filtering done at the API level @@ -202,8 +203,6 @@ gitlabber --store-token -u https://gitlab.com # Use stored token (no -t flag needed) gitlabber -u https://gitlab.com . ``` -<|tool▁call▁begin|> -run_terminal_cmd ## Common Use Cases diff --git a/README.rst b/README.rst index e1337b3..8c9a29f 100644 --- a/README.rst +++ b/README.rst @@ -189,7 +189,8 @@ Usage number of concurrent git operations (default: 1) --api-concurrency N number of concurrent API calls for tree building (default: 5) -r, --recursive clone/pull git submodules recursively - -F, --use-fetch clone/fetch git repository (mirrored repositories) + -F, --use-fetch use git fetch instead of pull for updates (normal repositories with working tree) + --mirror create bare mirror repositories (for backups, automatically uses fetch) -s, --include-shared include shared projects in the results -g term, --group-search term only include groups matching the search term, filtering done at the API level (useful for large projects, see: https://docs.gitlab.com/ee/api/groups.html#search-for-group works with partial names of path or name) diff --git a/docs/index.rst b/docs/index.rst index e1337b3..8c9a29f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -189,7 +189,8 @@ Usage number of concurrent git operations (default: 1) --api-concurrency N number of concurrent API calls for tree building (default: 5) -r, --recursive clone/pull git submodules recursively - -F, --use-fetch clone/fetch git repository (mirrored repositories) + -F, --use-fetch use git fetch instead of pull for updates (normal repositories with working tree) + --mirror create bare mirror repositories (for backups, automatically uses fetch) -s, --include-shared include shared projects in the results -g term, --group-search term only include groups matching the search term, filtering done at the API level (useful for large projects, see: https://docs.gitlab.com/ee/api/groups.html#search-for-group works with partial names of path or name) diff --git a/gitlabber/cli.py b/gitlabber/cli.py index d5e2b79..6fdb044 100644 --- a/gitlabber/cli.py +++ b/gitlabber/cli.py @@ -211,6 +211,7 @@ def run_gitlabber( exclude: Optional[str], recursive: bool, use_fetch: bool, + mirror: bool, include_shared: bool, group_search: Optional[str], user_projects: bool, @@ -244,6 +245,7 @@ def run_gitlabber( exclude: Comma-separated glob patterns to exclude recursive: Clone submodules recursively use_fetch: Use git fetch instead of pull + mirror: Create bare mirror repositories (implies use_fetch) include_shared: Include shared projects group_search: Search term for filtering groups at API level user_projects: Fetch only user personal projects @@ -295,6 +297,7 @@ def run_gitlabber( "recursive": recursive, "include_shared": include_shared, "use_fetch": use_fetch, + "mirror": mirror, "hide_token": hide_token, "user_projects": user_projects, "group_search": group_search, @@ -318,6 +321,7 @@ def run_gitlabber( disable_progress=verbose, include_shared=include_shared, use_fetch=use_fetch, + mirror=mirror, hide_token=hide_token, user_projects=user_projects, group_search=group_search, @@ -458,7 +462,7 @@ def cli( False, "-F", "--use-fetch", - help="Use git fetch instead of pull (mirrored repositories)", + help="Use git fetch instead of pull for updates (normal repositories with working tree)", ), exclude_shared: bool = typer.Option( False, @@ -495,6 +499,11 @@ def cli( "--store-token", help="Store token securely in OS keyring (requires keyring package)", ), + mirror: bool = typer.Option( + False, + "--mirror", + help="Create bare mirror repositories (for backups, automatically uses fetch)", + ), ) -> None: """Main CLI command for gitlabber. @@ -560,6 +569,7 @@ def cli( exclude=exclude, recursive=recursive, use_fetch=use_fetch, + mirror=mirror, include_shared=include_shared_value, group_search=group_search, user_projects=user_projects, diff --git a/gitlabber/config.py b/gitlabber/config.py index c5a8cef..50ecec1 100644 --- a/gitlabber/config.py +++ b/gitlabber/config.py @@ -80,6 +80,7 @@ class GitlabberConfig(BaseModel): disable_progress: bool = False include_shared: bool = True use_fetch: bool = False + mirror: bool = False hide_token: bool = False user_projects: bool = False group_search: Optional[str] = None diff --git a/gitlabber/git.py b/gitlabber/git.py index 1c009b3..902a7ec 100644 --- a/gitlabber/git.py +++ b/gitlabber/git.py @@ -26,6 +26,7 @@ class GitAction: use_fetch: bool = False hide_token: bool = False git_options: Optional[str] = None + mirror: bool = False # New parameter class GitRepository: @@ -68,7 +69,7 @@ def clone(action: GitAction, progress_bar: ProgressBar) -> None: multi_options: list[str] = [] if action.recursive: multi_options.append('--recursive') - if action.use_fetch: + if action.mirror: # New parameter multi_options.append('--mirror') if action.git_options: multi_options += action.git_options.split(',') @@ -132,13 +133,15 @@ def pull(action: GitAction, progress_bar: ProgressBar, repo=None) -> None: GitlabberGitError: If pull operation fails """ log.debug("updating existing project %s", action.path) - operation = 'fetching' if action.use_fetch else 'pulling' + # Mirror implies use_fetch, so check both + should_fetch = action.use_fetch or action.mirror + operation = 'fetching' if should_fetch else 'pulling' progress_bar.show_progress_detailed(action.node.name, 'project', operation) try: if repo is None: repo = git.Repo(action.path) - if not action.use_fetch: + if not should_fetch: repo.remotes.origin.pull() else: repo.remotes.origin.fetch() @@ -226,6 +229,7 @@ def __init__( dest: str, recursive: bool = False, use_fetch: bool = False, + mirror: bool = False, hide_token: bool = False, git_options: Optional[str] = None ): @@ -235,12 +239,14 @@ def __init__( dest: Destination directory for repositories recursive: Whether to clone recursively use_fetch: Whether to use git fetch instead of pull + mirror: Whether to create bare mirror repositories hide_token: Whether to hide token in URLs git_options: Additional git options as comma-separated string """ self.dest = Path(dest) self.recursive = recursive self.use_fetch = use_fetch + self.mirror = mirror self.hide_token = hide_token self.git_options = git_options @@ -272,9 +278,11 @@ def _collect_from_node(self, node: Node, actions: list[GitAction]) -> None: path_str = str(path) if child.is_leaf: + # Mirror implies use_fetch (mirrors should always use fetch) + effective_use_fetch = self.use_fetch or self.mirror actions.append(GitAction( child, path_str, self.recursive, - self.use_fetch, self.hide_token, self.git_options + effective_use_fetch, self.hide_token, self.git_options, self.mirror )) if not child.is_leaf: @@ -307,6 +315,7 @@ def sync( dest: str, recursive: bool = False, use_fetch: bool = False, + mirror: bool = False, hide_token: bool = False, git_options: Optional[str] = None ) -> None: @@ -317,6 +326,7 @@ def sync( dest: Destination directory recursive: Whether to clone recursively use_fetch: Whether to use git fetch instead of pull + mirror: Whether to create bare mirror repositories hide_token: Whether to hide token in URLs git_options: Additional git options as comma-separated string """ @@ -324,7 +334,7 @@ def sync( self.progress_bar.init_progress(len(root.leaves)) collector = GitActionCollector( - dest, recursive, use_fetch, hide_token, git_options + dest, recursive, use_fetch, mirror, hide_token, git_options ) actions = collector.collect(root) @@ -343,6 +353,7 @@ def sync_tree( disable_progress: bool = False, recursive: bool = False, use_fetch: bool = False, + mirror: bool = False, hide_token: bool = False, git_options: Optional[str] = None ) -> None: @@ -356,11 +367,12 @@ def sync_tree( disable_progress: Whether to disable progress reporting recursive: Whether to clone recursively use_fetch: Whether to use git fetch instead of pull + mirror: Whether to create bare mirror repositories hide_token: Whether to hide token in URLs git_options: Additional git options as comma-separated string """ manager = GitSyncManager(concurrency, disable_progress) - manager.sync(root, dest, recursive, use_fetch, hide_token, git_options) + manager.sync(root, dest, recursive, use_fetch, mirror, hide_token, git_options) def get_git_actions( @@ -368,6 +380,7 @@ def get_git_actions( dest: str, recursive: bool, use_fetch: bool, + mirror: bool, hide_token: bool, git_options: Optional[str] = None ) -> list[GitAction]: @@ -378,13 +391,14 @@ def get_git_actions( dest: Destination directory recursive: Whether to clone recursively use_fetch: Whether to use git fetch instead of pull + mirror: Whether to create bare mirror repositories hide_token: Whether to hide token in URLs git_options: Additional git options as comma-separated string Returns: List of GitAction objects to execute """ - collector = GitActionCollector(dest, recursive, use_fetch, hide_token, git_options) + collector = GitActionCollector(dest, recursive, use_fetch, mirror, hide_token, git_options) return collector.collect(root) diff --git a/gitlabber/gitlab_tree.py b/gitlabber/gitlab_tree.py index 2ad50d4..a16c4a8 100644 --- a/gitlabber/gitlab_tree.py +++ b/gitlabber/gitlab_tree.py @@ -45,6 +45,7 @@ def __init__(self, disable_progress: bool = False, include_shared: bool = True, use_fetch: bool = False, + mirror: bool = False, hide_token: bool = False, user_projects: bool = False, group_search: Optional[str] = None, @@ -69,6 +70,7 @@ def __init__(self, disable_progress: Whether to disable progress bar (used if config not provided) include_shared: Whether to include shared projects (used if config not provided) use_fetch: Whether to use git fetch instead of pull (used if config not provided) + mirror: Whether to create bare mirror repositories (used if config not provided) hide_token: Whether to hide token in URLs (used if config not provided) user_projects: Whether to fetch only user projects (used if config not provided) group_search: Search term for filtering groups (used if config not provided) @@ -98,6 +100,7 @@ def __init__(self, disable_progress = config.disable_progress include_shared = config.include_shared use_fetch = config.use_fetch + mirror = config.mirror hide_token = config.hide_token user_projects = config.user_projects group_search = config.group_search @@ -175,6 +178,7 @@ def __init__(self, self.token = token self.include_shared = include_shared self.use_fetch = use_fetch + self.mirror = mirror self.hide_token = hide_token self.user_projects = user_projects self.group_search = group_search @@ -308,7 +312,7 @@ def sync_tree(self, dest: str) -> None: len(self.root.descendants) - len(self.root.leaves), len(self.root.leaves)) sync_tree(self.root, dest, concurrency=self.concurrency, disable_progress=self.disable_progress, recursive=self.recursive, - use_fetch=self.use_fetch, hide_token=self.hide_token, + use_fetch=self.use_fetch, mirror=self.mirror, hide_token=self.hide_token, git_options=self.git_options) except GitlabberGitError: # Re-raise git errors as-is diff --git a/pyproject.toml b/pyproject.toml index 49fd7f9..1b3420b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "gitlabber" -version = "2.1.0" +version = "2.1.1" description = "A Gitlab clone/pull utility for backing up or cloning Gitlab groups" readme = "README.rst" requires-python = ">=3.11" diff --git a/tests/test_git.py b/tests/test_git.py index dc02474..e7c6ce1 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -216,4 +216,284 @@ def test_clone_repo_options_with_recursive(mock_is_git_repo, mock_git): action = GitAction(node, "dummy_dir", recursive=True, git_options="--opt1=1,--opt2=2") git.clone_or_pull_project(action) - mock_git_repo.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=['--recursive','--opt1=1','--opt2=2']) \ No newline at end of file + mock_git_repo.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=['--recursive','--opt1=1','--opt2=2']) + +@mock.patch('gitlabber.git.git') +@mock.patch('gitlabber.git.is_git_repo') +def test_clone_repo_mirror(mock_is_git_repo, mock_git): + """Test cloning with mirror flag creates bare repository.""" + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=False) + mock.patch('gitlabber.git.git', mock_git_repo).start() + mock_is_git_repo.return_value = False + + node = Node(type="project", name="dummy_url", url="dummy_url") + action = GitAction(node, "dummy_dir", mirror=True) + git.clone_or_pull_project(action) + + mock_git_repo.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=['--mirror']) + +@mock.patch('gitlabber.git.git') +@mock.patch('gitlabber.git.is_git_repo') +def test_clone_repo_mirror_with_recursive(mock_is_git_repo, mock_git): + """Test cloning with mirror and recursive flags.""" + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=False) + mock.patch('gitlabber.git.git', mock_git_repo).start() + mock_is_git_repo.return_value = False + + node = Node(type="project", name="dummy_url", url="dummy_url") + action = GitAction(node, "dummy_dir", recursive=True, mirror=True) + git.clone_or_pull_project(action) + + mock_git_repo.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=['--recursive', '--mirror']) + +def test_pull_repo_mirror_uses_fetch(): + """Test that mirror repositories use fetch instead of pull.""" + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=True) + with mock.patch('gitlabber.git.git', mock_git_repo): + with mock.patch('gitlabber.git.is_git_repo', return_value=True): + node = Node(type="project", name="test") + action = GitAction(node, "dummy_dir", mirror=True) + git.clone_or_pull_project(action) + + mock_git_repo.Repo.assert_called_once_with("dummy_dir") + mock_git_repo.Repo.return_value.remotes.origin.fetch.assert_called_once() + mock_git_repo.Repo.return_value.remotes.origin.pull.assert_not_called() + +@mock.patch('gitlabber.git.git') +@mock.patch('gitlabber.git.is_git_repo') +def test_clone_repo_use_fetch_normal_repo(mock_is_git_repo, mock_git): + """Test that use_fetch creates normal repo (not bare) but uses fetch for updates.""" + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=False) + mock.patch('gitlabber.git.git', mock_git_repo).start() + mock_is_git_repo.return_value = False + + node = Node(type="project", name="dummy_url", url="dummy_url") + action = GitAction(node, "dummy_dir", use_fetch=True) + git.clone_or_pull_project(action) + + # Should NOT include --mirror, just normal clone + mock_git_repo.Repo.clone_from.assert_called_once_with("dummy_url", "dummy_dir", multi_options=[]) + +@mock.patch('gitlabber.git.git') +@mock.patch('gitlabber.git.is_git_repo') +def test_pull_repo_use_fetch(mock_is_git_repo, mock_git): + """Test that use_fetch uses fetch instead of pull for updates.""" + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=True) + mock.patch('gitlabber.git.git', mock_git_repo).start() + mock_is_git_repo.return_value = True + + node = Node(type="project", name="test") + action = GitAction(node, "dummy_dir", use_fetch=True) + git.clone_or_pull_project(action) + + mock_git_repo.Repo.assert_called_once_with("dummy_dir") + mock_git_repo.Repo.return_value.remotes.origin.fetch.assert_called_once() + mock_git_repo.Repo.return_value.remotes.origin.pull.assert_not_called() + +def test_use_fetch_and_mirror_interaction(): + """Test comprehensive interaction between use_fetch and mirror flags. + + This test ensures: + 1. use_fetch alone: normal repo, uses fetch for updates + 2. mirror alone: bare repo, uses fetch for updates (mirror implies use_fetch) + 3. both flags: bare repo (mirror takes precedence), uses fetch for updates + 4. mirror=True, use_fetch=False: still uses fetch (mirror implies use_fetch) + """ + # Test 1: use_fetch=True, mirror=False - normal repo, fetch for updates + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=False) + with mock.patch('gitlabber.git.git', mock_git_repo): + with mock.patch('gitlabber.git.is_git_repo', return_value=False): + node = Node(type="project", name="test1", url="test1_url") + action = GitAction(node, "test1_dir", use_fetch=True, mirror=False) + git.clone_or_pull_project(action) + + # Should create normal repo (no --mirror) + mock_git_repo.Repo.clone_from.assert_called_once_with("test1_url", "test1_dir", multi_options=[]) + + # Test 2: use_fetch=False, mirror=True - bare repo, fetch for updates (mirror implies use_fetch) + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=False) + with mock.patch('gitlabber.git.git', mock_git_repo): + with mock.patch('gitlabber.git.is_git_repo', return_value=False): + node2 = Node(type="project", name="test2", url="test2_url") + action2 = GitAction(node2, "test2_dir", use_fetch=False, mirror=True) + git.clone_or_pull_project(action2) + + # Should create bare repo (with --mirror) + mock_git_repo.Repo.clone_from.assert_called_once_with("test2_url", "test2_dir", multi_options=['--mirror']) + + # Test 3: use_fetch=True, mirror=True - bare repo (mirror takes precedence), fetch for updates + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=False) + with mock.patch('gitlabber.git.git', mock_git_repo): + with mock.patch('gitlabber.git.is_git_repo', return_value=False): + node3 = Node(type="project", name="test3", url="test3_url") + action3 = GitAction(node3, "test3_dir", use_fetch=True, mirror=True) + git.clone_or_pull_project(action3) + + # Should create bare repo (mirror takes precedence) + mock_git_repo.Repo.clone_from.assert_called_once_with("test3_url", "test3_dir", multi_options=['--mirror']) + + # Test 4: Verify pull behavior - mirror=True should use fetch even if use_fetch=False + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=True) + with mock.patch('gitlabber.git.git', mock_git_repo): + with mock.patch('gitlabber.git.is_git_repo', return_value=True): + node4 = Node(type="project", name="test4") + action4 = GitAction(node4, "test4_dir", use_fetch=False, mirror=True) + git.clone_or_pull_project(action4) + + # Mirror should imply use_fetch, so should use fetch, not pull + mock_git_repo.Repo.assert_called_once_with("test4_dir") + mock_git_repo.Repo.return_value.remotes.origin.fetch.assert_called_once() + mock_git_repo.Repo.return_value.remotes.origin.pull.assert_not_called() + + +def test_git_action_collector_mirror_implies_use_fetch(): + """Test that GitActionCollector correctly implements mirror implies use_fetch logic. + + This ensures that when mirror=True, the collected GitAction has use_fetch=True + even if use_fetch was originally False. + """ + from gitlabber.git import GitActionCollector + from anytree import Node + + # Create a simple tree with one project + root = Node("root", type="root", root_path="") + project = Node(type="project", name="test_project", + root_path="/test_project", url="test_url", parent=root) + + # Test: mirror=True, use_fetch=False - should result in use_fetch=True + collector = GitActionCollector( + dest="/tmp/test", + recursive=False, + use_fetch=False, # Explicitly False + mirror=True, # But mirror is True + hide_token=False, + git_options=None + ) + + actions = collector.collect(root) + + assert len(actions) == 1 + action = actions[0] + assert action.mirror is True + # Mirror should imply use_fetch, so this should be True + assert action.use_fetch is True, "mirror=True should imply use_fetch=True" + + # Test: mirror=True, use_fetch=True - should result in use_fetch=True + collector2 = GitActionCollector( + dest="/tmp/test", + recursive=False, + use_fetch=True, + mirror=True, + hide_token=False, + git_options=None + ) + + actions2 = collector2.collect(root) + assert len(actions2) == 1 + action2 = actions2[0] + assert action2.mirror is True + assert action2.use_fetch is True + + # Test: mirror=False, use_fetch=True - should result in use_fetch=True + collector3 = GitActionCollector( + dest="/tmp/test", + recursive=False, + use_fetch=True, + mirror=False, + hide_token=False, + git_options=None + ) + + actions3 = collector3.collect(root) + assert len(actions3) == 1 + action3 = actions3[0] + assert action3.mirror is False + assert action3.use_fetch is True + + # Test: mirror=False, use_fetch=False - should result in use_fetch=False + collector4 = GitActionCollector( + dest="/tmp/test", + recursive=False, + use_fetch=False, + mirror=False, + hide_token=False, + git_options=None + ) + + actions4 = collector4.collect(root) + assert len(actions4) == 1 + action4 = actions4[0] + assert action4.mirror is False + assert action4.use_fetch is False + + +@mock.patch('gitlabber.git.git') +@mock.patch('gitlabber.git.is_git_repo') +def test_all_flag_combinations_clone_behavior(mock_is_git_repo, mock_git): + """Test all combinations of use_fetch and mirror flags for clone behavior. + + Verifies that: + - Neither flag: normal clone, no special options + - use_fetch only: normal clone, no --mirror + - mirror only: bare clone with --mirror + - both flags: bare clone with --mirror (mirror takes precedence) + """ + test_cases = [ + # (use_fetch, mirror, expected_multi_options, description) + (False, False, [], "Neither flag: normal clone"), + (True, False, [], "use_fetch only: normal clone (no --mirror)"), + (False, True, ['--mirror'], "mirror only: bare clone"), + (True, True, ['--mirror'], "both flags: bare clone (mirror takes precedence)"), + ] + + for use_fetch, mirror, expected_options, description in test_cases: + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=False) + mock.patch('gitlabber.git.git', mock_git_repo).start() + mock_is_git_repo.return_value = False + + node = Node(type="project", name=f"test_{use_fetch}_{mirror}", url="test_url") + action = GitAction(node, f"test_dir_{use_fetch}_{mirror}", + use_fetch=use_fetch, mirror=mirror) + git.clone_or_pull_project(action) + + mock_git_repo.Repo.clone_from.assert_called_once_with( + "test_url", + f"test_dir_{use_fetch}_{mirror}", + multi_options=expected_options + ) + assert mock_git_repo.Repo.clone_from.call_args[1]['multi_options'] == expected_options, \ + f"Failed for {description}: expected {expected_options}, got {mock_git_repo.Repo.clone_from.call_args[1]['multi_options']}" + + +def test_all_flag_combinations_pull_behavior(): + """Test all combinations of use_fetch and mirror flags for pull/fetch behavior. + + Verifies that: + - Neither flag: uses pull + - use_fetch only: uses fetch + - mirror only: uses fetch (mirror implies use_fetch) + - both flags: uses fetch + """ + test_cases = [ + # (use_fetch, mirror, should_use_fetch, description) + (False, False, False, "Neither flag: should use pull"), + (True, False, True, "use_fetch only: should use fetch"), + (False, True, True, "mirror only: should use fetch (mirror implies use_fetch)"), + (True, True, True, "both flags: should use fetch"), + ] + + for use_fetch, mirror, should_use_fetch, description in test_cases: + mock_git_repo = MockGitRepo.create_mock_repo(is_git_repo=True) + with mock.patch('gitlabber.git.git', mock_git_repo): + with mock.patch('gitlabber.git.is_git_repo', return_value=True): + node = Node(type="project", name=f"test_{use_fetch}_{mirror}") + action = GitAction(node, f"test_dir_{use_fetch}_{mirror}", + use_fetch=use_fetch, mirror=mirror) + git.clone_or_pull_project(action) + + if should_use_fetch: + mock_git_repo.Repo.return_value.remotes.origin.fetch.assert_called_once() + mock_git_repo.Repo.return_value.remotes.origin.pull.assert_not_called() + else: + mock_git_repo.Repo.return_value.remotes.origin.pull.assert_called_once() + mock_git_repo.Repo.return_value.remotes.origin.fetch.assert_not_called() \ No newline at end of file diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 7eaddc6..b1503bf 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -31,6 +31,7 @@ def create_mock_repo( mock_git = mock.Mock() mock_repo_instance = mock.Mock() mock_repo_instance.remotes.origin.pull = mock.Mock() + mock_repo_instance.remotes.origin.fetch = mock.Mock() if pull_side_effect: mock_repo_instance.remotes.origin.pull.side_effect = pull_side_effect mock_repo_instance.submodule_update = mock.Mock() @@ -42,7 +43,8 @@ def create_mock_repo( # Mock is_git_repo behavior if is_git_repo: - mock_git.Repo.side_effect = lambda p: mock_repo_instance if p == path else mock.Mock() + # Return the same mock_repo_instance for any path when is_git_repo=True + mock_git.Repo.side_effect = lambda p: mock_repo_instance else: mock_git.Repo.side_effect = lambda p: mock.Mock() @@ -286,6 +288,8 @@ def create_git_action( path: str, url: Optional[str] = None, recursive: bool = False, + use_fetch: bool = False, + mirror: bool = False, git_options: Optional[str] = None, ) -> GitAction: """Create a GitAction from a node. @@ -295,6 +299,8 @@ def create_git_action( path: Destination path url: Repository URL (sets node.url if provided) recursive: Whether to clone recursively + use_fetch: Whether to use fetch instead of pull + mirror: Whether to create bare mirror repository git_options: Additional git options Returns: @@ -306,6 +312,8 @@ def create_git_action( node=node, path=path, recursive=recursive, + use_fetch=use_fetch, + mirror=mirror, git_options=git_options, ) From 0319f8b8b054e09f085954e90112846d626cb743 Mon Sep 17 00:00:00 2001 From: Erez Date: Wed, 17 Dec 2025 08:07:28 +0700 Subject: [PATCH 2/2] fix: remove unused variable in test_git.py --- tests/test_git.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_git.py b/tests/test_git.py index e7c6ce1..bdd4572 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -357,8 +357,8 @@ def test_git_action_collector_mirror_implies_use_fetch(): # Create a simple tree with one project root = Node("root", type="root", root_path="") - project = Node(type="project", name="test_project", - root_path="/test_project", url="test_url", parent=root) + Node(type="project", name="test_project", + root_path="/test_project", url="test_url", parent=root) # Test: mirror=True, use_fetch=False - should result in use_fetch=True collector = GitActionCollector(