diff --git a/cycode/cli/consts.py b/cycode/cli/consts.py index 1b1497bd..e06f9b00 100644 --- a/cycode/cli/consts.py +++ b/cycode/cli/consts.py @@ -269,6 +269,7 @@ # git consts COMMIT_DIFF_DELETED_FILE_CHANGE_TYPE = 'D' +COMMIT_RANGE_ALL_COMMITS = '--all' GIT_HEAD_COMMIT_REV = 'HEAD' GIT_EMPTY_TREE_OBJECT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' EMPTY_COMMIT_SHA = '0000000000000000000000000000000000000000' diff --git a/cycode/cli/files_collector/commit_range_documents.py b/cycode/cli/files_collector/commit_range_documents.py index d92aea81..a4a1a784 100644 --- a/cycode/cli/files_collector/commit_range_documents.py +++ b/cycode/cli/files_collector/commit_range_documents.py @@ -351,10 +351,10 @@ def calculate_pre_push_commit_range(push_update_details: str) -> Optional[str]: return f'{merge_base}..{local_object_name}' logger.debug('Failed to find merge base with any default branch') - return '--all' + return consts.COMMIT_RANGE_ALL_COMMITS except Exception as e: logger.debug('Failed to get repo for pre-push commit range calculation: %s', exc_info=e) - return '--all' + return consts.COMMIT_RANGE_ALL_COMMITS # If deleting a branch (local_object_name is all zeros), no need to scan if local_object_name == consts.EMPTY_COMMIT_SHA: @@ -448,9 +448,25 @@ def parse_commit_range(commit_range: str, path: str) -> tuple[Optional[str], Opt - 'commit' (interpreted as 'commit..HEAD') - '..to' (interpreted as 'HEAD..to') - 'from..' (interpreted as 'from..HEAD') + - '--all' (interpreted as 'first_commit..HEAD' to scan all commits) """ repo = git_proxy.get_repo(path) + # Handle '--all' special case: scan all commits from first to HEAD + # Usually represents an empty remote repository + if commit_range == consts.COMMIT_RANGE_ALL_COMMITS: + try: + head_commit = repo.rev_parse(consts.GIT_HEAD_COMMIT_REV).hexsha + all_commits = repo.git.rev_list('--reverse', head_commit).strip() + if all_commits: + first_commit = all_commits.splitlines()[0] + return first_commit, head_commit, '..' + logger.warning("No commits found for range '%s'", commit_range) + return None, None, None + except Exception as e: + logger.warning("Failed to parse commit range '%s'", commit_range, exc_info=e) + return None, None, None + separator = '..' if '...' in commit_range: from_spec, to_spec = commit_range.split('...', 1) diff --git a/tests/cli/files_collector/test_commit_range_documents.py b/tests/cli/files_collector/test_commit_range_documents.py index 0779f678..501c1811 100644 --- a/tests/cli/files_collector/test_commit_range_documents.py +++ b/tests/cli/files_collector/test_commit_range_documents.py @@ -882,6 +882,67 @@ def test_single_commit_spec(self) -> None: parsed_from, parsed_to, separator = parse_commit_range(a, temp_dir) assert (parsed_from, parsed_to, separator) == (a, c, '..') + def test_parse_all_for_empty_remote_scenario(self) -> None: + """Test that '--all' is parsed correctly for empty remote repository. + This repository has one commit locally. + """ + with temporary_git_repository() as (temp_dir, repo): + # Create a local commit (simulating first commit to empty remote) + test_file = os.path.join(temp_dir, 'test.py') + with open(test_file, 'w') as f: + f.write("print('test')") + + repo.index.add(['test.py']) + commit = repo.index.commit('Initial commit') + + # Test that '--all' (returned by calculate_pre_push_commit_range for empty remote) + # can be parsed to a valid commit range + parsed_from, parsed_to, separator = parse_commit_range('--all', temp_dir) + + # Should return first commit to HEAD (which is the only commit in this case) + assert parsed_from == commit.hexsha + assert parsed_to == commit.hexsha + assert separator == '..' + + def test_parse_all_for_empty_remote_scenario_with_two_commits(self) -> None: + """Test that '--all' is parsed correctly for empty remote repository. + This repository has two commits locally. + """ + with temporary_git_repository() as (temp_dir, repo): + # Create first commit + test_file = os.path.join(temp_dir, 'test.py') + with open(test_file, 'w') as f: + f.write("print('test')") + + repo.index.add(['test.py']) + commit1 = repo.index.commit('First commit') + + # Create second commit + test_file2 = os.path.join(temp_dir, 'test2.py') + with open(test_file2, 'w') as f: + f.write("print('test2')") + + repo.index.add(['test2.py']) + commit2 = repo.index.commit('Second commit') + + # Test that '--all' returns first commit to HEAD (second commit) + parsed_from, parsed_to, separator = parse_commit_range('--all', temp_dir) + + # Should return first commit to HEAD (second commit) + assert parsed_from == commit1.hexsha # First commit + assert parsed_to == commit2.hexsha # HEAD (second commit) + assert separator == '..' + + def test_parse_all_with_empty_repository_returns_none(self) -> None: + """Test that '--all' returns None when repository has no commits.""" + with temporary_git_repository() as (temp_dir, repo): + # Empty repository with no commits + parsed_from, parsed_to, separator = parse_commit_range('--all', temp_dir) + # Should return None, None, None when HEAD doesn't exist + assert parsed_from is None + assert parsed_to is None + assert separator is None + class TestParsePreReceiveInput: """Test the parse_pre_receive_input function with various pre-receive hook input scenarios."""