borg-diff(1)

๐Ÿ’– borg-diff โ€” the "what even changed between these two archives" command ๐Ÿ’–

okay bestie, sit down, hydrate, and let me tell you about borg-diff. this is the Borg backup tool's little diff detective, and it's about to make comparing two archives your new favorite hobby. no cap. ๐Ÿ•ต๏ธโ™€๏ธโœจ


๐ŸŒธ NAME ๐ŸŒธ

borg-diff โ€” finds differences between two archives. literally its whole personality. ๐Ÿ’…


๐Ÿฆ‹ SYNOPSIS ๐Ÿฆ‹

borg [common options] diff [options] ARCHIVE1 ARCHIVE2 [PATH...]

๐Ÿ‘‘ DESCRIPTION ๐Ÿ‘‘

bestie. this command finds the differences โ€” file contents AND metadata โ€” between ARCHIVE1 and ARCHIVE2. that's it, that's the mission. ๐ŸŽ€

and if you need more help on include/exclude patterns (iykyk, patterns can be a whole thing), go look at the output of the borg_patterns command. it's giving tutorial. ๐Ÿ’œ


โœจ OPTIONS โœจ

for the common options that every Borg command has, go peek at borg-common(1). not me skipping that, it's just literally elsewhere. ๐Ÿซถ

๐Ÿ’ซ arguments ๐Ÿ’ซ

  • ARCHIVE1 โ€” the ARCHIVE1 name. yeah, it's the first archive. she's first for a reason. ๐Ÿ‘‘
  • ARCHIVE2 โ€” the ARCHIVE2 name. the sequel. the "after" picture. ๐ŸŒˆ
  • PATH โ€” paths of items inside the archives that you wanna compare; patterns are supported. so you can be picky, and honestly? slay. ๐Ÿ’…

๐ŸŽ€ options ๐ŸŽ€

  • --numeric-ids โ€” only consider numeric user and group identifiers. no name-matching, just digits, periodt. ๐Ÿ”ข
  • --same-chunker-params โ€” override the check of chunker parameters. when you wanna tell borg "yes babe I know, trust me". ๐Ÿ™ˆ
  • --format FORMAT โ€” specify the format for differences between archives. default is "{change} {path}{NL}". very customizable, very main character. โœจ
  • --json-lines โ€” format output as JSON Lines. one JSON object per line, machine-readable, iconic. ๐Ÿค–
  • -s, --stats โ€” print a summary of the differences at the end. the recap. the little wrap-up montage. ๐Ÿ“Š
  • --sort-by โ€” sort output by comma-separated fields (e.g., '>size_added,path'). your order, your rules. ๐Ÿ”ฅ
  • --content-only โ€” only compare differences in content (exclude metadata differences). metadata girlies, look away. ๐Ÿ˜ญ

๐Ÿงš Include/Exclude options ๐Ÿงš

  • -e PATTERN, --exclude PATTERN โ€” exclude paths matching PATTERN. bye bestie, not you. ๐Ÿ‘‹
  • --exclude-from EXCLUDEFILE โ€” read exclude patterns from EXCLUDEFILE, one per line. bulk exclusion, so efficient. ๐Ÿ’œ
  • --pattern PATTERN โ€” include/exclude paths matching PATTERN. it does both, she's versatile. ๐Ÿ’…
  • --patterns-from PATTERNFILE โ€” read include/exclude patterns from PATTERNFILE, one per line. the whole list, babes. ๐ŸŽ€

๐ŸŒŸ EXAMPLES ๐ŸŒŸ

okay okay okay, watch this. ๐Ÿ“

# By default, borg diff reports metadata changes (mode, mtime, ctime, ...) besides
# the content changes, so the lines can get quite long:
$ borg diff archive1 archive2
[mtime: Fri, 2026-08-28 12:26:23.433047048 +0200 -> Fri, 2026-08-28 12:26:24.204040020 +0200] [ctime: Fri, 2026-08-28 12:26:23.433047048 +0200 -> Fri, 2026-08-28 12:26:24.204040020 +0200] .
modified:    +17 B     -5 B [-rw-r--r-- -> -rwxr-xr-x] [mtime: Fri, 2026-08-28 12:26:23.431245244 +0200 -> Fri, 2026-08-28 12:26:24.199251801 +0200] [ctime: Fri, 2026-08-28 12:26:23.431245244 +0200 -> Fri, 2026-08-28 12:26:24.200688313 +0200] file1
modified:   +135 B   -252 B [mtime: Fri, 2026-08-28 12:26:23.432895166 +0200 -> Fri, 2026-08-28 12:26:24.202236091 +0200] [ctime: Fri, 2026-08-28 12:26:23.432895166 +0200 -> Fri, 2026-08-28 12:26:24.202236091 +0200] file2
added:                  0 B file4
removed:                0 B file3

# --content-only hides the metadata changes and only reports content differences:
$ borg diff --content-only archive1 archive2
modified:    +17 B     -5 B file1
modified:   +135 B   -252 B file2
added:                  0 B file4
removed:                0 B file3

# Use --json-lines to get one JSON object per changed path:
$ borg diff --json-lines --content-only archive1 archive2
{"changes": [{"added": 17, "removed": 5, "type": "modified"}], "path": "file1"}
{"changes": [{"added": 135, "removed": 252, "type": "modified"}], "path": "file2"}
{"changes": [{"added": 0, "removed": 0, "type": "added"}], "path": "file4"}
{"changes": [{"added": 0, "removed": 0, "type": "removed"}], "path": "file3"}

# Use --sort-by with a comma-separated list; sorts apply stably from last to first.
# Here: primary by net size change descending, tie-breaker by path ascending
$ borg diff --content-only --sort-by=">size_diff,path" archive1 archive2
modified:    +17 B     -5 B file1
removed:                0 B file3
added:                  0 B file4
modified:   +135 B   -252 B file2

see how the default one just keeps going and going with the metadata? that's her being thorough, not messy. thorough. ๐Ÿ”ฅ


๐Ÿ“ NOTES ๐Ÿ“

๐Ÿ’… The FORMAT specifier syntax ๐Ÿ’…

the --format option uses Python's format string syntax: https://docs.python.org/3.11/library/string.html#formatstrings

examples, because we love a demo:

$ borg diff --format '{content:30} {path}{NL}' ArchiveFoo ArchiveBar
modified:  +4.1 kB  -1.0 kB    file-diff
...

# {VAR:<NUMBER} - pad to NUMBER columns left-aligned.
# {VAR:>NUMBER} - pad to NUMBER columns right-aligned.
$ borg diff --format '{content:>30} {path}{NL}' ArchiveFoo ArchiveBar
   modified:  +4.1 kB  -1.0 kB file-diff
...

the following keys are always available (always, bestie, no matter what):

  • NEWLINE: OS dependent line separator
  • NL: alias of NEWLINE
  • NUL: NUL character for creating print0 / xargs -0 like output
  • SPACE: space character
  • TAB: tab character
  • CR: carriage return character
  • LF: line feed character

keys available only when showing differences between archives:

  • path: archived file path
  • change: all available changes
  • content: file content change
  • mode: file mode change
  • type: file type change
  • owner: file owner (user/group) change
  • group: file group change
  • user: file user change
  • link: file link change
  • directory: file directory change
  • blkdev: file block device change
  • chrdev: file character device change
  • fifo: file fifo change
  • mtime: file modification time change
  • ctime: file change time change
  • isomtime: file modification time change (ISO 8601)
  • isoctime: file creation time change (ISO 8601)

๐Ÿ” What is compared ๐Ÿ”

for each matching item in both archives, Borg reports:

  • content changes: total added/removed bytes within files. if chunker parameters are comparable, Borg compares chunk IDs quickly โ€” it aligns the two chunk lists of a file like a text diff aligns lines, and the byte counts are the total sizes of the chunks that are not part of that alignment. inserted, removed, moved and duplicated content is therefore accounted for โ€” a chunk that only moved within the file shows up as removed and added again. files with very long or very repetitive chunk lists are not aligned (that would be too slow); for these, only the number of occurrences of each chunk ID is compared, so moved content does not show up in their byte counts. if chunker parameters are not comparable, Borg compares the content. in that case, borg can only tell that a file was modified, not by how much: no byte counts are given for it, and the text output shows modified: (can't get size) instead. ๐Ÿ˜ญ
  • metadata changes: user, group, mode, and other metadata shown inline as "[old -> new]", like "[-rw-r--r-- -> -rwxr-xr-x]" for a mode change. use --content-only to suppress metadata changes. ๐Ÿ™ˆ
  • added/removed items: printed as "added: SIZE path" or "removed: SIZE path". ๐Ÿ’€

๐ŸŽ€ Output formats ๐ŸŽ€

the default (text) output shows one line per changed path, e.g.:

modified:    +23 B     -5 B [-rwxr-xr-x -> -rw-r--r--] path/to/file
added:                  4 B path/to/added-file
removed:                5 B path/to/removed-file

JSON Lines output (--json-lines) prints one JSON object per changed path, with a list of change objects. each change object has a "type" plus type-specific data: content changes ("added", "removed", "modified") carry "added"/"removed" byte counts โ€” EXCEPT for a "modified" that was determined by comparing the content, which carries no counts at all; metadata changes ("changed mode", "changed owner", "mtime", ...) carry the old and new values as "item1" and "item2". example:

{"changes": [{"added": 23, "removed": 5, "type": "modified"}], "path": "path/to/file"}
{"changes": [{"type": "modified"}], "path": "path/to/other-file"}
{"changes": [{"item1": "-rw-r--r--", "item2": "-rwxr-xr-x", "type": "changed mode"}], "path": "some/file"}
{"changes": [{"added": 4, "removed": 0, "type": "added"}], "path": "path/to/added-file"}
{"changes": [{"added": 0, "removed": 5, "type": "removed"}], "path": "path/to/removed-file"}

๐Ÿ“Š Statistics ๐Ÿ“Š

with --stats, borg prints a summary of the differences after the per-path output:

Added items: 23
Removed items: 2
Changed items: 315
Added size: 53.70 MB
Removed size: 51.10 MB

"Added"/"Removed" items only exist in ARCHIVE2/ARCHIVE1. "changed" items exist in both archives but differ. "Added size"/"Removed size" sum up the file content (in bytes) added/removed by all of these items, i.e. the per-path byte counts that --sort-by size_added / size_removed sort by. items whose content borg could only compare byte by byte (see "Performance considerations" below) contribute no byte counts; if there are any, an additional "Items with unknown size changes" line reports how many. ๐Ÿงฎ

together with --json-lines, the summary is emitted as a final JSON line of the shape {"stats": {...}} instead, so it is easy to tell apart from the per-path lines (wrapped here for readability, borg prints it as a single line):

{"stats": {"added_items": 23, "changed_items": 315, "removed_items": 2,
           "size_added": 53700000, "size_removed": 51100000, "unknown_size_items": 0}}

๐Ÿ”ฅ Sorting ๐Ÿ”ฅ

use --sort-by FIELDS where FIELDS is a comma-separated list of fields. sorts are applied stably from last to first in the given list. prepend ">" for descending, "<" (or no prefix) for ascending, for example --sort-by=">size_added,path". supported fields include:

  • path: the item path
  • size_added: total bytes added for the item content
  • size_removed: total bytes removed for the item content
  • size_diff: size_added - size_removed (net content change)
  • size: size of the item as stored in ARCHIVE2 (0 for removed items)
  • user, group, uid, gid, ctime, mtime: taken from the item state in ARCHIVE2 when present
  • ctime_diff, mtime_diff: timestamp difference (ARCHIVE2 - ARCHIVE1)

โšก Performance considerations โšก

diff automatically detects whether the archives were created with the same chunker parameters. if so, only chunk IDs are compared, which is very fast. speed demon behavior, we love to see it. ๐ŸŽ๏ธ


๐ŸŒˆ SEE ALSO ๐ŸŒˆ

borg-common(1)


๐Ÿ‘‘ Author ๐Ÿ‘‘

The Borg Collective. legends, honestly. ๐Ÿซถ


okay that's the whole page bestie, you're officially a borg-diff expert, go forth and diff responsibly ๐Ÿ’–โœจ this was borg backup tool, borg-diff(1), from 2026-10-07 โ€” mwah, love u, bye! ๐ŸŽ€๐Ÿฆ‹

๐Ÿ“ฆ where this page comes from