diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 89f00fa19..7acbc309a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,8 @@ # Release notes -## New in git-machete 3.29.4 +## New in git-machete 3.30.0 + +- added: flags `-H`/`--sync-github-prs` and `-L`/`--sync-gitlab-mrs` to `traverse` to automatically retarget PRs/MRs when traversing (suggested by @chriscz) ## New in git-machete 3.29.3 diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash index cde9a3507..51a0344d2 100644 --- a/completion/git-machete.completion.bash +++ b/completion/git-machete.completion.bash @@ -17,7 +17,7 @@ _git_machete() { local common_opts="--debug -h --help -v --verbose" local add_opts="-f --as-first-child -o --onto= -R --as-root -y --yes" local advance_opts="-y --yes" - local anno_opts="-b --branch= -H -L --sync-github-prs --sync-gitlab-mrs" + local anno_opts="-b --branch= -H --sync-github-prs -L --sync-gitlab-mrs" local delete_unmanaged_opts="-y --yes" local diff_opts="-s --stat" local discover_opts="-C --checked-out-since= -l --list-commits -r --roots= -y --yes" @@ -34,7 +34,7 @@ _git_machete() { local slide_out_opts="-d --down-fork-point= --delete -M --merge -n --no-edit-merge --no-interactive-rebase --removed-from-remote" local squash_opts="-f --fork-point=" local status_opts="--color= -L --list-commits-with-hashes -l --list-commits --no-detect-squash-merges" - local traverse_opts="-F --fetch -l --list-commits -M --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase --no-push --no-push-untracked --push --push-untracked --return-to= --start-from= -w --whole -W -y --yes" + local traverse_opts="-F --fetch -H --sync-github-prs -L --sync-gitlab-mrs -l --list-commits -M --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase --no-push --no-push-untracked --push --push-untracked --return-to= --start-from= -w --whole -W -y --yes" local update_opts="-f --fork-point= -M --merge -n --no-edit-merge --no-interactive-rebase" cur=${COMP_WORDS[$COMP_CWORD]} diff --git a/completion/git-machete.completion.zsh b/completion/git-machete.completion.zsh index 9d960b3db..30b0929d6 100644 --- a/completion/git-machete.completion.zsh +++ b/completion/git-machete.completion.zsh @@ -140,6 +140,8 @@ _git-machete() { (t|traverse) _arguments \ '(-F --fetch)'{-F,--fetch}'[Fetch the remotes of all managed branches at the beginning of traversal]' \ + '(-H --sync-github-prs)'{-H,--sync-github-prs}'[Retarget GitHub PR when its base branch is different than in machete file]' \ + '(-L --sync-gitlab-mrs)'{-L,--sync-gitlab-mrs}'[Retarget GitLab MR when its target branch is different than in machete file]' \ '(-l --list-commits)'{-l,--list-commits}'[List the messages of commits introduced on each branch]' \ '(-M --merge)'{-M,--merge}'[Update by merge rather than by rebase]' \ '(-n)'-n'[If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge]' \ diff --git a/completion/git-machete.fish b/completion/git-machete.fish index a5688778f..992240579 100644 --- a/completion/git-machete.fish +++ b/completion/git-machete.fish @@ -199,22 +199,24 @@ complete -c git-machete -n "__fish_seen_subcommand_from status s; and not __fish # git machete traverse complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a traverse -d 'Walk through the tree of branch dependencies and rebase, merge, slide out, push and/or pull each branch one by one' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --fetch -F" -f -l fetch -s F -d 'Fetch the remotes of all managed branches at the beginning of traversal (no git pull involved, only git fetch)' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --list-commits -l" -f -l list-commits -s l -d 'When printing the status, additionally list the messages of commits introduced on each branch' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --merge -M" -f -l merge -s M -d 'Update by merge rather than by rebase' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y" -f -s n -d 'If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-detect-squash-merges" -f -l no-detect-squash-merges -d 'Only consider "strict" (fast-forward or 2-parent) merges, rather than rebase/squash merges, when detecting if a branch is merged into its upstream (parent)' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-edit-merge" -f -l no-edit-merge -d 'If updating by merge, skip opening the editor for merge commit message while doing git merge (i.e. pass --no-edit flag to underlying git merge). Not allowed if updating by rebase' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-interactive-rebase" -f -l no-interactive-rebase -d 'If updating by rebase, run git rebase in non-interactive mode (without -i/--interactive flag). Not allowed if updating by merge' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push" -f -l no-push -d 'Do not push any (neither tracked nor untracked) branches to remote, re-enable via --push' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push-untracked" -f -l no-push-untracked -d 'Do not push untracked branches to remote, re-enable via --push-untracked' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push" -f -l push -d 'Push all (both tracked and untracked) branches to remote - default behavior' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push-untracked" -f -l push-untracked -d 'Push untracked branches to remote - default behavior' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --return-to" -x -l return-to -a 'stay here nearest-remaining' -d 'Specifies the branch to return after traversal is successfully completed' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --start-from" -x -l start-from -a 'here root first-root' -d 'Specifies the branch to start the traversal from' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --whole -w" -f -l whole -s w -d 'Equivalent to -n --start-from=first-root --return-to=nearest-remaining' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from -W" -f -s W -d 'Equivalent to --fetch --whole; useful for even more automated traversal of all branches' -complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y" -f -l yes -s y -d 'Do not ask for any interactive input, including confirmation of rebase/push/pull. Implies -n' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --fetch -F" -f -l fetch -s F -d 'Fetch the remotes of all managed branches at the beginning of traversal (no git pull involved, only git fetch)' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --list-commits -l" -f -l list-commits -s l -d 'When printing the status, additionally list the messages of commits introduced on each branch' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --sync-gitlab-mrs" -f -l sync-github-prs -s H -d 'Retarget GitHub PR when its base branch is different than in machete file' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --sync-github-prs" -f -l sync-gitlab-mrs -s L -d 'Retarget GitLab MR when its target branch is different than in machete file' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --merge -M" -f -l merge -s M -d 'Update by merge rather than by rebase' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y" -f -s n -d 'If updating by rebase, equivalent to --no-interactive-rebase. If updating by merge, equivalent to --no-edit-merge' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-detect-squash-merges" -f -l no-detect-squash-merges -d 'Only consider "strict" (fast-forward or 2-parent) merges, rather than rebase/squash merges, when detecting if a branch is merged into its upstream (parent)' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-edit-merge" -f -l no-edit-merge -d 'If updating by merge, skip opening the editor for merge commit message while doing git merge (i.e. pass --no-edit flag to underlying git merge). Not allowed if updating by rebase' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-interactive-rebase" -f -l no-interactive-rebase -d 'If updating by rebase, run git rebase in non-interactive mode (without -i/--interactive flag). Not allowed if updating by merge' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push" -f -l no-push -d 'Do not push any (neither tracked nor untracked) branches to remote, re-enable via --push' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --no-push-untracked" -f -l no-push-untracked -d 'Do not push untracked branches to remote, re-enable via --push-untracked' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push" -f -l push -d 'Push all (both tracked and untracked) branches to remote - default behavior' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --push-untracked" -f -l push-untracked -d 'Push untracked branches to remote - default behavior' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --return-to" -x -l return-to -a 'stay here nearest-remaining' -d 'Specifies the branch to return after traversal is successfully completed' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --start-from" -x -l start-from -a 'here root first-root' -d 'Specifies the branch to start the traversal from' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --whole -w" -f -l whole -s w -d 'Equivalent to -n --start-from=first-root --return-to=nearest-remaining' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from -W" -f -s W -d 'Equivalent to --fetch --whole; useful for even more automated traversal of all branches' +complete -c git-machete -n "__fish_seen_subcommand_from traverse t; and not __fish_seen_subcommand_from --yes -y" -f -l yes -s y -d 'Do not ask for any interactive input, including confirmation of rebase/push/pull. Implies -n' # git machete update complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a update -d 'Sync the current branch with its upstream (parent) branch via rebase or merge' diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1 index d42a726b8..8d34e2ccc 100644 --- a/docs/man/git-machete.1 +++ b/docs/man/git-machete.1 @@ -29,7 +29,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .. .TH "GIT-MACHETE" "1" "Sep 24, 2024" "" "git-machete" .SH NAME -git-machete \- git-machete 3.29.4 +git-machete \- git-machete 3.30.0 .sp git machete is a robust tool that \fBsimplifies your git workflows\fP\&. .sp @@ -1956,6 +1956,7 @@ by setting \fBgit config machete.status.extraSpaceBeforeBranchName true\fP\&. git machete t[raverse] [\-F|\-\-fetch] [\-l|\-\-list\-commits] [\-M|\-\-merge] [\-n|\-\-no\-edit\-merge|\-\-no\-interactive\-rebase] [\-\-[no\-]push] [\-\-[no\-]push\-untracked] [\-\-return\-to=WHERE] [\-\-start\-from=WHERE] [\-\-squash\-merge\-detection=MODE] + [\-H|\-\-sync\-github\-prs|\-L|\-\-sync\-gitlab\-mrs] [\-w|\-\-whole] [\-W] [\-y|\-\-yes] .EE .UNINDENT @@ -2002,6 +2003,13 @@ otherwise, if the branch is behind its remote counterpart: asks the user whether to \fBpull\fP the branch; .UNINDENT .IP \(bu 2 +if \fB\-H\fP/\fB\-\-sync\-github\-prs\fP or \fB\-L\fP/\fB\-\-sync\-gitlab\-mrs\fP option is present: +.INDENT 2.0 +.IP \(bu 2 +retargets the PR/MR if it exists for the given branch and has a different base/target branch in GitHub/GitLab than the upstream in machete file, +just as \fBgit machete github retarget\-pr\fP and \fBgit machete gitlab retarget\-mr\fP would do; +.UNINDENT +.IP \(bu 2 and finally, if any of the above operations has been successfully completed: .INDENT 2.0 .IP \(bu 2 @@ -2046,6 +2054,14 @@ when the current user is NOT the author of the PR/MR associated with that branch .B \-F\fP,\fB \-\-fetch Fetch the remotes of all managed branches at the beginning of traversal (no \fBgit pull\fP involved, only \fBgit fetch\fP). .TP +.B \-H\fP,\fB \-\-sync\-github\-prs +Retarget the PR if it exists for the given branch and has a different base branch in GitHub than the upstream in machete file, +just as \fBgit machete github retarget\-pr\fP would do +.TP +.B \-L\fP,\fB \-\-sync\-gitlab\-mrs +Retarget the MR if it exists for the given branch and has a different target branch in GitLab than the upstream in machete file, +just as \fBgit machete gitlab retarget\-mr\fP would do +.TP .B \-l\fP,\fB \-\-list\-commits When printing the status, additionally list the messages of commits introduced on each branch. .TP diff --git a/docs/source/cli/traverse.rst b/docs/source/cli/traverse.rst index 331f6bda8..9d56216a0 100644 --- a/docs/source/cli/traverse.rst +++ b/docs/source/cli/traverse.rst @@ -21,6 +21,7 @@ traverse git machete t[raverse] [-F|--fetch] [-l|--list-commits] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [--[no-]push] [--[no-]push-untracked] [--return-to=WHERE] [--start-from=WHERE] [--squash-merge-detection=MODE] + [-H|--sync-github-prs|-L|--sync-gitlab-mrs] [-w|--whole] [-W] [-y|--yes] Traverses the branches in the order as they occur in branch layout file. @@ -52,6 +53,11 @@ For each branch, the command: - asks the user whether to **pull** the branch; +* if ``-H``/``--sync-github-prs`` or ``-L``/``--sync-gitlab-mrs`` option is present: + + - retargets the PR/MR if it exists for the given branch and has a different base/target branch in GitHub/GitLab than the upstream in machete file, + just as ``git machete github retarget-pr`` and ``git machete gitlab retarget-mr`` would do; + * and finally, if any of the above operations has been successfully completed: - prints the updated ``status``. @@ -88,6 +94,12 @@ when the current user is NOT the author of the PR/MR associated with that branch -F, --fetch Fetch the remotes of all managed branches at the beginning of traversal (no ``git pull`` involved, only ``git fetch``). +-H, --sync-github-prs Retarget the PR if it exists for the given branch and has a different base branch in GitHub than the upstream in machete file, + just as ``git machete github retarget-pr`` would do + +-L, --sync-gitlab-mrs Retarget the MR if it exists for the given branch and has a different target branch in GitLab than the upstream in machete file, + just as ``git machete gitlab retarget-mr`` would do + -l, --list-commits When printing the status, additionally list the messages of commits introduced on each branch. -M, --merge Update by merge rather than by rebase. diff --git a/git_machete/__init__.py b/git_machete/__init__.py index 90194cb38..b1fa6743b 100644 --- a/git_machete/__init__.py +++ b/git_machete/__init__.py @@ -1 +1 @@ -__version__ = '3.29.4' +__version__ = '3.30.0' diff --git a/git_machete/cli.py b/git_machete/cli.py index 940f36673..8d5b1d5c2 100644 --- a/git_machete/cli.py +++ b/git_machete/cli.py @@ -376,21 +376,23 @@ def add_code_hosting_parser(command: str, subcommand_suffix: str, include_sync: add_help=False, parents=[common_args_parser]) traverse_parser.add_argument('-F', '--fetch', action='store_true') + traverse_parser.add_argument('-H', '--sync-github-prs', action='store_true') traverse_parser.add_argument('-l', '--list-commits', action='store_true') + traverse_parser.add_argument('-L', '--sync-gitlab-mrs', action='store_true') traverse_parser.add_argument('-M', '--merge', action='store_true') traverse_parser.add_argument('-n', action='store_true') + traverse_parser.add_argument('--no-detect-squash-merges', action='store_true') traverse_parser.add_argument('--no-edit-merge', action='store_true') traverse_parser.add_argument('--no-interactive-rebase', action='store_true') - traverse_parser.add_argument('--no-detect-squash-merges', action='store_true') - traverse_parser.add_argument('--squash-merge-detection') - traverse_parser.add_argument('--push', action='store_true') traverse_parser.add_argument('--no-push', action='store_true') - traverse_parser.add_argument('--push-untracked', action='store_true') traverse_parser.add_argument('--no-push-untracked', action='store_true') + traverse_parser.add_argument('--push', action='store_true') + traverse_parser.add_argument('--push-untracked', action='store_true') traverse_parser.add_argument('--return-to') + traverse_parser.add_argument('--squash-merge-detection') traverse_parser.add_argument('--start-from') - traverse_parser.add_argument('-w', '--whole', action='store_true') traverse_parser.add_argument('-W', action='store_true') + traverse_parser.add_argument('-w', '--whole', action='store_true') traverse_parser.add_argument('-y', '--yes', action='store_true') update_parser = subparsers.add_parser( @@ -921,6 +923,8 @@ def strip_remote_name(remote_branch: RemoteBranchShortName) -> LocalBranchShortN opt_return_to=opt_return_to, opt_squash_merge_detection=opt_squash_merge_detection, opt_start_from=opt_start_from, + opt_sync_github_prs=cli_opts.opt_sync_github_prs, + opt_sync_gitlab_mrs=cli_opts.opt_sync_gitlab_mrs, opt_yes=cli_opts.opt_yes) elif cmd == "update": machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out) diff --git a/git_machete/client.py b/git_machete/client.py index 1beb5bdd1..441f3ef8d 100644 --- a/git_machete/client.py +++ b/git_machete/client.py @@ -27,9 +27,11 @@ GitFormatPatterns, GitLogEntry, LocalBranchShortName, RemoteBranchShortName, SyncToRemoteStatus) +from .github import GitHubClient +from .gitlab import GitLabClient from .utils import (AnsiEscapeCodes, PopenResult, bold, colored, debug, dim, - excluding, flat_map, fmt, get_pretty_choices, get_second, - tupled, underline, warn) + excluding, find_or_none, flat_map, fmt, get_pretty_choices, + get_second, tupled, underline, warn) class SyncToParentStatus(Enum): @@ -765,6 +767,8 @@ def traverse( opt_return_to: TraverseReturnTo, opt_squash_merge_detection: SquashMergeDetection, opt_start_from: TraverseStartFrom, + opt_sync_github_prs: bool, + opt_sync_gitlab_mrs: bool, opt_yes: bool ) -> None: self.expect_at_least_one_managed_branch() @@ -779,6 +783,21 @@ def traverse( if self.__git.get_remotes(): print("") + code_hosting_client: Optional[CodeHostingClient] = None + current_user: Optional[str] = None + all_open_prs: Dict[int, PullRequest] = {} + if opt_sync_github_prs or opt_sync_gitlab_mrs: + spec = GitHubClient.spec() if opt_sync_github_prs else GitLabClient.spec() + domain = self.__derive_code_hosting_domain(spec) + org_repo_remote = self.__derive_org_repo_and_remote(spec, domain=domain) + code_hosting_client = spec.create_client( + domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository) + print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True) + current_user = code_hosting_client.get_current_user_login() + debug(f'Current {spec.display_name} user is ' + (bold(current_user or ''))) + all_open_prs = {pr.number: pr for pr in code_hosting_client.get_open_pull_requests()} + print(fmt('OK')) + initial_branch = nearest_remaining_branch = self.__git.get_current_branch() if opt_start_from == TraverseStartFrom.ROOT: @@ -823,6 +842,10 @@ def traverse( else: needs_remote_sync = False + # if neither -L and -H flag is passed, then `all_open_prs` is empty, so `pr` is None and `needs_retarget_pr` is False + pr = find_or_none(lambda pr: pr.head == branch, all_open_prs.values()) + needs_retarget_pr = pr and upstream and pr.base != upstream + use_merge = opt_merge or (branch in self.annotations and self.annotations[branch].qualifiers.update_with_merge) if needs_slide_out: @@ -848,7 +871,7 @@ def traverse( if needs_parent_sync and branch in self.annotations: needs_parent_sync = self.annotations[branch].qualifiers.rebase - if branch != current_branch and (needs_slide_out or needs_parent_sync or needs_remote_sync): + if branch != current_branch and (needs_slide_out or needs_parent_sync or needs_remote_sync or needs_retarget_pr): self.__print_new_line(False) print(f"Checking out {bold(branch)}") self.__git.checkout(branch) @@ -1006,6 +1029,42 @@ def traverse( except InteractionStopped: return + if needs_retarget_pr: + any_action_suggested = True + assert pr is not None + assert upstream is not None + assert code_hosting_client is not None + spec = code_hosting_client._spec + ans_intro = f"Branch {bold(str(branch))} has a different {spec.pr_short_name} {spec.base_branch_name} ({bold(pr.base)}) " \ + f"in {spec.display_name} than in machete file ({bold(str(upstream))}). " + ans = self.ask_if( + ans_intro + f"Retarget {pr.display_text()} to {bold(str(upstream))}?" + get_pretty_choices('y', 'N', 'q', 'yq'), + ans_intro + f"Retargeting {pr.display_text()} to {bold(str(upstream))}...", + opt_yes=opt_yes) + if ans in ('y', 'yes', 'yq'): + code_hosting_client.set_base_of_pull_request(pr.number, base=upstream) + print(f'{spec.base_branch_name.capitalize()} branch of {pr.display_text()} has been switched to {bold(str(upstream))}') + pr = pr._replace(base=upstream) + all_open_prs[pr.number] = pr + + new_description = self.__get_updated_pull_request_description( + code_hosting_client, pr, all_open_prs_preloaded=list(all_open_prs.values())) + if pr.description != new_description: + code_hosting_client.set_description_of_pull_request(pr.number, description=new_description) + print(f'Description of {pr.display_text()} has been updated') + pr = pr._replace(description=new_description) + all_open_prs[pr.number] = pr + + if self.__annotations.get(branch) and self.__annotations[branch].qualifiers_text: + self.__annotations[branch] = Annotation(self.__pull_request_annotation(spec, pr, current_user) + ' ' + + self.__annotations[branch].qualifiers_text) + else: + self.__annotations[branch] = Annotation(self.__pull_request_annotation(spec, pr, current_user)) + self.save_branch_layout_file() + + elif ans in ('q', 'quit'): + return + if opt_return_to == TraverseReturnTo.HERE: self.__git.checkout(initial_branch) elif opt_return_to == TraverseReturnTo.NEAREST_REMAINING: @@ -2452,14 +2511,14 @@ def restack_pull_request(self, spec: CodeHostingSpec) -> None: self.retarget_pr(spec, head, ignore_if_missing=False) - def __get_updated_pull_request_description(self, code_hosting_client: CodeHostingClient, - pr: PullRequest, old_description: Optional[str]) -> str: + def __get_updated_pull_request_description(self, code_hosting_client: CodeHostingClient, pr: PullRequest, + all_open_prs_preloaded: Optional[List[PullRequest]]) -> str: 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 [] + lines = skip_leading_empty(pr.description.splitlines()) if pr.description else [] 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) + text_to_prepend = self.__generate_pr_description_intro(code_hosting_client, pr, style, all_open_prs_preloaded) 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) @@ -2512,14 +2571,14 @@ def retarget_pr(self, spec: CodeHostingSpec, head: LocalBranchShortName, ignore_ else: print(f'{spec.base_branch_name.capitalize()} branch of {pr.display_text()} is already {bold(new_base)}') - new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, pr.description) + new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, all_open_prs_preloaded=None) if pr.description != new_description: code_hosting_client.set_description_of_pull_request(pr.number, description=new_description) print(f'Description of {pr.display_text()} has been updated') current_user: Optional[str] = code_hosting_client.get_current_user_login() if self.__annotations.get(head) and self.__annotations[head].qualifiers_text: - self.__annotations[head] = Annotation(f'{self.__pull_request_annotation(spec, pr, current_user)} ' + + self.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user) + ' ' + self.__annotations[head].qualifiers_text) else: self.__annotations[head] = Annotation(self.__pull_request_annotation(spec, pr, current_user)) @@ -2628,25 +2687,34 @@ def __get_pr_description_into_style_from_config(self, spec: CodeHostingSpec) -> ) def __generate_pr_description_intro(self, code_hosting_client: CodeHostingClient, - pr: PullRequest, style: PRDescriptionIntroStyle) -> str: + pr: PullRequest, style: PRDescriptionIntroStyle, + all_open_prs_preloaded: Optional[List[PullRequest]]) -> 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/MRs (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. - prs_for_base_branch = code_hosting_client.get_open_pull_requests_by_head(LocalBranchShortName(pr.base)) + if all_open_prs_preloaded is not None: + prs_for_base_branch = list(filter(lambda _pr: _pr.head == pr.base, all_open_prs_preloaded)) + else: + # 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/MRs (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. + 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')) + if all_open_prs_preloaded is not None: + all_open_prs = all_open_prs_preloaded + else: + display_name = spec.display_name + determine_what = 'chain' if style == PRDescriptionIntroStyle.UP_ONLY else 'tree' + print(f'Checking for open {display_name} {pr_short_name}s ' + f'(to determine {pr_short_name} {determine_what})... ', end='', flush=True) + all_open_prs = code_hosting_client.get_open_pull_requests() + print(fmt('OK')) + 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) @@ -2793,7 +2861,7 @@ def create_pull_request( 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). - new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, description) + new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, all_open_prs_preloaded=None) if new_description.strip() != description.strip(): print(f'Updating description of {pr.display_text()} to include ' f'the chain of {spec.pr_short_name}s... ', end='', flush=True) diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py index fb4ff9dac..a5b95a9ad 100644 --- a/git_machete/generated_docs.py +++ b/git_machete/generated_docs.py @@ -1360,6 +1360,7 @@ git machete t[raverse] [-F|--fetch] [-l|--list-commits] [-M|--merge] [-n|--no-edit-merge|--no-interactive-rebase] [--[no-]push] [--[no-]push-untracked] [--return-to=WHERE] [--start-from=WHERE] [--squash-merge-detection=MODE] + [-H|--sync-github-prs|-L|--sync-gitlab-mrs] [-w|--whole] [-W] [-y|--yes] Traverses the branches in the order as they occur in branch layout file. @@ -1380,6 +1381,9 @@ - asks the user whether to reset (`git reset --keep`) the branch to its remote counterpart * otherwise, if the branch is behind its remote counterpart: - asks the user whether to pull the branch; + * if `-H`/`--sync-github-prs` or `-L`/`--sync-gitlab-mrs` option is present: + - retargets the PR/MR if it exists for the given branch and has a different base/target branch in GitHub/GitLab than the upstream in machete file, + just as `git machete github retarget-pr` and `git machete gitlab retarget-mr` would do; * and finally, if any of the above operations has been successfully completed: - prints the updated `status`. @@ -1412,6 +1416,12 @@ Options: -F, --fetch Fetch the remotes of all managed branches at the beginning of traversal (no `git pull` involved, only `git fetch`). + -H, --sync-github-prs + Retarget the PR if it exists for the given branch and has a different base branch in GitHub than the upstream in machete file, + just as `git machete github retarget-pr` would do + -L, --sync-gitlab-mrs + Retarget the MR if it exists for the given branch and has a different target branch in GitLab than the upstream in machete file, + just as `git machete gitlab retarget-mr` would do -l, --list-commits When printing the status, additionally list the messages of commits introduced on each branch. -M, --merge diff --git a/tests/completion_e2e/test_completion_e2e.py b/tests/completion_e2e/test_completion_e2e.py index 5f94fc5d6..a891d96dd 100644 --- a/tests/completion_e2e/test_completion_e2e.py +++ b/tests/completion_e2e/test_completion_e2e.py @@ -121,11 +121,15 @@ "git machete status --color=": "always auto never", "git machete t -": - "-F -M -W --debug --fetch -h --help -l --list-commits --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase " - "--no-push --no-push-untracked --push --push-untracked --return-to --start-from -v --verbose -w --whole -y --yes", + "-F -H -L -M -W --debug --fetch -h --help -l --list-commits --merge " + "-n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase " + "--no-push --no-push-untracked --push --push-untracked --return-to --start-from " + "--sync-github-prs --sync-gitlab-mrs -v --verbose -w --whole -y --yes", "git machete traverse -": - "-F -M -W --debug --fetch -h --help -l --list-commits --merge -n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase " - "--no-push --no-push-untracked --push --push-untracked --return-to --start-from -v --verbose -w --whole -y --yes", + "-F -H -L -M -W --debug --fetch -h --help -l --list-commits --merge " + "-n --no-detect-squash-merges --no-edit-merge --no-interactive-rebase " + "--no-push --no-push-untracked --push --push-untracked --return-to --start-from " + "--sync-github-prs --sync-gitlab-mrs -v --verbose -w --whole -y --yes", "git machete update -": "-M --debug -f --fork-point -h --help --merge -n --no-edit-merge --no-interactive-rebase -v --verbose", "git machete update -f ":