Repository: VirtusLab/git-machete
Pull request #839 (2023-03-03 10:21:55)

Release v3.16.0


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 459bcdc..95879e8 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,9 @@
 # Release notes
 
+## New in git-machete 3.16.0
+
+- deprecated: `machete.overrideForkPoint.<branch>.whileDescendantOf` is no longer taken into account; it's still written, however, for compatibility reasons
+
 ## New in git-machete 3.15.2
 
 - fixed: zsh shell completion for the `slide-out` command no longer fails
diff --git a/ci/checks/enforce-consistent-style-for-github.sh b/ci/checks/enforce-consistent-style-for-github.sh
index 62fb8dc..b1220dd 100755
--- a/ci/checks/enforce-consistent-style-for-github.sh
+++ b/ci/checks/enforce-consistent-style-for-github.sh
@@ -6,6 +6,6 @@ self_dir=$(cd "$(dirname "$0")" &>/dev/null; pwd -P)
 source "$self_dir"/utils.sh
 self_name=$(basename "$0")
 
-if git grep -w Github -- :!**/$self_name; then
+if git grep Github -- :!**/$self_name; then
   die "Please use 'GitHub' instead of 'Github'."
 fi
diff --git a/ci/homebrew/ci-deploy.sh b/ci/homebrew/ci-deploy.sh
index 3388498..7ba2d32 100644
--- a/ci/homebrew/ci-deploy.sh
+++ b/ci/homebrew/ci-deploy.sh
@@ -3,6 +3,8 @@
 set -e -o pipefail -u -x
 
 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" < /dev/null
+## The two lines below are added to avoid ->  Warning: /home/linuxbrew/.linuxbrew/bin is not in your PATH
+(echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /home/circleci/.bash_profile
 eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
 
 if [[ ${1-} == "--dry-run" || ${CIRCLE_BRANCH-} != "master" ]]; then
@@ -24,12 +26,22 @@ git config --global user.name "Git Machete Bot"
 
 # Relying on HOMEBREW_GITHUB_API_TOKEN, provided by the CI
 # See https://docs.brew.sh/Manpage -> Ctrl+F HOMEBREW_GITHUB_API_TOKEN
+
+# We need to run `brew tap homebrew/core` manually because since Homebrew 4.0.0 it is no longer done by default when installing `brew`; see https://brew.sh/2023/02/16/homebrew-4.0.0/
+brew tap homebrew/core
 echo "Bump Homebrew formula"
+# `--force` ignores the existence of open PRs for the same formula.
+# It is useful for the rare cases where a develop/master build runs while a PR for the previously released version is still pending.
+# See https://app.circleci.com/pipelines/github/VirtusLab/git-machete/3140/workflows/6ee0916d-fc11-49b2-b29a-5bbc95cb25c4/jobs/16418:
+#   Error: These open pull requests may be duplicates:
+#   git-machete 3.15.1 https://github.com/Homebrew/homebrew-core/pull/123123
+#   Duplicate PRs should not be opened. Use --force to override this error.
+flags=(--force --no-browse --verbose --url "$url" --sha256 "$sha256")
 if [[ $do_push == true ]]; then
-  brew bump-formula-pr --no-browse --verbose --url "$url" --sha256 "$sha256" git-machete
+  brew bump-formula-pr "${flags[@]}" git-machete
 else
   echo "Refraining from push since it's a dry run"
-  brew bump-formula-pr --write-only --no-browse --verbose --url "$url" --sha256 "$sha256" git-machete
+  brew bump-formula-pr --write-only "${flags[@]}" git-machete
 
   export HOMEBREW_NO_INSTALL_FROM_API=1
   brew config
diff --git a/ci/pypi/ci-wait-for-package.sh b/ci/pypi/ci-wait-for-package.sh
index 5b43241..27eebb8 100644
--- a/ci/pypi/ci-wait-for-package.sh
+++ b/ci/pypi/ci-wait-for-package.sh
@@ -7,6 +7,7 @@ timeout_minutes=${2-30}
 version=$(python3 setup.py --version)
 url=https://$host/packages/source/g/git-machete/git-machete-$version.tar.gz
 
+# Typically just 2 minutes of waiting are enough.
 for i in $(seq 1 "$timeout_minutes"); do
   echo "Checking package availability at $url, attempt #$i out of $timeout_minutes..."
   if curl --fail --location --silent --show-error --output /dev/null "$url"; then
diff --git a/docs/source/cli_help/config.rst b/docs/source/cli_help/config.rst
index 6c37eca..d6ea155 100644
--- a/docs/source/cli_help/config.rst
+++ b/docs/source/cli_help/config.rst
@@ -14,13 +14,13 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
     .. include:: github_config_keys.rst
         :start-line: 2
 
-``machete.overrideForkPoint.<branch>.{to,whileDescendantOf}``:
-    Executing ``git machete fork-point --override-to=<revision> [<branch>]`` sets up a fork point override for <branch>.
+``machete.overrideForkPoint.<branch>.to``:
+    Executing ``git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]`` sets up a fork point override for <branch>.
 
-    The override data is stored under ``machete.overrideForkPoint.<branch>.to`` and
-    ``machete.overrideForkPoint.<branch>.whileDescendantOf`` git config keys.
+    The override data is stored under ``machete.overrideForkPoint.<branch>.to`` git config key.
 
-    There should be **no** need for the user to interact with these keys directly.
+    There should be **no** need for the user to interact with this key directly,
+    ``git machete fork-point`` with flags should be used instead.
 
 ``machete.status.extraSpaceBeforeBranchName``:
     .. include:: status_config_key.rst
diff --git a/docs/source/cli_help/fork-point.rst b/docs/source/cli_help/fork-point.rst
index e4fcf7b..208965b 100644
--- a/docs/source/cli_help/fork-point.rst
+++ b/docs/source/cli_help/fork-point.rst
@@ -19,21 +19,21 @@ fork-point
   git machete fork-point --override-to=<revision>|--override-to-inferred|--override-to-parent [<branch>]
   git machete fork-point --unset-override [<branch>]
 
-Note: in all three forms, if no <branch> is specified, the currently checked out branch is assumed.
+Note: in all three forms, if no ``<branch>`` is specified, the currently checked out branch is assumed.
 The branch in question does not need to occur in the definition file.
 
 
-Without any option, displays full hash of the fork point commit for the <branch>.
-Fork point of the given <branch> is the commit at which the history of the <branch> diverges from history of any other branch.
+Without any option, displays full hash of the fork point commit for the ``<branch>``.
+Fork point of the given ``<branch>`` is the commit at which the history of the ``<branch>`` diverges from history of any other branch.
 
-Fork point is assumed by many ``git machete`` commands as the place where the unique history of the <branch> starts.
+Fork point is assumed by many ``git machete`` commands as the place where the unique history of the ``<branch>`` starts.
 The range of commits between the fork point and the tip of the given branch is, for instance:
 
     * listed for each branch by ``git machete status --list-commits``
     * passed to ``git rebase`` by ``git machete`` ``reapply``/``slide-out``/``traverse``/``update``
     * provided to ``git diff``/``log`` by ``git machete`` ``diff``/``log``.
 
-``git machete`` assumes fork point of <branch> is the most recent commit in the log of <branch> that has NOT been introduced on that very branch,
+``git machete`` assumes fork point of ``<branch>`` is the most recent commit in the log of ``<branch>`` that has NOT been introduced on that very branch,
 but instead occurs on a reflog (see help for ``git reflog``) of some other, usually chronologically earlier, branch.
 This yields a correct result in typical cases, but there are some situations
 (esp. when some local branches have been deleted) where the fork point might not be determined correctly.
@@ -47,26 +47,25 @@ while the former scans reflogs of all local branches and their remote tracking b
 This makes git machete's ``fork-point`` more resilient to modifications of ``.git/machete`` :ref:`file` where certain branches are re-attached under new parents (upstreams).
 
 
-With ``--override-to=<revision>``, sets up a fork point override for <branch>.
-Fork point for <branch> will be overridden to the provided <revision> (commit) as long as the <branch> still points to (or is descendant of) the commit X
-that <branch> pointed to at the moment the override is set up.
+With ``--override-to=<revision>``, sets up a fork point override for ``<branch>``.
+Fork point for ``<branch>`` will be overridden to the provided <revision> (commit) as long as the ``<branch>`` still points to (or is descendant of) that commit.
 Even if revision is a symbolic name (e.g. other branch name or ``HEAD~3)`` and not explicit commit hash (like ``a1b2c3ff``),
 it's still resolved to a specific commit hash at the moment the override is set up (and not later when the override is actually used).
-The override data is stored under ``machete.overrideForkPoint.<branch>.to`` and ``machete.overrideForkPoint.<branch>.whileDescendantOf`` git config keys.
-Note: the provided fork point <revision> must be an ancestor of the current <branch> commit X.
+The override data is stored under ``machete.overrideForkPoint.<branch>.to`` git config key.
+Note: the provided fork point <revision> must be an ancestor of the current ``<branch>`` commit.
 
-With ``--override-to-parent``, overrides fork point of the <branch> to the commit currently pointed by <branch>'s parent in the branch dependency tree.
-Note: this will only work if <branch> has a parent at all (i.e. is not a root) and parent of <branch> is an ancestor of current <branch> commit X.
+With ``--override-to-parent``, overrides fork point of the ``<branch>`` to the commit currently pointed by ``<branch>``'s parent in the branch dependency tree.
+Note: this will only work if ``<branch>`` has a parent at all (i.e. is not a root) and parent of ``<branch>`` is an ancestor of current ``<branch>`` commit.
 
-With ``--inferred``, displays the commit that ``git machete fork-point`` infers to be the fork point of <branch>.
-If there is NO fork point override for <branch>, this is identical to the output of ``git machete fork-point``.
-If there is a fork point override for <branch>, this is identical to the what the output of ``git machete fork-point`` would be if the override was NOT present.
+With ``--inferred``, displays the commit that ``git machete fork-point`` infers to be the fork point of ``<branch>``.
+If there is NO fork point override for ``<branch>``, this is identical to the output of ``git machete fork-point``.
+If there is a fork point override for ``<branch>``, this is identical to the what the output of ``git machete fork-point`` would be if the override was NOT present.
 
-With ``--override-to-inferred`` option, overrides fork point of the <branch> to the commit that ``git machete fork-point`` infers to be the fork point of <branch>.
+With ``--override-to-inferred`` option, overrides fork point of the ``<branch>`` to the commit that ``git machete fork-point`` infers to be the fork point of ``<branch>``.
 Note: this piece of information is also displayed by ``git machete status --list-commits`` in case a :yellow:`yellow` edge occurs.
 
-With ``--unset-override``, the fork point override for <branch> is unset.
-This is simply done by removing the corresponding ``machete.overrideForkPoint.<branch>.*`` config entries.
+With ``--unset-override``, the fork point override for ``<branch>`` is unset.
+This is simply done by removing the corresponding ``machete.overrideForkPoint.<branch>.to`` config entry.
 
 
 Note: if an overridden fork point applies to a branch ``B``, then it's considered to be connected with a :green:`green` edge to its upstream (parent) ``U``,
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 832ff3c..0c9d57a 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.15.2'
+__version__ = '3.16.0'
diff --git a/git_machete/client.py b/git_machete/client.py
index 5a8dc7a..c270dbc 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -1502,7 +1502,7 @@ class MacheteClient:
                 if pr.base != upstream:
                     warn(f'branch {bold(pr.head)} has a different base in PR #{bold(str(pr.number))} ({bold(pr.base)}) '
                          f'than in machete file ({bold(upstream) if upstream else "<none, is a root>"})')
-                    anno += f" WRONG PR BASE or MACHETE PARENT? PR has {bold(pr.base)}"
+                    anno += f" WRONG PR BASE or MACHETE PARENT? PR has {pr.base}"
                 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
@@ -1631,71 +1631,46 @@ class MacheteClient:
                     debug(f"upstream candidate {candidate} rejected ({reject_reason_message})")
         return None
 
-    # Also includes config that is incomplete (only one entry out of two) or otherwise invalid.
+    # Also includes config that is invalid (corresponding to a non-existent/GCed commit etc.).
     def has_any_fork_point_override_config(self, branch: LocalBranchShortName) -> bool:
         return (self.__git.get_config_attr_or_none(git_config_keys.override_fork_point_to(branch)) or
+                # Note that we still include the now-deprecated `whileDescendantOf` key for this purpose.
                 self.__git.get_config_attr_or_none(git_config_keys.override_fork_point_while_descendant_of(branch))) is not None
 
     def __get_fork_point_override_data(self, branch: LocalBranchShortName) -> Optional[ForkPointOverrideData]:
-        to, while_descendant_of = None, None
+        # Note that here we ignore the now-deprecated `whileDescendantOf`.
         to_key = git_config_keys.override_fork_point_to(branch)
         if FullCommitHash.is_valid(value=self.__git.get_config_attr_or_none(to_key), git_context=self.__git):
             to = FullCommitHash.of(self.__git.get_config_attr_or_none(to_key))
-        while_descendant_of_key = git_config_keys.override_fork_point_while_descendant_of(branch)
-        if FullCommitHash.is_valid(value=self.__git.get_config_attr_or_none(while_descendant_of_key), git_context=self.__git):
-            while_descendant_of = FullCommitHash.of(self.__git.get_config_attr_or_none(while_descendant_of_key))
-        if not to and not while_descendant_of:
-            return None
-        if to and not while_descendant_of:
-            warn(f"`{to_key}` config is set but `{while_descendant_of_key}` config is missing or invalid")
-            return None
-        if not to and while_descendant_of:
-            warn(f"`{while_descendant_of_key}` config is set but `{to_key}` config is missing or invalid")
+        else:
             return None
 
         to_hash: Optional[FullCommitHash] = self.__git.get_commit_hash_by_revision(to)
-        while_descendant_of_hash: Optional[FullCommitHash] = self.__git.get_commit_hash_by_revision(while_descendant_of)
-        if not to_hash or not while_descendant_of_hash:
-            if not to_hash:
-                warn(f"`{to_key}` config value {bold(to)} does not point to a valid commit")
-            if not while_descendant_of_hash:
-                warn(f"`{while_descendant_of_key}` config value {bold(while_descendant_of)} does not point to a valid commit")
-            return None
-        # This check needs to be performed every time the config is retrieved.
-        # We can't rely on the values being validated in set_fork_point_override(),
-        # since the config could have been modified outside of git-machete.
-        if not self.__git.is_ancestor_or_equal(to_hash.full_name(), while_descendant_of_hash.full_name()):
-            warn(
-                f"commit {bold(self.__git.get_short_commit_hash_by_revision(to))} pointed by `{to_key}` config "
-                f"is not an ancestor of commit {bold(self.__git.get_short_commit_hash_by_revision(while_descendant_of))} "
-                f"pointed by `{while_descendant_of_key}` config")
+        if not to_hash:
+            warn(f"`{to_key}` config value {bold(to)} does not point to a valid commit")
             return None
-        return ForkPointOverrideData(to_hash, while_descendant_of_hash)
+
+        return ForkPointOverrideData(to_hash)
 
     def __get_overridden_fork_point(self, branch: LocalBranchShortName) -> Optional[FullCommitHash]:
         override_data: ForkPointOverrideData = self.__get_fork_point_override_data(branch)
         if not override_data:
             return None
 
-        to, while_descendant_of = override_data.to_hash, override_data.while_descendant_of_hash
-        # Note that this check is distinct from the is_ancestor check performed in
-        # get_fork_point_override_data.
-        # While the latter checks the sanity of fork point override configuration,
-        # the former checks if the override still applies to wherever the given
-        # branch currently points.
-        if not self.__git.is_ancestor_or_equal(while_descendant_of.full_name(), branch.full_name()):
+        to = override_data.to_hash
+        # Checks if the override still applies to wherever the given branch currently points.
+        if not self.__git.is_ancestor_or_equal(to.full_name(), branch.full_name()):
             warn(fmt(
-                f"since branch {bold(branch)} is no longer a descendant of commit "
-                f"{bold(while_descendant_of)}, ",
-                f"the fork point override to commit {bold(to)} no longer applies.\n",
+                f"since branch {bold(branch)} is no longer a descendant of commit {bold(to)}, ",
+                "the fork point override to this commit no longer applies.\n",
                 f"Consider running:\n  `git machete fork-point --unset-override {branch}`\n"))
             return None
-        debug(f"since branch {branch} is descendant of while_descendant_of={while_descendant_of}, "
-              f"fork point of {branch} is overridden to {to}")
+        debug(f"since branch {branch} is descendant of {to}, fork point of {branch} is overridden to {to}")
         return to
 
     def unset_fork_point_override(self, branch: LocalBranchShortName) -> None:
         self.__git.unset_config_attr(git_config_keys.override_fork_point_to(branch))
+        # Note that we still unset the now-deprecated `whileDescendantOf` key.
         self.__git.unset_config_attr(git_config_keys.override_fork_point_while_descendant_of(branch))
 
     def set_fork_point_override(self, branch: LocalBranchShortName, to_revision: AnyRevision) -> None:
@@ -1711,20 +1686,15 @@ class MacheteClient:
         to_key = git_config_keys.override_fork_point_to(branch)
         self.__git.set_config_attr(to_key, to_hash)
 
+        # Let's still set the now-deprecated `whileDescendantOf` key to maintain compatibility with older git-machete clients
+        # (esp. IntelliJ plugin) that still require that key for an override to apply.
         while_descendant_of_key = git_config_keys.override_fork_point_while_descendant_of(branch)
-        branch_hash = self.__git.get_commit_hash_by_revision(branch)
-        self.__git.set_config_attr(while_descendant_of_key, branch_hash)
-
-        print(fmt(f"Fork point for <b>{branch}</b> is overridden to "
-                  f"<b>{self.__git.get_revision_repr(to_revision)}</b>.\n",
-                  f"This applies as long as <b>{branch}</b> points to (or is descendant of)"
-                  " its current head (commit "
-                  f"<b>{self.__git.get_short_commit_hash_by_revision(branch_hash)}</b>).\n\n",
-                  f"This information is stored under git config keys:\n"
-                  f"  * `{to_key}`\n"
-                  f"  * `{while_descendant_of_key}`\n\n",
-                  "To unset this override, use:\n  `git machete fork-point "
-                  f"--unset-override {branch}`"))
+        self.__git.set_config_attr(while_descendant_of_key, to_hash)
+
+        print(fmt(f"Fork point for <b>{branch}</b> is overridden to {self.__git.get_revision_repr(to_revision)}.\n",
+                  f"This applies as long as <b>{branch}</b> is a descendant of commit <b>{to_hash}</b>.\n\n"
+                  f"This information is stored under `{to_key}` git config key.\n\n"
+                  f"To unset this override, use:\n  `git machete fork-point --unset-override {branch}`"))
 
     def __pick_remote(
             self,
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index 17f432c..20466d5 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -272,14 +272,14 @@ long_docs: Dict[str, str] = {
               Note that you do <b>not</b> need to set all four keys at once.
               For example, in a typical usage of GitHub Enterprise, it should be enough to just set `machete.github.domain`.
 
-           `machete.overrideForkPoint.<branch>.{to,whileDescendantOf}`:
+           `machete.overrideForkPoint.<branch>.to`:
  
-              Executing `git machete fork-point --override-to=<revision> [<branch>]` sets up a fork point override for <branch>.
+              Executing `git machete fork-point --override-to[-parent|-inferred|=<revision>] [<branch>]` sets up a fork point override for <branch>.
 
-              The override data is stored under `machete.overrideForkPoint.<branch>.to` and
-              `machete.overrideForkPoint.<branch>.whileDescendantOf` git config keys.
+              The override data is stored under `machete.overrideForkPoint.<branch>.to` git config key.
 
-              There should be <b>no</b> need for the user to interact with these keys directly.
+              There should be <b>no</b> need for the user to interact with this key directly,
+              `git machete fork-point` with flags should be used instead.
 
            `machete.status.extraSpaceBeforeBranchName`:
 
@@ -438,19 +438,19 @@ long_docs: Dict[str, str] = {
            git machete fork-point --override-to=<revision>|--override-to-inferred|--override-to-parent [<branch>]
            git machete fork-point --unset-override [<branch>]</b>
 
-        Note: in all three forms, if no <branch> is specified, the currently checked out branch is assumed.
+        Note: in all three forms, if no `<branch>` is specified, the currently checked out branch is assumed.
         The branch in question does not need to occur in the definition file.
 
-        Without any option, displays full hash of the fork point commit for the <branch>.
-        Fork point of the given <branch> is the commit at which the history of the <branch> diverges from history of any other branch.
+        Without any option, displays full hash of the fork point commit for the `<branch>`.
+        Fork point of the given `<branch>` is the commit at which the history of the `<branch>` diverges from history of any other branch.
 
-        Fork point is assumed by many `git machete` commands as the place where the unique history of the <branch> starts.
+        Fork point is assumed by many `git machete` commands as the place where the unique history of the `<branch>` starts.
         The range of commits between the fork point and the tip of the given branch is, for instance:
            * listed for each branch by `git machete status --list-commits`
            * passed to `git rebase` by `git machete` `reapply`/`slide-out`/`traverse`/`update`
            * provided to `git diff`/`log` by `git machete` `diff`/`log`.
 
-        `git machete` assumes fork point of <branch> is the most recent commit in the log of <branch> that has NOT been introduced on that very branch,
+        `git machete` assumes fork point of `<branch>` is the most recent commit in the log of `<branch>` that has NOT been introduced on that very branch,
         but instead occurs on a reflog (see help for `git reflog`) of some other, usually chronologically earlier, branch.
         This yields a correct result in typical cases, but there are some situations
         (esp. when some local branches have been deleted) where the fork point might not be determined correctly.
@@ -463,26 +463,25 @@ long_docs: Dict[str, str] = {
         while the former scans reflogs of all local branches and their remote tracking branches.
         This makes git machete's `fork-point` more resilient to modifications of `.git/machete` `file` where certain branches are re-attached under new parents (upstreams).
 
-        With `--override-to=<revision>`, sets up a fork point override for <branch>.
-        Fork point for <branch> will be overridden to the provided <revision> (commit) as long as the <branch> still points to (or is descendant of) the commit X
-        that <branch> pointed to at the moment the override is set up.
+        With `--override-to=<revision>`, sets up a fork point override for `<branch>`.
+        Fork point for `<branch>` will be overridden to the provided <revision> (commit) as long as the `<branch>` still points to (or is descendant of) that commit.
         Even if revision is a symbolic name (e.g. other branch name or `HEAD~3)` and not explicit commit hash (like `a1b2c3ff`),
         it's still resolved to a specific commit hash at the moment the override is set up (and not later when the override is actually used).
-        The override data is stored under `machete.overrideForkPoint.<branch>.to` and `machete.overrideForkPoint.<branch>.whileDescendantOf` git config keys.
-        Note: the provided fork point <revision> must be an ancestor of the current <branch> commit X.
+        The override data is stored under `machete.overrideForkPoint.<branch>.to` git config key.
+        Note: the provided fork point <revision> must be an ancestor of the current `<branch>` commit.
 
-        With `--override-to-parent`, overrides fork point of the <branch> to the commit currently pointed by <branch>'s parent in the branch dependency tree.
-        Note: this will only work if <branch> has a parent at all (i.e. is not a root) and parent of <branch> is an ancestor of current <branch> commit X.
+        With `--override-to-parent`, overrides fork point of the `<branch>` to the commit currently pointed by `<branch>`'s parent in the branch dependency tree.
+        Note: this will only work if `<branch>` has a parent at all (i.e. is not a root) and parent of `<branch>` is an ancestor of current `<branch>` commit.
 
-        With `--inferred`, displays the commit that `git machete fork-point` infers to be the fork point of <branch>.
-        If there is NO fork point override for <branch>, this is identical to the output of `git machete fork-point`.
-        If there is a fork point override for <branch>, this is identical to the what the output of `git machete fork-point` would be if the override was NOT present.
+        With `--inferred`, displays the commit that `git machete fork-point` infers to be the fork point of `<branch>`.
+        If there is NO fork point override for `<branch>`, this is identical to the output of `git machete fork-point`.
+        If there is a fork point override for `<branch>`, this is identical to the what the output of `git machete fork-point` would be if the override was NOT present.
 
-        With `--override-to-inferred` option, overrides fork point of the <branch> to the commit that `git machete fork-point` infers to be the fork point of <branch>.
+        With `--override-to-inferred` option, overrides fork point of the `<branch>` to the commit that `git machete fork-point` infers to be the fork point of `<branch>`.
         Note: this piece of information is also displayed by `git machete status --list-commits` in case a <yellow>yellow</yellow> edge occurs.
 
-        With `--unset-override`, the fork point override for <branch> is unset.
-        This is simply done by removing the corresponding `machete.overrideForkPoint.<branch>.*` config entries.
+        With `--unset-override`, the fork point override for `<branch>` is unset.
+        This is simply done by removing the corresponding `machete.overrideForkPoint.<branch>.to` config entry.
 
         Note: if an overridden fork point applies to a branch `B`, then it's considered to be connected with a <green>green</green> edge to its upstream (parent) `U`,
         even if the overridden fork point of `B` is NOT equal to the commit pointed by `U`.
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 078a719..ace674d 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -127,9 +127,7 @@ class FullCommitHash(AnyRevision):
 
     @staticmethod
     def is_valid(value: str, git_context: "GitContext") -> bool:
-        revision = AnyRevision(value)
-        return value is not None and len(value) == 40 and all(c in string.hexdigits for c in value) \
-            and git_context.is_commit_present_in_repository(revision)
+        return value is not None and len(value) == 40 and all(c in string.hexdigits for c in value)
 
     def full_name(self) -> "FullCommitHash":
         return self
@@ -157,9 +155,8 @@ class FullTreeHash(str):
 
 
 class ForkPointOverrideData:
-    def __init__(self, to_hash: FullCommitHash, while_descendant_of_hash: FullCommitHash):
+    def __init__(self, to_hash: FullCommitHash):
         self.to_hash: FullCommitHash = to_hash
-        self.while_descendant_of_hash: FullCommitHash = while_descendant_of_hash
 
 
 class GitLogEntry(NamedTuple):
@@ -193,7 +190,7 @@ class GitContext:
         self.__remotes_cached: Optional[List[str]] = None
         self.__counterparts_for_fetching_cached: Optional[
             Dict[LocalBranchShortName, Optional[RemoteBranchShortName]]] = None  # TODO (#110): default dict with None
-        self.__short_commit_hash_by_revision_cached: Dict[AnyRevision, ShortCommitHash] = {}
+        self.__short_commit_hash_by_revision_cached: Dict[AnyRevision, Optional[ShortCommitHash]] = {}
         self.__tree_hash_by_commit_hash_cached: Optional[
             Dict[FullCommitHash, Optional[FullTreeHash]]] = None  # TODO (#110): default dict with None
         self.__commit_hash_by_revision_cached: Optional[
@@ -424,9 +421,12 @@ class GitContext:
     def __find_short_commit_hash_by_revision(self, revision: AnyRevision) -> ShortCommitHash:
         return ShortCommitHash.of(self._popen_git("rev-parse", "--short", revision + "^{commit}").stdout.rstrip())  # noqa: FS003
 
-    def get_short_commit_hash_by_revision(self, revision: AnyRevision) -> ShortCommitHash:
+    def get_short_commit_hash_by_revision_or_none(self, revision: AnyRevision) -> Optional[ShortCommitHash]:
         if revision not in self.__short_commit_hash_by_revision_cached:
-            self.__short_commit_hash_by_revision_cached[revision] = self.__find_short_commit_hash_by_revision(revision)
+            try:
+                self.__short_commit_hash_by_revision_cached[revision] = self.__find_short_commit_hash_by_revision(revision)
+            except MacheteException:
+                self.__short_commit_hash_by_revision_cached[revision] = None
         return self.__short_commit_hash_by_revision_cached[revision]
 
     def __find_commit_hash_by_revision(self, revision: AnyRevision) -> Optional[FullCommitHash]:
@@ -682,11 +682,11 @@ class GitContext:
         self.__reflogs_cached = None
 
     def get_revision_repr(self, revision: AnyRevision) -> str:
-        short_hash = self.get_short_commit_hash_by_revision(revision)
-        if self.is_full_hash(revision.full_name()) or revision == short_hash:
-            return f"commit {revision}"
+        short_hash = self.get_short_commit_hash_by_revision_or_none(revision)
+        if not short_hash or self.is_full_hash(revision.full_name()) or revision == short_hash:
+            return f"commit <b>{revision}</b>"
         else:
-            return f"{revision} (commit {self.get_short_commit_hash_by_revision(revision)})"
+            return f"<b>{revision}</b> (commit <b>{short_hash}</b>)"
 
     # Note: while rebase is ongoing, the repository is always in a detached HEAD state,
     # so we need to extract the name of the currently rebased branch from the rebase-specific internals
diff --git a/git_machete/github.py b/git_machete/github.py
index 09de79b..b2bcbe5 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -71,9 +71,9 @@ class RemoteAndOrganizationAndRepository:
     def from_config(cls,
                     git: GitContext
                     ) -> "RemoteAndOrganizationAndRepository":
-        return RemoteAndOrganizationAndRepository(remote=git.get_config_attr_or_none(key=git_config_keys.GITHUB_REMOTE),
-                                                  organization=git.get_config_attr_or_none(key=git_config_keys.GITHUB_ORGANIZATION),
-                                                  repository=git.get_config_attr_or_none(key=git_config_keys.GITHUB_REPOSITORY))
+        return cls(remote=git.get_config_attr_or_none(key=git_config_keys.GITHUB_REMOTE),
+                   organization=git.get_config_attr_or_none(key=git_config_keys.GITHUB_ORGANIZATION),
+                   repository=git.get_config_attr_or_none(key=git_config_keys.GITHUB_REPOSITORY))
 
     @classmethod
     def from_url(cls,
@@ -86,77 +86,89 @@ class RemoteAndOrganizationAndRepository:
             if match:
                 org = match.group(1)
                 repo = match.group(2)
-                return RemoteAndOrganizationAndRepository(remote=remote,
-                                                          organization=org,
-                                                          repository=repo if repo[-4:] != '.git' else repo[:-4])
-        return RemoteAndOrganizationAndRepository(remote=None,
-                                                  organization=None,
-                                                  repository=None)
+                return cls(remote=remote,
+                           organization=org,
+                           repository=repo if repo[-4:] != '.git' else repo[:-4])
+        return cls(remote=None,
+                   organization=None,
+                   repository=None)
 
 
 class GitHubToken:
     GITHUB_TOKEN_ENV_VAR = 'GITHUB_TOKEN'
 
     def __init__(self,
-                 domain: str
+                 value: str,
+                 provider: str
                  ) -> None:
-        self.__domain = domain
-        self.__value: Optional[str] = None
-        self.__provider: Optional[str] = None
-        for token_retrieval_method in [self.__get_token_from_env,
-                                       self.__get_token_from_file_in_home_directory,
-                                       self.__get_token_from_gh,
-                                       self.__get_token_from_hub]:
-            if not (self.value and self.provider):
-                token_retrieval_method()
-            if self.value and self.provider:
-                debug("authenticating via " + self.provider)
-                break
+        self.__value = value
+        self.__provider = provider
+        debug("authenticating via " + self.provider)
 
-    def __bool__(self) -> bool:
-        return self.__value is not None and self.__provider is not None
+    @property
+    def value(self) -> Optional[str]:
+        return self.__value
 
-    def __get_token_from_hub(self) -> None:
-        home_path: str = str(Path.home())
-        config_hub_path: str = os.path.join(home_path, ".config", "hub")
-        if os.path.isfile(config_hub_path):
-            with open(config_hub_path) as config_hub:
-                # ~/.config/hub is a yaml file, with a structure similar to:
-                #
-                # {domain1}:
-                # - user: {username1}
-                #   oauth_token: *******************
-                #   protocol: {protocol}
+    @property
+    def provider(self) -> Optional[str]:
+        return self.__provider
+
+    @classmethod
+    def for_domain(cls, domain: str) -> Optional["GitHubToken"]:
+        return (cls.__get_token_from_env() or
+                cls.__get_token_from_file_in_home_directory(domain) or
+                cls.__get_token_from_gh(domain) or
+                cls.__get_token_from_hub(domain))
+
+    @classmethod
+    def __get_token_from_env(cls) -> Optional["GitHubToken"]:
+        debug(f"1. Trying to authenticate via `{cls.GITHUB_TOKEN_ENV_VAR}` environment variable...")
+        github_token = os.environ.get(cls.GITHUB_TOKEN_ENV_VAR)
+        if github_token:
+            return cls(value=github_token,
+                       provider=f'`{cls.GITHUB_TOKEN_ENV_VAR}` environment variable')
+        return None
+
+    @classmethod
+    def __get_token_from_file_in_home_directory(cls, domain: str) -> Optional["GitHubToken"]:
+        debug("2. Trying to authenticate via `~/.github-token`...")
+        required_file_name = '.github-token'
+        provider = f'auth token for {domain} from `~/.github-token`'
+        file_full_path = os.path.expanduser(f'~/{required_file_name}')
+
+        if os.path.isfile(file_full_path):
+            with open(file_full_path) as file:
+                # ~/.github-token is a file with a structure similar to:
                 #
-                # {domain2}:
-                # - user: {username2}
-                #   oauth_token: *******************
-                #   protocol: {protocol}
-                found_host = False
-                for line in config_hub.readlines():
-                    if line.rstrip() == self.__domain + ":":
-                        found_host = True
-                    elif found_host and line.lstrip().startswith("oauth_token:"):
-                        result = re.sub(' *oauth_token: *', '', line).rstrip().replace('"', '')
-                        self.__value = result
-                        self.__provider = f'auth token for {self.__domain} from `hub` GitHub CLI'
-                        break
+                # ghp_mytoken_for_github_com
+                # ghp_myothertoken_for_git_example_org git.example.org
+                # ghp_yetanothertoken_for_git_example_com git.example.com
+
+                for line in file.readlines():
+                    if line.endswith(" " + domain):
+                        token = line.split(" ")[0]
+                        return cls(value=token, provider=provider)
+                    elif domain == GitHubClient.DEFAULT_GITHUB_DOMAIN and " " not in line.rstrip():
+                        return cls(value=line.rstrip(), provider=provider)
+        return None
 
-    def __get_token_from_gh(self) -> None:
+    @classmethod
+    def __get_token_from_gh(cls, domain: str) -> Optional["GitHubToken"]:
+        debug("3. Trying to authenticate via `gh` GitHub CLI...")
         # Abort without error if `gh` isn't available
         gh = shutil.which('gh')
         if not gh:
-            return
+            return None
 
         # There is also `gh auth token`, but it's only been added in gh v2.17.0, in Oct 2022.
         # Let's stick to the older `gh auth status --show-token` for compatibility.
         proc = subprocess.run(
-            [gh, "auth", "status", "--hostname", self.__domain, "--show-token"],
+            [gh, "auth", "status", "--hostname", domain, "--show-token"],
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE
         )
         if proc.returncode != 0:
-            return
+            return None
 
         # `gh auth status --show-token` outputs to stderr in the form:
         #
@@ -169,36 +181,37 @@ class GitHubToken:
         stderr = proc.stderr.decode()
         match = re.search(r"Token: (\w+)", stderr)
         if match:
-            self.__value = match.group(1)
-            self.__provider = f'auth token for {self.__domain} from `hub` GitHub CLI'
-
-    def __get_token_from_env(self) -> None:
-        github_token = os.environ.get(self.GITHUB_TOKEN_ENV_VAR)
-        if github_token:
-            self.__value = github_token
-            self.__provider = f'`{self.GITHUB_TOKEN_ENV_VAR}` environment variable'
-
-    def __get_token_from_file_in_home_directory(self) -> None:
-        required_file_name = '.github-token'
-        self.__provider = f'auth token for {self.__domain} from `~/.github-token`'
-        file_full_path = os.path.expanduser(f'~/{required_file_name}')
-
-        if os.path.isfile(file_full_path):
-            with open(file_full_path) as file:
-                for line in file.readlines():
-                    if line.endswith(" " + self.__domain):
-                        token = line.split(" ")[0]
-                        self.__value = token
-                    elif self.__domain == GitHubClient.DEFAULT_GITHUB_DOMAIN and " " not in line.rstrip():
-                        self.__value = line.rstrip()
-
-    @property
-    def value(self) -> Optional[str]:
-        return self.__value
+            return cls(value=match.group(1),
+                       provider=f'auth token for {domain} from `gh` GitHub CLI')
+        return None
 
-    @property
-    def provider(self) -> Optional[str]:
-        return self.__provider
+    @classmethod
+    def __get_token_from_hub(cls, domain: str) -> Optional["GitHubToken"]:
+        debug("4. Trying to authenticate via `hub` GitHub CLI...")
+        home_path: str = str(Path.home())
+        config_hub_path: str = os.path.join(home_path, ".config", "hub")
+        if os.path.isfile(config_hub_path):
+            with open(config_hub_path) as config_hub:
+                # ~/.config/hub is a yaml file, with a structure similar to:
+                #
+                # {domain1}:
+                # - user: {username1}
+                #   oauth_token: *******************
+                #   protocol: {protocol}
+                #
+                # {domain2}:
+                # - user: {username2}
+                #   oauth_token: *******************
+                #   protocol: {protocol}
+                found_host = False
+                for line in config_hub.readlines():
+                    if line.rstrip() == domain + ":":
+                        found_host = True
+                    elif found_host and line.lstrip().startswith("oauth_token:"):
+                        result = re.sub(' *oauth_token:  *', '', line).rstrip().replace('"', '')
+                        return cls(value=result,
+                                   provider=f'auth token for {domain} from `hub` GitHub CLI')
+        return None
 
     @classmethod
     def get_possible_providers(cls) -> str:
@@ -219,7 +232,7 @@ class GitHubClient:
         self.__domain: str = domain
         self.__organization: str = organization
         self.__repository: str = repository
-        self.__token: GitHubToken = GitHubToken(domain=domain)
+        self.__token: Optional[GitHubToken] = GitHubToken.for_domain(domain)
 
     @property
     def organization(self) -> str:
@@ -282,7 +295,7 @@ class GitHubClient:
                 else:
                     raise MacheteException(
                         first_line + f'You might not have the required permissions for this repository.\n'
-                                     f'Provide a GitHub API token with `repo` access via {self.__token.provider}.\n'
+                                     f'Provide a GitHub API token with `repo` access.\n'
                                      f'Visit `https://{self.__domain}/settings/tokens` to generate a new one.\n'
                                      'You can also use a different token provider, available providers can be found '
                                      'when running `git machete help github`.')
diff --git a/requirements.coverage.txt b/requirements.coverage.txt
index aefbf82..41bc0c4 100644
--- a/requirements.coverage.txt
+++ b/requirements.coverage.txt
@@ -1,3 +1,3 @@
-coverage==7.1.0
+coverage==7.2.1
 pytest==7.2.1
 pytest-mock==3.10.0
diff --git a/requirements.mypy.txt b/requirements.mypy.txt
index 4f7bd69..4dc682c 100644
--- a/requirements.mypy.txt
+++ b/requirements.mypy.txt
@@ -1,2 +1,2 @@
-mypy==1.0.0
+mypy==1.0.1
 pytest==7.2.1


Test patch:
diff --git a/tests/mockers.py b/tests/mockers.py
index acdd3e0..006cadd 100644
--- a/tests/mockers.py
+++ b/tests/mockers.py
@@ -16,10 +16,7 @@ from urllib.error import HTTPError
 from urllib.parse import ParseResult, parse_qs, urlparse
 
 from git_machete import cli
-from git_machete.git_operations import (FullCommitHash, GitContext,
-                                        LocalBranchShortName)
-from git_machete.github import GitHubToken
-from git_machete.options import CommandLineOptions
+from git_machete.git_operations import FullCommitHash, GitContext
 from git_machete.utils import dim
 
 """
@@ -433,54 +430,3 @@ def mock_exit_script(status_code: int, error: Optional[BaseException] = None) ->
 
 def mock_exit_script_no_exit(status_code: int, error: Optional[BaseException] = None) -> None:
     return
-
-
-class FakeCommandLineOptions(CommandLineOptions):
-    def __init__(self) -> None:
-        super().__init__()
-        self.opt_no_interactive_rebase: bool = True
-        self.opt_yes: bool = True
-
-
-class FakeGitHubToken(GitHubToken):
-    def __bool__(self) -> bool:
-        return True
-
-    @property
-    def value(self) -> Optional[str]:
-        return 'fake_token'
-
-    @property
-    def provider(self) -> Optional[str]:
-        return 'fake_provider'
-
-
-class EmptyGitHubToken(GitHubToken):
-    def __bool__(self) -> bool:
-        return False
-
-    @property
-    def value(self) -> Optional[str]:
-        return 'dummy_token'
-
-    @property
-    def provider(self) -> Optional[str]:
-        return 'dummy_provider'
-
-
-def mock_github_remote_url_patterns(domain: str) -> List[str]:
-    return ['(.*)/(.*)']
-
-
-def mock_fetch_ref(cls: Any, remote: str, ref: str) -> None:
-    branch: LocalBranchShortName = LocalBranchShortName.of(ref[ref.index(':') + 1:])
-    git.create_branch(branch, get_current_commit_hash(), switch_head=True)
-
-
-def mock_derive_current_user_login(domain: str) -> str:
-    return "very_complex_user_token"
-
-
-def mock_input(msg: str) -> str:
-    print(msg)
-    return '1'
diff --git a/tests/test_fork_point.py b/tests/test_fork_point.py
index 788be80..dfde52b 100644
--- a/tests/test_fork_point.py
+++ b/tests/test_fork_point.py
@@ -71,23 +71,23 @@ class TestForkPoint:
 
         # invalid fork point with length not equal to 40
         self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 39 * 'a')
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.whileDescendantOf', 39 * 'b')
         assert launch_command('fork-point').strip() == develop_branch_fork_point
 
         # invalid, non-hexadecimal alphanumeric characters present in the fork point
         self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 20 * 'g1')
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.whileDescendantOf', 20 * 'g1')
         assert launch_command('fork-point').strip() == develop_branch_fork_point
 
         # invalid, non-hexadecimal special characters present in the fork point
         self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 40 * '#')
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.whileDescendantOf', 40 * '!')
         assert launch_command('fork-point').strip() == develop_branch_fork_point
 
         # valid commit hash but not present in the repository
         self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.to', 40 * 'a')
-        self.repo_sandbox.add_git_config_key('machete.overrideForkPoint.develop.whileDescendantOf', 40 * 'a')
-        assert launch_command('fork-point').strip() == develop_branch_fork_point
+        assert launch_command('fork-point').strip() == (
+               "Warn: since branch develop is no longer a descendant of commit aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, the fork point override to this commit no longer applies.\n"  # noqa: E501
+               "Consider running:\n"
+               "  git machete fork-point --unset-override develop\n\n" +
+               develop_branch_fork_point)
 
         # valid fork-point override commit hash
         launch_command('fork-point', f'--override-to={master_branch_first_commit_hash}')
diff --git a/tests/test_github.py b/tests/test_github.py
index 137954f..c79ccc8 100644
--- a/tests/test_github.py
+++ b/tests/test_github.py
@@ -1,25 +1,101 @@
 import json
 import os
-import subprocess
+from subprocess import CalledProcessError, CompletedProcess
 from tempfile import mkdtemp
-from typing import Any, Dict
+from textwrap import dedent
+from typing import Any, Callable, Dict, List, Optional
+from unittest.mock import mock_open
 
 import pytest
 
 from git_machete.exceptions import MacheteException
-from git_machete.github import GitHubClient, RemoteAndOrganizationAndRepository
-
-from .mockers import (EmptyGitHubToken, FakeCommandLineOptions,
-                      FakeGitHubToken, GitRepositorySandbox,
-                      MockContextManager, MockContextManagerRaise403,
-                      MockGitHubAPIState, MockHTTPError, assert_command,
-                      launch_command, mock_ask_if,
-                      mock_derive_current_user_login, mock_exit_script,
-                      mock_fetch_ref, mock_github_remote_url_patterns,
-                      mock_input, mock_run_cmd,
-                      mock_should_perform_interactive_slide_out,
+from git_machete.git_operations import LocalBranchShortName
+from git_machete.github import (GitHubClient, GitHubToken,
+                                RemoteAndOrganizationAndRepository)
+from git_machete.options import CommandLineOptions
+
+from .mockers import (GitRepositorySandbox, MockContextManager,
+                      MockContextManagerRaise403, MockGitHubAPIState,
+                      MockHTTPError, assert_command, get_current_commit_hash,
+                      git, launch_command, mock_ask_if, mock_exit_script,
+                      mock_run_cmd, mock_should_perform_interactive_slide_out,
                       rewrite_definition_file)
 
+
+class FakeCommandLineOptions(CommandLineOptions):
+    def __init__(self) -> None:
+        super().__init__()
+        self.opt_no_interactive_rebase: bool = True
+        self.opt_yes: bool = True
+
+
+def mock_for_domain_none(domain: str) -> None:
+    return None
+
+
+def mock_for_domain_fake(domain: str) -> GitHubToken:
+    return GitHubToken(value='dummy_token',
+                       provider='dummy_provider')
+
+
+def mock_github_remote_url_patterns(domain: str) -> List[str]:
+    return ['(.*)/(.*)']
+
+
+def mock_fetch_ref(cls: Any, remote: str, ref: str) -> None:
+    branch: LocalBranchShortName = LocalBranchShortName.of(ref[ref.index(':') + 1:])
+    git.create_branch(branch, get_current_commit_hash(), switch_head=True)
+
+
+def mock_derive_current_user_login(domain: str) -> str:
+    return "very_complex_user_token"
+
+
+def mock_input(msg: str) -> str:
+    print(msg)
+    return '1'
+
+
+def mock_is_file_true(file: Any) -> bool:
+    return True
+
+
+def mock_is_file_false(file: Any) -> bool:
+    return False
+
+
+def mock_is_file_not_github_token(file: Any) -> bool:
+    if '.github-token' not in file:
+        return True
+    return False
+
+
+def mock_os_environ_get_none(self: Any, key: str, default: Optional[str] = None) -> Any:
+    if key == GitHubToken.GITHUB_TOKEN_ENV_VAR:
+        return None
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+
+def mock_os_environ_get_github_token(self: Any, key: str, default: Optional[str] = None) -> Any:
+    if key == GitHubToken.GITHUB_TOKEN_ENV_VAR:
+        return 'github_token_from_env_var'
+    try:
+        return self[key]
+    except KeyError:
+        return default
+
+
+def mock_shutil_which_gh(path: Optional[str]) -> Callable[[Any], str]:
+    return lambda cmd: path
+
+
+def mock_subprocess_run(returncode: int, stdout: str = '', stderr: str = ''):  # type: ignore[no-untyped-def]
+    return lambda *args, **kwargs: CompletedProcess(args, returncode, bytes(stdout, 'utf-8'), bytes(stderr, 'utf-8'))
+
+
 prs_per_page = 3
 number_of_pages = 3
 
@@ -52,7 +128,7 @@ def mock_info(x: Any) -> Dict[str, Any]:
 mock_info.counter = mock_read.counter = 0  # type: ignore[attr-defined]
 
 
-class TestGithub:
+class TestGitHub:
     mock_repository_info: Dict[str, str] = {'full_name': 'testing/checkout_prs',
                                             'html_url': 'https://github.com/tester/repo_sandbox.git'}
 
@@ -602,7 +678,7 @@ class TestGithub:
         # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_create_pr`
         # due to `git fetch` executed by `create-pr` subcommand.
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
@@ -664,7 +740,7 @@ class TestGithub:
         # We need to mock GITHUB_REMOTE_PATTERNS in the tests for `test_github_create_pr`
         # due to `git fetch` executed by `create-pr` subcommand.
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('urllib.error.HTTPError', MockHTTPError)  # need to provide read() method, which does not actually read error from url
@@ -928,7 +1004,7 @@ class TestGithub:
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_checkout_prs.new_request())
         mocker.patch('urllib.request.urlopen', MockContextManager)
 
@@ -1462,7 +1538,7 @@ class TestGithub:
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('urllib.request.Request',
                      self.git_api_state_for_test_github_checkout_prs_of_current_user_and_other_users.new_request())
         mocker.patch('urllib.request.urlopen', MockContextManager)
@@ -1615,7 +1691,7 @@ class TestGithub:
         mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-        mocker.patch('git_machete.github.GitHubToken', FakeGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_fake)
         mocker.patch('urllib.request.urlopen', MockContextManager)
         mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_sync.new_request())
 
@@ -1668,7 +1744,7 @@ class TestGithub:
         )
         assert_command(['status'], expected_status_output)
 
-        with pytest.raises(subprocess.CalledProcessError):
+        with pytest.raises(CalledProcessError):
             self.repo_sandbox.check_out("mars")
 
     def test_github_remote_patterns(self) -> None:
@@ -1694,7 +1770,7 @@ class TestGithub:
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)  # to hide git outputs in tests
-        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('urllib.request.urlopen', MockContextManager)
         mocker.patch('git_machete.github.GitHubClient.derive_current_user_login', mock_derive_current_user_login)
         mocker.patch('urllib.request.Request', MockGitHubAPIState([]).new_request())
@@ -1734,7 +1810,7 @@ class TestGithub:
         mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-        mocker.patch('git_machete.github.GitHubToken', EmptyGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_none)
         mocker.patch('urllib.request.urlopen', MockContextManagerRaise403)
         mocker.patch('git_machete.cli.exit_script', mock_exit_script)
 
@@ -1744,7 +1820,7 @@ class TestGithub:
         expected_error_message = (
             "GitHub API returned `403` HTTP status with error message: `Forbidden`\n"
             "You might not have the required permissions for this repository.\n"
-            "Provide a GitHub API token with `repo` access via dummy_provider.\n"
+            "Provide a GitHub API token with `repo` access.\n"
             f"Visit `https://{github_enterprise_domain}/settings/tokens` to generate a new one.\n"
             "You can also use a different token provider, available providers can be found when running `git machete help github`.")
 
@@ -1753,27 +1829,28 @@ class TestGithub:
         if e:
             assert e.value.args[0] == expected_error_message, 'Verify that expected error message has appeared.'
 
+    git_api_state_for_test_github_enterprise_domain = MockGitHubAPIState(
+        [
+            {
+                'head': {'ref': 'snickers', 'repo': mock_repository_info},
+                'user': {'login': 'other_user'},
+                'base': {'ref': 'develop'},
+                'number': '7',
+                'html_url': 'www.github.com',
+                'state': 'open'
+            }
+        ]
+    )
+
     def test_github_enterprise_domain(self, mocker: Any) -> None:
-        git_api_state_for_test_github_enterprise_domain = MockGitHubAPIState(
-            [
-                {
-                    'head': {'ref': 'snickers', 'repo': self.mock_repository_info},
-                    'user': {'login': 'other_user'},
-                    'base': {'ref': 'develop'},
-                    'number': '7',
-                    'html_url': 'www.github.com',
-                    'state': 'open'
-                }
-            ]
-        )
         mocker.patch('git_machete.client.MacheteClient.should_perform_interactive_slide_out', mock_should_perform_interactive_slide_out)
         mocker.patch('git_machete.utils.run_cmd', mock_run_cmd)
         mocker.patch('git_machete.client.MacheteClient.ask_if', mock_ask_if)
         mocker.patch('git_machete.options.CommandLineOptions', FakeCommandLineOptions)
         mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
-        mocker.patch('git_machete.github.GitHubToken', FakeGitHubToken)
+        mocker.patch('git_machete.github.GitHubToken.for_domain', mock_for_domain_fake)
         mocker.patch('urllib.request.urlopen', MockContextManager)
-        mocker.patch('urllib.request.Request', git_api_state_for_test_github_enterprise_domain.new_request())
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_enterprise_domain.new_request())
         mocker.patch('git_machete.cli.exit_script', mock_exit_script)
 
         github_enterprise_domain = 'git.example.org'
@@ -1789,3 +1866,107 @@ class TestGithub:
             .add_git_config_key('machete.github.domain', github_enterprise_domain)
         )
         launch_command('github', 'checkout-prs', '--all')
+
+    def test_github_token_retrieval_order(self, mocker: Any) -> None:
+        mocker.patch('os.path.isfile', mock_is_file_false)
+        mocker.patch('shutil.which', mock_shutil_which_gh(None))
+        mocker.patch('urllib.request.urlopen', MockContextManager)
+        mocker.patch('git_machete.github.github_remote_url_patterns', mock_github_remote_url_patterns)
+        mocker.patch('_collections_abc.Mapping.get', mock_os_environ_get_none)
+        mocker.patch('urllib.request.Request', self.git_api_state_for_test_github_enterprise_domain.new_request())
+
+        (
+            self.repo_sandbox.new_branch("develop")
+                .commit("first commit")
+                .push()
+                .new_branch("snickers")
+                .commit("first commit")
+                .push()
+                .check_out("develop")
+                .delete_branch("snickers")
+        )
+
+        expected_output = ["__get_token_from_env(cls=<class 'git_machete.github.GitHubToken'>): "
+                           "1. Trying to authenticate via `GITHUB_TOKEN` environment variable...",
+                           "__get_token_from_file_in_home_directory(cls=<class 'git_machete.github.GitHubToken'>, domain=github.com): "
+                           "2. Trying to authenticate via `~/.github-token`...",
+                           "__get_token_from_gh(cls=<class 'git_machete.github.GitHubToken'>, domain=github.com): "
+                           "3. Trying to authenticate via `gh` GitHub CLI...",
+                           "__get_token_from_hub(cls=<class 'git_machete.github.GitHubToken'>, domain=github.com): "
+                           "4. Trying to authenticate via `hub` GitHub CLI..."]
+
+        assert launch_command('github', 'anno-prs', '--debug').splitlines()[8:12] == expected_output
+
+    def test_get_token_from_env_var(self, mocker: Any) -> None:
+        mocker.patch('_collections_abc.Mapping.get', mock_os_environ_get_github_token)
+
+        github_token = GitHubToken.for_domain(domain=GitHubClient.DEFAULT_GITHUB_DOMAIN)
+        assert github_token.provider == '`GITHUB_TOKEN` environment variable'
+        assert github_token.value == 'github_token_from_env_var'
+
+    def test_get_token_from_file_in_home_directory(self, mocker: Any) -> None:
+        github_token_contents = ('ghp_mytoken_for_github_com\n'
+                                 'ghp_myothertoken_for_git_example_org git.example.org\n'
+                                 'ghp_yetanothertoken_for_git_example_com git.example.com')
+        _mock_open = mock_open(read_data=github_token_contents)
+        _mock_open.return_value.readlines.return_value = github_token_contents.split('\n')
+        mocker.patch('builtins.open', _mock_open)
+        mocker.patch('os.path.isfile', mock_is_file_true)
+
+        domain = GitHubClient.DEFAULT_GITHUB_DOMAIN
+        github_token = GitHubToken.for_domain(domain=domain)
+        assert github_token.provider == f'auth token for {domain} from `~/.github-token`'
+        assert github_token.value == 'ghp_mytoken_for_github_com'
+
+        domain = 'git.example.org'
+        github_token = GitHubToken.for_domain(domain=domain)
+        assert github_token.provider == f'auth token for {domain} from `~/.github-token`'
+        assert github_token.value == 'ghp_myothertoken_for_git_example_org'
+
+        domain = 'git.example.com'
+        github_token = GitHubToken.for_domain(domain=domain)
+        assert github_token.provider == f'auth token for {domain} from `~/.github-token`'
+        assert github_token.value == 'ghp_yetanothertoken_for_git_example_com'
+
+    def test_get_token_from_gh(self, mocker: Any) -> None:
+        mocker.patch('os.path.isfile', mock_is_file_false)
+        mocker.patch('_collections_abc.Mapping.get', mock_os_environ_get_none)
+        mocker.patch('shutil.which', mock_shutil_which_gh('/path/to/gh'))
+        mocker.patch('subprocess.run', mock_subprocess_run(returncode=0, stdout='stdout', stderr='''
+        github.com
+            ✓ Logged in to github.com as Foo Bar (/Users/foo_bar/.config/gh/hosts.yml)
+            ✓ Git operations for github.com configured to use ssh protocol.
+            ✓ Token: ghp_mytoken_for_github_com_from_gh_cli
+            ✓ Token scopes: gist, read:discussion, read:org, repo, workflow
+        '''))
+
+        domain = 'git.example.com'
+        github_token = GitHubToken.for_domain(domain=domain)
+        assert github_token.provider == f'auth token for {domain} from `gh` GitHub CLI'
+        assert github_token.value == 'ghp_mytoken_for_github_com_from_gh_cli'
+
+    def test_get_token_from_hub(self, mocker: Any) -> None:
+        domain1 = GitHubClient.DEFAULT_GITHUB_DOMAIN
+        domain2 = 'git.example.org'
+        config_hub_contents = f'''        {domain1}:
+        - user: username1
+          oauth_token: ghp_mytoken_for_github_com
+          protocol: protocol
+
+        {domain2}:
+        - user: username2
+          oauth_token: ghp_myothertoken_for_git_example_org
+          protocol: protocol
+        '''
+
+        mocker.patch('builtins.open', mock_open(read_data=dedent(config_hub_contents)))
+        mocker.patch('os.path.isfile', mock_is_file_not_github_token)
+        mocker.patch('subprocess.run', mock_subprocess_run(returncode=1))
+
+        github_token = GitHubToken.for_domain(domain=domain1)
+        assert github_token.provider == f'auth token for {domain1} from `hub` GitHub CLI'
+        assert github_token.value == 'ghp_mytoken_for_github_com'
+
+        github_token = GitHubToken.for_domain(domain=domain2)
+        assert github_token.provider == f'auth token for {domain2} from `hub` GitHub CLI'
+        assert github_token.value == 'ghp_myothertoken_for_git_example_org'