Skip to content

Commit c7a7cd8

Browse files
committed
Feat: add repository lock detection and validation
Implement lock handling to prevent Git operations on a locked repository. - Add `GitCommands.get_repo_lock` to locate an existing `index.lock` via `git rev-parse` and return its path. - Add `validate_repo_not_locked` utility that raises `GitError` with remediation instructions when a lock file is present. - Integrate `validate_repo_not_locked` into `validate_git_repository` to abort actions on a locked repo. - Add comprehensive unit tests for successful validation and for error handling when a repository is locked.
1 parent 8bebf5b commit c7a7cd8

3 files changed

Lines changed: 56 additions & 0 deletions

File tree

src/codestory/core/git/git_commands.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
# -----------------------------------------------------------------------------
1818

1919

20+
from pathlib import Path
21+
2022
from codestory.core.git.git_const import EMPTYTREEHASH
2123
from codestory.core.git.git_interface import GitInterface
2224

@@ -302,6 +304,23 @@ def is_bare_repository(self) -> bool:
302304
res = self.git.run_git_text_out(["rev-parse", "--is-bare-repository"])
303305
return res.strip() == "true" if res else False
304306

307+
def get_repo_lock(self) -> str | None:
308+
"""Checks if the repository is locked by another git process.
309+
310+
Returns the path to the lock file if it exists, otherwise None.
311+
"""
312+
# index.lock is the most common git lock.
313+
# We use rev-parse --git-path to correctly handle worktrees and submodules.
314+
res = self.git.run_git_text_out(["rev-parse", "--git-path", "index.lock"])
315+
if res:
316+
lock_path = Path(res.strip())
317+
# Handle both absolute and relative paths from git
318+
if not lock_path.is_absolute():
319+
lock_path = self.git.repo_path / lock_path
320+
if lock_path.exists():
321+
return str(lock_path)
322+
return None
323+
305324
def try_get_parent_hash(
306325
self, commit_hash: str, empty_on_fail: bool = False
307326
) -> str | None:

src/codestory/core/validation.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,29 @@ def validate_git_repository(git_commands: GitCommands) -> None:
261261
if not os.path.exists(os.path.join(str(git_commands.git.repo_path), ".git")):
262262
raise GitError("Not a git repository")
263263

264+
# Check if the repository is locked
265+
validate_repo_not_locked(git_commands)
266+
267+
268+
def validate_repo_not_locked(git_commands: GitCommands) -> None:
269+
"""Validate that the repository is not locked by another git process.
270+
271+
Args:
272+
git_commands: Git commands to run
273+
274+
Raises:
275+
GitError: If the repository is locked
276+
"""
277+
lock_file = git_commands.get_repo_lock()
278+
if lock_file:
279+
raise GitError(
280+
"Another git process seems to be running in this repository, e.g.\n"
281+
"an editor opened by 'git commit'. Please make sure all processes\n"
282+
"are terminated then try again. If it still fails, a git process\n"
283+
"may have crashed in this repository earlier:\n"
284+
f"remove the file '{lock_file}' manually to continue."
285+
)
286+
264287

265288
def validate_default_branch(git_commands: GitCommands) -> None:
266289
"""Validate that we are on a branch (not in detached HEAD state).

src/tests/unit/core/test_validation.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ def test_validate_git_repository_success(mock_git_commands):
9191

9292
# Should not raise when .git exists
9393
with patch("os.path.exists", return_value=True):
94+
mock_git_commands.get_repo_lock.return_value = None
95+
validate_git_repository(mock_git_commands)
96+
97+
98+
def test_validate_git_repository_locked(mock_git_commands):
99+
mock_git_commands.is_git_repo.return_value = True
100+
mock_git_commands.git = Mock()
101+
mock_git_commands.git.repo_path = "/fake"
102+
mock_git_commands.get_repo_lock.return_value = "/fake/.git/index.lock"
103+
104+
with (
105+
patch("os.path.exists", return_value=True),
106+
pytest.raises(GitError, match="Another git process seems to be running"),
107+
):
94108
validate_git_repository(mock_git_commands)
95109

96110

0 commit comments

Comments
 (0)