Repository: borgbackup/borg

Issue:
borg list --dry-run status chars, shall we change them?
Current output (borg <= 2.0):
```
- /some/file              # "-" means "not backed up" (due to dry-run)
x /some/other/file        # "x" means excluded
```
pstn on IRC found it confusing that they are not related to the exclusion chars (+, -, !).

So, we could actually do that, for borg >= 2.0:
```
+ /some/file              # included (but "not backed up" due to dry-run)
- /some/other/file        # excluded
! /some/other/file        # excluded no-recurse
```


Discussion:

Patch:
diff --git a/src/borg/archive.py b/src/borg/archive.py
index a7b457a5..af869871 100644
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -2341,10 +2341,10 @@ def process_items(self, archive, target):
 
         for item in archive.iter_items():
             if not matcher.match(item.path):
-                self.print_file_status("x", item.path)
+                self.print_file_status("-", item.path)  # excluded (either by "-" or by "!")
                 continue
             if self.dry_run:
-                self.print_file_status("-", item.path)
+                self.print_file_status("+", item.path)  # included
             else:
                 self.process_item(archive, target, item)
         if self.progress:
diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py
index 7c77d14e..b3d2bf7b 100644
--- a/src/borg/archiver/create_cmd.py
+++ b/src/borg/archiver/create_cmd.py
@@ -85,7 +85,7 @@ def create_inner(archive, cache, fso):
                         self.print_error("%s: %s", path, e)
                         return self.exit_code
                 else:
-                    status = "-"
+                    status = "+"  # included
                 self.print_file_status(status, path)
             elif args.paths_from_command or args.paths_from_stdin:
                 paths_sep = eval_escapes(args.paths_delimiter) if args.paths_delimiter is not None else "\n"
@@ -145,7 +145,7 @@ def create_inner(archive, cache, fso):
                                 status = "E"
                                 self.print_warning("%s: %s", path, e)
                         else:
-                            status = "-"
+                            status = "+"  # included
                         self.print_file_status(status, path)
                         if not dry_run and status is not None:
                             fso.stats.files_stats[status] += 1
@@ -285,7 +285,7 @@ def _process_any(self, *, path, parent_fd, name, st, fso, cache, read_special, d
         """
 
         if dry_run:
-            return "-"
+            return "+"  # included
         elif stat.S_ISREG(st.st_mode):
             return fso.process_file(path=path, parent_fd=parent_fd, name=name, st=st, cache=cache)
         elif stat.S_ISDIR(st.st_mode):
@@ -373,7 +373,7 @@ def _rec_walk(
                 with backup_io("stat"):
                     st = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)
             else:
-                self.print_file_status("x", path)
+                self.print_file_status("-", path)  # excluded
                 # get out here as quickly as possible:
                 # we only need to continue if we shall recurse into an excluded directory.
                 # if we shall not recurse, then do not even touch (stat()) the item, it
@@ -397,7 +397,7 @@ def _rec_walk(
                 # Ignore if nodump flag is set
                 with backup_io("flags"):
                     if get_flags(path=path, st=st) & stat.UF_NODUMP:
-                        self.print_file_status("x", path)
+                        self.print_file_status("-", path)  # excluded
                         return
 
             if not stat.S_ISDIR(st.st_mode):
@@ -447,13 +447,13 @@ def _rec_walk(
                                             read_special=read_special,
                                             dry_run=dry_run,
                                         )
-                                self.print_file_status("x", path)
+                                self.print_file_status("-", path)  # excluded
                             return
                     if not recurse_excluded_dir:
                         if not dry_run:
                             status = fso.process_dir_with_fd(path=path, fd=child_fd, st=st)
                         else:
-                            status = "-"
+                            status = "+"  # included (dir)
                     if recurse:
                         with backup_io("scandir"):
                             entries = helpers.scandir_inorder(path=path, fd=child_fd)
@@ -630,9 +630,9 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
 
         Other flags used include:
 
+        - '+' = included, item would be backed up (if not in dry-run mode)
+        - '-' = excluded, item would not be / was not backed up
         - 'i' = backup data was read from standard input (stdin)
-        - '-' = dry run, item was *not* backed up
-        - 'x' = excluded, item was *not* backed up
         - '?' = missing status code (if you see this, please file a bug report!)
 
         Reading from stdin


Test patch:
diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py
index cde67d8b..7c608896 100644
--- a/src/borg/testsuite/archiver/create_cmd.py
+++ b/src/borg/testsuite/archiver/create_cmd.py
@@ -332,8 +332,8 @@ def test_create_pattern(self):
             "input",
         )
         self.assert_in("A input/file_important", output)
-        self.assert_in("x input/file1", output)
-        self.assert_in("x input/file2", output)
+        self.assert_in("- input/file1", output)
+        self.assert_in("- input/file2", output)
 
     def test_create_pattern_file(self):
         """test file patterns during create"""
@@ -353,9 +353,9 @@ def test_create_pattern_file(self):
             "input",
         )
         self.assert_in("A input/file_important", output)
-        self.assert_in("x input/file1", output)
-        self.assert_in("x input/file2", output)
-        self.assert_in("x input/otherfile", output)
+        self.assert_in("- input/file1", output)
+        self.assert_in("- input/file2", output)
+        self.assert_in("- input/otherfile", output)
 
     def test_create_pattern_exclude_folder_but_recurse(self):
         """test when patterns exclude a parent folder, but include a child"""
@@ -376,7 +376,7 @@ def test_create_pattern_exclude_folder_but_recurse(self):
             "test",
             "input",
         )
-        self.assert_in("x input/x/a/foo_a", output)
+        self.assert_in("- input/x/a/foo_a", output)
         self.assert_in("A input/x/b/foo_b", output)
         self.assert_in("A input/y/foo_y", output)
 
@@ -645,7 +645,7 @@ def test_file_status_excluded(self):
         self.assert_in("A input/file1", output)
         self.assert_in("A input/file2", output)
         if has_lchflags:
-            self.assert_in("x input/file3", output)
+            self.assert_in("- input/file3", output)
         # should find second file as excluded
         output = self.cmd(
             f"--repo={self.repository_location}",
@@ -658,9 +658,9 @@ def test_file_status_excluded(self):
             "*/file2",
         )
         self.assert_in("U input/file1", output)
-        self.assert_in("x input/file2", output)
+        self.assert_in("- input/file2", output)
         if has_lchflags:
-            self.assert_in("x input/file3", output)
+            self.assert_in("- input/file3", output)
 
     def test_file_status_counters(self):
         """Test file status counters in the stats of `borg create --stats`"""
diff --git a/src/borg/testsuite/archiver/recreate_cmd.py b/src/borg/testsuite/archiver/recreate_cmd.py
index 435cafb2..0cbb864d 100644
--- a/src/borg/testsuite/archiver/recreate_cmd.py
+++ b/src/borg/testsuite/archiver/recreate_cmd.py
@@ -241,22 +241,22 @@ def test_recreate_list_output(self):
         )
         self.check_cache()
         self.assert_in("input/file1", output)
-        self.assert_in("x input/file2", output)
+        self.assert_in("- input/file2", output)
 
         output = self.cmd(f"--repo={self.repository_location}", "recreate", "-a", "test", "--list", "-e", "input/file3")
         self.check_cache()
         self.assert_in("input/file1", output)
-        self.assert_in("x input/file3", output)
+        self.assert_in("- input/file3", output)
 
         output = self.cmd(f"--repo={self.repository_location}", "recreate", "-a", "test", "-e", "input/file4")
         self.check_cache()
         self.assert_not_in("input/file1", output)
-        self.assert_not_in("x input/file4", output)
+        self.assert_not_in("- input/file4", output)
 
         output = self.cmd(f"--repo={self.repository_location}", "recreate", "-a", "test", "--info", "-e", "input/file5")
         self.check_cache()
         self.assert_not_in("input/file1", output)
-        self.assert_not_in("x input/file5", output)
+        self.assert_not_in("- input/file5", output)
 
     def test_comment(self):
         self.create_regular_file("file1", size=1024 * 80)