Skip to content

Commit b5b2591

Browse files
authored
CM-51935 - Fix pre-receive hook for bare repositories (#345)
1 parent 89ec2e4 commit b5b2591

File tree

2 files changed

+208
-18
lines changed

2 files changed

+208
-18
lines changed

cycode/cli/files_collector/commit_range_documents.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -212,24 +212,33 @@ def parse_pre_receive_input() -> str:
212212

213213

214214
def get_diff_file_path(diff: 'Diff', relative: bool = False, repo: Optional['Repo'] = None) -> Optional[str]:
215-
if relative:
216-
# relative to the repository root
217-
return diff.b_path if diff.b_path else diff.a_path
218-
219-
# Try blob-based paths first (most reliable when available)
220-
if diff.b_blob:
221-
return diff.b_blob.abspath
222-
if diff.a_blob:
223-
return diff.a_blob.abspath
224-
225-
# Fallback: construct an absolute path from a relative path
226-
# This handles renames and other cases where blobs might be None
227-
if repo and repo.working_tree_dir:
228-
target_path = diff.b_path if diff.b_path else diff.a_path
229-
if target_path:
230-
return os.path.abspath(os.path.join(repo.working_tree_dir, target_path))
231-
232-
return None
215+
"""Get the file path from a git Diff object.
216+
217+
Args:
218+
diff: Git Diff object representing the file change
219+
relative: If True, return the path relative to the repository root;
220+
otherwise, return an absolute path IF possible
221+
repo: Optional Git Repo object, used to resolve absolute paths
222+
223+
Note:
224+
It tries to get the absolute path, falling back to relative paths. `relative` flag forces relative paths.
225+
226+
One case of relative paths is when the repository is bare and does not have a working tree directory.
227+
"""
228+
# try blob-based paths first (most reliable when available)
229+
blob = diff.b_blob if diff.b_blob else diff.a_blob
230+
if blob:
231+
if relative:
232+
return blob.path
233+
if repo and repo.working_tree_dir:
234+
return blob.abspath
235+
236+
path = diff.b_path if diff.b_path else diff.a_path # relative path within the repo
237+
if not relative and path and repo and repo.working_tree_dir:
238+
# convert to the absolute path using the repo's working tree directory
239+
path = os.path.join(repo.working_tree_dir, path)
240+
241+
return path
233242

234243

235244
def get_diff_file_content(diff: 'Diff') -> str:

tests/cli/files_collector/test_commit_range_documents.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,184 @@ def test_git_mv_pre_commit_scan() -> None:
155155
for diff in diff_index:
156156
file_path = get_path_by_os(get_diff_file_path(diff, repo=repo))
157157
assert file_path == renamed_path
158+
159+
160+
class TestGetDiffFilePath:
161+
"""Test the get_diff_file_path function with various diff scenarios."""
162+
163+
def test_diff_with_b_blob_and_working_tree(self) -> None:
164+
"""Test that blob.abspath is returned when b_blob is available and repo has a working tree."""
165+
with temporary_git_repository() as (temp_dir, repo):
166+
test_file = os.path.join(temp_dir, 'test_file.py')
167+
with open(test_file, 'w') as f:
168+
f.write("print('original content')")
169+
170+
repo.index.add(['test_file.py'])
171+
repo.index.commit('Initial commit')
172+
173+
with open(test_file, 'w') as f:
174+
f.write("print('modified content')")
175+
176+
repo.index.add(['test_file.py'])
177+
178+
# Get diff of staged changes
179+
head_ref = get_safe_head_reference_for_diff(repo)
180+
diff_index = repo.index.diff(head_ref)
181+
182+
assert len(diff_index) == 1
183+
184+
result = get_diff_file_path(diff_index[0], repo=repo)
185+
186+
assert result == test_file
187+
assert os.path.isabs(result)
188+
189+
def test_diff_with_a_blob_only_and_working_tree(self) -> None:
190+
"""Test that a_blob.abspath is used when b_blob is None but a_blob exists."""
191+
with temporary_git_repository() as (temp_dir, repo):
192+
test_file = os.path.join(temp_dir, 'to_delete.py')
193+
with open(test_file, 'w') as f:
194+
f.write("print('will be deleted')")
195+
196+
repo.index.add(['to_delete.py'])
197+
repo.index.commit('Initial commit')
198+
199+
os.remove(test_file)
200+
repo.index.remove(['to_delete.py'])
201+
202+
# Get diff of staged changes
203+
head_ref = get_safe_head_reference_for_diff(repo)
204+
diff_index = repo.index.diff(head_ref)
205+
206+
assert len(diff_index) == 1
207+
208+
result = get_diff_file_path(diff_index[0], repo=repo)
209+
210+
assert result == test_file
211+
assert os.path.isabs(result)
212+
213+
def test_diff_with_b_path_fallback(self) -> None:
214+
"""Test that b_path is used with working_tree_dir when blob is not available."""
215+
with temporary_git_repository() as (temp_dir, repo):
216+
test_file = os.path.join(temp_dir, 'new_file.py')
217+
with open(test_file, 'w') as f:
218+
f.write("print('new file')")
219+
220+
repo.index.add(['new_file.py'])
221+
222+
# for new files, there's no a_blob
223+
head_ref = get_safe_head_reference_for_diff(repo)
224+
diff_index = repo.index.diff(head_ref)
225+
diff = diff_index[0]
226+
227+
assert len(diff_index) == 1
228+
229+
result = get_diff_file_path(diff, repo=repo)
230+
assert result == test_file
231+
assert os.path.isabs(result)
232+
233+
result = get_diff_file_path(diff, relative=True, repo=repo)
234+
assert test_file.endswith(result)
235+
assert not os.path.isabs(result)
236+
237+
def test_diff_with_a_path_fallback(self) -> None:
238+
"""Test that a_path is used when b_path is None."""
239+
with temporary_git_repository() as (temp_dir, repo):
240+
test_file = os.path.join(temp_dir, 'deleted_file.py')
241+
with open(test_file, 'w') as f:
242+
f.write("print('will be deleted')")
243+
244+
repo.index.add(['deleted_file.py'])
245+
repo.index.commit('Initial commit')
246+
247+
# for deleted files, b_path might be None, so a_path should be used
248+
os.remove(test_file)
249+
repo.index.remove(['deleted_file.py'])
250+
251+
head_ref = get_safe_head_reference_for_diff(repo)
252+
diff_index = repo.index.diff(head_ref)
253+
254+
assert len(diff_index) == 1
255+
diff = diff_index[0]
256+
257+
result = get_diff_file_path(diff, repo=repo)
258+
assert result == test_file
259+
assert os.path.isabs(result)
260+
261+
result = get_diff_file_path(diff, relative=True, repo=repo)
262+
assert test_file.endswith(result)
263+
assert not os.path.isabs(result)
264+
265+
def test_diff_without_repo(self) -> None:
266+
"""Test behavior when repo is None."""
267+
with temporary_git_repository() as (temp_dir, repo):
268+
test_file = os.path.join(temp_dir, 'test.py')
269+
with open(test_file, 'w') as f:
270+
f.write("print('test')")
271+
272+
repo.index.add(['test.py'])
273+
head_ref = get_safe_head_reference_for_diff(repo)
274+
diff_index = repo.index.diff(head_ref)
275+
276+
assert len(diff_index) == 1
277+
diff = diff_index[0]
278+
279+
result = get_diff_file_path(diff, repo=None)
280+
281+
expected_path = diff.b_path or diff.a_path
282+
assert result == expected_path
283+
assert not os.path.isabs(result)
284+
285+
def test_diff_with_bare_repository(self) -> None:
286+
"""Test behavior when the repository has no working tree directory."""
287+
with tempfile.TemporaryDirectory() as temp_dir:
288+
bare_repo = Repo.init(temp_dir, bare=True)
289+
290+
try:
291+
# Create a regular repo to push to the bare repo
292+
with tempfile.TemporaryDirectory() as work_dir:
293+
work_repo = Repo.init(work_dir, b='main')
294+
try:
295+
test_file = os.path.join(work_dir, 'test.py')
296+
with open(test_file, 'w') as f:
297+
f.write("print('test')")
298+
299+
work_repo.index.add(['test.py'])
300+
work_repo.index.commit('Initial commit')
301+
302+
work_repo.create_remote('origin', temp_dir)
303+
work_repo.remotes.origin.push('main:main')
304+
305+
with open(test_file, 'w') as f:
306+
f.write("print('modified')")
307+
work_repo.index.add(['test.py'])
308+
309+
# Get diff
310+
diff_index = work_repo.index.diff('HEAD')
311+
assert len(diff_index) == 1
312+
diff = diff_index[0]
313+
314+
# Test with bare repo (no working_tree_dir)
315+
result = get_diff_file_path(diff, repo=bare_repo)
316+
317+
# Should return a relative path since bare repo has no working tree
318+
expected_path = diff.b_path or diff.a_path
319+
assert result == expected_path
320+
assert not os.path.isabs(result)
321+
finally:
322+
work_repo.close()
323+
finally:
324+
bare_repo.close()
325+
326+
def test_diff_with_no_paths(self) -> None:
327+
"""Test behavior when the diff has neither a_path nor b_path."""
328+
with temporary_git_repository() as (temp_dir, repo):
329+
330+
class MockDiff:
331+
def __init__(self) -> None:
332+
self.a_path = None
333+
self.b_path = None
334+
self.a_blob = None
335+
self.b_blob = None
336+
337+
result = get_diff_file_path(MockDiff(), repo=repo)
338+
assert result is None

0 commit comments

Comments
 (0)