Repository: VirtusLab/git-machete
Pull request #1236 (2024-04-25 08:25:52)

Make `...=no` qualifiers work for `advance` and `slide-out`


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 9c32da0..2d4d038 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -3,6 +3,8 @@
 ## New in git-machete 3.25.3
 
 - fixed: `-y` option in `git machete traverse` automatically sets `--no-edit-merge` flag, to retain behavior when the `update=merge` qualifier is set (contributed by @gjulianm)
+- fixed: `push=no` and `slide-out=no` qualifiers now work in `git machete advance` now
+- fixed: `rebase=no` qualifier now works in `git machete slide-out`
 
 ## New in git-machete 3.25.2
 
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index d7f2e96..45933f4 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "GIT-MACHETE" "1" "Apr 24, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Apr 25, 2024" "" "git-machete"
 .SH NAME
 git-machete \- git-machete 3.25.3
 .sp
@@ -246,6 +246,10 @@ o\-develop *
 .sp
 Note that the current branch after the operation is still \fBdevelop\fP, just pointing to \fBfeature/add\-from\-remote\fP\(aqs tip now.
 .sp
+If the current branch \fBC\fP is annotated with \fBpush=no\fP qualifier, the push is not performed.
+If the downstream branch \fBD\fP is annotated with \fBslide\-out=no\fP qualifier, the slide\-out is not performed.
+See help for \fI\%traverse\fP for more details on the qualifiers.
+.sp
 \fBOptions:\fP
 .INDENT 0.0
 .TP
@@ -1706,13 +1710,16 @@ The provided branches must form a chain \-\-\- all of the following conditions m
 .IP \(bu 2
 for i=1..N\-1, (i+1)\-th branch must be the only downstream (child) branch of the i\-th branch,
 .IP \(bu 2
-all provided branches must have an upstream branch (so, in other words, roots of branch dependency tree cannot be slid out).
+all provided branches must have an upstream branch (so, in other words, roots of branch layout cannot be slid out).
 .UNINDENT
 .UNINDENT
 .UNINDENT
 .sp
 Note: Unless \fB\-\-delete\fP is passed, \fBslide\-out\fP doesn\(aqt delete any branches from git, just removes them from the tree of branch dependencies.
 .sp
+Note: if a child branch is annotated with \fBrebase=no\fP qualifier, the rebase is not performed.
+See help for \fI\%traverse\fP for more details on the qualifiers.
+.sp
 \fBOptions:\fP
 .INDENT 0.0
 .TP
diff --git a/docs/source/cli/advance.rst b/docs/source/cli/advance.rst
index 7eb8f92..7e4a99e 100644
--- a/docs/source/cli/advance.rst
+++ b/docs/source/cli/advance.rst
@@ -56,6 +56,10 @@ After ``advance`` completes, ``status`` will show:
 
 Note that the current branch after the operation is still ``develop``, just pointing to ``feature/add-from-remote``'s tip now.
 
+If the current branch ``C`` is annotated with ``push=no`` qualifier, the push is not performed.
+If the downstream branch ``D`` is annotated with ``slide-out=no`` qualifier, the slide-out is not performed.
+See help for :ref:`traverse` for more details on the qualifiers.
+
 **Options:**
 
 -y, --yes         Don't ask for confirmation whether to fast-forward the current branch or whether to slide-out the downstream.
diff --git a/docs/source/cli/slide-out.rst b/docs/source/cli/slide-out.rst
index eff4c94..3a57016 100644
--- a/docs/source/cli/slide-out.rst
+++ b/docs/source/cli/slide-out.rst
@@ -44,10 +44,13 @@ The most common use is to slide out a single branch whose upstream was a ``devel
 The provided branches must form a chain --- all of the following conditions must be met:
 
     * for i=1..N-1, (i+1)-th branch must be the only downstream (child) branch of the i-th branch,
-    * all provided branches must have an upstream branch (so, in other words, roots of branch dependency tree cannot be slid out).
+    * all provided branches must have an upstream branch (so, in other words, roots of branch layout cannot be slid out).
 
 Note: Unless ``--delete`` is passed, ``slide-out`` doesn't delete any branches from git, just removes them from the tree of branch dependencies.
 
+Note: if a child branch is annotated with ``rebase=no`` qualifier, the rebase is not performed.
+See help for :ref:`traverse` for more details on the qualifiers.
+
 **Options:**
 
 -d, --down-fork-point=<down-fork-point-commit>    If updating by rebase, specifies the alternative fork point for downstream branches for the operation.
diff --git a/git_machete/annotation.py b/git_machete/annotation.py
index de32369..07841fd 100644
--- a/git_machete/annotation.py
+++ b/git_machete/annotation.py
@@ -9,11 +9,11 @@ class Qualifiers:
         self.__push_text: str = ''
         self.__rebase_text: str = ''
         self.__slide_out_text: str = ''
-        self.__merge_text: str = ""
+        self.__update_with_merge_text: str = ""
         self.rebase: bool = True
         self.push: bool = True
         self.slide_out: bool = True
-        self.merge: bool = False
+        self.update_with_merge: bool = False
 
         def match_pattern(pattern: str) -> str:
             return f".*\\b{pattern}\\b.*"
@@ -42,18 +42,18 @@ class Qualifiers:
             self.__slide_out_text = slide_out_anno
             self.__annotation_without_qualifiers = re.sub(sub_pattern(slide_out_anno), ' ', self.__annotation_without_qualifiers)
 
-        merge_anno = 'update=merge'
-        merge_match = re.match(match_pattern(merge_anno), annotation)
-        if merge_match:
-            self.merge = True
-            self.__merge_text = merge_anno
-            self.__annotation_without_qualifiers = re.sub(sub_pattern(merge_anno), ' ', self.__annotation_without_qualifiers)
+        update_with_merge_anno = 'update=merge'
+        update_with_merge_match = re.match(match_pattern(update_with_merge_anno), annotation)
+        if update_with_merge_match:
+            self.update_with_merge = True
+            self.__update_with_merge_text = update_with_merge_anno
+            self.__annotation_without_qualifiers = re.sub(sub_pattern(update_with_merge_anno), ' ', self.__annotation_without_qualifiers)
 
     def get_annotation_text_without_qualifiers(self) -> str:
         return self.__annotation_without_qualifiers.strip()
 
     def get_qualifiers_text(self) -> str:
-        text = f'{self.__rebase_text} {self.__push_text} {self.__slide_out_text} {self.__merge_text}'
+        text = f'{self.__rebase_text} {self.__push_text} {self.__slide_out_text} {self.__update_with_merge_text}'
         return re.sub(' +', ' ', text).strip()
 
 
diff --git a/git_machete/client.py b/git_machete/client.py
index 33bd5a3..adc3a33 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -346,7 +346,7 @@ class MacheteClient:
             self, *, opt_merge: bool, opt_no_edit_merge: bool,
             opt_no_interactive_rebase: bool, opt_fork_point: Optional[AnyRevision]) -> None:
         current_branch = self.__git.get_current_branch()
-        use_merge = opt_merge or (current_branch in self.annotations and self.annotations[current_branch].qualifiers.merge)
+        use_merge = opt_merge or (current_branch in self.annotations and self.annotations[current_branch].qualifiers.update_with_merge)
         if use_merge:
             with_branch = self.up(
                 current_branch,
@@ -515,9 +515,13 @@ class MacheteClient:
                   opt_no_interactive_rebase: bool,
                   opt_no_edit_merge: bool
                   ) -> None:
-        # Verify that all branches exist, are managed, and have an upstream.
+        # Verify that all branches exist, are managed, have an upstream and are NOT annotated with slide-out=no qualifier.
         for branch in branches_to_slide_out:
             self.expect_in_managed_branches(branch)
+            anno = self.annotations.get(branch)
+            if anno and not anno.qualifiers.slide_out:
+                raise MacheteException(f"Branch `{branch}` is annotated with `slide-out=no` qualifier, aborting.\n"
+                                       f"Remove the qualifier using `git machete anno` or edit branch layout file directly.")
             new_upstream = self.__up_branch.get(branch)
             if not new_upstream:
                 raise MacheteException(f"No upstream branch defined for {bold(branch)}, cannot slide out")
@@ -577,12 +581,15 @@ class MacheteClient:
 
         self.__git.checkout(new_upstream)
         for new_downstream in new_downstreams:
-            self.__git.checkout(new_downstream)
-            use_merge = opt_merge or (new_downstream in self.annotations and self.annotations[new_downstream].qualifiers.merge)
+            anno = self.annotations.get(new_downstream)
+            use_merge = opt_merge or (anno and anno.qualifiers.update_with_merge)
+            use_rebase = not use_merge and (not anno or anno.qualifiers.rebase)
+            if use_merge or use_rebase:
+                self.__git.checkout(new_downstream)
             if use_merge:
                 print(f"Merging {bold(new_upstream)} into {bold(new_downstream)}...")
                 self.__git.merge(new_upstream, new_downstream, opt_no_edit_merge)
-            else:
+            elif use_rebase:
                 print(f"Rebasing {bold(new_downstream)} onto {bold(new_upstream)}...")
                 down_fork_point = opt_down_fork_point or self.fork_point(new_downstream, use_overrides=True)
                 self.rebase(
@@ -633,12 +640,13 @@ class MacheteClient:
                 return
 
         remote = self.__git.get_combined_remote_for_fetching_of_branch(branch)
-        if remote:
-            ans = self.ask_if(f"\nBranch {bold(branch)} is now fast-forwarded to match {bold(down_branch)}. "
-                              f"Push {bold(branch)} to {bold(remote)}?" + get_pretty_choices('y', 'N'),
-                              f"\nBranch {bold(branch)} is now fast-forwarded to match {bold(down_branch)}. "
-                              f"Pushing {bold(branch)} to {bold(remote)}...",
-                              opt_yes=opt_yes)
+        anno = self.annotations.get(branch)
+        if remote and (not anno or anno.qualifiers.push):
+            push_msg = f"\nBranch {bold(branch)} is now fast-forwarded to match {bold(down_branch)}. " \
+                f"Push {bold(branch)} to {bold(remote)}?" + get_pretty_choices('y', 'N')
+            opt_yes_push_msg = f"\nBranch {bold(branch)} is now fast-forwarded to match {bold(down_branch)}. " \
+                f"Pushing {bold(branch)} to {bold(remote)}..."
+            ans = self.ask_if(push_msg, opt_yes_push_msg, opt_yes=opt_yes)
             if ans in ('y', 'yes'):
                 self.__git.push(remote, branch)
                 branch_pushed_or_fast_forwarded_msg = f"\nBranch {bold(branch)} is now pushed to {bold(remote)}."
@@ -647,19 +655,22 @@ class MacheteClient:
         else:
             branch_pushed_or_fast_forwarded_msg = f"\nBranch {bold(branch)} is now fast-forwarded to match {bold(down_branch)}."
 
-        ans = self.ask_if(f"{branch_pushed_or_fast_forwarded_msg} Slide {bold(down_branch)} out of the tree of branch dependencies?" +
-                          get_pretty_choices('y', 'N'),
-                          f"{branch_pushed_or_fast_forwarded_msg} Sliding {bold(down_branch)} out of the tree of branch dependencies...",
-                          opt_yes=opt_yes)
-        if ans in ('y', 'yes'):
-            dds = self.__down_branches.get(LocalBranchShortName.of(down_branch)) or []
-            for dd in dds:
-                self.__up_branch[dd] = branch
-            self.__down_branches[branch] = flat_map(
-                lambda bd: dds if bd == down_branch else [bd],
-                self.__down_branches.get(branch) or [])
-            self.save_branch_layout_file()
-            self.__run_post_slide_out_hook(branch, down_branch, dds)
+        down_anno = self.annotations.get(down_branch)
+        if not down_anno or down_anno.qualifiers.slide_out:
+            slide_out_msg = (f"{branch_pushed_or_fast_forwarded_msg} Slide {bold(down_branch)} out "
+                             f"of the tree of branch dependencies?{get_pretty_choices('y', 'N')}")
+            slide_out_opt_yes_msg = (f"{branch_pushed_or_fast_forwarded_msg} Sliding {bold(down_branch)} out "
+                                     "of the tree of branch dependencies...")
+            ans = self.ask_if(slide_out_msg, slide_out_opt_yes_msg, opt_yes=opt_yes)
+            if ans in ('y', 'yes'):
+                dds = self.__down_branches.get(LocalBranchShortName.of(down_branch)) or []
+                for dd in dds:
+                    self.__up_branch[dd] = branch
+                self.__down_branches[branch] = flat_map(
+                    lambda bd: dds if bd == down_branch else [bd],
+                    self.__down_branches.get(branch) or [])
+                self.save_branch_layout_file()
+                self.__run_post_slide_out_hook(branch, down_branch, dds)
 
     def __print_new_line(self, new_status: bool) -> None:
         if not self.__empty_line_status:
@@ -737,7 +748,7 @@ class MacheteClient:
             else:
                 needs_remote_sync = False
 
-            use_merge = opt_merge or (branch in self.annotations and self.annotations[branch].qualifiers.merge)
+            use_merge = opt_merge or (branch in self.annotations and self.annotations[branch].qualifiers.update_with_merge)
 
             if needs_slide_out:
                 # Avoid unnecessary fork point check if we already know that the
@@ -1167,18 +1178,22 @@ class MacheteClient:
     def __popen_hook(*args: str, cwd: str, env: Dict[str, str]) -> PopenResult:
         if sys.platform == "win32":
             # This is a poor-man's solution to the problem of Windows **not** recognizing Unix-style shebangs :/
-            return utils.popen_cmd("sh", *args, cwd=cwd, env=env)  # pragma: no cover; coverage not measured on Windows
+            return utils.popen_cmd("sh", *args, cwd=cwd, env=env)
         else:
             return utils.popen_cmd(*args, cwd=cwd, env=env)
 
     def __run_hook(self, *args: str, cwd: str) -> int:
         self.__git.flush_caches()
         if sys.platform == "win32":
-            return utils.run_cmd("sh", *args, cwd=cwd)  # pragma: no cover; coverage not measured on Windows
+            return utils.run_cmd("sh", *args, cwd=cwd)
         else:
             return utils.run_cmd(*args, cwd=cwd)
 
     def rebase(self, onto: AnyRevision, from_exclusive: AnyRevision, branch: LocalBranchShortName, opt_no_interactive_rebase: bool) -> None:
+        anno = self.annotations.get(branch)
+        if anno and not anno.qualifiers.rebase:
+            raise MacheteException(f"Branch `{branch}` is annotated with `rebase=no` qualifier, aborting.\n"
+                                   f"Remove the qualifier using `git machete anno` or edit branch layout file directly.")
         # Let's use `OPTS` suffix for consistency with git's built-in env var `GIT_DIFF_OPTS`
         extra_rebase_opts = os.environ.get('GIT_MACHETE_REBASE_OPTS', '').split()
 
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index 77f1f06..ebc99ca 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -110,6 +110,10 @@ long_docs: Dict[str, str] = {
 
         Note that the current branch after the operation is still `develop`, just pointing to `feature/add-from-remote`'s tip now.
 
+        If the current branch `C` is annotated with `push=no` qualifier, the push is not performed.
+        If the downstream branch `D` is annotated with `slide-out=no` qualifier, the slide-out is not performed.
+        See help for `traverse` for more details on the qualifiers.
+
         <b>Options:</b>
            <b>-y</b>, <b>--yes</b>
               Don't ask for confirmation whether to fast-forward the current branch or whether to slide-out the downstream.
@@ -1083,10 +1087,13 @@ long_docs: Dict[str, str] = {
 
         The provided branches must form a chain — all of the following conditions must be met:
            * for i=1..N-1, (i+1)-th branch must be the only downstream (child) branch of the i-th branch,
-           * all provided branches must have an upstream branch (so, in other words, roots of branch dependency tree cannot be slid out).
+           * all provided branches must have an upstream branch (so, in other words, roots of branch layout cannot be slid out).
 
         Note: Unless `--delete` is passed, `slide-out` doesn't delete any branches from git, just removes them from the tree of branch dependencies.
 
+        Note: if a child branch is annotated with `rebase=no` qualifier, the rebase is not performed.
+        See help for `traverse` for more details on the qualifiers.
+
         <b>Options:</b>
            <b>-d</b>, <b>--down-fork-point=<down-fork-point-commit></b>
               If updating by rebase, specifies the alternative fork point for downstream branches for the operation.


Test patch:
diff --git a/tests/test_advance.py b/tests/test_advance.py
index def76c8..946fa95 100644
--- a/tests/test_advance.py
+++ b/tests/test_advance.py
@@ -5,7 +5,8 @@ from git_machete.exceptions import UnderlyingGitException
 
 from .base_test import BaseTest
 from .mockers import (assert_failure, assert_success, launch_command,
-                      mock_input_returning, rewrite_branch_layout_file)
+                      mock_input_returning, mock_input_returning_y,
+                      rewrite_branch_layout_file)
 
 
 class TestAdvance(BaseTest):
@@ -209,3 +210,75 @@ class TestAdvance(BaseTest):
             x-level-1b-branch (untracked)
             """
         )
+
+    def test_advance_when_push_no_qualifier_present(self, mocker: MockerFixture) -> None:
+        (
+            self.repo_sandbox
+            .new_branch("root")
+            .commit()
+            .push()
+            .new_branch("level-1-branch")
+            .commit()
+            .check_out("root")
+        )
+
+        body: str = \
+            """
+            root  push=no
+                level-1-branch
+            """
+        rewrite_branch_layout_file(body)
+
+        self.patch_symbol(mocker, "builtins.input", mock_input_returning_y)
+        assert_success(
+            ["advance"],
+            """
+            Fast-forward root to match level-1-branch? (y, N)
+
+            Branch root is now fast-forwarded to match level-1-branch. Slide level-1-branch out of the tree of branch dependencies? (y, N)
+            """
+        )
+
+        assert_success(
+            ["status"],
+            """
+            root *  push=no (ahead of origin)
+            """
+        )
+
+    def test_advance_when_slide_out_no_qualifier_present(self, mocker: MockerFixture) -> None:
+        (
+            self.repo_sandbox
+            .new_branch("root")
+            .commit()
+            .push()
+            .new_branch("level-1-branch")
+            .commit()
+            .check_out("root")
+        )
+
+        body: str = \
+            """
+            root
+                level-1-branch  slide-out=no
+            """
+        rewrite_branch_layout_file(body)
+
+        self.patch_symbol(mocker, "builtins.input", mock_input_returning_y)
+        assert_success(
+            ["advance"],
+            """
+            Fast-forward root to match level-1-branch? (y, N)
+
+            Branch root is now fast-forwarded to match level-1-branch. Push root to origin? (y, N)
+            """
+        )
+
+        assert_success(
+            ["status"],
+            """
+            root *
+            |
+            m-level-1-branch  slide-out=no (untracked)
+            """
+        )
diff --git a/tests/test_reapply.py b/tests/test_reapply.py
index ae1a97c..5c882eb 100644
--- a/tests/test_reapply.py
+++ b/tests/test_reapply.py
@@ -1,8 +1,7 @@
-
 from .base_test import BaseTest
-from .mockers import (assert_success, fixed_author_and_committer_date_in_past,
-                      launch_command, overridden_environment,
-                      rewrite_branch_layout_file)
+from .mockers import (assert_failure, assert_success,
+                      fixed_author_and_committer_date_in_past, launch_command,
+                      overridden_environment, rewrite_branch_layout_file)
 
 
 class TestReapply(BaseTest):
@@ -99,3 +98,27 @@ class TestReapply(BaseTest):
             """
         )
         assert launch_command("fork-point", "level-2-branch").strip() == "1b657a15fa4c619fcb4e871176d1471cdbce9093"
+
+    def test_reapply_with_rebase_no_qualifier(self) -> None:
+        (
+            self.repo_sandbox
+            .remove_remote()
+            .new_branch("level-0-branch")
+            .commit("Basic commit.")
+            .new_branch("level-1-branch")
+            .commit("First level-1 commit.")
+            .commit("Second level-1 commit.")
+        )
+
+        body: str = \
+            """
+            level-0-branch
+                level-1-branch  rebase=no
+            """
+        rewrite_branch_layout_file(body)
+
+        with overridden_environment(GIT_SEQUENCE_EDITOR="sed -i.bak '2s/^pick /fixup /'"):
+            assert_failure(
+                ["reapply"],
+                "Branch level-1-branch is annotated with rebase=no qualifier, aborting.\n"
+                "Remove the qualifier using git machete anno or edit branch layout file directly.")
diff --git a/tests/test_slide_out.py b/tests/test_slide_out.py
index c273761..50ee45f 100644
--- a/tests/test_slide_out.py
+++ b/tests/test_slide_out.py
@@ -279,6 +279,69 @@ class TestSlideOut(BaseTest):
 
         assert_success(['status', '-l'], expected_status_output)
 
+    def test_slide_out_with_rebase_no_qualifier(self, mocker: MockerFixture) -> None:
+        (
+            self.repo_sandbox
+            .remove_remote('origin')
+            .new_branch('branch-0')
+            .commit()
+            .new_branch('branch-1')
+            .commit()
+            .new_branch('branch-2')
+            .commit('Commit on branch-2.')
+            .check_out('branch-0')
+            .amend_commit('New commit message')
+        )
+        body: str = \
+            """
+            branch-0
+                branch-1
+                    branch-2  rebase=no
+            """
+        rewrite_branch_layout_file(body)
+
+        self.patch_symbol(mocker, 'builtins.input', mock_input_returning_y)
+        assert_success(
+            ['slide-out', '-n', 'branch-1', '--delete'],
+            "Delete branch branch-1 (unmerged to HEAD)? (y, N, q)\n"
+        )
+
+        expected_status_output = (
+            """
+            branch-0 *
+            |
+            | Some commit message.
+            | Commit on branch-2.
+            x-branch-2  rebase=no
+            """
+        )
+        assert_success(['status', '-l'], expected_status_output)
+
+    def test_slide_out_with_slide_out_no_qualifier(self) -> None:
+        (
+            self.repo_sandbox
+            .remove_remote('origin')
+            .new_branch('branch-0')
+            .commit()
+            .new_branch('branch-1')
+            .commit()
+            .new_branch('branch-2')
+            .commit()
+        )
+        body: str = \
+            """
+            branch-0
+                branch-1  slide-out=no
+                    branch-2
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_failure(
+            ['slide-out', 'branch-1'],
+            "Branch branch-1 is annotated with slide-out=no qualifier, aborting.\n"
+            "Remove the qualifier using git machete anno or edit branch layout file directly."
+        )
+
     def test_slide_out_with_down_fork_point_and_multiple_children_of_last_branch(self) -> None:
         (
             self.repo_sandbox.new_branch('branch-0')
diff --git a/tests/test_update.py b/tests/test_update.py
index 4075656..70fd76b 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -170,15 +170,15 @@ class TestUpdate(BaseTest):
         with fixed_author_and_committer_date_in_past():
             (
                 self.repo_sandbox.new_branch('branch-0')
-                    .commit("Commit on branch-0.")
-                    .new_branch("branch-1a")
-                    .commit("Commit on branch-1a.")
+                .commit("Commit on branch-0.")
+                .new_branch("branch-1a")
+                .commit("Commit on branch-1a.")
             )
             branch_1a_hash = self.repo_sandbox.get_current_commit_hash()
             (
                 self.repo_sandbox.check_out('branch-0')
-                    .new_branch("branch-1b")
-                    .commit("Commit on branch-1b.")
+                .new_branch("branch-1b")
+                .commit("Commit on branch-1b.")
             )
 
         body: str = \
@@ -193,6 +193,28 @@ class TestUpdate(BaseTest):
                                  "is not ancestor of or the tip of the branch-1b branch."
         assert_failure(['update', '-f', branch_1a_hash], expected_error_message)
 
+    def test_update_with_rebase_no_qualifier(self) -> None:
+        with fixed_author_and_committer_date_in_past():
+            (
+                self.repo_sandbox.new_branch('branch-0')
+                .commit("Commit on branch-0.")
+                .new_branch("branch-1")
+                .commit("Commit on branch-1.")
+            )
+
+        body: str = \
+            """
+            branch-0
+                branch-1 rebase=no
+            """
+        rewrite_branch_layout_file(body)
+
+        assert_failure(
+            ['update'],
+            "Branch branch-1 is annotated with rebase=no qualifier, aborting.\n"
+            "Remove the qualifier using git machete anno or edit branch layout file directly."
+        )
+
     def test_update_with_stop_for_edit(self) -> None:
 
         (