Skip to content

Commit f272dd3

Browse files
Byroncodex
andcommitted
Make locking-in-one-step work on Windows as well
Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6 <codex@openai.com>
1 parent b0ae041 commit f272dd3

2 files changed

Lines changed: 38 additions & 9 deletions

File tree

‎git/util.py‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,20 +1166,34 @@ def _obtain_lock_or_raise(self) -> None:
11661166
if self._has_lock():
11671167
return
11681168
lock_file = self._lock_file_path()
1169-
# Create the lock in one step, the way Git and gitdb's LockedFD do. Testing
1170-
# for the file first leaves a window in which another holder creates it and
1171-
# both proceed, and O_CREAT|O_EXCL additionally refuses to follow a symbolic
1172-
# link planted at the lock path instead of writing through it.
1169+
# Create the lock in one step. Checking for it first would allow another
1170+
# holder to create it between the check and the open.
11731171
try:
1174-
fd = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
1172+
if sys.platform == "win32":
1173+
import _winapi
1174+
1175+
# Unlike POSIX, Windows follows dangling symlinks even with O_EXCL.
1176+
# Open the reparse point itself so an existing link is rejected.
1177+
handle = _winapi.CreateFile(
1178+
lock_file,
1179+
_winapi.GENERIC_WRITE,
1180+
0,
1181+
0,
1182+
1, # CREATE_NEW
1183+
0x00200000, # FILE_FLAG_OPEN_REPARSE_POINT
1184+
0,
1185+
)
1186+
_winapi.CloseHandle(handle)
1187+
else:
1188+
fd = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
1189+
os.close(fd)
11751190
except FileExistsError as e:
11761191
raise OSError(
11771192
"Lock for file %r did already exist, delete %r in case the lock is illegal"
11781193
% (self._file_path, lock_file)
11791194
) from e
11801195
except OSError as e:
11811196
raise OSError(str(e)) from e
1182-
os.close(fd)
11831197

11841198
self._owns_lock = True
11851199

‎test/test_util.py‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,17 +447,28 @@ def test_lock_file(self):
447447
lock_file._obtain_lock_or_raise()
448448
lock_file._release_lock()
449449

450+
@ddt.data(False, True)
450451
@requires_symlinks
451-
def test_lock_file_does_not_follow_a_symlink(self):
452+
def test_lock_file_does_not_follow_a_symlink(self, target_exists):
452453
with tempfile.TemporaryDirectory() as tdir:
453454
my_file = os.path.join(tdir, "my-lock-file")
454455
outside = os.path.join(tdir, "outside-the-lock")
456+
content = b"Do not modify the symlink target."
457+
if target_exists:
458+
with open(outside, "wb") as stream:
459+
stream.write(content)
455460
os.symlink(outside, my_file + ".lock")
456461

457462
lock_file = LockFile(my_file)
458463
self.assertRaises(IOError, lock_file._obtain_lock_or_raise)
459464
assert not lock_file._has_lock()
460-
assert not os.path.exists(outside)
465+
lock_file._release_lock()
466+
assert os.path.islink(my_file + ".lock")
467+
if target_exists:
468+
with open(outside, "rb") as stream:
469+
self.assertEqual(stream.read(), content)
470+
else:
471+
assert not os.path.exists(outside)
461472

462473
def test_lock_file_is_obtained_by_a_single_holder(self):
463474
with tempfile.TemporaryDirectory() as tdir:
@@ -483,7 +494,11 @@ def obtain():
483494
for thread in threads:
484495
thread.join()
485496

486-
self.assertEqual(1, len(holders))
497+
try:
498+
self.assertEqual(1, len(holders))
499+
finally:
500+
for lock_file in holders:
501+
lock_file._release_lock()
487502

488503
def test_blocking_lock_file(self):
489504
with tempfile.TemporaryDirectory() as tdir:

0 commit comments

Comments
 (0)