Skip to content

Commit bca85c1

Browse files
authored
Merge pull request #31 from chetan/fix/unicode-decode-err
feat: update mtime script (release 2025.08)
2 parents cbf8161 + cb02da7 commit bca85c1

2 files changed

Lines changed: 21 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# git-restore-mtime action
22

3-
A GitHub Workflow Action which restores timestamps of files in the current tree based on their last commit times. Uses the [git-restore-mtime](https://github.com/MestreLion/git-tools) script (v2022.12 release) by [@MestreLion](https://github.com/MestreLion).
3+
A GitHub Workflow Action which restores timestamps of files in the current tree based on their last commit times. Uses the [git-restore-mtime](https://github.com/MestreLion/git-tools) script (v2025.08 release) by [@MestreLion](https://github.com/MestreLion).
44

55
## Usage
66

git-restore-mtime

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ assuming the actual modification date and its commit date are close.
3030
"""
3131

3232
# TODO:
33-
# - Add -z on git whatchanged/ls-files, so we don't deal with filename decoding
34-
# - When Python is bumped to 3.7, use text instead of universal_newlines on subprocess
33+
# - Add -z on git log/ls-files, so we don't deal with filename decoding
3534
# - Update "Statistics for some large projects" with modern hardware and repositories.
3635
# - Create a README.md for git-restore-mtime alone. It deserves extensive documentation
3736
# - Move Statistics there
@@ -76,10 +75,13 @@ import subprocess
7675
import sys
7776
import time
7877

79-
__version__ = "2022.12"
78+
if sys.version_info < (3, 8):
79+
sys.exit("Python 3.8 or later required.")
80+
81+
__version__ = "2025.08"
8082

8183
# Update symlinks only if the platform supports not following them
82-
UPDATE_SYMLINKS = bool(os.utime in getattr(os, 'supports_follow_symlinks', []))
84+
UPDATE_SYMLINKS = bool({os.utime, os.stat} <= getattr(os, 'supports_follow_symlinks', set()))
8385

8486
# Call os.path.normpath() only if not in a POSIX platform (Windows)
8587
NORMALIZE_PATHS = (os.path.sep != '/')
@@ -241,17 +243,23 @@ def normalize(path):
241243
normalize(r'"Back\\slash_double\"quote_a\303\247a\303\255"') =>
242244
r'Back\slash_double"quote_açaí')
243245
244-
See notes on `windows/non-ascii-paths.txt` about path encodings on non-UTF-8
245-
platforms and filesystems.
246+
Paths with invalid UTF-8 encoding, such as single 0x80-0xFF bytes (e.g, from
247+
Latin1/Windows-1251 encoding) are decoded using surrogate escape, the same
248+
method used by Python for filesystem paths. So 0xE6 ("æ" in Latin1, r'\\346'
249+
from Git) is decoded as "\udce6". See https://peps.python.org/pep-0383/ and
250+
https://vstinner.github.io/painful-history-python-filesystem-encoding.html
251+
252+
Also see notes on `windows/non-ascii-paths.txt` about path encodings on
253+
non-UTF-8 platforms and filesystems.
246254
"""
247255
if path and path[0] == '"':
248256
# Python 2: path = path[1:-1].decode("string-escape")
249257
# Python 3: https://stackoverflow.com/a/46650050/624066
250258
path = (path[1:-1] # Remove enclosing double quotes
251259
.encode('latin1') # Convert to bytes, required by 'unicode-escape'
252260
.decode('unicode-escape') # Perform the actual octal-escaping decode
253-
.encode('latin1') # 1:1 mapping to bytes, forming UTF-8 encoding
254-
.decode('utf8')) # Decode from UTF-8
261+
.encode('latin1') # 1:1 mapping to bytes, UTF-8 encoded
262+
.decode('utf8', 'surrogateescape')) # Decode from UTF-8
255263
if NORMALIZE_PATHS:
256264
# Make sure the slash matches the OS; for Windows we need a backslash
257265
path = os.path.normpath(path)
@@ -292,7 +300,7 @@ def get_mtime_ns(secs: int, idx: int):
292300

293301

294302
def get_mtime_path(path):
295-
return os.path.getmtime(path)
303+
return os.stat(path, **UTIME_KWS).st_mtime
296304

297305

298306
# Git class and parse_log(), the heart of the script ##########################
@@ -318,8 +326,8 @@ class Git:
318326

319327
def log(self, merge=False, first_parent=False, commit_time=False,
320328
reverse_order=False, paths: list = None):
321-
cmd = 'log --raw --pretty={}'.format('%ct' if commit_time else '%at')
322-
if merge: cmd += ' -m'
329+
cmd = 'log --raw --no-show-signature --pretty={}'.format('%ct' if commit_time else '%at')
330+
cmd += ' -m' if merge else ' --no-merges'
323331
if first_parent: cmd += ' --first-parent'
324332
if reverse_order: cmd += ' --reverse'
325333
return self._run(cmd, paths)
@@ -345,7 +353,7 @@ class Git:
345353
if paths:
346354
cmdlist.append('--')
347355
cmdlist.extend(paths)
348-
popen_args = dict(universal_newlines=True, encoding='utf8')
356+
popen_args = dict(text=True, encoding='utf8')
349357
if not self.errors:
350358
popen_args['stderr'] = subprocess.DEVNULL
351359
log.trace("Executing: %s", ' '.join(cmdlist))

0 commit comments

Comments
 (0)