From da3aa07a754d490e729edf351d4799de480b6cf9 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Fri, 24 Oct 2025 20:41:07 +0200 Subject: [PATCH 1/5] add tests --- .coveragerc | 2 + .gitignore | 2 + test/test_git_restore_mtime.py | 175 +++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 .coveragerc create mode 100644 test/test_git_restore_mtime.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..16fede8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +concurrency = multiprocessing diff --git a/.gitignore b/.gitignore index a834a23..eb91213 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ git_restore_mtime.py +.coverage +__pycache__ diff --git a/test/test_git_restore_mtime.py b/test/test_git_restore_mtime.py new file mode 100644 index 0000000..52041cd --- /dev/null +++ b/test/test_git_restore_mtime.py @@ -0,0 +1,175 @@ +import multiprocessing +from importlib.util import spec_from_loader, module_from_spec +from importlib.machinery import SourceFileLoader +import sys +import os +import tempfile +import shutil +import subprocess + + +def get_git_restore_mtime(tmpdir, *args): + sys.argv = args + spec = spec_from_loader("__main__", SourceFileLoader("__main__", "git-restore-mtime")) + git_restore_mtime = module_from_spec(spec) + spec.loader.exec_module(git_restore_mtime) + + +def git_init(d): + subprocess.check_call(["git", "init", d]) + subprocess.check_call(["git", "config", "user.email", "test@example.com"], cwd=d) + subprocess.check_call(["git", "config", "user.name", "test test"], cwd=d) + + +def git_add(d, contents="", symlink=None, files=[]): + for file in files: + if symlink: + os.symlink(symlink, f"{d}/{file}") + elif contents: + with open(f"{d}/{file}", 'w') as f: + f.write(contents) + subprocess.check_call(["git", "add", file], cwd=d) + + +def git_commit(d, date): + subprocess.check_call(["git", "commit", "--allow-empty-message", "-m", "", "--date", date, "--no-edit"], cwd=d) + + +def git_switch(d, branch, create=False): + cmd_list = ["git", "switch"] + if create: + cmd_list += ["-c", branch] + else: + cmd_list += [branch] + + subprocess.check_call(cmd_list, cwd=d) + + +def git_merge(d, branch): + subprocess.check_call(["git", "merge", "--no-ff", branch, "--no-edit", "-s", "ours"], cwd=d) + + +def get_mtime_path(path): + return os.stat(path).st_mtime + + +def test_main(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1", "file2", "☺"]) + git_commit(git_dir, date="1761123456 UTC") + git_add(git_dir, "b", files=["file2"]) + git_commit(git_dir, date="1761123457 UTC") + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir)) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 + assert get_mtime_path(f"{git_dir}/file2") == 1761123457.0 + + +def test_skip_older_than(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1"]) + git_add(git_dir, symlink="file1", files=["file2"]) + git_commit(git_dir, date="1761123456 UTC") + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--skip-older-than", "-1000000000")) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") != 1761123456.0 + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--skip-older-than", "1000000000")) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 + + +def test_unique_times(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1"]) + git_commit(git_dir, date="1761123456 UTC") + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--unique-times")) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") == 1761123456.000001 + + +def test_verbose(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1"]) + git_commit(git_dir, date="1761123456 UTC") + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--verbose")) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 + + +def test_test(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1"]) + git_commit(git_dir, date="1761123456 UTC") + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--test")) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") != 1761123456.0 + + +def test_missing(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1"]) + git_commit(git_dir, date="1761123456 UTC") + + git_switch(git_dir, "branch1", True) + + git_add(git_dir, "a", files=["file2"]) + git_commit(git_dir, date="1761123456 UTC") + + git_switch(git_dir, "master") + + git_switch(git_dir, "branch2", True) + + git_add(git_dir, "b", files=["file2"]) + git_commit(git_dir, date="1761123456 UTC") + + git_switch(git_dir, "branch1") + + git_merge(git_dir, "branch2") + + git_switch(git_dir, "master") + + git_merge(git_dir, "branch1") + + subprocess.check_call(["git", "log", "--graph", "--raw", "--no-show-signature"], cwd=git_dir) + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--merge")) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 From ed3ee2738b4733b6960e7f23d3ccb6962c190785 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sun, 26 Oct 2025 15:25:47 +0100 Subject: [PATCH 2/5] fix get_mtime_path --- test/test_git_restore_mtime.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/test/test_git_restore_mtime.py b/test/test_git_restore_mtime.py index 52041cd..8df061e 100644 --- a/test/test_git_restore_mtime.py +++ b/test/test_git_restore_mtime.py @@ -45,10 +45,6 @@ def git_switch(d, branch, create=False): subprocess.check_call(cmd_list, cwd=d) -def git_merge(d, branch): - subprocess.check_call(["git", "merge", "--no-ff", branch, "--no-edit", "-s", "ours"], cwd=d) - - def get_mtime_path(path): return os.stat(path).st_mtime @@ -148,28 +144,21 @@ def test_missing(): git_switch(git_dir, "branch1", True) - git_add(git_dir, "a", files=["file2"]) - git_commit(git_dir, date="1761123456 UTC") - - git_switch(git_dir, "master") - - git_switch(git_dir, "branch2", True) - git_add(git_dir, "b", files=["file2"]) - git_commit(git_dir, date="1761123456 UTC") - - git_switch(git_dir, "branch1") - - git_merge(git_dir, "branch2") + git_commit(git_dir, date="1761123457 UTC") git_switch(git_dir, "master") - git_merge(git_dir, "branch1") + subprocess.check_call(["git", "merge", "--no-ff", "--no-commit", "branch1"], cwd=git_dir) - subprocess.check_call(["git", "log", "--graph", "--raw", "--no-show-signature"], cwd=git_dir) + git_add(git_dir, "b", files=["file3"]) - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--merge")) + git_commit(git_dir, date="1761123458 UTC") + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir)) p.start() p.join() assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 + assert get_mtime_path(f"{git_dir}/file2") == 1761123457.0 + assert get_mtime_path(f"{git_dir}/file3") == 1761123458.0 From 5921e4b07694f135b65b598971b11b0ca991ec47 Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sun, 26 Oct 2025 15:37:24 +0100 Subject: [PATCH 3/5] add dirty tests --- git-restore-mtime | 1 + test/test_git_restore_mtime.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/git-restore-mtime b/git-restore-mtime index 77ce215..d9f7e9e 100755 --- a/git-restore-mtime +++ b/git-restore-mtime @@ -518,6 +518,7 @@ def main(): # Otherwise, ignore any dirty files else: dirty = set(git.ls_dirty()) + print(dirty) if dirty: log.warning("WARNING: Modified files in the working directory were ignored." "\nTo include such files, commit your changes or use --force.") diff --git a/test/test_git_restore_mtime.py b/test/test_git_restore_mtime.py index 8df061e..39c7b3b 100644 --- a/test/test_git_restore_mtime.py +++ b/test/test_git_restore_mtime.py @@ -162,3 +162,42 @@ def test_missing(): assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 assert get_mtime_path(f"{git_dir}/file2") == 1761123457.0 assert get_mtime_path(f"{git_dir}/file3") == 1761123458.0 + + +def test_dirty(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1"]) + git_commit(git_dir, date="1761123456 UTC") + + with open(f'{git_dir}/file1', 'w') as f: + f.write("dirty") + + subprocess.check_call(["git", "status", "--porcelain"], cwd=git_dir) + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir)) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") != 1761123456.0 + +def test_force(): + with tempfile.TemporaryDirectory() as tmpdir: + git_dir = f"{tmpdir}/git" + + git_init(git_dir) + git_add(git_dir, "a", files=["file1"]) + git_commit(git_dir, date="1761123456 UTC") + + with open(f'{git_dir}/file1', 'w') as f: + f.write("dirty") + + subprocess.check_call(["git", "status", "--porcelain"], cwd=git_dir) + + p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--force")) + p.start() + p.join() + + assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 From 6354f4e362b2c3b28b2ff5edc650da66aafcb68d Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Thu, 18 Dec 2025 21:23:20 +0100 Subject: [PATCH 4/5] remove debug leftover --- git-restore-mtime | 1 - 1 file changed, 1 deletion(-) diff --git a/git-restore-mtime b/git-restore-mtime index d9f7e9e..77ce215 100755 --- a/git-restore-mtime +++ b/git-restore-mtime @@ -518,7 +518,6 @@ def main(): # Otherwise, ignore any dirty files else: dirty = set(git.ls_dirty()) - print(dirty) if dirty: log.warning("WARNING: Modified files in the working directory were ignored." "\nTo include such files, commit your changes or use --force.") From 16a210fb58e65484ab8b23b7c70fec39ae4db2cf Mon Sep 17 00:00:00 2001 From: Dick Marinus Date: Sun, 21 Dec 2025 16:42:59 +0100 Subject: [PATCH 5/5] wip --- git-restore-mtime | 69 ++++++++++--------- ...tore_mtime.py => test_git_restore_mtime.py | 49 +++---------- 2 files changed, 48 insertions(+), 70 deletions(-) rename test/test_git_restore_mtime.py => test_git_restore_mtime.py (71%) diff --git a/git-restore-mtime b/git-restore-mtime index 77ce215..0fc6fad 100755 --- a/git-restore-mtime +++ b/git-restore-mtime @@ -62,7 +62,8 @@ assuming the actual modification date and its commit date are close. # painfully slow. First pass without merge commits is not accurate. Maybe add a new # `--accurate` mode for `--cc`? -if __name__ != "__main__": +import sys +if __name__ != "__main__" and "pytest" not in sys.modules: raise ImportError("{} should not be used as a module.".format(__name__)) import argparse @@ -72,7 +73,6 @@ import os.path import shlex import signal import subprocess -import sys import time if sys.version_info < (3, 8): @@ -95,7 +95,7 @@ UTIME_KWS = {} if not UPDATE_SYMLINKS else {'follow_symlinks': False} # Command-line interface ###################################################### -def parse_args(): +def parse_args(argv=None): parser = argparse.ArgumentParser( description=__doc__.split('\n---')[0]) @@ -204,10 +204,25 @@ def parse_args(): parser.add_argument('--version', '-V', action='version', version='%(prog)s version {version}'.format(version=get_version())) - args_ = parser.parse_args() + args_ = parser.parse_args(argv) if args_.verbose: args_.loglevel = max(logging.TRACE, logging.DEBUG // args_.verbose) args_.debug = args_.loglevel <= logging.DEBUG + + # Keep only essential, global assignments here. Any other logic must be in main() + + # Set the actual touch() and other functions based on command-line arguments + if args_.unique_times: + args_.touch = touch_ns + args_.isodate = isodate_ns + else: + args_.touch = touch + args_.isodate = isodate + + # Make sure this is always set last to ensure --test behaves as intended + if args_.test: + args_.touch = dummy + return args_ @@ -375,9 +390,9 @@ class Git: """Error from git executable""" -def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None): +def parse_log(args, filelist, dirlist, stats, git, merge=False, filterlist=None): mtime = 0 - datestr = isodate(0) + datestr = args.isodate(0) for line in git.log( merge, args.first_parent, @@ -398,7 +413,7 @@ def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None): if args.unique_times: mtime = get_mtime_ns(mtime, stats['commits']) if args.debug: - datestr = isodate(mtime) + datestr = args.isodate(mtime) continue # File line: three tokens if it describes a renaming, otherwise two @@ -426,7 +441,7 @@ def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None): stats['loglines'], stats['commits'], stats['files'], datestr, file) try: - touch(os.path.join(git.workdir, file), mtime) + args.touch(os.path.join(git.workdir, file), mtime) stats['touches'] += 1 except Exception as e: log.error("ERROR: %s: %s", e, file) @@ -438,7 +453,7 @@ def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None): stats['loglines'], stats['commits'], datestr, "{}/".format(dirname or '.')) try: - touch(os.path.join(git.workdir, dirname), mtime) + args.touch(os.path.join(git.workdir, dirname), mtime) stats['dirtouches'] += 1 except Exception as e: log.error("ERROR: %s: %s", e, dirname) @@ -463,7 +478,7 @@ def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None): # Main Logic ################################################################## -def main(): +def main(args): start = time.time() # yes, Wall time. CPU time is not realistic for users. stats = {_: 0 for _ in ('loglines', 'commits', 'touches', 'skip', 'errors', 'dirtouches', 'direrrors')} @@ -535,7 +550,7 @@ def main(): # Process the log until all files are 'touched' log.debug("Line #\tLog #\tF.Left\tModification Time\tFile Name") - parse_log(filelist, dirlist, stats, git, args.merge, args.pathspec) + parse_log(args, filelist, dirlist, stats, git, args.merge, args.pathspec) # Missing files if filelist: @@ -546,7 +561,7 @@ def main(): missing = len(filterlist) log.info("{0:,} files not found in log, trying merge commits".format(missing)) for i in range(0, missing, STEPMISSING): - parse_log(filelist, dirlist, stats, git, + parse_log(args, filelist, dirlist, stats, git, merge=True, filterlist=filterlist[i:i + STEPMISSING]) # Still missing some? @@ -584,23 +599,15 @@ def main(): log.info("TEST RUN - No files modified!") -# Keep only essential, global assignments here. Any other logic must be in main() log = setup_logging() -args = parse_args() - -# Set the actual touch() and other functions based on command-line arguments -if args.unique_times: - touch = touch_ns - isodate = isodate_ns - -# Make sure this is always set last to ensure --test behaves as intended -if args.test: - touch = dummy - -# UI done, it's showtime! -try: - sys.exit(main()) -except KeyboardInterrupt: - log.info("\nAborting") - signal.signal(signal.SIGINT, signal.SIG_DFL) - os.kill(os.getpid(), signal.SIGINT) + + +if __name__ == "__main__": + args = parse_args(sys.argv[1:]) + # UI done, it's showtime! + try: + sys.exit(main(args)) + except KeyboardInterrupt: + log.info("\nAborting") + signal.signal(signal.SIGINT, signal.SIG_DFL) + os.kill(os.getpid(), signal.SIGINT) diff --git a/test/test_git_restore_mtime.py b/test_git_restore_mtime.py similarity index 71% rename from test/test_git_restore_mtime.py rename to test_git_restore_mtime.py index 39c7b3b..aa6ede1 100644 --- a/test/test_git_restore_mtime.py +++ b/test_git_restore_mtime.py @@ -1,18 +1,7 @@ -import multiprocessing -from importlib.util import spec_from_loader, module_from_spec -from importlib.machinery import SourceFileLoader -import sys import os import tempfile -import shutil import subprocess - - -def get_git_restore_mtime(tmpdir, *args): - sys.argv = args - spec = spec_from_loader("__main__", SourceFileLoader("__main__", "git-restore-mtime")) - git_restore_mtime = module_from_spec(spec) - spec.loader.exec_module(git_restore_mtime) +from git_restore_mtime import main, parse_args def git_init(d): @@ -59,9 +48,7 @@ def test_main(): git_add(git_dir, "b", files=["file2"]) git_commit(git_dir, date="1761123457 UTC") - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir)) - p.start() - p.join() + main(parse_args(("--cwd", git_dir))) assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 assert get_mtime_path(f"{git_dir}/file2") == 1761123457.0 @@ -76,15 +63,11 @@ def test_skip_older_than(): git_add(git_dir, symlink="file1", files=["file2"]) git_commit(git_dir, date="1761123456 UTC") - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--skip-older-than", "-1000000000")) - p.start() - p.join() + main(parse_args(("--cwd", git_dir, "--skip-older-than", "-1000000000"))) assert get_mtime_path(f"{git_dir}/file1") != 1761123456.0 - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--skip-older-than", "1000000000")) - p.start() - p.join() + main(parse_args(("--cwd", git_dir, "--skip-older-than", "1000000000"))) assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 @@ -97,9 +80,7 @@ def test_unique_times(): git_add(git_dir, "a", files=["file1"]) git_commit(git_dir, date="1761123456 UTC") - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--unique-times")) - p.start() - p.join() + main(parse_args(("--cwd", git_dir, "--unique-times"))) assert get_mtime_path(f"{git_dir}/file1") == 1761123456.000001 @@ -112,9 +93,7 @@ def test_verbose(): git_add(git_dir, "a", files=["file1"]) git_commit(git_dir, date="1761123456 UTC") - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--verbose")) - p.start() - p.join() + main(parse_args(("--cwd", git_dir, "--verbose"))) assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 @@ -127,9 +106,7 @@ def test_test(): git_add(git_dir, "a", files=["file1"]) git_commit(git_dir, date="1761123456 UTC") - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--test")) - p.start() - p.join() + main(parse_args(("--cwd", git_dir, "--test"))) assert get_mtime_path(f"{git_dir}/file1") != 1761123456.0 @@ -155,9 +132,7 @@ def test_missing(): git_commit(git_dir, date="1761123458 UTC") - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir)) - p.start() - p.join() + main(parse_args(("--cwd", git_dir))) assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0 assert get_mtime_path(f"{git_dir}/file2") == 1761123457.0 @@ -177,9 +152,7 @@ def test_dirty(): subprocess.check_call(["git", "status", "--porcelain"], cwd=git_dir) - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir)) - p.start() - p.join() + main(parse_args(("--cwd", git_dir))) assert get_mtime_path(f"{git_dir}/file1") != 1761123456.0 @@ -196,8 +169,6 @@ def test_force(): subprocess.check_call(["git", "status", "--porcelain"], cwd=git_dir) - p = multiprocessing.Process(target=get_git_restore_mtime, args=(tmpdir, "", "--cwd", git_dir, "--force")) - p.start() - p.join() + main(parse_args(("--cwd", git_dir, "--force"))) assert get_mtime_path(f"{git_dir}/file1") == 1761123456.0