|
12 | 12 | from cycode.cli.files_collector.commit_range_documents import ( |
13 | 13 | _get_default_branches_for_merge_base, |
14 | 14 | calculate_pre_push_commit_range, |
| 15 | + calculate_pre_receive_commit_range, |
15 | 16 | get_diff_file_path, |
16 | 17 | get_safe_head_reference_for_diff, |
17 | 18 | parse_commit_range, |
18 | 19 | parse_pre_push_input, |
| 20 | + parse_pre_receive_input, |
19 | 21 | ) |
20 | 22 | from cycode.cli.utils.path_utils import get_path_by_os |
21 | 23 |
|
| 24 | +DUMMY_SHA_0 = '0' * 40 |
| 25 | +DUMMY_SHA_1 = '1' * 40 |
| 26 | +DUMMY_SHA_2 = '2' * 40 |
| 27 | +DUMMY_SHA_A = 'a' * 40 |
| 28 | +DUMMY_SHA_B = 'b' * 40 |
| 29 | +DUMMY_SHA_C = 'c' * 40 |
| 30 | + |
22 | 31 |
|
23 | 32 | @contextmanager |
24 | 33 | def git_repository(path: str) -> Generator[Repo, None, None]: |
@@ -871,3 +880,170 @@ def test_single_commit_spec(self) -> None: |
871 | 880 |
|
872 | 881 | parsed_from, parsed_to = parse_commit_range(a, temp_dir) |
873 | 882 | assert (parsed_from, parsed_to) == (a, c) |
| 883 | + |
| 884 | + |
| 885 | +class TestParsePreReceiveInput: |
| 886 | + """Test the parse_pre_receive_input function with various pre-receive hook input scenarios.""" |
| 887 | + |
| 888 | + def test_parse_single_update_input(self) -> None: |
| 889 | + """Test parsing a single branch update input.""" |
| 890 | + pre_receive_input = f'{DUMMY_SHA_1} {DUMMY_SHA_2} refs/heads/main' |
| 891 | + |
| 892 | + with patch('sys.stdin', StringIO(pre_receive_input)): |
| 893 | + result = parse_pre_receive_input() |
| 894 | + assert result == pre_receive_input |
| 895 | + |
| 896 | + def test_parse_multiple_update_input_returns_first_line(self) -> None: |
| 897 | + """Test parsing multiple branch updates returns only the first line.""" |
| 898 | + pre_receive_input = f"""{DUMMY_SHA_0} {DUMMY_SHA_A} refs/heads/main |
| 899 | +{DUMMY_SHA_B} {DUMMY_SHA_C} refs/heads/feature""" |
| 900 | + |
| 901 | + with patch('sys.stdin', StringIO(pre_receive_input)): |
| 902 | + result = parse_pre_receive_input() |
| 903 | + assert result == f'{DUMMY_SHA_0} {DUMMY_SHA_A} refs/heads/main' |
| 904 | + |
| 905 | + def test_parse_empty_input_raises_error(self) -> None: |
| 906 | + """Test that empty input raises ValueError.""" |
| 907 | + match = 'Pre receive input was not found' |
| 908 | + with patch('sys.stdin', StringIO('')), pytest.raises(ValueError, match=match): |
| 909 | + parse_pre_receive_input() |
| 910 | + |
| 911 | + |
| 912 | +class TestCalculatePreReceiveCommitRange: |
| 913 | + """Test the calculate_pre_receive_commit_range function with representative scenarios.""" |
| 914 | + |
| 915 | + def test_branch_deletion_returns_none(self) -> None: |
| 916 | + """When end commit is all zeros (deletion), no scan is needed.""" |
| 917 | + update_details = f'{DUMMY_SHA_A} {consts.EMPTY_COMMIT_SHA} refs/heads/feature' |
| 918 | + assert calculate_pre_receive_commit_range(os.getcwd(), update_details) is None |
| 919 | + |
| 920 | + def test_no_new_commits_returns_none(self) -> None: |
| 921 | + """When there are no commits not in remote, return None.""" |
| 922 | + with tempfile.TemporaryDirectory() as server_dir: |
| 923 | + server_repo = Repo.init(server_dir, bare=True) |
| 924 | + try: |
| 925 | + with tempfile.TemporaryDirectory() as work_dir: |
| 926 | + work_repo = Repo.init(work_dir, b='main') |
| 927 | + try: |
| 928 | + # Create a single commit and push it to the server as main (end commit is already on a ref) |
| 929 | + test_file = os.path.join(work_dir, 'file.txt') |
| 930 | + with open(test_file, 'w') as f: |
| 931 | + f.write('base') |
| 932 | + work_repo.index.add(['file.txt']) |
| 933 | + end_commit = work_repo.index.commit('initial') |
| 934 | + |
| 935 | + work_repo.create_remote('origin', server_dir) |
| 936 | + work_repo.remotes.origin.push('main:main') |
| 937 | + |
| 938 | + update_details = f'{DUMMY_SHA_A} {end_commit.hexsha} refs/heads/main' |
| 939 | + assert calculate_pre_receive_commit_range(server_dir, update_details) is None |
| 940 | + finally: |
| 941 | + work_repo.close() |
| 942 | + finally: |
| 943 | + server_repo.close() |
| 944 | + |
| 945 | + def test_returns_triple_dot_range_from_oldest_unupdated(self) -> None: |
| 946 | + """Returns '<oldest>~1...<end>' when there are new commits to scan.""" |
| 947 | + with tempfile.TemporaryDirectory() as server_dir: |
| 948 | + server_repo = Repo.init(server_dir, bare=True) |
| 949 | + try: |
| 950 | + with tempfile.TemporaryDirectory() as work_dir: |
| 951 | + work_repo = Repo.init(work_dir, b='main') |
| 952 | + try: |
| 953 | + # Create commit A and push it to server as main (server has A on a ref) |
| 954 | + a_path = os.path.join(work_dir, 'a.txt') |
| 955 | + with open(a_path, 'w') as f: |
| 956 | + f.write('A') |
| 957 | + work_repo.index.add(['a.txt']) |
| 958 | + work_repo.index.commit('A') |
| 959 | + |
| 960 | + work_repo.create_remote('origin', server_dir) |
| 961 | + work_repo.remotes.origin.push('main:main') |
| 962 | + |
| 963 | + # Create commits B and C locally (not yet on server ref) |
| 964 | + b_path = os.path.join(work_dir, 'b.txt') |
| 965 | + with open(b_path, 'w') as f: |
| 966 | + f.write('B') |
| 967 | + work_repo.index.add(['b.txt']) |
| 968 | + b_commit = work_repo.index.commit('B') |
| 969 | + |
| 970 | + c_path = os.path.join(work_dir, 'c.txt') |
| 971 | + with open(c_path, 'w') as f: |
| 972 | + f.write('C') |
| 973 | + work_repo.index.add(['c.txt']) |
| 974 | + end_commit = work_repo.index.commit('C') |
| 975 | + |
| 976 | + # Push the objects to a temporary ref and then delete that ref on server, |
| 977 | + # so the objects exist but are not reachable from any ref. |
| 978 | + work_repo.remotes.origin.push(f'{end_commit.hexsha}:refs/tmp/hold') |
| 979 | + Repo(server_dir).git.update_ref('-d', 'refs/tmp/hold') |
| 980 | + |
| 981 | + update_details = f'{DUMMY_SHA_A} {end_commit.hexsha} refs/heads/main' |
| 982 | + result = calculate_pre_receive_commit_range(server_dir, update_details) |
| 983 | + assert result == f'{b_commit.hexsha}~1...{end_commit.hexsha}' |
| 984 | + finally: |
| 985 | + work_repo.close() |
| 986 | + finally: |
| 987 | + server_repo.close() |
| 988 | + |
| 989 | + def test_initial_oldest_commit_without_parent_returns_single_commit_range(self) -> None: |
| 990 | + """If oldest commit has no parent, avoid '~1' and scan from end commit only.""" |
| 991 | + with tempfile.TemporaryDirectory() as server_dir: |
| 992 | + server_repo = Repo.init(server_dir, bare=True) |
| 993 | + try: |
| 994 | + with tempfile.TemporaryDirectory() as work_dir: |
| 995 | + work_repo = Repo.init(work_dir, b='main') |
| 996 | + try: |
| 997 | + # Create a single root commit locally |
| 998 | + p = os.path.join(work_dir, 'root.txt') |
| 999 | + with open(p, 'w') as f: |
| 1000 | + f.write('root') |
| 1001 | + work_repo.index.add(['root.txt']) |
| 1002 | + end_commit = work_repo.index.commit('root') |
| 1003 | + |
| 1004 | + work_repo.create_remote('origin', server_dir) |
| 1005 | + # Push objects to a temporary ref and delete it so server has objects but no refs |
| 1006 | + work_repo.remotes.origin.push(f'{end_commit.hexsha}:refs/tmp/hold') |
| 1007 | + Repo(server_dir).git.update_ref('-d', 'refs/tmp/hold') |
| 1008 | + |
| 1009 | + update_details = f'{DUMMY_SHA_A} {end_commit.hexsha} refs/heads/main' |
| 1010 | + result = calculate_pre_receive_commit_range(server_dir, update_details) |
| 1011 | + assert result == end_commit.hexsha |
| 1012 | + finally: |
| 1013 | + work_repo.close() |
| 1014 | + finally: |
| 1015 | + server_repo.close() |
| 1016 | + |
| 1017 | + def test_initial_oldest_commit_without_parent_with_two_commits_returns_single_commit_range(self) -> None: |
| 1018 | + """If there are two new commits and the oldest has no parent, avoid '~1' and scan from end commit only.""" |
| 1019 | + with tempfile.TemporaryDirectory() as server_dir: |
| 1020 | + server_repo = Repo.init(server_dir, bare=True) |
| 1021 | + try: |
| 1022 | + with tempfile.TemporaryDirectory() as work_dir: |
| 1023 | + work_repo = Repo.init(work_dir, b='main') |
| 1024 | + try: |
| 1025 | + # Create two commits locally: oldest has no parent, second on top |
| 1026 | + a_path = os.path.join(work_dir, 'a.txt') |
| 1027 | + with open(a_path, 'w') as f: |
| 1028 | + f.write('A') |
| 1029 | + work_repo.index.add(['a.txt']) |
| 1030 | + work_repo.index.commit('A') |
| 1031 | + |
| 1032 | + d_path = os.path.join(work_dir, 'd.txt') |
| 1033 | + with open(d_path, 'w') as f: |
| 1034 | + f.write('D') |
| 1035 | + work_repo.index.add(['d.txt']) |
| 1036 | + end_commit = work_repo.index.commit('D') |
| 1037 | + |
| 1038 | + work_repo.create_remote('origin', server_dir) |
| 1039 | + # Push objects to a temporary ref and delete it so server has objects but no refs |
| 1040 | + work_repo.remotes.origin.push(f'{end_commit.hexsha}:refs/tmp/hold') |
| 1041 | + Repo(server_dir).git.update_ref('-d', 'refs/tmp/hold') |
| 1042 | + |
| 1043 | + update_details = f'{consts.EMPTY_COMMIT_SHA} {end_commit.hexsha} refs/heads/main' |
| 1044 | + result = calculate_pre_receive_commit_range(server_dir, update_details) |
| 1045 | + assert result == end_commit.hexsha |
| 1046 | + finally: |
| 1047 | + work_repo.close() |
| 1048 | + finally: |
| 1049 | + server_repo.close() |
0 commit comments