diff --git a/README.md b/README.md
index 55e7e538a..8f094682a 100644
--- a/README.md
+++ b/README.md
@@ -127,8 +127,6 @@ nix-env -i git-machete
The latest released version, however, is generally available in the unstable channel.
Stable channels may lag behind; see [repology](https://repology.org/project/git-machete/versions) for the current channel-package mapping.
-
-
### Using Pex
The [Pex tool](https://github.com/pex-tool/pex) (short for Python EXecutable) allows you to build "pex" files which are executable Python environments in a single file.
@@ -141,6 +139,8 @@ pex git-machete -m git_machete.bin:main -o git-machete
Then put the produced `git-machete` file somewhere on your `PATH`.
+
+
## Quick start
### Discover the branch layout
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index e6049a035..e856b377a 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,11 @@
# Release notes
-## New in git-machete 3.28.1
+## New in git-machete 3.29.0
+
+- added: git config keys `machete.github.prDescriptionIntroStyle` and `machete.gitlab.mrDescriptionIntroStyle`
+- added: ability to turn off PR/MR description intro completely by setting the git config keys to `none` (suggested by @tir38)
+- added: ability to also include downstream PRs/MRs in PR/MR description intro (suggested by @aouaki)
+- changed: layout and ordering of PRs/MRs in PR/MR description intro to better match `git machete status`
## New in git-machete 3.28.0
diff --git a/ci/checks/prohibit-trailing-whitespace.sh b/ci/checks/prohibit-trailing-whitespace.sh
index 4504d4292..99c49ef0d 100755
--- a/ci/checks/prohibit-trailing-whitespace.sh
+++ b/ci/checks/prohibit-trailing-whitespace.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
-if git grep -EIn ' +$' -- :!'*.py'; then
+if git grep -EIn ' +$' -- :!git_machete/generated_docs.py; then
echo 'The above lines contain trailing whitespace, please tidy up'
exit 1
fi
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 07a790953..cec4a36d2 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "GIT-MACHETE" "1" "Aug 22, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Aug 23, 2024" "" "git-machete"
.SH NAME
-git-machete \- git-machete 3.28.1
+git-machete \- git-machete 3.29.0
.sp
git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
.sp
@@ -360,6 +360,12 @@ but also \fBgithub checkout\-prs\fP, \fBgithub create\-pr\fP, \fBgithub retarget
Setting this config key to \fBtrue\fP will force \fBgit machete github create\-pr\fP to take PR description
from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.github/pull_request_template.md\fP is present.
.TP
+.B \fBmachete.github.prDescriptionIntroStyle\fP:
+Select the style of the intro prepended to PR description:
+* \fBfull\fP \-\-\- include both a chain of upstream PRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream PRs
+* \fBup\-only\fP \-\-\- default, include only a chain of upstream PRs
+* \fBnone\fP \-\-\- prepend no intro to the PR description at all
+.TP
.B \fBmachete.gitlab.{domain,remote,namespace,project}\fP:
.INDENT 7.0
.INDENT 3.5
@@ -392,6 +398,12 @@ but also \fBgitlab checkout\-mrs\fP, \fBgitlab create\-mr\fP, \fBgitlab retarget
Setting this config key to \fBtrue\fP will force \fBgit machete gitlab create\-mr\fP to take MR description
from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.gitlab/merge_request_templates/Default.md\fP is present.
.TP
+.B \fBmachete.gitlab.mrDescriptionIntroStyle\fP:
+Select the style of the intro prepended to MR description:
+* \fBfull\fP \-\-\- include both a chain of upstream MRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream MRs
+* \fBup\-only\fP \-\-\- default, include only a chain of upstream MRs
+* \fBnone\fP \-\-\- prepend no intro to the MR description at all
+.TP
.B \fBmachete.overrideForkPoint..to\fP:
Executing \fBgit machete fork\-point \-\-override\-to[\-parent|\-inferred|=] []\fP sets up a fork point override for \fB\fP\&.
.sp
@@ -1009,10 +1021,10 @@ Draft PRs don\(aqt get code owners automatically added as reviewers.
.B \fBretarget\-pr [\-b|\-\-branch=] [\-\-ignore\-if\-missing]\fP:
Sets the base of the current (or specified) branch\(aqs PR to upstream (parent) branch, as seen by git machete (see \fBgit machete show up\fP).
.sp
-If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended with a section
-listing the entire related chain of PRs.
+If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
+with an intro section listing the entire related chain of PRs.
.sp
-This header will be updated or removed accordingly with the subsequent runs of \fBretarget\-pr\fP\&.
+This header will be updated or removed accordingly with the subsequent runs of \fBretarget\-pr\fP, even if the base branch is already up to date.
.sp
\fBOptions:\fP
.INDENT 7.0
@@ -1075,6 +1087,12 @@ but also \fBgithub checkout\-prs\fP, \fBgithub create\-pr\fP, \fBgithub retarget
.B \fBmachete.github.forceDescriptionFromCommitMessage\fP (\fBcreate\-pr\fP only):
Setting this config key to \fBtrue\fP will force \fBgit machete github create\-pr\fP to take PR description
from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.github/pull_request_template.md\fP is present.
+.TP
+.B \fBmachete.github.prDescriptionIntroStyle\fP (\fBcreate\-pr\fP, \fBrestack\-pr\fP and \fBretarget\-pr\fP):
+Select the style of the intro prepended to PR description:
+* \fBfull\fP \-\-\- include both a chain of upstream PRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream PRs
+* \fBup\-only\fP \-\-\- default, include only a chain of upstream PRs
+* \fBnone\fP \-\-\- prepend no intro to the PR description at all
.UNINDENT
.sp
\fBEnvironment variables (all subcommands):\fP
@@ -1233,10 +1251,10 @@ Draft MRs don\(aqt get code owners automatically added as reviewers.
.B \fBretarget\-mr [\-b|\-\-branch=] [\-\-ignore\-if\-missing]\fP:
Sets the target of the current (or specified) branch\(aqs MR to upstream (parent) branch, as seen by git machete (see \fBgit machete show up\fP).
.sp
-If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended with a section
-listing the entire related chain of MRs.
+If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
+with an intro section listing the entire related chain of MRs.
.sp
-This header will be updated or removed accordingly with the subsequent runs of \fBretarget\-mr\fP\&.
+This header will be updated or removed accordingly with the subsequent runs of \fBretarget\-mr\fP, even if the target branch is already up to date.
.sp
\fBOptions:\fP
.INDENT 7.0
@@ -1285,6 +1303,12 @@ but also \fBgitlab checkout\-mrs\fP, \fBgitlab create\-mr\fP, \fBgitlab retarget
.B \fBmachete.gitlab.forceDescriptionFromCommitMessage\fP (\fBcreate\-mr\fP only):
Setting this config key to \fBtrue\fP will force \fBgit machete gitlab create\-mr\fP to take MR description
from the message body of the first unique commit of the branch, even if \fB\&.git/info/description\fP and/or \fB\&.gitlab/merge_request_templates/Default.md\fP is present.
+.TP
+.B \fBmachete.gitlab.mrDescriptionIntroStyle\fP (\fBcreate\-mr\fP, \fBrestack\-mr\fP and \fBretarget\-mr\fP):
+Select the style of the intro prepended to PR description:
+* \fBfull\fP \-\-\- include both a chain of upstream PRs (typically leading to \fBmain\fP, \fBmaster\fP, \fBdevelop\fP etc.) and a tree of downstream PRs
+* \fBup\-only\fP \-\-\- default, include only a chain of upstream PRs
+* \fBnone\fP \-\-\- prepend no intro to the PR description at all
.UNINDENT
.sp
\fBEnvironment variables (all subcommands):\fP
diff --git a/docs/source/cli/config.rst b/docs/source/cli/config.rst
index 6c3e93ff7..cf9b26b28 100644
--- a/docs/source/cli/config.rst
+++ b/docs/source/cli/config.rst
@@ -18,6 +18,9 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
``machete.github.forceDescriptionFromCommitMessage``:
.. include:: git-config-keys/github_forceDescriptionFromCommitMessage.rst
+``machete.github.prDescriptionIntroStyle``:
+ .. include:: git-config-keys/github_prDescriptionIntroStyle.rst
+
``machete.gitlab.{domain,remote,namespace,project}``:
.. include:: git-config-keys/gitlab_access.rst
:start-line: 3
@@ -28,6 +31,9 @@ Note: ``config`` is not a command as such, just a help topic (there is no ``git
``machete.gitlab.forceDescriptionFromCommitMessage``:
.. include:: git-config-keys/gitlab_forceDescriptionFromCommitMessage.rst
+``machete.gitlab.mrDescriptionIntroStyle``:
+ .. include:: git-config-keys/gitlab_mrDescriptionIntroStyle.rst
+
``machete.overrideForkPoint..to``:
Executing ``git machete fork-point --override-to[-parent|-inferred|=] []`` sets up a fork point override for ````.
diff --git a/docs/source/cli/github.rst b/docs/source/cli/github.rst
index f76593769..c330710b4 100644
--- a/docs/source/cli/github.rst
+++ b/docs/source/cli/github.rst
@@ -112,10 +112,10 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
``retarget-pr [-b|--branch=] [--ignore-if-missing]``:
Sets the base of the current (or specified) branch's PR to upstream (parent) branch, as seen by git machete (see ``git machete show up``).
- If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended with a section
- listing the entire related chain of PRs.
+ If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
+ with an intro section listing the entire related chain of PRs.
- This header will be updated or removed accordingly with the subsequent runs of ``retarget-pr``.
+ This header will be updated or removed accordingly with the subsequent runs of ``retarget-pr``, even if the base branch is already up to date.
**Options:**
@@ -144,6 +144,9 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc
``machete.github.forceDescriptionFromCommitMessage`` (``create-pr`` only):
.. include:: git-config-keys/github_forceDescriptionFromCommitMessage.rst
+``machete.github.prDescriptionIntroStyle`` (``create-pr``, ``restack-pr`` and ``retarget-pr``):
+ .. include:: git-config-keys/github_prDescriptionIntroStyle.rst
+
**Environment variables (all subcommands):**
``GITHUB_TOKEN``
diff --git a/docs/source/cli/gitlab.rst b/docs/source/cli/gitlab.rst
index 535ef7bb8..65eb230c6 100644
--- a/docs/source/cli/gitlab.rst
+++ b/docs/source/cli/gitlab.rst
@@ -113,10 +113,10 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
``retarget-mr [-b|--branch=] [--ignore-if-missing]``:
Sets the target of the current (or specified) branch's MR to upstream (parent) branch, as seen by git machete (see ``git machete show up``).
- If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended with a section
- listing the entire related chain of MRs.
+ If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
+ with an intro section listing the entire related chain of MRs.
- This header will be updated or removed accordingly with the subsequent runs of ``retarget-mr``.
+ This header will be updated or removed accordingly with the subsequent runs of ``retarget-mr``, even if the target branch is already up to date.
**Options:**
@@ -135,6 +135,9 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc
``machete.gitlab.forceDescriptionFromCommitMessage`` (``create-mr`` only):
.. include:: git-config-keys/gitlab_forceDescriptionFromCommitMessage.rst
+``machete.gitlab.mrDescriptionIntroStyle`` (``create-mr``, ``restack-mr`` and ``retarget-mr``):
+ .. include:: git-config-keys/github_prDescriptionIntroStyle.rst
+
**Environment variables (all subcommands):**
``GITLAB_TOKEN``
diff --git a/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst b/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst
new file mode 100644
index 000000000..c104e1d19
--- /dev/null
+++ b/docs/source/git-config-keys/github_prDescriptionIntroStyle.rst
@@ -0,0 +1,4 @@
+Select the style of the intro prepended to PR description:
+* ``full`` --- include both a chain of upstream PRs (typically leading to ``main``, ``master``, ``develop`` etc.) and a tree of downstream PRs
+* ``up-only`` --- default, include only a chain of upstream PRs
+* ``none`` --- prepend no intro to the PR description at all
diff --git a/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst b/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst
new file mode 100644
index 000000000..93e9bd881
--- /dev/null
+++ b/docs/source/git-config-keys/gitlab_mrDescriptionIntroStyle.rst
@@ -0,0 +1,4 @@
+Select the style of the intro prepended to MR description:
+* ``full`` --- include both a chain of upstream MRs (typically leading to ``main``, ``master``, ``develop`` etc.) and a tree of downstream MRs
+* ``up-only`` --- default, include only a chain of upstream MRs
+* ``none`` --- prepend no intro to the MR description at all
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index d5631d736..c9c983224 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1 +1 @@
-__version__ = '3.28.1'
+__version__ = '3.29.0'
diff --git a/git_machete/client.py b/git_machete/client.py
index c0d951c0f..ef23a274c 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -8,7 +8,7 @@
import textwrap
from collections import OrderedDict
from enum import Enum, auto
-from typing import (Callable, Dict, Iterator, List, Optional, Tuple, Type,
+from typing import (Callable, Dict, Iterator, List, Optional, Set, Tuple, Type,
TypeVar)
from . import git_config_keys, utils
@@ -63,9 +63,10 @@ def from_string(cls: Type[E], value: str, from_where: Optional[str]) -> E:
try:
return cls[value.upper().replace("-", "_")]
except KeyError:
- valid_values = ', '.join(e.name.lower().replace("_", "-") for e in cls)
+ valid_values = ', '.join('`' + e.name.lower().replace("_", "-") + '`' for e in cls)
prefix = f"Invalid value for {from_where}" if from_where else "Invalid value"
- raise MacheteException(f"{prefix}: `{value}`. Valid values are `{valid_values}`")
+ printed_value = value or ''
+ raise MacheteException(f"{prefix}: `{printed_value}`. Valid values are {valid_values}")
class PickRoot(Enum):
@@ -73,6 +74,12 @@ class PickRoot(Enum):
LAST = auto()
+class PRDescriptionIntroStyle(ParsableEnum):
+ FULL = auto()
+ UP_ONLY = auto()
+ NONE = auto()
+
+
class SquashMergeDetection(ParsableEnum):
NONE = auto()
SIMPLE = auto()
@@ -2220,7 +2227,7 @@ def checkout_pull_requests(self,
warn(f'{pr.display_text()} is already closed.')
debug(f'found {pr}')
- pr_path: List[PullRequest] = self.__get_path_from_pr_chain(spec, pr, all_open_prs)
+ pr_path: List[PullRequest] = self.__get_upwards_path_including_pr(spec, pr, all_open_prs)
prs_to_annotate.update(pr_path)
reversed_pr_path: List[PullRequest] = pr_path[::-1] # need to add from root downwards
if reversed_pr_path[0].base not in self.managed_branches:
@@ -2251,7 +2258,24 @@ def checkout_pull_requests(self,
self.__git.checkout(LocalBranchShortName.of(applicable_prs[0].head))
@staticmethod
- def __get_path_from_pr_chain(spec: CodeHostingSpec, original_pr: PullRequest, all_open_prs: List[PullRequest]) -> List[PullRequest]:
+ def __get_downwards_tree_excluding_pr(original_pr: PullRequest,
+ all_open_prs: List[PullRequest]) -> List[Tuple[PullRequest, int]]:
+ """Returns pairs of (PR, level below the given PR)"""
+
+ visited_head_branches: Set[str] = set([])
+
+ def reverse_pr_dfs(pr: PullRequest, depth: int) -> Iterator[Tuple[PullRequest, int]]:
+ visited_head_branches.add(pr.head)
+ down_prs = filter(lambda x: x.base == pr.head, all_open_prs)
+ for down_pr in sorted(down_prs, key=lambda x: x.number):
+ if down_pr.head not in visited_head_branches:
+ yield (down_pr, depth + 1)
+ yield from reverse_pr_dfs(down_pr, depth + 1)
+ return list(reverse_pr_dfs(original_pr, 0))
+
+ @staticmethod
+ def __get_upwards_path_including_pr(spec: CodeHostingSpec, original_pr: PullRequest,
+ all_open_prs: List[PullRequest]) -> List[PullRequest]:
visited_head_branches: List[str] = [original_pr.head]
path: List[PullRequest] = [original_pr]
pr_base: Optional[str] = original_pr.base
@@ -2432,7 +2456,8 @@ def skip_leading_empty(strs: List[str]) -> List[str]:
return list(itertools.dropwhile(lambda line: line.strip() == '', strs))
lines = skip_leading_empty(old_description.splitlines()) if old_description else []
- text_to_prepend = self.__generate_text_to_prepend_to_pr_description(code_hosting_client, pr)
+ style = self.__get_pr_description_into_style_from_config(code_hosting_client._spec)
+ text_to_prepend = self.__generate_pr_description_intro(code_hosting_client, pr, style)
lines_to_prepend = text_to_prepend.splitlines() if text_to_prepend else []
if self.START_GIT_MACHETE_GENERATED_COMMENT in lines and self.END_GIT_MACHETE_GENERATED_COMMENT in lines:
start_index = lines.index(self.START_GIT_MACHETE_GENERATED_COMMENT)
@@ -2592,31 +2617,70 @@ def __derive_org_repo_and_remote(
START_GIT_MACHETE_GENERATED_COMMENT = ''
END_GIT_MACHETE_GENERATED_COMMENT = ''
- def __generate_text_to_prepend_to_pr_description(self, code_hosting_client: CodeHostingClient, pr: PullRequest) -> str:
+ def __get_pr_description_into_style_from_config(self, spec: CodeHostingSpec) -> PRDescriptionIntroStyle:
+ config_key = spec.git_config_keys.pr_description_intro_style
+ return PRDescriptionIntroStyle.from_string(
+ value=self.__git.get_config_attr(key=config_key, default_value="up-only"),
+ from_where=f"`{config_key}` git config key"
+ )
- prs_for_base_branch = code_hosting_client.get_open_pull_requests_by_head(LocalBranchShortName(pr.base))
- if len(prs_for_base_branch) < 1:
+ def __generate_pr_description_intro(self, code_hosting_client: CodeHostingClient,
+ pr: PullRequest, style: PRDescriptionIntroStyle) -> str:
+ if style == PRDescriptionIntroStyle.NONE:
return ''
+
# For determining the PR chain, we need to fetch all PRs from the repo.
# We could just fetch them straight away... but this list can be quite long for commercial monorepos,
- # esp. given that GitHub and GitLab limit the single page to 100 PRs (so multiple HTTP requests would be needed).
- # As a slight optimization, let's fetch the full PR list only if the current PR has a base PR at all.
- config = code_hosting_client._spec
- display_name = config.display_name
- pr_short_name = config.pr_short_name
- print(f'Checking for open {display_name} {pr_short_name}s (to determine {pr_short_name} chain)... ', end='', flush=True)
+ # esp. given that GitHub and GitLab limit the single page to 100 PRs (so multiple HTTP requests may be needed).
+ # As a slight optimization, in the default UP_ONLY style,
+ # let's fetch the full PR list only if the current PR has a base PR at all.
+ # In FULL style, we need to check for downstream PRs as well, so the full PR list needs to be fetched anyway.
+ # That's also the performance reason behind selecting UP_ONLY and not FULL as the default style.
+ prs_for_base_branch = code_hosting_client.get_open_pull_requests_by_head(LocalBranchShortName(pr.base))
+ if style == PRDescriptionIntroStyle.UP_ONLY and len(prs_for_base_branch) == 0:
+ return ''
+ spec = code_hosting_client._spec
+ display_name = spec.display_name
+ pr_short_name = spec.pr_short_name
+ determine_what = 'chain' if style == PRDescriptionIntroStyle.UP_ONLY else 'tree'
+ print(f'Checking for open {display_name} {pr_short_name}s (to determine {pr_short_name} {determine_what})... ', end='', flush=True)
all_open_prs: List[PullRequest] = code_hosting_client.get_open_pull_requests()
print(fmt('OK'))
- pr_path = self.__get_path_from_pr_chain(config, pr, all_open_prs)
+ pr_up_path = reversed(self.__get_upwards_path_including_pr(spec, pr, all_open_prs))
+ if style == PRDescriptionIntroStyle.FULL:
+ pr_down_tree = self.__get_downwards_tree_excluding_pr(pr, all_open_prs)
+ else:
+ pr_down_tree = []
prepend = f'{self.START_GIT_MACHETE_GENERATED_COMMENT}\n\n'
- prepend += f'# Based on {prs_for_base_branch[0].display_text(fmt=False)}\n\n'
+ # In FULL mode, we're likely to generate the intro even when there are NO upstream PRs above
+ if len(prs_for_base_branch) >= 1:
+ prepend += f'# Based on {prs_for_base_branch[0].display_text(fmt=False)}\n\n'
+
+ if pr_down_tree:
+ prepend += f'## Chain of upstream {pr_short_name}s & tree of downstream {pr_short_name}s'
+ else:
+ prepend += f'## Chain of upstream {pr_short_name}s'
current_date = utils.get_current_date()
- prepend += f'## Full chain of {pr_short_name}s as of {current_date}\n\n'
- for ancestor_pr in pr_path:
- prepend += f'* {ancestor_pr.display_text(fmt=False)}:\n'
- prepend += f' `{ancestor_pr.head}` ➔ `{ancestor_pr.base}`\n'
- prepend += '\n'
+ prepend += f' as of {current_date}\n\n'
+
+ def pr_entry(_pr: PullRequest, _depth: int) -> str:
+ result = ' ' * _depth
+ display_text = _pr.display_text(fmt=False)
+ if _pr.number == pr.number:
+ result += f'* **{display_text} (THIS ONE)**:\n'
+ else:
+ result += f'* {display_text}:\n'
+ result += ' ' * _depth
+ result += f' `{_pr.base}` ← `{_pr.head}`\n\n'
+ return result
+
+ base_depth = 0
+ for up_pr in pr_up_path:
+ prepend += pr_entry(up_pr, base_depth)
+ base_depth += 1
+ for (down_pr, depth) in pr_down_tree:
+ prepend += pr_entry(down_pr, base_depth + depth)
prepend += f'{self.END_GIT_MACHETE_GENERATED_COMMENT}\n'
return prepend
@@ -2721,12 +2785,14 @@ def create_pull_request(
title=title, description=description, draft=opt_draft)
print(fmt(f'{ok_str}, see `{pr.html_url}`'))
+ style = self.__get_pr_description_into_style_from_config(spec)
# If base branch has NOT originally been found on the remote,
- # we can be sure that a longer chain of PRs above the newly-created PR does NOT exist
- if base_branch_found_on_remote:
+ # we can be sure that a longer chain of PRs above the newly-created PR does NOT exist.
+ # So in the default UP_ONLY mode, we can skip generating the intro completely.
+ if base_branch_found_on_remote or style == PRDescriptionIntroStyle.FULL:
# As the description may include the reference to this PR itself (in case of a chain of >=2 PRs),
# let's update the PR description after it's already created (so that we know the current PR's number).
- text_to_prepend = self.__generate_text_to_prepend_to_pr_description(code_hosting_client, pr)
+ text_to_prepend = self.__generate_pr_description_intro(code_hosting_client, pr, style)
if text_to_prepend:
if description:
text_to_prepend += '\n'
diff --git a/git_machete/code_hosting.py b/git_machete/code_hosting.py
index 0eb4ddbae..6c138a3b8 100644
--- a/git_machete/code_hosting.py
+++ b/git_machete/code_hosting.py
@@ -89,6 +89,7 @@ class CodeHostingGitConfigKeys(NamedTuple):
remote: str
annotate_with_urls: str
force_description_from_commit_message: str
+ pr_description_intro_style: str
def for_locating_repo_message(self) -> str:
return f"`{self.domain}`, `{self.organization}`, `{self.repository}`, `{self.remote}`"
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index af04dc60d..f86756188 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -249,6 +249,12 @@
Setting this config key to `true` will force `git machete github create-pr` to take PR description
from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.github/pull_request_template.md` is present.
+ `machete.github.prDescriptionIntroStyle`:
+ Select the style of the intro prepended to PR description:
+ * `full` — include both a chain of upstream PRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream PRs
+ * `up-only` — default, include only a chain of upstream PRs
+ * `none` — prepend no intro to the PR description at all
+
`machete.gitlab.{domain,remote,namespace,project}`:
GitLab self-managed domain
@@ -276,6 +282,12 @@
Setting this config key to `true` will force `git machete gitlab create-mr` to take MR description
from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.gitlab/merge_request_templates/Default.md` is present.
+ `machete.gitlab.mrDescriptionIntroStyle`:
+ Select the style of the intro prepended to MR description:
+ * `full` — include both a chain of upstream MRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream MRs
+ * `up-only` — default, include only a chain of upstream MRs
+ * `none` — prepend no intro to the MR description at all
+
`machete.overrideForkPoint..to`:
Executing `git machete fork-point --override-to[-parent|-inferred|=] []` sets up a fork point override for ``.
@@ -656,10 +668,10 @@
Sets the base of the current (or specified) branch's PR to upstream (parent) branch, as seen by git machete (see `git machete show up`).
- If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended with a section
- listing the entire related chain of PRs.
+ If after changing the base the PR ends up stacked atop another PR, the PR description posted to GitHub will be prepended
+ with an intro section listing the entire related chain of PRs.
- This header will be updated or removed accordingly with the subsequent runs of `retarget-pr`.
+ This header will be updated or removed accordingly with the subsequent runs of `retarget-pr`, even if the base branch is already up to date.
Options:
@@ -715,6 +727,12 @@
Setting this config key to `true` will force `git machete github create-pr` to take PR description
from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.github/pull_request_template.md` is present.
+ `machete.github.prDescriptionIntroStyle` (`create-pr`, `restack-pr` and `retarget-pr`):
+ Select the style of the intro prepended to PR description:
+ * `full` — include both a chain of upstream PRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream PRs
+ * `up-only` — default, include only a chain of upstream PRs
+ * `none` — prepend no intro to the PR description at all
+
Environment variables (all subcommands):
`GITHUB_TOKEN`
GitHub API token.
@@ -837,10 +855,10 @@
Sets the target of the current (or specified) branch's MR to upstream (parent) branch, as seen by git machete (see `git machete show up`).
- If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended with a section
- listing the entire related chain of MRs.
+ If after changing the target the MR ends up stacked atop another MR, the MR description posted to GitLab will be prepended
+ with an intro section listing the entire related chain of MRs.
- This header will be updated or removed accordingly with the subsequent runs of `retarget-mr`.
+ This header will be updated or removed accordingly with the subsequent runs of `retarget-mr`, even if the target branch is already up to date.
Options:
@@ -883,6 +901,12 @@
Setting this config key to `true` will force `git machete gitlab create-mr` to take MR description
from the message body of the first unique commit of the branch, even if `.git/info/description` and/or `.gitlab/merge_request_templates/Default.md` is present.
+ `machete.gitlab.mrDescriptionIntroStyle` (`create-mr`, `restack-mr` and `retarget-mr`):
+ Select the style of the intro prepended to PR description:
+ * `full` — include both a chain of upstream PRs (typically leading to `main`, `master`, `develop` etc.) and a tree of downstream PRs
+ * `up-only` — default, include only a chain of upstream PRs
+ * `none` — prepend no intro to the PR description at all
+
Environment variables (all subcommands):
`GITLAB_TOKEN`
GitLab API token.
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 010a705f3..29cc057ec 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -344,6 +344,10 @@ def __ensure_config_loaded(self) -> None:
else:
raise UnexpectedMacheteException(f"Cannot parse config entry: {config_entry}.")
+ def get_config_attr(self, key: str, default_value: str) -> str:
+ value = self.get_config_attr_or_none(key)
+ return default_value if value is None else value
+
def get_config_attr_or_none(self, key: str) -> Optional[str]:
self.__ensure_config_loaded()
assert self.__config_cached is not None
diff --git a/git_machete/github.py b/git_machete/github.py
index a76f7ae74..081c3adcf 100644
--- a/git_machete/github.py
+++ b/git_machete/github.py
@@ -179,6 +179,7 @@ def spec(cls) -> CodeHostingSpec:
remote='machete.github.remote',
annotate_with_urls='machete.github.annotateWithUrls',
force_description_from_commit_message='machete.github.forceDescriptionFromCommitMessage',
+ pr_description_intro_style='machete.github.prDescriptionIntroStyle',
)
)
diff --git a/git_machete/gitlab.py b/git_machete/gitlab.py
index 3fc9c2ff0..93f7feb0d 100644
--- a/git_machete/gitlab.py
+++ b/git_machete/gitlab.py
@@ -134,6 +134,7 @@ def spec(cls) -> CodeHostingSpec:
remote='machete.gitlab.remote',
annotate_with_urls='machete.gitlab.annotateWithUrls',
force_description_from_commit_message='machete.gitlab.forceDescriptionFromCommitMessage',
+ pr_description_intro_style='machete.gitlab.mrDescriptionIntroStyle',
)
)
diff --git a/tests/pytest_full_operands.py b/tests/pytest_full_operands.py
index 9a3adac48..6de36721a 100644
--- a/tests/pytest_full_operands.py
+++ b/tests/pytest_full_operands.py
@@ -23,8 +23,8 @@ def lines_for(arg):
return [''] + [' ' + x for x in arg.splitlines()] + ['']
return [
'Comparing values:',
- 'left:', *lines_for(left),
- 'right:', *lines_for(right)
+ 'left (typically meaning actual):', *lines_for(left),
+ 'right (typically meaning expected):', *lines_for(right)
]
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 332e57959..db3ef234c 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -159,12 +159,13 @@ def test_github_create_pr(self, mocker: MockerFixture) -> None:
# Based on PR #3
- ## Full chain of PRs as of 2023-12-31
+ ## Chain of upstream PRs as of 2023-12-31
- * PR #5:
- `chore/fields` ➔ `ignore-trailing`
* PR #3:
- `ignore-trailing` ➔ `hotfix/add-trigger`
+ `hotfix/add-trigger` ← `ignore-trailing`
+
+ * **PR #5 (THIS ONE)**:
+ `ignore-trailing` ← `chore/fields`
@@ -364,14 +365,16 @@ def test_github_create_pr_for_chain_in_description(self, mocker: MockerFixture)
# Based on PR #2
- ## Full chain of PRs as of 2023-12-31
+ ## Chain of upstream PRs as of 2023-12-31
- * PR #3:
- `call-ws` ➔ `build-chain`
- * PR #2:
- `build-chain` ➔ `allow-ownership-link`
* PR #1:
- `allow-ownership-link` ➔ `develop`
+ `develop` ← `allow-ownership-link`
+
+ * PR #2:
+ `allow-ownership-link` ← `build-chain`
+
+ * **PR #3 (THIS ONE)**:
+ `build-chain` ← `call-ws`
''')[1:]
@@ -386,16 +389,19 @@ def test_github_create_pr_for_chain_in_description(self, mocker: MockerFixture)
# Based on PR #3
- ## Full chain of PRs as of 2023-12-31
+ ## Chain of upstream PRs as of 2023-12-31
- * PR #4:
- `drop-constraint` ➔ `call-ws`
- * PR #3:
- `call-ws` ➔ `build-chain`
- * PR #2:
- `build-chain` ➔ `allow-ownership-link`
* PR #1:
- `allow-ownership-link` ➔ `develop`
+ `develop` ← `allow-ownership-link`
+
+ * PR #2:
+ `allow-ownership-link` ← `build-chain`
+
+ * PR #3:
+ `build-chain` ← `call-ws`
+
+ * **PR #4 (THIS ONE)**:
+ `call-ws` ← `drop-constraint`
@@ -598,12 +604,13 @@ def test_github_create_pr_with_multiple_non_origin_remotes(self, mocker: MockerF
# Based on PR #15
- ## Full chain of PRs as of 2023-12-31
+ ## Chain of upstream PRs as of 2023-12-31
- * PR #16:
- `feature` ➔ `branch-1`
* PR #15:
- `branch-1` ➔ `root`
+ `root` ← `branch-1`
+
+ * **PR #16 (THIS ONE)**:
+ `branch-1` ← `feature`
diff --git a/tests/test_github_retarget_pr.py b/tests/test_github_retarget_pr.py
index 5b1a1bdca..57a2455d4 100644
--- a/tests/test_github_retarget_pr.py
+++ b/tests/test_github_retarget_pr.py
@@ -19,7 +19,6 @@ def github_api_state_for_test_retarget_pr() -> MockGitHubAPIState:
mock_pr_json(head='feature_2', base='master', number=25, body=None),
mock_pr_json(head='feature_3', base='master', number=30),
mock_pr_json(head='feature_4', base='feature', number=35),
- # Let's include another PR for `feature_2`, but with a different base branch
mock_pr_json(head='feature_4', base='feature', number=40),
)
@@ -119,20 +118,20 @@ def test_github_retarget_pr_explicit_branch(self, mocker: MockerFixture) -> None
branch_second_commit_msg = "Second commit on branch."
(
self.repo_sandbox.new_branch("root")
- .commit("First commit on root.")
- .new_branch("branch-1")
- .commit(branch_first_commit_msg)
- .commit(branch_second_commit_msg)
- .push()
- .new_branch('feature')
- .commit('introduce feature')
- .push()
- .check_out('root')
- .new_branch('branch-without-pr')
- .commit('branch-without-pr')
- .push()
- .add_remote('new_origin', 'https://github.com/user/repo.git')
- .check_out('root')
+ .commit("First commit on root.")
+ .new_branch("branch-1")
+ .commit(branch_first_commit_msg)
+ .commit(branch_second_commit_msg)
+ .push()
+ .new_branch('feature')
+ .commit('introduce feature')
+ .push()
+ .check_out('root')
+ .new_branch('branch-without-pr')
+ .commit('branch-without-pr')
+ .push()
+ .add_remote('new_origin', 'https://github.com/user/repo.git')
+ .check_out('root')
)
body: str = \
@@ -201,21 +200,21 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
# branch feature present in each remote, no branch tracking data
(
self.repo_sandbox.remove_remote()
- .new_branch("root")
- .add_remote('origin_1', origin_1_remote_path)
- .add_remote('origin_2', origin_2_remote_path)
- .commit("First commit on root.")
- .push(remote='origin_1')
- .push(remote='origin_2')
- .new_branch("branch-1")
- .commit(branch_first_commit_msg)
- .commit(branch_second_commit_msg)
- .push(remote='origin_1')
- .push(remote='origin_2')
- .new_branch('feature')
- .commit('introduce feature')
- .push(remote='origin_1', set_upstream=False)
- .push(remote='origin_2', set_upstream=False)
+ .new_branch("root")
+ .add_remote('origin_1', origin_1_remote_path)
+ .add_remote('origin_2', origin_2_remote_path)
+ .commit("First commit on root.")
+ .push(remote='origin_1')
+ .push(remote='origin_2')
+ .new_branch("branch-1")
+ .commit(branch_first_commit_msg)
+ .commit(branch_second_commit_msg)
+ .push(remote='origin_1')
+ .push(remote='origin_2')
+ .new_branch('feature')
+ .commit('introduce feature')
+ .push(remote='origin_1', set_upstream=False)
+ .push(remote='origin_2', set_upstream=False)
)
body: str = \
@@ -236,10 +235,10 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
# branch feature_1 present in each remote, tracking data present
(
self.repo_sandbox.check_out('feature')
- .new_branch('feature_1')
- .commit('introduce feature 1')
- .push(remote='origin_1')
- .push(remote='origin_2')
+ .new_branch('feature_1')
+ .commit('introduce feature 1')
+ .push(remote='origin_1')
+ .push(remote='origin_2')
)
body = \
@@ -265,12 +264,13 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
# Based on PR #15
- ## Full chain of PRs as of 2023-12-31
+ ## Chain of upstream PRs as of 2023-12-31
- * PR #20:
- `feature_1` ➔ `feature`
* PR #15:
- `feature` ➔ `master`
+ `master` ← `feature`
+
+ * **PR #20 (THIS ONE)**:
+ `feature` ← `feature_1`
# Summary''')[1:]
@@ -278,8 +278,8 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
# branch feature_2 is not present in any of the remotes
(
self.repo_sandbox.check_out('feature')
- .new_branch('feature_2')
- .commit('introduce feature 2')
+ .new_branch('feature_2')
+ .commit('introduce feature 2')
)
body = \
@@ -297,38 +297,26 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
# branch feature_2 present in only one remote: origin_1 and there is no tracking data available -> infer the remote
(
self.repo_sandbox.check_out('feature_2')
- .push(remote='origin_1', set_upstream=False)
+ .push(remote='origin_1', set_upstream=False)
)
+ self.repo_sandbox.set_git_config_key("machete.github.prDescriptionIntroStyle", "none")
assert_success(
['github', 'retarget-pr'],
'Base branch of PR #25 has been switched to feature\n'
- 'Checking for open GitHub PRs (to determine PR chain)... OK\n'
'Description of PR #25 has been updated\n'
)
pr25 = github_api_state.get_pull_by_number(25)
assert pr25 is not None
assert pr25['base']['ref'] == 'feature'
- assert pr25['body'] == textwrap.dedent('''
-
-
- # Based on PR #15
-
- ## Full chain of PRs as of 2023-12-31
-
- * PR #25:
- `feature_2` ➔ `feature`
- * PR #15:
- `feature` ➔ `master`
-
- ''')[1:]
+ assert pr25['body'] == ''
# branch feature_3 present in only one remote: origin_1 and has tracking data
(
self.repo_sandbox.check_out('feature_2')
- .new_branch('feature_3')
- .commit('introduce feature 3')
- .push(remote='origin_1')
+ .new_branch('feature_3')
+ .commit('introduce feature 3')
+ .push(remote='origin_1')
)
body = \
@@ -342,6 +330,7 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
"""
rewrite_branch_layout_file(body)
+ self.repo_sandbox.unset_git_config_key("machete.github.prDescriptionIntroStyle")
assert_success(
['github', 'retarget-pr'],
'Base branch of PR #30 has been switched to feature_2\n'
@@ -356,14 +345,16 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
# Based on PR #25
- ## Full chain of PRs as of 2023-12-31
+ ## Chain of upstream PRs as of 2023-12-31
- * PR #30:
- `feature_3` ➔ `feature_2`
- * PR #25:
- `feature_2` ➔ `feature`
* PR #15:
- `feature` ➔ `master`
+ `master` ← `feature`
+
+ * PR #25:
+ `feature` ← `feature_2`
+
+ * **PR #30 (THIS ONE)**:
+ `feature_2` ← `feature_3`
# Summary''')[1:]
@@ -393,12 +384,13 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
# Based on PR #15
- ## Full chain of PRs as of 2023-12-31
+ ## Chain of upstream PRs as of 2023-12-31
- * PR #30:
- `feature_3` ➔ `feature`
* PR #15:
- `feature` ➔ `master`
+ `master` ← `feature`
+
+ * **PR #30 (THIS ONE)**:
+ `feature` ← `feature_3`
# Summary''')[1:]
@@ -424,6 +416,39 @@ def test_github_retarget_pr_multiple_non_origin_remotes(self, mocker: MockerFixt
assert pr30['base']['ref'] == 'root'
assert pr30['body'] == '# Summary'
+ self.repo_sandbox.check_out('feature')
+ self.repo_sandbox.remove_remote('origin_2')
+ self.repo_sandbox.set_git_config_key("machete.github.prDescriptionIntroStyle", "full")
+
+ assert_success(
+ ['github', 'retarget-pr'],
+ 'Base branch of PR #15 has been switched to branch-1\n'
+ 'Checking for open GitHub PRs (to determine PR tree)... OK\n'
+ 'Description of PR #15 has been updated\n'
+ )
+ pr15 = github_api_state.get_pull_by_number(15)
+ assert pr15 is not None
+ assert pr15['base']['ref'] == 'branch-1'
+ assert pr15['body'] == textwrap.dedent('''
+
+
+ ## Chain of upstream PRs & tree of downstream PRs as of 2023-12-31
+
+ * **PR #15 (THIS ONE)**:
+ `branch-1` ← `feature`
+
+ * PR #20:
+ `feature` ← `feature_1`
+
+ * PR #25:
+ `feature` ← `feature_2`
+
+ * PR #35:
+ `feature` ← `feature_4`
+
+
+ # Summary''')[1:]
+
@staticmethod
def github_api_state_for_test_retarget_pr_root_branch() -> MockGitHubAPIState:
return MockGitHubAPIState.with_prs(
diff --git a/tests/test_gitlab_create_mr.py b/tests/test_gitlab_create_mr.py
index 63e51f0c6..4578ea401 100644
--- a/tests/test_gitlab_create_mr.py
+++ b/tests/test_gitlab_create_mr.py
@@ -158,12 +158,13 @@ def test_gitlab_create_mr(self, mocker: MockerFixture) -> None:
# Based on MR !3
- ## Full chain of MRs as of 2023-12-31
+ ## Chain of upstream MRs as of 2023-12-31
- * MR !5:
- `chore/fields` ➔ `ignore-trailing`
* MR !3:
- `ignore-trailing` ➔ `hotfix/add-trigger`
+ `hotfix/add-trigger` ← `ignore-trailing`
+
+ * **MR !5 (THIS ONE)**:
+ `ignore-trailing` ← `chore/fields`
@@ -350,47 +351,37 @@ def test_gitlab_create_mr_for_chain_in_description(self, mocker: MockerFixture)
"""
rewrite_branch_layout_file(body)
+ self.repo_sandbox.check_out("drop-constraint")
+ launch_command("gitlab", "create-mr", "--yes")
+ pr = gitlab_api_state.get_mr_by_number(3)
+ assert pr is not None
+ assert pr['description'] == '' # no chain at this moment
+
+ self.repo_sandbox.write_to_file(".gitlab/merge_request_templates/Default.md", "# MR title\n## Summary\n## Test plan\n")
+ self.repo_sandbox.set_git_config_key("machete.gitlab.mrDescriptionIntroStyle", "full")
+
self.repo_sandbox.check_out("call-ws")
launch_command("gitlab", "create-mr")
- pr = gitlab_api_state.get_mr_by_number(3)
+ pr = gitlab_api_state.get_mr_by_number(4)
assert pr is not None
assert pr['description'] == textwrap.dedent('''
# Based on MR !2
- ## Full chain of MRs as of 2023-12-31
+ ## Chain of upstream MRs & tree of downstream MRs as of 2023-12-31
- * MR !3:
- `call-ws` ➔ `build-chain`
- * MR !2:
- `build-chain` ➔ `allow-ownership-link`
* MR !1:
- `allow-ownership-link` ➔ `develop`
-
-
- ''')[1:]
+ `develop` ← `allow-ownership-link`
- self.repo_sandbox.write_to_file(".gitlab/merge_request_templates/Default.md", "# MR title\n## Summary\n## Test plan\n")
- self.repo_sandbox.check_out("drop-constraint")
- launch_command("gitlab", "create-mr", "--yes")
- pr = gitlab_api_state.get_mr_by_number(4)
- assert pr is not None
- assert pr['description'] == textwrap.dedent('''
-
-
- # Based on MR !3
+ * MR !2:
+ `allow-ownership-link` ← `build-chain`
- ## Full chain of MRs as of 2023-12-31
+ * **MR !4 (THIS ONE)**:
+ `build-chain` ← `call-ws`
- * MR !4:
- `drop-constraint` ➔ `call-ws`
- * MR !3:
- `call-ws` ➔ `build-chain`
- * MR !2:
- `build-chain` ➔ `allow-ownership-link`
- * MR !1:
- `allow-ownership-link` ➔ `develop`
+ * MR !3:
+ `call-ws` ← `drop-constraint`
@@ -593,12 +584,13 @@ def test_gitlab_create_mr_with_multiple_non_origin_remotes(self, mocker: MockerF
# Based on MR !15
- ## Full chain of MRs as of 2023-12-31
+ ## Chain of upstream MRs as of 2023-12-31
- * MR !16:
- `feature` ➔ `branch-1`
* MR !15:
- `branch-1` ➔ `root`
+ `root` ← `branch-1`
+
+ * **MR !16 (THIS ONE)**:
+ `branch-1` ← `feature`
diff --git a/tests/test_gitlab_retarget_mr.py b/tests/test_gitlab_retarget_mr.py
index 4e8a189ba..c85945c8a 100644
--- a/tests/test_gitlab_retarget_mr.py
+++ b/tests/test_gitlab_retarget_mr.py
@@ -19,7 +19,6 @@ def gitlab_api_state_for_test_retarget_mr() -> MockGitLabAPIState:
mock_mr_json(head='feature_2', base='master', number=25, body=None),
mock_mr_json(head='feature_3', base='master', number=30),
mock_mr_json(head='feature_4', base='feature', number=35),
- # Let's include another MR for `feature_2`, but with a different target branch
mock_mr_json(head='feature_4', base='feature', number=40),
)
@@ -271,12 +270,13 @@ def test_gitlab_retarget_mr_multiple_non_origin_remotes(self, mocker: MockerFixt
# Based on MR !15
- ## Full chain of MRs as of 2023-12-31
+ ## Chain of upstream MRs as of 2023-12-31
- * MR !20:
- `feature_1` ➔ `feature`
* MR !15:
- `feature` ➔ `master`
+ `master` ← `feature`
+
+ * **MR !20 (THIS ONE)**:
+ `feature` ← `feature_1`
# Summary''')[1:]
@@ -320,12 +320,13 @@ def test_gitlab_retarget_mr_multiple_non_origin_remotes(self, mocker: MockerFixt
# Based on MR !15
- ## Full chain of MRs as of 2023-12-31
+ ## Chain of upstream MRs as of 2023-12-31
- * MR !25:
- `feature_2` ➔ `feature`
* MR !15:
- `feature` ➔ `master`
+ `master` ← `feature`
+
+ * **MR !25 (THIS ONE)**:
+ `feature` ← `feature_2`
''')[1:]
@@ -362,14 +363,16 @@ def test_gitlab_retarget_mr_multiple_non_origin_remotes(self, mocker: MockerFixt
# Based on MR !25
- ## Full chain of MRs as of 2023-12-31
+ ## Chain of upstream MRs as of 2023-12-31
- * MR !30:
- `feature_3` ➔ `feature_2`
- * MR !25:
- `feature_2` ➔ `feature`
* MR !15:
- `feature` ➔ `master`
+ `master` ← `feature`
+
+ * MR !25:
+ `feature` ← `feature_2`
+
+ * **MR !30 (THIS ONE)**:
+ `feature_2` ← `feature_3`
# Summary''')[1:]
@@ -399,12 +402,13 @@ def test_gitlab_retarget_mr_multiple_non_origin_remotes(self, mocker: MockerFixt
# Based on MR !15
- ## Full chain of MRs as of 2023-12-31
+ ## Chain of upstream MRs as of 2023-12-31
- * MR !30:
- `feature_3` ➔ `feature`
* MR !15:
- `feature` ➔ `master`
+ `master` ← `feature`
+
+ * **MR !30 (THIS ONE)**:
+ `feature` ← `feature_3`
# Summary''')[1:]