Repository: VirtusLab/git-machete
Pull request #730 (2022-11-09 18:27:22)

Issue #293: A way to mark branches that should not be rebased and/or pushed during `traverse`
TODO:

- [x] Add qualifiers ( rebase/push=no/yes, read-only) that control behaviour of `traverse`
- [x] Add tests
   - [x] `traverse`
   - [x] `discover`
   - [x] `github anno-prs`
   - [x] `github checkout-prs`
- [x] Prevent commands: `discover, github anno-prs, github checkout-prs` (check if there are more commands that can modify annotation) from modifying qualifiers except command `github checkout-prs`
  - [x] `discover`
  - [x] `anno-prs`
  - [x] `checkout-prs`
    -  add `rebase=no push=no` qualifiers for the branch associated with the PR where the owner is different than the current user and there are no qualifiers present for that branch
- [x] update docs
   - [x] `traverse`  
   - [x] `discover`
   - [x] `anno-prs`
   - [x] `checkout-prs`

codecov-commenter: # [Codecov](https://codecov.io/gh/VirtusLab/git-machete/pull/730?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) Report
> Merging [#730](https://codecov.io/gh/VirtusLab/git-machete/pull/730?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) (7c0afd9) into [develop](https://codecov.io/gh/VirtusLab/git-machete/commit/7c0afd99f642a28971b5f5bb444d43f53be042c7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab) (7c0afd9) will **not change** coverage.
> The diff coverage is `n/a`.

> :exclamation: Current head 7c0afd9 differs from pull request most recent head 805687d. Consider uploading reports for the commit 805687d to get more accurate results

```diff
@@           Coverage Diff            @@
##           develop     #730   +/-   ##
========================================
  Coverage    70.57%   70.57%           
========================================
  Files           10       10           
  Lines         2980     2980           
  Branches       695      695           
========================================
  Hits          2103     2103           
  Misses         607      607           
  Partials       270      270           
```



Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VirtusLab)

amalota: @PawelLipski wdyt?

```
master
develop
	bug/717-prevent-machete-github-annotations-overwriting xdd3 push=no
	build/use_SNAPCRAFT_STORE_CREDENTIALS_env_var_for_logging
	docs/709-traverse-push-untracked-default xd1rebase=no xd2
	docs/710-git-machete-help-topics rebase=no xdd1 push=no xdd2
	feature/645-config-keys-for-default-settings-for-subcommands
	feature/293-add-qualifiers-to-control-traverse-behaviour rebase=no PR #730
```

->

<img width="783" alt="image" src="https://user-images.githubusercontent.com/86779115/201209073-2fb5ffc7-1576-4c8d-8265-5cc274ce59ea.png">

PawelLipski: > @PawelLipski wdyt?
> 
> ```
> master
> develop
> 	bug/717-prevent-machete-github-annotations-overwriting xdd3 push=no
> 	build/use_SNAPCRAFT_STORE_CREDENTIALS_env_var_for_logging
> 	docs/709-traverse-push-untracked-default xd1rebase=no xd2
> 	docs/710-git-machete-help-topics rebase=no xdd1 push=no xdd2
> 	feature/645-config-keys-for-default-settings-for-subcommands
> 	feature/293-add-qualifiers-to-control-traverse-behaviour rebase=no PR #730
> ```
> 
> ->
> 
> <img alt="image" width="783" src="https://user-images.githubusercontent.com/86779115/201209073-2fb5ffc7-1576-4c8d-8265-5cc274ce59ea.png">

:benissimo: !
amalota: @PawelLipski, which cases of the remote sync should the `push` qualifier control? I propose this:
- __handle_behind_state -> pulls the branch (NO control)
- __handle_ahead_state -> pushes the branch (control)
- __handle_diverged_and_older_state -> resets the branch (NO control)
- __handle_diverged_and_newer_state -> pushes with force (NO control)
- __handle_untracked_state -> push untracked branch (NO control)

Also by `rebase` qualifier we mean both rebase and merge right? If so, maybe we need to change the naming to e.g. `rebase_or_merge` ?
PawelLipski: > I propose this:

Out of 5 proposed, I agree to the first 3. Wrt. the final two:

> __handle_diverged_and_newer_state -> pushes with force (NO control)
> __handle_untracked_state -> push untracked branch (NO control)

I'd say that both should also be controlled by `push=no`, for consistency 🤔
PawelLipski: > Also by rebase qualifier we mean both rebase and merge right? If so, maybe we need to change the naming to e.g. rebase_or_merge ?

Whoops good spot 🤔 renaming to sth long like `rebase_or_merge` would make it cumbersome... also, we don't want to promote merge-based flows too much due to https://github.com/VirtusLab/git-machete#can-i-use-git-merge-for-dealing-with-stacked-prs ... in fact I wouldn't add `--merge` option to `traverse` if I were to decide today (even back in 2019, I've done it mostly on user requests, not of my own initiative).

If it comes to the qualifier... looking for users' perspective I'd assume the following:
* first of all, one shouldn't really use `traverse --merge` ;D https://github.com/VirtusLab/git-machete/issues/731
* even if they do, I'd expect that if they've put `rebase=no` to disable rebases in `traverse`, they also **typically** won't expect a merge to be suggested for that branch 🤔 

Still, for the sake of Sacred Consistency... maybe introduce separate `rebase=no` and `merge=no` qualifiers, independent from each other?
amalota: > > Also by rebase qualifier we mean both rebase and merge right? If so, maybe we need to change the naming to e.g. rebase_or_merge ?
> 
> Whoops good spot 🤔 renaming to sth long like `rebase_or_merge` would make it cumbersome... also, we don't want to promote merge-based flows too much due to https://github.com/VirtusLab/git-machete#can-i-use-git-merge-for-dealing-with-stacked-prs ... in fact I wouldn't add `--merge` option to `traverse` if I were to decide today (even back in 2019, I've done it mostly on user requests, not of my own initiative).
> 
> If it comes to the qualifier... looking for users' perspective I'd assume the following:
> 
> * first of all, one shouldn't really use `traverse --merge` ;D [Discourage users from using `--merge` in `traverse`, `update` etc. #731](https://github.com/VirtusLab/git-machete/issues/731)
> * even if they do, I'd expect that if they've put `rebase=no` to disable rebases in `traverse`, they also **typically** won't expect a merge to be suggested for that branch 🤔
> 
> Still, for the sake of Sacred Consistency... maybe introduce separate `rebase=no` and `merge=no` qualifiers, independent from each other?

Hmm, ok, but since `merge` option default value is `no` i am thinking about allowing `merge` to have both `yes` and `no` values and that `merge=yes` would have higher priority than default `rebase` traverse option, since we add it manually, @PawelLipski wdyt?
PawelLipski: >  to have both yes and no values and that merge=yes would have higher priority than default rebase traverse option

Hmmm okay so then could you fill up what would be the **effective** behavior in the following cases:
1. `rebase=no` => rebase=NO? merge=NO?
2. `rebase=no merge=no` => rebase=NO? merge=NO?
3. `merge=yes` => rebase=YES? merge=YES?
4. `rebase=no merge=yes` => rebase=NO? merge=YES?
amalota: > > to have both yes and no values and that merge=yes would have higher priority than default rebase traverse option
> 
> Hmmm okay so then could you fill up what would be the **effective** behavior in the following cases:
> 
> 1. `rebase=no` => rebase=NO? merge=NO?
> 2. `rebase=no merge=no` => rebase=NO? merge=NO?
> 3. `merge=yes` => rebase=YES? merge=YES?
> 4. `rebase=no merge=yes` => rebase=NO? merge=YES?

@PawelLipski Technically yes, except in 3., where explicit `merge=yes` would have priority over `rebase=yes`.
amalota: @PawelLipski I just discover a case where the flag `traverse --merge` is provided and also qualifier `merge=no`, i would say that qualifier should have higher priority, wdyt?
PawelLipski: > @PawelLipski I just discovered a case where the flag `traverse --merge` is provided and also qualifier `merge=no`, i would say that qualifier should have higher priority, wdyt?

:yes:, qualifier definitely should have a higher priority
PawelLipski: > > > to have both yes and no values and that merge=yes would have higher priority than default rebase traverse option
> > 
> > 
> > Hmmm okay so then could you fill up what would be the **effective** behavior in the following cases:
> > 
> > 1. `rebase=no` => rebase=NO? merge=NO?
> > 2. `rebase=no merge=no` => rebase=NO? merge=NO?
> > 3. `merge=yes` => rebase=YES? merge=YES?
> > 4. `rebase=no merge=yes` => rebase=NO? merge=YES?
> 
> @PawelLipski Technically yes, except in 3., where explicit `merge=yes` would have priority over `rebase=yes`.

Hmmm okay so what's gonna be the effective behavior in case (3) ? 🤔 
amalota: > > > > to have both yes and no values and that merge=yes would have higher priority than default rebase traverse option
> > > 
> > > 
> > > Hmmm okay so then could you fill up what would be the **effective** behavior in the following cases:
> > > 
> > > 1. `rebase=no` => rebase=NO? merge=NO?
> > > 2. `rebase=no merge=no` => rebase=NO? merge=NO?
> > > 3. `merge=yes` => rebase=YES? merge=YES?
> > > 4. `rebase=no merge=yes` => rebase=NO? merge=YES?
> > 
> > 
> > @PawelLipski Technically yes, except in 3., where explicit `merge=yes` would have priority over `rebase=yes`.
> 
> Hmmm okay so what's gonna be the effective behavior in case (3) ? 🤔

@PawelLipski 3. `merge=yes` => rebase=YES? merge=YES? => merge has priority over rebase so traverse will be done using merge
PawelLipski: > merge=yes => rebase=YES? merge=YES? => merge has priority over rebase so traverse will be done using merge

Hmmm okay so `merge=yes` would effectively **force merge for the given branch** even if `--merge` is NOT passed to `traverse`? if so, this is very unlikely to be useful. The idea was that the qualifiers only serve to **opt-out** from certain actions is traverse (like push or rebase), but not to *modify* the behavior (like using merge instead of rebase) :goncern:
PawelLipski: Or maybe... is it correct that your idea is that:
* `rebase=no`, `push=no`, `merge=no` opt out from certain actions,
* `merge=yes` switches from rebase to merge for given branch, even if `--merge` is not passed? 🤔 
PawelLipski: TBH as a general remark... I'd like you to provide full docs for the feature on this PR, that's gonna be the best place to ease out such details! 🕵🏻 
amalota: > TBH as a general remark... I'd like you to provide full docs for the feature on this PR, that's gonna be the best place to ease out such details! 🕵🏻

Yes, adding/updating docs is the last step of this PR
PawelLipski: > Yes, adding/updating docs is the last step of this PR

My suggestion is to expedite adding the docs, as it's gonna make it easier to determine the expected behavior, esp. around `merge` qualifier

amalota: > > merge=yes => rebase=YES? merge=YES? => merge has priority over rebase so traverse will be done using merge
> 
> Hmmm okay so `merge=yes` would effectively **force merge for the given branch** even if `--merge` is NOT passed to `traverse`? if so, this is very unlikely to be useful. The idea was that the qualifiers only serve to **opt-out** from certain actions is traverse (like push or rebase), but not to _modify_ the behavior (like using merge instead of rebase) :goncern:

Or maybe... is it correct that your idea is that:
* `rebase=no`, `push=no`, `merge=no` opt out from certain actions,
* `merge=yes` switches from rebase to merge for given branch, even if `--merge` is not passed? 🤔 
* 
Sorry, i guess i explained it badly,  my idea is just like you wrote above. AFAIR, qualifiers were added to expand `traverse` cmd so that users can run `traverse` with some options but also have the possibility to customize what to do on each branch. So by default `rebase` and `push` are done, but what if a user has a branch on which he wanted to use `merge` instead of rebase (but still use `rebase` in all of the other branches), that's where the `merge=yes` qualifier comes in. As for `merge=no` qualifier, let's say user wants to use `merge` on all the branches except one, were he wants to use rebase and adds `merge=no` qualifier for that branch.
amalota: > > Yes, adding/updating docs is the last step of this PR
> 
> My suggestion is to expedite adding the docs, as it's gonna make it easier to determine the expected behavior, esp. around `merge` qualifier

Ok, i mean AFAICS everything is implemented so i will start updating the docs now
PawelLipski: Taking this one over for now...

Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 77dd5de..3e2072d 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,8 @@
 
 ## New in git-machete 3.14.0
 
+- added: `push=no` and `rebase=no` branch qualifiers that control push and rebase behaviour of `git machete traverse`
+
 ## New in git-machete 3.13.2
 
 - fixed: redo the failed release
diff --git a/docs/source/cli_help/anno.rst b/docs/source/cli_help/anno.rst
index b0875bf..a69a3ee 100644
--- a/docs/source/cli_help/anno.rst
+++ b/docs/source/cli_help/anno.rst
@@ -21,7 +21,9 @@ If invoked with a single empty string <annotation text>, like:
 then clears the annotation for the current branch (or a branch specified with ``-b/--branch``).
 
 If invoked with ``-H`` or ``--sync-github-prs``, annotates the branches based on their corresponding GitHub PR numbers and authors.
-Any existing annotations are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
+When the current user is **not** the owner of the PR associated with that branch, adds ``rebase=no push=no`` branch qualifiers used by ``git machete traverse``,
+so that you don't rebase or push someone else's PR by accident (see help for :ref:`traverse`).
+Any existing annotations (except branch qualifiers) are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
 
 .. include:: github_api_access.rst
 .. include:: github_config_keys.rst
@@ -29,6 +31,8 @@ Any existing annotations are overwritten for the branches that have an opened PR
 In any other case, sets the annotation for the given/current branch to the given <annotation text>.
 If multiple <annotation text>'s are passed to the command, they are concatenated with a single space.
 
+Note: ``anno`` command is able to overwrite existing branch qualifiers.
+
 Note: all the effects of ``anno`` can be always achieved by manually editing the definition file.
 
 **Options:**
diff --git a/docs/source/cli_help/discover.rst b/docs/source/cli_help/discover.rst
index 23a4b56..096ec2a 100644
--- a/docs/source/cli_help/discover.rst
+++ b/docs/source/cli_help/discover.rst
@@ -13,6 +13,7 @@ Discovers and displays tree of branch dependencies using a heuristic based on re
 If confirmed with a ``y[es]`` or ``e[dit]`` reply, backs up the current definition file (if it exists) as ``$GIT_DIR/machete~``
 and saves the new tree under the usual ``$GIT_DIR/machete`` path.
 If the reply was ``e[dit]``, additionally an editor is opened (as in: ``git machete`` :ref:`edit`) after saving the new definition file.
+``discover`` retains the existing branch qualifiers used by ``git machete traverse`` (see help for :ref:`traverse`).
 
 **Options:**
 
diff --git a/docs/source/cli_help/format.rst b/docs/source/cli_help/format.rst
index 72e718b..ef07c4d 100644
--- a/docs/source/cli_help/format.rst
+++ b/docs/source/cli_help/format.rst
@@ -9,12 +9,12 @@ The format of the definition file should be as follows:
 .. code-block::
 
     develop
-        adjust-reads-prec PR #234
-            block-cancel-order PR #235
+        adjust-reads-prec PR #234 rebase=no push=no
+            block-cancel-order PR #235 rebase=no
                 change-table
                     drop-location-type
         edit-margin-not-allowed
-            full-load-gatling
+            full-load-gatling push=no
         grep-errors-script
     master
         hotfix/receipt-trigger PR #236
@@ -23,8 +23,8 @@ In the above example ``develop`` and ``master`` are roots of the tree of branch
 Branches ``adjust-reads-prec``, ``edit-margin-not-allowed`` and ``grep-errors-script`` are direct downstream branches for ``develop``.
 ``block-cancel-order`` is a downstream branch of ``adjust-reads-prec``, ``change-table`` is a downstream branch of ``block-cancel-order`` and so on.
 
-Every branch name can be followed (after a single space as a delimiter) by a custom annotation --- a PR number in the above example.
-The annotations don't influence the way ``git machete`` operates other than that they are displayed in the output of the ``status`` command.
+Every branch name can be followed (after a single space as a delimiter) by a custom annotation, e.g. ``PR #234 rebase=no push=no``, ``PR #235 rebase=no`` or ``push=no``.
+Annotations might contain underlined branch qualifiers (``rebase=no``, ``push=no``) that control rebase and push behaviour of ``traverse`` (see help for :ref:`traverse`).
 Also see help for :ref:`anno` command.
 
 Tabs or any number of spaces can be used as indentation.
diff --git a/docs/source/cli_help/github.rst b/docs/source/cli_help/github.rst
index b6f4a86..16efd6c 100644
--- a/docs/source/cli_help/github.rst
+++ b/docs/source/cli_help/github.rst
@@ -21,6 +21,8 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
     Annotates the branches based on their corresponding GitHub PR numbers and authors.
     Any existing annotations are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
     Equivalent to ``git machete anno --sync-github-prs``.
+    When the current user is **not** the owner of the PR associated with that branch, adds ``rebase=no push=no`` branch qualifiers used by ``git machete traverse``,
+    so that you don't rebase or push someone else's PR by accident (see help for :ref:`traverse`).
 
 
 ``checkout-prs [--all | --by=<github-login> | --mine | <PR-number-1> ... <PR-number-N>]``:
@@ -28,6 +30,8 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
     also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally.
     Once the specified pull requests are checked out locally, annotate local branches with corresponding pull request numbers.
     If only one PR has been checked out, then switch the local repository's HEAD to its head branch.
+    When the current user is **not** the owner of the PR associated with that branch, adds ``rebase=no push=no`` branch qualifiers used by ``git machete traverse``,
+    so that you don't rebase or push someone else's PR by accident (see help for :ref:`traverse`).
 
     **Options:**
 
diff --git a/docs/source/cli_help/status.rst b/docs/source/cli_help/status.rst
index 415be80..a94847a 100644
--- a/docs/source/cli_help/status.rst
+++ b/docs/source/cli_help/status.rst
@@ -41,7 +41,8 @@ Apart from simply ASCII-formatting the definition file, this also:
     * prints (``untracked``/``ahead of <remote>``/``behind <remote>``/``diverged from [& older than] <remote>``) message if the branch
       is not in sync with its remote counterpart;
 
-    * displays the custom annotations (see help for :ref:`format` and :ref:`anno`) next to each branch, if present;
+    * displays the custom annotations (see help for :ref:`format` and :ref:`anno`) next to each branch, if present. Annotations might contain underlined branch
+      qualifiers (``rebase=no``, ``push=no``) that control rebase and push behaviour of ``traverse`` (see help for :ref:`traverse`);
 
     * displays the output of ``machete-status-branch hook`` (see help for :ref:`hooks`), if present;
 
diff --git a/docs/source/cli_help/traverse.rst b/docs/source/cli_help/traverse.rst
index 2ecb0ac..faa0778 100644
--- a/docs/source/cli_help/traverse.rst
+++ b/docs/source/cli_help/traverse.rst
@@ -65,6 +65,24 @@ It will pick up the walk from the current branch (unless ``--start-from=`` or ``
 Unlike with e.g. ``git rebase``, there is no special ``--continue`` flag, as ``traverse`` is stateless
 (doesn't keep a state of its own like ``git rebase`` does in ``.git/rebase-apply/``).
 
+The rebase and push behaviour of ``traverse`` can also be customized for each branch separately using *branch qualifiers*.
+There are ``rebase=no`` and ``push=no`` qualifiers that can be used to opt out of default behaviour (rebasing and pushing).
+The qualifier can appear anywhere in the annotation but needs to be separated by a whitespace from any other character, e.g. ``some_annotation_text rebase=no push=no``.
+Qualifiers can only be overwritten by manually editing ``.git/machete`` file or modifying it with ``git machete e[dit]`` or by updating annotations with ``git machete anno``.
+Example machete file with branch qualifiers:
+
+.. code-block::
+
+    master
+      develop  rebase=no
+        my-branch  PR #123
+        someone-elses-branch  PR #124 rebase=no push=no
+        branch-for-local-experiments  push=no
+
+Operations like ``git machete github anno-prs`` and ``git machete github checkout-prs`` add ``rebase=no push=no`` branch qualifiers
+when the current user is **not** the owner of the PR associated with that branch. ``git machete discover`` doesn't modify the qualifiers.
+
+
 **Options:**
 
 -F, --fetch                  Fetch the remotes of all managed branches at the beginning of traversal (no ``git pull`` involved, only ``git fetch``).
diff --git a/git_machete/annotation.py b/git_machete/annotation.py
new file mode 100644
index 0000000..ee40db0
--- /dev/null
+++ b/git_machete/annotation.py
@@ -0,0 +1,58 @@
+import re
+from typing import Callable
+
+from git_machete import utils
+
+
+class Qualifiers:
+    rebase: bool
+    push: bool
+
+    def __init__(self, annotation: str):
+        self._annotation = annotation
+        self._annotation_without_qualifiers = annotation
+        self._rebase_text = ''
+        self._push_text = ''
+        self.rebase = True
+        self.push = True
+
+        match_pattern: Callable[[str], str] = lambda text: f'.*\\b{text}=no\\b.*'
+        sub_pattern: Callable[[str], str] = lambda text: f'[ ]?{text}=no[ ]?'
+
+        rebase_match = re.match(match_pattern('rebase'), annotation)
+        if rebase_match:
+            self.rebase = False
+            self._rebase_text = 'rebase=no'
+            self._annotation_without_qualifiers = re.sub(sub_pattern('rebase'), ' ', self._annotation_without_qualifiers)
+
+        push_match = re.match(match_pattern('push'), annotation)
+        if push_match:
+            self.push = False
+            self._push_text = 'push=no'
+            self._annotation_without_qualifiers = re.sub(sub_pattern('push'), ' ', self._annotation_without_qualifiers)
+
+    def get_annotation_text_without_qualifiers(self) -> str:
+        return self._annotation_without_qualifiers.strip()
+
+    def get_qualifiers_text(self) -> str:
+        return f'{self._rebase_text.strip()} {self._push_text.strip()}'.replace('  ', ' ').strip()
+
+
+class Annotation:
+    text: str
+    text_without_qualifiers: str
+    qualifiers_text: str
+    qualifiers: Qualifiers
+
+    def __init__(self, text: str):
+        self.text = text
+        self.qualifiers = Qualifiers(text)
+        self.text_without_qualifiers = self.qualifiers.get_annotation_text_without_qualifiers()
+        self.qualifiers_text = self.qualifiers.get_qualifiers_text()
+
+    def get_formatted_text(self) -> str:
+        annotation_text = f"  {utils.dim(self.text_without_qualifiers)}"
+        if self.qualifiers_text != '':
+            annotation_text += ' ' if self.text_without_qualifiers != '' else ''
+            annotation_text += utils.dim(utils.underline(s=self.qualifiers_text))
+        return annotation_text
diff --git a/git_machete/cli.py b/git_machete/cli.py
index b6f6624..3804237 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -536,21 +536,20 @@ def launch(orig_args: List[str]) -> None:
 
         machete_client = MacheteClient(git)
 
-        if cmd != 'discover':
-            if not os.path.exists(machete_client.definition_file_path):
-                # We're opening in "append" and not "write" mode to avoid a race condition:
-                # if other process writes to the file between we check the
-                # result of `os.path.exists` and call `open`,
-                # then open(..., "w") would result in us clearing up the file
-                # contents, while open(..., "a") has no effect.
-                with open(machete_client.definition_file_path, "a"):
-                    pass
-            elif os.path.isdir(machete_client.definition_file_path):
-                # Extremely unlikely case, basically checking if anybody
-                # tampered with the repository.
-                raise MacheteException(
-                    f"{machete_client.definition_file_path} is a directory "
-                    "rather than a regular file, aborting")
+        if not os.path.exists(machete_client.definition_file_path):
+            # We're opening in "append" and not "write" mode to avoid a race condition:
+            # if other process writes to the file between we check the
+            # result of `os.path.exists` and call `open`,
+            # then open(..., "w") would result in us clearing up the file
+            # contents, while open(..., "a") has no effect.
+            with open(machete_client.definition_file_path, "a"):
+                pass
+        elif os.path.isdir(machete_client.definition_file_path):
+            # Extremely unlikely case, basically checking if anybody
+            # tampered with the repository.
+            raise MacheteException(
+                f"{machete_client.definition_file_path} is a directory "
+                "rather than a regular file, aborting")
 
         should_perform_interactive_slide_out = MacheteClient.should_perform_interactive_slide_out(cmd)
         if cmd == "add":
@@ -596,7 +595,7 @@ def launch(orig_args: List[str]) -> None:
             machete_client.diff(branch=get_local_branch_short_name_from_arg(cli_opts.opt_branch),
                                 opt_stat=cli_opts.opt_stat)  # passing None if not specified
         elif cmd == "discover":
-            # No need to read definition file.
+            machete_client.read_definition_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             machete_client.discover_tree(
                 opt_checked_out_since=cli_opts.opt_checked_out_since,
                 opt_list_commits=cli_opts.opt_list_commits,
diff --git a/git_machete/client.py b/git_machete/client.py
index 60bd77b..25bbe61 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -24,8 +24,8 @@ from git_machete.github import (
     is_github_remote_url, RemoteAndOrganizationAndRepository, set_base_of_pull_request, set_milestone_of_pull_request)
 from git_machete.utils import (
     AnsiEscapeCodes, sync_to_parent_status_to_junction_ascii_only_map, SyncToParentStatus, get_pretty_choices, flat_map, excluding, fmt,
-    sync_to_parent_status_to_edge_color_map, tupled, warn, debug, bold,
-    colored, underline, dim, get_second)
+    sync_to_parent_status_to_edge_color_map, tupled, warn, debug, bold, colored, underline, dim, get_second)
+from git_machete.annotation import Annotation
 
 
 # Allowed parameter values for show/go command
@@ -44,7 +44,7 @@ class MacheteClient:
         self.__down_branches: Dict[LocalBranchShortName, Optional[List[LocalBranchShortName]]] = {}  # TODO (#110): default dict with []
         self.__indent: Optional[str] = None
         self.__roots: List[LocalBranchShortName] = []
-        self.__annotations: Dict[LocalBranchShortName, str] = {}
+        self.__annotations: Dict[LocalBranchShortName, Annotation] = {}
         self.__empty_line_status: Optional[bool] = None
         self.__branch_pairs_by_hash_in_reflog: Optional[Dict[FullCommitHash, Optional[List[BranchPair]]]] = None
 
@@ -68,6 +68,10 @@ class MacheteClient:
     def up_branch(self, val: Dict[LocalBranchShortName, Optional[LocalBranchShortName]]) -> None:
         self._up_branch = val
 
+    @property
+    def annotations(self) -> Dict[LocalBranchShortName, Annotation]:
+        return self.__annotations
+
     def get_childless_branches(self) -> List[LocalBranchShortName]:
         parent_branches = [parent_branch for parent_branch, child_branches in self.__down_branches.items() if len(child_branches) > 0]
         return excluding(self.managed_branches, parent_branches)
@@ -108,7 +112,7 @@ class MacheteClient:
                                                                        line.strip().split(" ", 1)]
             branch = branch_and_maybe_annotation[0]
             if len(branch_and_maybe_annotation) > 1:
-                self.__annotations[branch] = branch_and_maybe_annotation[1]
+                self.__annotations[branch] = Annotation(branch_and_maybe_annotation[1])
             if branch in self.managed_branches:
                 raise MacheteException(
                     f"{self._definition_file_path}, line {index + 1}: branch "
@@ -202,7 +206,8 @@ class MacheteClient:
             self.__indent = "\t"
 
         def render_dfs(branch: LocalBranchShortName, depth: int) -> List[str]:
-            self.annotation = f" {self.__annotations[branch]}" if branch in self.__annotations else ""
+            self.annotation = f" {self.__annotations[branch].text_without_qualifiers} {self.__annotations[branch].qualifiers_text}" \
+                if branch in self.__annotations else ""
             res: List[str] = [depth * self.__indent + branch + self.annotation]
             for down_branch in self.__down_branches.get(branch, []):
                 res += render_dfs(down_branch, depth + 1)
@@ -311,12 +316,12 @@ class MacheteClient:
         if branch in self.__annotations and words == ['']:
             del self.__annotations[branch]
         else:
-            self.__annotations[branch] = " ".join(words)
+            self.__annotations[branch] = Annotation(" ".join(words))
         self.save_definition_file()
 
     def print_annotation(self, branch: LocalBranchShortName) -> None:
         if branch in self.__annotations:
-            print(self.__annotations[branch])
+            print(self.__annotations[branch].text)
 
     def update(
             self, *, opt_merge: bool, opt_no_edit_merge: bool,
@@ -371,7 +376,8 @@ class MacheteClient:
         self.__down_branches = {}
         self.up_branch = {}
         self.__indent = "\t"
-        self.__annotations = {}
+        for branch in self.annotations.keys():
+            self.annotations[branch].text_without_qualifiers = ''
 
         root_of = dict((branch, branch) for branch in all_local_branches)
 
@@ -693,13 +699,19 @@ class MacheteClient:
             needs_slide_out: bool = self.__is_merged_to_upstream(
                 branch, opt_no_detect_squash_merges=opt_no_detect_squash_merges)
             s, remote = self.__git.get_strict_remote_sync_status(branch)
-            statuses_to_sync = (
-                SyncToRemoteStatuses.UNTRACKED,
-                SyncToRemoteStatuses.AHEAD_OF_REMOTE,
-                SyncToRemoteStatuses.BEHIND_REMOTE,
-                SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE,
-                SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
-            needs_remote_sync = s in statuses_to_sync
+            if s in (
+                    SyncToRemoteStatuses.BEHIND_REMOTE,
+                    SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE):
+                needs_remote_sync = True
+            elif s in (
+                    SyncToRemoteStatuses.UNTRACKED,
+                    SyncToRemoteStatuses.AHEAD_OF_REMOTE,
+                    SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE):
+                needs_remote_sync = True
+                if branch in self.annotations:
+                    needs_remote_sync = self.annotations[branch].qualifiers.push
+            else:
+                needs_remote_sync = False
 
             if needs_slide_out:
                 # Avoid unnecessary fork point check if we already know that the
@@ -723,6 +735,8 @@ class MacheteClient:
                               branch, use_overrides=True,
                               opt_no_detect_squash_merges=opt_no_detect_squash_merges)))
                 )
+                if needs_parent_sync and branch in self.annotations:
+                    needs_parent_sync = self.annotations[branch].qualifiers.rebase
 
             if branch != current_branch and (needs_slide_out or needs_parent_sync or needs_remote_sync):
                 self.__print_new_line(False)
@@ -819,7 +833,20 @@ class MacheteClient:
 
                     self.flush_caches()
                     s, remote = self.__git.get_strict_remote_sync_status(branch)
-                    needs_remote_sync = s in statuses_to_sync
+                    if s in (
+                            SyncToRemoteStatuses.BEHIND_REMOTE,
+                            SyncToRemoteStatuses.DIVERGED_FROM_AND_OLDER_THAN_REMOTE):
+                        needs_remote_sync = True
+                    elif s in (
+                            SyncToRemoteStatuses.UNTRACKED,
+                            SyncToRemoteStatuses.AHEAD_OF_REMOTE,
+                            SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE):
+                        needs_remote_sync = True
+                        if branch in self.annotations:
+                            needs_remote_sync = self.annotations[branch].qualifiers.push
+                    else:
+                        needs_remote_sync = False
+
                 elif ans in ('q', 'quit'):
                     return
 
@@ -1029,7 +1056,9 @@ class MacheteClient:
             else:
                 current = bold(branch)
 
-            anno: str = f"  {dim(self.__annotations[branch])}" if branch in self.__annotations else ""
+            anno: str = ''
+            if branch in self.__annotations:
+                anno = self.__annotations[branch].get_formatted_text()
 
             s, remote = self.__git.get_combined_remote_sync_status(branch)
             sync_status = {
@@ -1434,7 +1463,7 @@ class MacheteClient:
         current_user: Optional[str] = git_machete.github.derive_current_user_login()
         debug('Current GitHub user is ' + (current_user or '<none>'))
         all_prs: List[GitHubPullRequest] = derive_pull_requests(org, repo)
-        self.__sync_annotations_to_definition_file(all_prs, current_user)
+        self.__sync_annotations_to_definition_file(all_prs, current_user=current_user)
 
     def __sync_annotations_to_definition_file(self,
                                               prs: List[GitHubPullRequest],
@@ -1452,10 +1481,20 @@ class MacheteClient:
                     warn(f'branch `{pr.head}` has a different base in PR #{pr.number} (`{pr.base}`) '
                          f'than in machete file (`{upstream or "<none, is a root>"}`)')
                     anno += f" WRONG PR BASE or MACHETE PARENT? PR has '{pr.base}'"
-                if self.__annotations.get(LocalBranchShortName.of(pr.head)) != anno:
+                old_annotation_text, old_annotation_qualifiers_text = '', ''
+                if LocalBranchShortName.of(pr.head) in self.__annotations:
+                    old_annotation_text = self.__annotations[LocalBranchShortName.of(pr.head)].text
+                    old_annotation_qualifiers_text = self.__annotations[LocalBranchShortName.of(pr.head)].qualifiers_text
+
+                if pr.user != current_user and old_annotation_qualifiers_text == '':
+                    if verbose:
+                        print(fmt(f'Annotating `{pr.head}` as `{anno} rebase=no push=no`'))
+                    self.__annotations[LocalBranchShortName.of(pr.head)] = Annotation(f'{anno} rebase=no push=no')
+                elif old_annotation_text != anno:
                     if verbose:
                         print(fmt(f'Annotating `{pr.head}` as `{anno}`'))
-                    self.__annotations[LocalBranchShortName.of(pr.head)] = anno
+                    self.__annotations[LocalBranchShortName.of(pr.head)] = Annotation(f'{anno} {old_annotation_qualifiers_text}') \
+                        if old_annotation_text is not None else Annotation(anno)
             else:
                 debug(f'{pr} does NOT correspond to a managed branch')
         self.save_definition_file()
@@ -2012,7 +2051,7 @@ class MacheteClient:
                         checked_out_prs.append(pr)
 
         debug('Current GitHub user is ' + (current_user or '<none>'))
-        self.__sync_annotations_to_definition_file(all_open_prs, current_user, verbose=verbose)
+        self.__sync_annotations_to_definition_file(all_open_prs, current_user=current_user, verbose=verbose)
         if len(applicable_prs) == 1:
             self.__git.checkout(LocalBranchShortName.of(pr.head))
             if verbose:
@@ -2113,7 +2152,7 @@ class MacheteClient:
         else:
             print(fmt(f'The base branch of PR #{pr.number} is already `{new_base}`'))
 
-        self.__annotations[head] = f'PR #{pr.number}'
+        self.__annotations[head] = Annotation(f'PR #{pr.number}')
         self.save_definition_file()
 
     def __derive_remote_and_github_org_and_repo(self,
@@ -2241,7 +2280,7 @@ class MacheteClient:
                     raise e
             print(fmt(ok_str))
 
-        self.__annotations[head] = f'PR #{pr.number}'
+        self.__annotations[head] = Annotation(f'PR #{pr.number}')
         self.save_definition_file()
 
     def __handle_diverged_and_newer_state(
@@ -2413,42 +2452,41 @@ class MacheteClient:
                 f'All commits in `{current_branch}` branch are already included in `{up_branch}` branch.\nCannot create pull request.')
 
         s, remote = self.__git.get_strict_remote_sync_status(current_branch)
-        statuses_to_sync = (
+        statuses_to_push = (
             SyncToRemoteStatuses.UNTRACKED,
             SyncToRemoteStatuses.AHEAD_OF_REMOTE,
             SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE)
-        needs_remote_sync = s in statuses_to_sync
-
-        if needs_remote_sync:
-            if s == SyncToRemoteStatuses.AHEAD_OF_REMOTE:
-                self.__handle_ahead_state(
-                    current_branch=current_branch,
-                    remote=remote,
-                    is_called_from_traverse=False,
-                    opt_push_tracked=True,
-                    opt_yes=opt_yes)
-            elif s == SyncToRemoteStatuses.UNTRACKED:
-                self.__handle_untracked_state(
-                    branch=current_branch,
-                    is_called_from_traverse=False,
-                    opt_push_tracked=True,
-                    opt_push_untracked=True,
-                    opt_yes=opt_yes)
-            elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
-                self.__handle_diverged_and_newer_state(
-                    current_branch=current_branch,
-                    remote=remote,
-                    is_called_from_traverse=False,
-                    opt_push_tracked=True,
-                    opt_yes=opt_yes)
+        if s in statuses_to_push:
+            if current_branch not in self.annotations or self.annotations[current_branch].qualifiers.push:
+                if s == SyncToRemoteStatuses.AHEAD_OF_REMOTE:
+                    self.__handle_ahead_state(
+                        current_branch=current_branch,
+                        remote=remote,
+                        is_called_from_traverse=False,
+                        opt_push_tracked=True,
+                        opt_yes=opt_yes)
+                elif s == SyncToRemoteStatuses.UNTRACKED:
+                    self.__handle_untracked_state(
+                        branch=current_branch,
+                        is_called_from_traverse=False,
+                        opt_push_tracked=True,
+                        opt_push_untracked=True,
+                        opt_yes=opt_yes)
+                elif s == SyncToRemoteStatuses.DIVERGED_FROM_AND_NEWER_THAN_REMOTE:
+                    self.__handle_diverged_and_newer_state(
+                        current_branch=current_branch,
+                        remote=remote,
+                        is_called_from_traverse=False,
+                        opt_push_tracked=True,
+                        opt_yes=opt_yes)
 
-            self.__print_new_line(False)
-            self.status(
-                warn_when_branch_in_sync_but_fork_point_off=True,
-                opt_list_commits=False,
-                opt_list_commits_with_hashes=False,
-                opt_no_detect_squash_merges=False)
-            self.__print_new_line(False)
+                self.__print_new_line(False)
+                self.status(
+                    warn_when_branch_in_sync_but_fork_point_off=True,
+                    opt_list_commits=False,
+                    opt_list_commits_with_hashes=False,
+                    opt_no_detect_squash_merges=False)
+                self.__print_new_line(False)
 
         else:
             if s == SyncToRemoteStatuses.BEHIND_REMOTE:
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index 903b3e5..d4c1b00 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -124,7 +124,9 @@ long_docs: Dict[str, str] = {
         then clears the annotation for the current branch (or a branch specified with `-b/--branch`).
 
         If invoked with `-H` or `--sync-github-prs`, annotates the branches based on their corresponding GitHub PR numbers and authors.
-        Any existing annotations are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
+        When the current user is <b>not</b> the owner of the PR associated with that branch, adds `rebase=no push=no` branch qualifiers used by `git machete traverse`,
+        so that you don't rebase or push someone else's PR by accident (see help for `traverse`).
+        Any existing annotations (except branch qualifiers) are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
 
         To allow GitHub API access for private repositories (and also to perform side-effecting actions like opening a PR,
         even in case of public repositories), a GitHub API token with `repo` scope is required, see https://github.com/settings/tokens.
@@ -156,6 +158,8 @@ long_docs: Dict[str, str] = {
         In any other case, sets the annotation for the given/current branch to the given <annotation text>.
         If multiple <annotation text>'s are passed to the command, they are concatenated with a single space.
 
+        Note: `anno` command is able to overwrite existing branch qualifiers.
+
         Note: all the effects of `anno` can be always achieved by manually editing the definition file.
 
         <b>Options:</b>
@@ -339,6 +343,7 @@ long_docs: Dict[str, str] = {
         If confirmed with a `y[es]` or `e[dit]` reply, backs up the current definition file (if it exists) as `$GIT_DIR/machete~`
         and saves the new tree under the usual `$GIT_DIR/machete` path.
         If the reply was `e[dit]`, additionally an editor is opened (as in: `git machete` `edit`) after saving the new definition file.
+        `discover` retains the existing branch qualifiers used by `git machete traverse` (see help for `traverse`).
 
         <b>Options:</b>
            <b>-C</b>, <b>--checked-out-since=<date></b>
@@ -462,12 +467,12 @@ long_docs: Dict[str, str] = {
         The format of the definition file should be as follows:
         <dim>
           develop
-              adjust-reads-prec PR #234
-                  block-cancel-order PR #235
+              adjust-reads-prec PR #234 rebase=no push=no
+                  block-cancel-order PR #235 rebase=no
                       change-table
                           drop-location-type
               edit-margin-not-allowed
-                  full-load-gatling
+                  full-load-gatling push=no
               grep-errors-script
           master
               hotfix/receipt-trigger PR #236
@@ -477,8 +482,8 @@ long_docs: Dict[str, str] = {
         Branches `adjust-reads-prec`, `edit-margin-not-allowed` and `grep-errors-script` are direct downstream branches for `develop`.
         `block-cancel-order` is a downstream branch of `adjust-reads-prec`, `change-table` is a downstream branch of `block-cancel-order` and so on.
 
-        Every branch name can be followed (after a single space as a delimiter) by a custom annotation — a PR number in the above example.
-        The annotations don't influence the way `git machete` operates other than that they are displayed in the output of the `status` command.
+        Every branch name can be followed (after a single space as a delimiter) by a custom annotation, e.g. `PR #234 rebase=no push=no`, `PR #235 rebase=no` or `push=no`.
+        Annotations might contain underlined branch qualifiers (`rebase=no`, `push=no`) that control rebase and push behaviour of `traverse` (see help for `traverse`).
         Also see help for `anno` command.
 
         Tabs or any number of spaces can be used as indentation.
@@ -524,6 +529,8 @@ long_docs: Dict[str, str] = {
               Annotates the branches based on their corresponding GitHub PR numbers and authors.
               Any existing annotations are overwritten for the branches that have an opened PR; annotations for the other branches remain untouched.
               Equivalent to `git machete anno --sync-github-prs`.
+              When the current user is <b>not</b> the owner of the PR associated with that branch, adds `rebase=no push=no` branch qualifiers used by `git machete traverse`,
+              so that you don't rebase or push someone else's PR by accident (see help for `traverse`).
 
            `checkout-prs [--all | --by=<github-login> | --mine | <PR-number-1> ... <PR-number-N>]`:
  
@@ -531,6 +538,8 @@ long_docs: Dict[str, str] = {
               also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally.
               Once the specified pull requests are checked out locally, annotate local branches with corresponding pull request numbers.
               If only one PR has been checked out, then switch the local repository's HEAD to its head branch.
+              When the current user is <b>not</b> the owner of the PR associated with that branch, adds `rebase=no push=no` branch qualifiers used by `git machete traverse`,
+              so that you don't rebase or push someone else's PR by accident (see help for `traverse`).
 
               <b>Options:</b>
 
@@ -867,7 +876,8 @@ long_docs: Dict[str, str] = {
                 detected by commit equivalency (default), or by strict detection of merge commits (if `--no-detect-squash-merges` passed).
            * prints (`untracked`/`ahead of <remote>`/`behind <remote>`/`diverged from [& older than] <remote>`) message if the branch
              is not in sync with its remote counterpart;
-           * displays the custom annotations (see help for `format` and `anno`) next to each branch, if present;
+           * displays the custom annotations (see help for `format` and `anno`) next to each branch, if present. Annotations might contain underlined branch
+             qualifiers (`rebase=no`, `push=no`) that control rebase and push behaviour of `traverse` (see help for `traverse`);
            * displays the output of `machete-status-branch hook` (see help for `hooks`), if present;
            * optionally lists commits introduced on each branch if `-l/--list-commits` or `-L/--list-commits-with-hashes` is supplied.
 
@@ -971,6 +981,22 @@ long_docs: Dict[str, str] = {
         Unlike with e.g. `git rebase`, there is no special `--continue` flag, as `traverse` is stateless
         (doesn't keep a state of its own like `git rebase` does in `.git/rebase-apply/`).
 
+        The rebase and push behaviour of `traverse` can also be customized for each branch separately using branch qualifiers.
+        There are `rebase=no` and `push=no` qualifiers that can be used to opt out of default behaviour (rebasing and pushing).
+        The qualifier can appear anywhere in the annotation but needs to be separated by a whitespace from any other character, e.g. `some_annotation_text rebase=no push=no`.
+        Qualifiers can only be overwritten by manually editing `.git/machete` file or modifying it with `git machete e[dit]` or by updating annotations with `git machete anno`.
+        Example machete file with branch qualifiers:
+        <dim>
+          master
+            develop  rebase=no
+              my-branch  PR #123
+              someone-elses-branch  PR #124 rebase=no push=no
+              branch-for-local-experiments  push=no
+        </dim>
+
+        Operations like `git machete github anno-prs` and `git machete github checkout-prs` add `rebase=no push=no` branch qualifiers
+        when the current user is <b>not</b> the owner of the PR associated with that branch. `git machete discover` doesn't modify the qualifiers.
+
         <b>Options:</b>
            <b>-F</b>, <b>--fetch</b>
               Fetch the remotes of all managed branches at the beginning of traversal (no `git pull` involved, only `git fetch`).


Test patch:
diff --git a/tests/mockers.py b/tests/mockers.py
index 3c2becb..808d43f 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -354,7 +354,7 @@ def launch_command(*args: str) -> str:
 def assert_command(cmds: Iterable[str], expected_result: str, strip_indentation: bool = True, indent: str = '  ') -> None:
     expected_result = adapt(expected_result, indent) if strip_indentation else expected_result
     actual_result = launch_command(*cmds)
-    assert actual_result == expected_result, f'Actual result:\n`{actual_result}`\nExpected result: `{expected_result}`'
+    assert actual_result == expected_result, f'Actual result:\n`{actual_result}`\nExpected result:\n`{expected_result}`'
 
 
 def rewrite_definition_file(new_body: str) -> None:
diff --git a/tests/test_anno.py b/tests/test_anno.py
index 949ce04..f14d15d 100644
--- a/tests/test_anno.py
+++ b/tests/test_anno.py
@@ -69,3 +69,27 @@ class TestAnno:
             x-feature  Custom annotation for `feature` branch (untracked)
             """,
         )
+
+        # check if annotation qualifiers are parsed correctly and that they can be overwritten by `git machete anno`
+        launch_command('anno', '-b=refs/heads/feature', 'push=no Custom annotation for `feature` branch rebase=no')
+        assert_command(
+            ["status"],
+            """
+            master (untracked)
+
+            develop *  Custom annotation for `develop` branch (untracked)
+            |
+            x-feature  Custom annotation for `feature` branch rebase=no push=no (untracked)
+            """,
+        )
+        launch_command('anno', '-b=refs/heads/feature', 'Custom annotation for `feature` branch')
+        assert_command(
+            ["status"],
+            """
+            master (untracked)
+
+            develop *  Custom annotation for `develop` branch (untracked)
+            |
+            x-feature  Custom annotation for `feature` branch (untracked)
+            """,
+        )
diff --git a/tests/test_client.py b/tests/test_client.py
new file mode 100644
index 0000000..a6f74df
--- /dev/null
+++ b/tests/test_client.py
@@ -0,0 +1,127 @@
+from textwrap import dedent
+from typing import Dict
+
+from git_machete.client import MacheteClient
+from git_machete.git_operations import GitContext, LocalBranchShortName
+from git_machete.annotation import Annotation
+from .mockers import (GitRepositorySandbox, rewrite_definition_file)
+
+
+class TestClient:
+
+    def setup_method(self) -> None:
+
+        self.repo_sandbox = GitRepositorySandbox()
+
+        (
+            self.repo_sandbox
+            # Create the remote and sandbox repos, chdir into sandbox repo
+            .new_repo(self.repo_sandbox.remote_path, "--bare")
+            .new_repo(self.repo_sandbox.local_path)
+            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
+            .execute('git config user.email "tester@test.com"')
+            .execute('git config user.name "Tester Test"')
+        )
+
+    def test_annotations_read_definition_file(self) -> None:
+        """
+        Verify behaviour of a 'MacheteClient.read_definition_file()' method
+        """
+        (
+            self.repo_sandbox
+                .new_branch('master')
+                .commit()
+                .push()
+                .new_branch('feature1')
+                .commit()
+                .new_branch('feature2')
+                .commit()
+                .check_out("master")
+                .new_branch('feature3')
+                .commit()
+                .push()
+                .new_branch('feature4')
+                .commit()
+                .check_out("master")
+                .new_branch('feature5')
+                .commit()
+                .check_out("master")
+                .new_branch('feature6')
+                .commit()
+                .check_out("master")
+                .new_branch('feature7')
+                .commit()
+                .check_out("master")
+                .new_branch('feature8')
+                .commit()
+        )
+
+        body: str = \
+            """
+            master
+            feature1
+            feature2 annotation
+            feature3 annotation rebase=no push=no
+            feature4 rebase=no push=no annotation
+            feature5 annotation1 rebase=no annotation2 push=no annotation3
+            feature6 annotation1 rebase=nopush=no annotation2
+            feature7 annotation1rebase=no push=noannotation2
+            feature8 annotation rebase=no push=no rebase=no push=no
+            """
+        body = dedent(body)
+
+        self.repo_sandbox.new_branch("root")
+        rewrite_definition_file(body)
+        git = GitContext()
+        machete_client = MacheteClient(git)
+        machete_client.read_definition_file(perform_interactive_slide_out=False)
+        annotations: Dict[LocalBranchShortName, Annotation] = machete_client.annotations
+
+        feature_2_branch = LocalBranchShortName.of('feature2')
+        assert annotations[feature_2_branch].text == 'annotation'
+        assert annotations[feature_2_branch].qualifiers.rebase is True
+        assert annotations[feature_2_branch].qualifiers.push is True
+        assert annotations[feature_2_branch].text_without_qualifiers == 'annotation'
+        assert annotations[feature_2_branch].qualifiers_text == ''
+
+        feature_3_branch = LocalBranchShortName.of('feature3')
+        assert annotations[feature_3_branch].text == 'annotation rebase=no push=no'
+        assert annotations[feature_3_branch].qualifiers.rebase is False
+        assert annotations[feature_3_branch].qualifiers.push is False
+        assert annotations[feature_3_branch].text_without_qualifiers == 'annotation'
+        assert annotations[feature_3_branch].qualifiers_text == 'rebase=no push=no'
+
+        feature_4_branch = LocalBranchShortName.of('feature4')
+        assert annotations[feature_4_branch].text == 'rebase=no push=no annotation'
+        assert annotations[feature_4_branch].qualifiers.rebase is False
+        assert annotations[feature_4_branch].qualifiers.push is False
+        assert annotations[feature_4_branch].text_without_qualifiers == 'annotation'
+        assert annotations[feature_4_branch].qualifiers_text == 'rebase=no push=no'
+
+        feature_5_branch = LocalBranchShortName.of('feature5')
+        assert annotations[feature_5_branch].text == 'annotation1 rebase=no annotation2 push=no annotation3'
+        assert annotations[feature_5_branch].qualifiers.rebase is False
+        assert annotations[feature_5_branch].qualifiers.push is False
+        assert annotations[feature_5_branch].text_without_qualifiers == 'annotation1 annotation2 annotation3'
+        assert annotations[feature_5_branch].qualifiers_text == 'rebase=no push=no'
+
+        feature_6_branch = LocalBranchShortName.of('feature6')
+        assert annotations[feature_6_branch].text == 'annotation1 rebase=nopush=no annotation2'
+        assert annotations[feature_6_branch].qualifiers.rebase is True
+        assert annotations[feature_6_branch].qualifiers.push is True
+        assert annotations[feature_6_branch].text_without_qualifiers == 'annotation1 rebase=nopush=no annotation2'
+        assert annotations[feature_6_branch].qualifiers_text == ''
+
+        feature_7_branch = LocalBranchShortName.of('feature7')
+        assert annotations[feature_7_branch].text == 'annotation1rebase=no push=noannotation2'
+        assert annotations[feature_7_branch].qualifiers.rebase is True
+        assert annotations[feature_7_branch].qualifiers.push is True
+        assert annotations[feature_7_branch].text_without_qualifiers == 'annotation1rebase=no push=noannotation2'
+        assert annotations[feature_7_branch].qualifiers_text == ''
+
+        feature_8_branch = LocalBranchShortName.of('feature8')
+        assert annotations[feature_8_branch].text == 'annotation rebase=no push=no rebase=no push=no'
+        assert annotations[feature_8_branch].qualifiers.rebase is False
+        assert annotations[feature_8_branch].qualifiers.push is False
+        assert annotations[feature_8_branch].text_without_qualifiers == 'annotation'
+        assert annotations[feature_8_branch].qualifiers_text == 'rebase=no push=no'
diff --git a/tests/test_discover.py b/tests/test_discover.py
new file mode 100644
index 0000000..1d781f8
--- /dev/null
+++ b/tests/test_discover.py
@@ -0,0 +1,62 @@
+from textwrap import dedent
+from .mockers import (assert_command, GitRepositorySandbox, launch_command, rewrite_definition_file)
+
+
+class TestDiscover:
+
+    def setup_method(self) -> None:
+        self.repo_sandbox = GitRepositorySandbox()
+
+        (
+            self.repo_sandbox
+            # Create the remote and sandbox repos, chdir into sandbox repo
+            .new_repo(self.repo_sandbox.remote_path, "--bare")
+            .new_repo(self.repo_sandbox.local_path)
+            .execute(f"git remote add origin {self.repo_sandbox.remote_path}")
+            .execute('git config user.email "tester@test.com"')
+            .execute('git config user.name "Tester Test"')
+        )
+
+    def test_discover(self) -> None:
+        (
+            self.repo_sandbox
+                .new_branch('master')
+                .commit()
+                .push()
+                .new_branch('feature1')
+                .commit()
+                .new_branch('feature2')
+                .commit()
+                .check_out("master")
+                .new_branch('feature3')
+                .commit()
+                .push()
+                .new_branch('feature4')
+                .commit()
+        )
+
+        body: str = \
+            """
+            master
+            feature1
+            feature2 annotation
+            feature3 annotation rebase=no push=no
+            """
+        body = dedent(body)
+
+        rewrite_definition_file(body)
+        launch_command('discover', '-y')
+        expected_status_output = (
+            """
+            master
+            |
+            o-feature1 (untracked)
+            | |
+            | o-feature2 (untracked)
+            |
+            o-feature3  rebase=no push=no
+              |
+              o-feature4 * (untracked)
+            """
+        )
+        assert_command(['status'], expected_status_output)
diff --git a/tests/test_github.py b/tests/test_github.py
index bbfe7a7..76e5275 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -125,7 +125,7 @@ class TestGithub:
         |
         o-branch-1
           |
-          o-feature *  PR #15 (github_user) WRONG PR BASE or MACHETE PARENT? PR has 'root'
+          o-feature *  PR #15 (github_user) WRONG PR BASE or MACHETE PARENT? PR has 'root' rebase=no push=no
         """
         assert_command(
             ['status'],
@@ -278,7 +278,7 @@ class TestGithub:
             },
             {
                 'head': {'ref': 'call-ws', 'repo': mock_repository_info},
-                'user': {'login': 'github_user'},
+                'user': {'login': 'very_complex_user_token'},
                 'base': {'ref': 'develop'},
                 'number': '31',
                 'html_url': 'www.github.com',
@@ -333,6 +333,11 @@ class TestGithub:
                 .add_remote('new_origin', 'https://github.com/user/repo.git')
         )
         launch_command("discover", "-y")
+
+        # test that `anno-prs` add `rebase=no push=no` qualifiers to branches associated with the PRs whose owner
+        # is different than the current user, overwrite annotation text but doesn't overwrite existing qualifiers
+        launch_command('anno', '-b=allow-ownership-link', 'rebase=no')
+        launch_command('anno', '-b=build-chain', 'rebase=no push=no')
         launch_command('github', 'anno-prs')
         assert_command(
             ["status"],
@@ -341,15 +346,15 @@ class TestGithub:
             |
             o-hotfix/add-trigger (diverged from origin)
               |
-              o-ignore-trailing *  PR #3 (github_user) (diverged from & older than origin)
+              o-ignore-trailing *  PR #3 (github_user) rebase=no push=no (diverged from & older than origin)
 
             develop
             |
-            x-allow-ownership-link  PR #7 (github_user) (ahead of origin)
+            x-allow-ownership-link  PR #7 (github_user) rebase=no (ahead of origin)
             | |
-            | x-build-chain (untracked)
+            | x-build-chain  rebase=no push=no (untracked)
             |
-            o-call-ws  PR #31 (github_user) (ahead of origin)
+            o-call-ws  PR #31 (ahead of origin)
               |
               x-drop-constraint (untracked)
             """
@@ -373,15 +378,15 @@ class TestGithub:
             |
             o-hotfix/add-trigger (diverged from origin)
               |
-              o-ignore-trailing *  PR #3 (github_user) (diverged from & older than origin)
+              o-ignore-trailing *  PR #3 (github_user) rebase=no push=no (diverged from & older than origin)
 
             develop
             |
-            x-allow-ownership-link  PR #7 (github_user) (ahead of origin)
+            x-allow-ownership-link  PR #7 (github_user) rebase=no (ahead of origin)
             | |
-            | x-build-chain (untracked)
+            | x-build-chain  rebase=no push=no (untracked)
             |
-            o-call-ws  PR #31 (github_user) (ahead of origin)
+            o-call-ws  PR #31 (ahead of origin)
               |
               x-drop-constraint (untracked)
             """,
@@ -979,7 +984,7 @@ class TestGithub:
             |
             o-hotfix/add-trigger
               |
-              o-ignore-trailing  PR #3 (github_user)
+              o-ignore-trailing  PR #3 (github_user) rebase=no push=no
                 |
                 o-chore/fields
 
@@ -987,13 +992,13 @@ class TestGithub:
             |
             o-enhance/feature
               |
-              o-bugfix/feature  PR #6 (github_user)
+              o-bugfix/feature  PR #6 (github_user) rebase=no push=no
                 |
-                o-allow-ownership-link  PR #12 (github_user)
+                o-allow-ownership-link  PR #12 (github_user) rebase=no push=no
                   |
-                  o-restrict_access  PR #17 (github_user)
+                  o-restrict_access  PR #17 (github_user) rebase=no push=no
                     |
-                    o-chore/redundant_checks *  PR #18 (github_user)
+                    o-chore/redundant_checks *  PR #18 (github_user) rebase=no push=no
             """
         )
         # broken chain of pull requests (add new root)
@@ -1005,7 +1010,7 @@ class TestGithub:
             |
             o-hotfix/add-trigger
               |
-              o-ignore-trailing  PR #3 (github_user)
+              o-ignore-trailing  PR #3 (github_user) rebase=no push=no
                 |
                 o-chore/fields
 
@@ -1013,19 +1018,19 @@ class TestGithub:
             |
             o-enhance/feature
               |
-              o-bugfix/feature  PR #6 (github_user)
+              o-bugfix/feature  PR #6 (github_user) rebase=no push=no
                 |
-                o-allow-ownership-link  PR #12 (github_user)
+                o-allow-ownership-link  PR #12 (github_user) rebase=no push=no
                   |
-                  o-restrict_access  PR #17 (github_user)
+                  o-restrict_access  PR #17 (github_user) rebase=no push=no
                     |
-                    o-chore/redundant_checks  PR #18 (github_user)
+                    o-chore/redundant_checks  PR #18 (github_user) rebase=no push=no
 
             bugfix/add_user
             |
-            o-testing/add_user  PR #22 (github_user)
+            o-testing/add_user  PR #22 (github_user) rebase=no push=no
               |
-              o-chore/comments *  PR #24 (github_user)
+              o-chore/comments *  PR #24 (github_user) rebase=no push=no
             """
         )
 
@@ -1038,7 +1043,7 @@ class TestGithub:
             |
             o-hotfix/add-trigger
               |
-              o-ignore-trailing  PR #3 (github_user)
+              o-ignore-trailing  PR #3 (github_user) rebase=no push=no
                 |
                 o-chore/fields
 
@@ -1046,19 +1051,19 @@ class TestGithub:
             |
             o-enhance/feature
               |
-              o-bugfix/feature  PR #6 (github_user)
+              o-bugfix/feature  PR #6 (github_user) rebase=no push=no
                 |
-                o-allow-ownership-link  PR #12 (github_user)
+                o-allow-ownership-link  PR #12 (github_user) rebase=no push=no
                   |
-                  o-restrict_access  PR #17 (github_user)
+                  o-restrict_access  PR #17 (github_user) rebase=no push=no
                     |
-                    o-chore/redundant_checks  PR #18 (github_user)
+                    o-chore/redundant_checks  PR #18 (github_user) rebase=no push=no
 
             bugfix/add_user
             |
-            o-testing/add_user  PR #22 (github_user)
+            o-testing/add_user  PR #22 (github_user) rebase=no push=no
               |
-              o-chore/comments *  PR #24 (github_user)
+              o-chore/comments *  PR #24 (github_user) rebase=no push=no
             """
         )
 
@@ -1071,7 +1076,7 @@ class TestGithub:
             |
             o-hotfix/add-trigger
               |
-              o-ignore-trailing  PR #3 (github_user)
+              o-ignore-trailing  PR #3 (github_user) rebase=no push=no
                 |
                 o-chore/fields
 
@@ -1079,21 +1084,21 @@ class TestGithub:
             |
             o-enhance/feature
             | |
-            | o-bugfix/feature  PR #6 (github_user)
+            | o-bugfix/feature  PR #6 (github_user) rebase=no push=no
             |   |
-            |   o-allow-ownership-link  PR #12 (github_user)
+            |   o-allow-ownership-link  PR #12 (github_user) rebase=no push=no
             |     |
-            |     o-restrict_access  PR #17 (github_user)
+            |     o-restrict_access  PR #17 (github_user) rebase=no push=no
             |       |
-            |       o-chore/redundant_checks  PR #18 (github_user)
+            |       o-chore/redundant_checks  PR #18 (github_user) rebase=no push=no
             |
-            o-enhance/add_user  PR #19 (github_user)
+            o-enhance/add_user  PR #19 (github_user) rebase=no push=no
 
             bugfix/add_user
             |
-            o-testing/add_user  PR #22 (github_user)
+            o-testing/add_user  PR #22 (github_user) rebase=no push=no
               |
-              o-chore/comments *  PR #24 (github_user)
+              o-chore/comments *  PR #24 (github_user) rebase=no push=no
             """
         )
 
@@ -1270,9 +1275,9 @@ class TestGithub:
 
             chore/sync_to_docs
             |
-            o-improve/refactor  PR #1 (github_user)
+            o-improve/refactor  PR #1 (github_user) rebase=no push=no
               |
-              o-comments/add_docstrings *  PR #2 (github_user)
+              o-comments/add_docstrings *  PR #2 (github_user) rebase=no push=no
             """
         )
 
@@ -1294,9 +1299,9 @@ class TestGithub:
 
             chore/sync_to_docs
             |
-            o-improve/refactor  PR #1 (github_user)
+            o-improve/refactor  PR #1 (github_user) rebase=no push=no
               |
-              o-comments/add_docstrings  PR #2 (github_user)
+              o-comments/add_docstrings  PR #2 (github_user) rebase=no push=no
                 |
                 o-sphinx_export *
             """
@@ -1357,6 +1362,222 @@ class TestGithub:
              "the head branch of given pull request."
              )
 
+    git_api_state_for_test_github_checkout_prs_of_current_user_and_other_users = MockGitHubAPIState(
+        [
+            {
+                'head': {'ref': 'chore/redundant_checks', 'repo': mock_repository_info},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'restrict_access'},
+                'number': '18',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            },
+            {
+                'head': {'ref': 'restrict_access', 'repo': mock_repository_info},
+                'user': {'login': 'very_complex_user_token'},
+                'base': {'ref': 'allow-ownership-link'},
+                'number': '17',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            },
+            {
+                'head': {'ref': 'allow-ownership-link', 'repo': mock_repository_info},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'bugfix/feature'},
+                'number': '12',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            },
+            {
+                'head': {'ref': 'bugfix/feature', 'repo': mock_repository_info},
+                'user': {'login': 'very_complex_user_token'},
+                'base': {'ref': 'enhance/feature'},
+                'number': '6',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            },
+            {
+                'head': {'ref': 'enhance/add_user', 'repo': mock_repository_info},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'develop'},
+                'number': '19',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            },
+            {
+                'head': {'ref': 'testing/add_user', 'repo': mock_repository_info},
+                'user': {'login': 'very_complex_user_token'},
+                'base': {'ref': 'bugfix/add_user'},
+                'number': '22',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            },
+            {'head': {'ref': 'chore/comments', 'repo': mock_repository_info},
+             'user': {'login': 'github_user'},
+             'base': {'ref': 'testing/add_user'},
+             'number': '24',
+             'html_url': 'www.github.com',
+             'state': 'open'
+             },
+            {
+                'head': {'ref': 'ignore-trailing', 'repo': mock_repository_info},
+                'user': {'login': 'very_complex_user_token'},
+                'base': {'ref': 'hotfix/add-trigger'},
+                'number': '3',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            },
+            {
+                'head': {'ref': 'bugfix/remove-n-option',
+                         'repo': {'full_name': 'testing/checkout_prs', 'html_url': GitRepositorySandbox.second_remote_path}},
+                'user': {'login': 'github_user'},
+                'base': {'ref': 'develop'},
+                'number': '5',
+                'html_url': 'www.github.com',
+                'state': 'closed'
+            }
+        ]
+    )
+
+    @mock.patch('git_machete.cli.exit_script', mock_exit_script)
+    # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_checkout_prs`
+    # due to `git fetch` executed by `checkout-prs` subcommand.
+    @mock.patch('git_machete.github.GITHUB_REMOTE_PATTERNS', FAKE_GITHUB_REMOTE_PATTERNS)
+    @mock.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
+    @mock.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+    @mock.patch('git_machete.github.__get_github_token', mock__get_github_token)
+    @mock.patch('urllib.request.Request', git_api_state_for_test_github_checkout_prs_of_current_user_and_other_users.new_request())
+    @mock.patch('urllib.request.urlopen', MockContextManager)
+    @mock.patch('git_machete.github.derive_current_user_login', mock_derive_current_user_login)
+    def test_github_checkout_prs_of_current_user_and_other_users(self, tmp_path: Any) -> None:
+        (
+            self.repo_sandbox.new_branch("root")
+            .commit("initial commit")
+            .new_branch("develop")
+            .commit("first commit")
+            .push()
+            .new_branch("enhance/feature")
+            .commit("introduce feature")
+            .push()
+            .new_branch("bugfix/feature")
+            .commit("bugs removed")
+            .push()
+            .new_branch("allow-ownership-link")
+            .commit("fixes")
+            .push()
+            .new_branch('restrict_access')
+            .commit('authorized users only')
+            .push()
+            .new_branch("chore/redundant_checks")
+            .commit('remove some checks')
+            .push()
+            .check_out("root")
+            .new_branch("master")
+            .commit("Master commit")
+            .push()
+            .new_branch("hotfix/add-trigger")
+            .commit("HOTFIX Add the trigger")
+            .push()
+            .new_branch("ignore-trailing")
+            .commit("Ignore trailing data")
+            .push()
+            .delete_branch("root")
+            .new_branch('chore/fields')
+            .commit("remove outdated fields")
+            .push()
+            .check_out('develop')
+            .new_branch('enhance/add_user')
+            .commit('allow externals to add users')
+            .push()
+            .new_branch('bugfix/add_user')
+            .commit('first round of fixes')
+            .push()
+            .new_branch('testing/add_user')
+            .commit('add test set for add_user feature')
+            .push()
+            .new_branch('chore/comments')
+            .commit('code maintenance')
+            .push()
+            .check_out('master')
+        )
+        for branch in ('chore/redundant_checks', 'restrict_access', 'allow-ownership-link', 'bugfix/feature', 'enhance/add_user',
+                       'testing/add_user', 'chore/comments', 'bugfix/add_user'):
+            self.repo_sandbox.execute(f"git branch -D {branch}")
+
+        launch_command('discover')
+
+        # test that `checkout-prs` add `rebase=no push=no` qualifiers to branches associated with the PRs whose owner
+        # is different than the current user
+        launch_command('github', 'checkout-prs', '--all')
+        assert_command(
+            ["status"],
+            """
+            master *
+            |
+            o-hotfix/add-trigger
+              |
+              o-ignore-trailing  PR #3
+                |
+                o-chore/fields
+
+            develop
+            |
+            o-enhance/feature
+            | |
+            | o-bugfix/feature  PR #6
+            |   |
+            |   o-allow-ownership-link  PR #12 (github_user) rebase=no push=no
+            |     |
+            |     o-restrict_access  PR #17
+            |       |
+            |       o-chore/redundant_checks  PR #18 (github_user) rebase=no push=no
+            |
+            o-enhance/add_user  PR #19 (github_user) rebase=no push=no
+
+            bugfix/add_user
+            |
+            o-testing/add_user  PR #22
+              |
+              o-chore/comments  PR #24 (github_user) rebase=no push=no
+            """
+        )
+
+        # test that `checkout-prs` doesn't overwrite annotation qualifiers but overwrites annotation text
+        launch_command('anno', '-b=allow-ownership-link', 'branch_annotation rebase=no')
+        launch_command('github', 'checkout-prs', '--all')
+        assert_command(
+            ["status"],
+            """
+            master *
+            |
+            o-hotfix/add-trigger
+              |
+              o-ignore-trailing  PR #3
+                |
+                o-chore/fields
+
+            develop
+            |
+            o-enhance/feature
+            | |
+            | o-bugfix/feature  PR #6
+            |   |
+            |   o-allow-ownership-link  PR #12 (github_user) rebase=no
+            |     |
+            |     o-restrict_access  PR #17
+            |       |
+            |       o-chore/redundant_checks  PR #18 (github_user) rebase=no push=no
+            |
+            o-enhance/add_user  PR #19 (github_user) rebase=no push=no
+
+            bugfix/add_user
+            |
+            o-testing/add_user  PR #22
+              |
+              o-chore/comments  PR #24 (github_user) rebase=no push=no
+            """
+        )
+
     git_api_state_for_test_github_sync = MockGitHubAPIState(
         [
             {
diff --git a/tests/test_traverse.py b/tests/test_traverse.py
index ec5ee88..0a5f6e8 100644
--- a/tests/test_traverse.py
+++ b/tests/test_traverse.py
@@ -1,7 +1,8 @@
+from textwrap import dedent
 from typing import Any
 
 from .mockers import (GitRepositorySandbox, assert_command, launch_command,
-                      mock_run_cmd)
+                      mock_run_cmd, rewrite_definition_file)
 
 
 class TestTraverse:
@@ -348,3 +349,126 @@ class TestTraverse:
               o-snickers *
             """
         )
+
+    def test_traverse_qualifiers_no_push(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        self.setup_discover_standard_tree()
+
+        body: str = \
+            """
+            develop
+            \tallow-ownership-link push=no
+            \t\tbuild-chain push=no
+            \tcall-ws push=no
+            \t\tdrop-constraint push=no
+            master
+            \thotfix/add-trigger push=no
+            \t\tignore-trailing
+            """
+
+        body = dedent(body).strip()
+        rewrite_definition_file(body)
+
+        launch_command("traverse", "-Wy")
+        assert_command(
+            ["status"],
+            """
+            develop
+            |
+            o-allow-ownership-link  push=no (diverged from origin)
+            | |
+            | o-build-chain  push=no (untracked)
+            |
+            o-call-ws  push=no (ahead of origin)
+              |
+              o-drop-constraint  push=no (untracked)
+
+            master
+            |
+            o-hotfix/add-trigger  push=no (diverged from origin)
+              |
+              o-ignore-trailing *
+            """,
+        )
+
+    def test_traverse_qualifiers_no_rebase(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        self.setup_discover_standard_tree()
+
+        body: str = \
+            """
+            develop
+            \tallow-ownership-link rebase=no
+            \t\tbuild-chain rebase=no
+            \tcall-ws rebase=no
+            \t\tdrop-constraint
+            master
+            \thotfix/add-trigger
+            \t\tignore-trailing
+            """
+
+        body = dedent(body).strip()
+        rewrite_definition_file(body)
+
+        launch_command("traverse", "-Wy")
+        assert_command(
+            ["status"],
+            """
+            develop
+            |
+            x-allow-ownership-link  rebase=no
+            | |
+            | x-build-chain  rebase=no
+            |
+            o-call-ws  rebase=no
+              |
+              o-drop-constraint
+
+            master
+            |
+            o-hotfix/add-trigger
+              |
+              o-ignore-trailing *
+            """,
+        )
+
+    def test_traverse_qualifiers_no_rebase_no_push(self, mocker: Any) -> None:
+        mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
+        self.setup_discover_standard_tree()
+
+        body: str = \
+            """
+            develop
+            \tallow-ownership-link rebase=no push=no
+            \t\tbuild-chain rebase=no push=no
+            \tcall-ws
+            \t\tdrop-constraint rebase=no push=no
+            master
+            \thotfix/add-trigger
+            \t\tignore-trailing
+            """
+
+        body = dedent(body).strip()
+        rewrite_definition_file(body)
+
+        launch_command("traverse", "-Wy")
+        assert_command(
+            ["status"],
+            """
+            develop
+            |
+            x-allow-ownership-link  rebase=no push=no (ahead of origin)
+            | |
+            | x-build-chain  rebase=no push=no (untracked)
+            |
+            o-call-ws
+              |
+              x-drop-constraint  rebase=no push=no (untracked)
+
+            master
+            |
+            o-hotfix/add-trigger
+              |
+              o-ignore-trailing *
+            """,
+        )