From b844dd76454dc2052ae01c0d16e34dd8b041e61b Mon Sep 17 00:00:00 2001 From: hiepau1231 Date: Sun, 5 Apr 2026 14:13:00 +0700 Subject: [PATCH] fix: DiffFormatter.format_time() uses microsecond precision for sub-second timestamp diffs Co-Authored-By: Claude Opus 4.6 --- src/borg/helpers/parseformat.py | 9 +++- .../testsuite/helpers/parseformat_test.py | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index bb38092d0..a9a5e2671 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -27,7 +27,7 @@ from .errors import Error from .fs import get_keys_dir, make_path_safe, slashify from .argparsing import Action, ArgumentError, ArgumentTypeError, register_type from .msgpack import Timestamp -from .time import OutputTimestamp, format_time, safe_timestamp +from .time import OutputTimestamp, format_time, format_timestamp_pair, safe_timestamp from .. import __version__ as borg_version from .. import __version_tuple__ as borg_version_tuple from ..constants import * # NOQA @@ -1235,7 +1235,12 @@ class DiffFormatter(BaseFormatter): def format_time(self, key, diff: "ItemDiff"): change = diff.changes().get(key) - return f"[{key}: {change.diff_data['item1']} -> {change.diff_data['item2']}]" if change else "" + if not change: + return "" + ts1 = change.diff_data["item1"].ts + ts2 = change.diff_data["item2"].ts + s1, s2 = format_timestamp_pair(ts1, ts2) + return f"[{key}: {s1} -> {s2}]" def format_iso_time(self, key, diff: "ItemDiff"): change = diff.changes().get(key) diff --git a/src/borg/testsuite/helpers/parseformat_test.py b/src/borg/testsuite/helpers/parseformat_test.py index c9cc1b5d5..f976d8a4c 100644 --- a/src/borg/testsuite/helpers/parseformat_test.py +++ b/src/borg/testsuite/helpers/parseformat_test.py @@ -25,8 +25,10 @@ from ...helpers.parseformat import ( swidth_slice, eval_escapes, ChunkerParams, + DiffFormatter, ) from ...helpers.time import format_timedelta, parse_timestamp +from ...item import ItemDiff, Item from ...platformflags import is_win32 @@ -642,3 +644,44 @@ def test_valid_chunkerparams(chunker_params, expected_return): def test_invalid_chunkerparams(invalid_chunker_params): with pytest.raises(ArgumentTypeError): ChunkerParams(invalid_chunker_params) + + +def _make_item_with_ctime(ctime_ns: int) -> Item: + """Helper: create a minimal Item with the given ctime nanoseconds. + Item.ctime is a PropDictProperty(int, encode=int_to_timestamp) — set via + attribute assignment, not dict subscript. + """ + return Item(path="test/file", mode=0o100644, mtime=0, ctime=ctime_ns) + + +def test_diff_formatter_format_time_shows_microseconds_when_same_second(): + """DiffFormatter.format_time() must use microsecond precision when + two ctimes differ only at sub-second level (the BSD hardlink issue).""" + # Two nanosecond timestamps that are the same second but different microsecond + # 2025-11-05 17:45:53.000123 UTC → 1746467153000123000 ns + # 2025-11-05 17:45:53.000456 UTC → 1746467153000456000 ns + ctime1_ns = 1746467153_000123_000 + ctime2_ns = 1746467153_000456_000 + + item1 = _make_item_with_ctime(ctime1_ns) + item2 = _make_item_with_ctime(ctime2_ns) + + diff = ItemDiff( + path="test/file", + item1=item1, + item2=item2, + chunk_1=iter([]), + chunk_2=iter([]), + can_compare_chunk_ids=True, + ) + + fmt = DiffFormatter("{ctime} {path}{NL}", content_only=False) + result = fmt.format_item(diff) + + # Must contain a dot — microseconds visible + assert "." in result, f"Expected microseconds in output, got: {result!r}" + # Must not look like [ctime: X -> X] (same string both sides) + import re + m = re.search(r"\[ctime: (.+?) -> (.+?)\]", result) + assert m is not None + assert m.group(1) != m.group(2), "Timestamps should differ in output"