diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cd503c99a..00ed21894 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,8 @@ # Release notes -## New in git-machete 3.30.1 +## New in git-machete 3.31.0 + +- added: `git machete git{hub,lab} update-{pr,mr}-descriptions` subcommands ## New in git-machete 3.30.0 diff --git a/completion/git-machete.completion.bash b/completion/git-machete.completion.bash index cde9a3507..f75fd7c6b 100644 --- a/completion/git-machete.completion.bash +++ b/completion/git-machete.completion.bash @@ -6,8 +6,8 @@ _git_machete() { local categories="addable childless managed slidable slidable-after unmanaged with-overridden-fork-point" local directions="down first last next prev root up" - local github_subcommands="anno-prs checkout-prs create-pr restack-pr retarget-pr" - local gitlab_subcommands="anno-mrs checkout-mrs create-mr restack-mr retarget-mr" + local github_subcommands="anno-prs checkout-prs create-pr restack-pr retarget-pr update-pr-descriptions" + local gitlab_subcommands="anno-mrs checkout-mrs create-mr restack-mr retarget-mr update-mr-descriptions" local locations="current $directions" local opt_color_args="always auto never" local opt_return_to_args="here nearest-remaining stay" @@ -22,14 +22,11 @@ _git_machete() { local diff_opts="-s --stat" local discover_opts="-C --checked-out-since= -l --list-commits -r --roots= -y --yes" local fork_point_opts="--inferred --override-to= --override-to-inferred --override-to-parent --unset-override" - local github_anno_prs_opts="--with-urls" - local github_create_pr_opts="--draft --title= --yes" - local github_checkout_prs_opts="--all --by= --mine" - local github_retarget_pr_opts="-b --branch= --ignore-if-missing" - local gitlab_anno_mrs_opts="--with-urls" - local gitlab_create_mr_opts="--draft --title= --yes" - local gitlab_checkout_mrs_opts="--all --by= --mine" - local gitlab_retarget_mr_opts="-b --branch= --ignore-if-missing" + local githublab_anno_opts="--with-urls" + local githublab_create_opts="--draft --title= --yes" + local githublab_checkout_opts="--all --by= --mine" + local githublab_retarget_opts="-b --branch= --ignore-if-missing" + local githublab_update_descriptions_opts="--all --mine --related" local reapply_opts="-f --fork-point=" 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=" @@ -57,25 +54,29 @@ _git_machete() { fork-point) __gitcomp "$common_opts $fork_point_opts" ;; github) if [[ ${COMP_WORDS[3]} == "anno-prs" ]]; then - __gitcomp "$common_opts $github_anno_prs_opts" + __gitcomp "$common_opts $githublab_anno_opts" elif [[ ${COMP_WORDS[3]} == "create-pr" ]]; then - __gitcomp "$common_opts $github_create_pr_opts" + __gitcomp "$common_opts $githublab_create_opts" elif [[ ${COMP_WORDS[3]} == "checkout-prs" ]]; then - __gitcomp "$common_opts $github_checkout_prs_opts" + __gitcomp "$common_opts $githublab_checkout_opts" elif [[ ${COMP_WORDS[3]} == "retarget-pr" ]]; then - __gitcomp "$common_opts $github_retarget_pr_opts" + __gitcomp "$common_opts $githublab_retarget_opts" + elif [[ ${COMP_WORDS[3]} == "update-pr-descriptions" ]]; then + __gitcomp "$common_opts $githublab_update_descriptions_opts" else __gitcomp "$common_opts" fi ;; gitlab) if [[ ${COMP_WORDS[3]} == "anno-mrs" ]]; then - __gitcomp "$common_opts $gitlab_anno_mrs_opts" + __gitcomp "$common_opts $githublab_anno_opts" elif [[ ${COMP_WORDS[3]} == "create-mr" ]]; then - __gitcomp "$common_opts $gitlab_create_mr_opts" + __gitcomp "$common_opts $githublab_create_opts" elif [[ ${COMP_WORDS[3]} == "checkout-mrs" ]]; then - __gitcomp "$common_opts $gitlab_checkout_mrs_opts" + __gitcomp "$common_opts $githublab_checkout_opts" elif [[ ${COMP_WORDS[3]} == "retarget-mr" ]]; then - __gitcomp "$common_opts $gitlab_retarget_mr_opts" + __gitcomp "$common_opts $githublab_retarget_opts" + elif [[ ${COMP_WORDS[3]} == "update-mr-descriptions" ]]; then + __gitcomp "$common_opts $githublab_update_descriptions_opts" else __gitcomp "$common_opts" fi ;; @@ -133,13 +134,15 @@ _git_machete() { if [[ $COMP_CWORD -eq 3 ]]; then __gitcomp "$github_subcommands" elif [[ ${COMP_WORDS[3]} == "anno-prs" ]]; then - __gitcomp "$common_opts $github_anno_prs_opts" + __gitcomp "$common_opts $githublab_anno_opts" elif [[ ${COMP_WORDS[3]} == "create-pr" ]]; then - __gitcomp "$common_opts $github_create_pr_opts" + __gitcomp "$common_opts $githublab_create_opts" elif [[ ${COMP_WORDS[3]} == "checkout-prs" ]]; then - __gitcomp "$common_opts $github_checkout_prs_opts" + __gitcomp "$common_opts $githublab_checkout_opts" elif [[ ${COMP_WORDS[3]} == "retarget-pr" ]]; then - __gitcomp "$common_opts $github_retarget_pr_opts" + __gitcomp "$common_opts $githublab_retarget_opts" + elif [[ ${COMP_WORDS[3]} == "update-pr-descriptions" ]]; then + __gitcomp "$common_opts $githublab_update_descriptions_opts" else COMPREPLY=('') fi ;; @@ -147,13 +150,15 @@ _git_machete() { if [[ $COMP_CWORD -eq 3 ]]; then __gitcomp "$gitlab_subcommands" elif [[ ${COMP_WORDS[3]} == "anno-mrs" ]]; then - __gitcomp "$common_opts $gitlab_anno_mrs_opts" + __gitcomp "$common_opts $githublab_anno_opts" elif [[ ${COMP_WORDS[3]} == "create-mr" ]]; then - __gitcomp "$common_opts $gitlab_create_mr_opts" + __gitcomp "$common_opts $githublab_create_opts" elif [[ ${COMP_WORDS[3]} == "checkout-mrs" ]]; then - __gitcomp "$common_opts $gitlab_checkout_mrs_opts" + __gitcomp "$common_opts $githublab_checkout_opts" elif [[ ${COMP_WORDS[3]} == "retarget-mr" ]]; then - __gitcomp "$common_opts $gitlab_retarget_mr_opts" + __gitcomp "$common_opts $githublab_retarget_opts" + elif [[ ${COMP_WORDS[3]} == "update-mr-descriptions" ]]; then + __gitcomp "$common_opts $githublab_update_descriptions_opts" else COMPREPLY=('') fi ;; diff --git a/completion/git-machete.completion.zsh b/completion/git-machete.completion.zsh index 9d960b3db..122b7022a 100644 --- a/completion/git-machete.completion.zsh +++ b/completion/git-machete.completion.zsh @@ -254,6 +254,7 @@ __git_machete_github_subcommands() { 'create-pr:create a PR for the current branch, using the upstream (parent) branch as the PR base' 'restack-pr:(force-)push and retarget the PR, without adding code owners as reviewers in the process' 'retarget-pr:set the base of the current branch PR to upstream (parent) branch' + 'update-pr-descriptions:update the generated sections of PR descriptions that lists the upstream and/or downstream PRs' ) _describe 'subcommand' github_subcommands ;; @@ -271,7 +272,7 @@ __git_machete_github_subcommands() { _arguments \ '(--all)'--all'[Checkout all open PRs]' \ '(--by)'--by='[Checkout open PRs authored by the given GitHub user]' \ - '(--mine)'--mine='[Checkout open PRs for the current user associated with the GitHub token]' \ + '(--mine)'--mine'[Checkout open PRs for the current user associated with the GitHub token]' \ "${common_flags[@]}" ;; @@ -289,6 +290,14 @@ __git_machete_github_subcommands() { '(--ignore-if-missing)'--ignore-if-missing'[Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch]' \ "${common_flags[@]}" ;; + + (update-pr-descriptions) + _arguments \ + '(--all)'--all'[Update PR descriptions for all PRs in the repository]' \ + '(--mine)'--mine'[Update PR descriptions for all PRs opened by the current user associated with the GitHub token]' \ + '(--related)'--related'[Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch]' \ + "${common_flags[@]}" + ;; esac ;; esac @@ -312,6 +321,7 @@ __git_machete_gitlab_subcommands() { 'create-mr:create a MR for the current branch, using the upstream (parent) branch as the MR source branch' 'restack-mr:(force-)push and retarget the MR, without adding code owners as reviewers in the process' 'retarget-mr:set the source branch of the current branch MR to upstream (parent) branch' + 'update-mr-descriptions:update the generated sections of MR descriptions that list the upstream and/or downstream MRs' ) _describe 'subcommand' gitlab_subcommands ;; @@ -329,7 +339,7 @@ __git_machete_gitlab_subcommands() { _arguments \ '(--all)'--all'[Checkout all open MRs]' \ '(--by)'--by='[Checkout open MRs authored by the given GitLab user]' \ - '(--mine)'--mine='[Checkout open MRs for the current user associated with the GitLab token]' \ + '(--mine)'--mine'[Checkout open MRs for the current user associated with the GitLab token]' \ "${common_flags[@]}" ;; @@ -347,6 +357,14 @@ __git_machete_gitlab_subcommands() { '(--ignore-if-missing)'--ignore-if-missing'[Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch]' \ "${common_flags[@]}" ;; + + (update-mr-descriptions) + _arguments \ + '(--all)'--all'[Update MR descriptions for all MRs in the project]' \ + '(--mine)'--mine'[Update MR descriptions for all MRs opened by the current user associated with the GitLab token]' \ + '(--related)'--related'[Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch]' \ + "${common_flags[@]}" + ;; esac ;; esac diff --git a/completion/git-machete.fish b/completion/git-machete.fish index a5688778f..d853df4ce 100644 --- a/completion/git-machete.fish +++ b/completion/git-machete.fish @@ -93,38 +93,46 @@ complete -c git-machete -n "__fish_seen_subcommand_from fork-point; and not __fi complete -c git-machete -n "__fish_seen_subcommand_from fork-point; and not __fish_seen_subcommand_from --inferred --override-to --override-to-inferred --override-to-parent" -x -l unset-override -a '(__machete_with_overridden_fork_point_branches)' # git machete github -complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a github -d 'Create, check out and manage GitHub PRs while keeping them reflected in git machete' -complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync" -f -a anno-prs -d 'Annotate the branches based on their corresponding GitHub PR numbers and authors' -complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync" -x -a checkout-prs -d 'Check out the head branch of the given pull requests (specified by number), also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally' -complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync" -f -a create-pr -d 'Create a PR for the current branch, using the upstream (parent) branch as the PR base' -complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync" -f -a restack-pr -d '(Force-)pushes and retargets the PR, without adding code owners as reviewers in the process' -complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync" -f -a retarget-pr -d 'Sets the base of PR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from anno-prs; and not __fish_seen_subcommand_from --with-urls" -f -l with-urls -d 'Include PR URLs in the annotations' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --all" -f -l all -d 'Checkout all open PRs' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --by" -x -l by -d "Checkout someone's open PRs" -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --mine" -x -l mine -d 'Checkout open PRs for the current user associated with the GitHub token' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --draft" -f -l draft -d 'Create the new PR as a draft' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --title" -x -l title -d 'Set the title for new PR explicitly' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --yes" -f -l yes -d 'Do not ask for confirmation whether to push the branch' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr; and not __fish_seen_subcommand_from --branch" -x -l branch -s b -a '(__machete_managed_branches)' -d 'Specify the branch for which the associated PR base will be set to its upstream (parent) branch' -complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr; and not __fish_seen_subcommand_from --ignore-if-missing" -f -l ignore-if-missing -d 'Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch' +complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a github -d 'Create, check out and manage GitHub PRs while keeping them reflected in git machete' +complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions" -f -a anno-prs -d 'Annotate the branches based on their corresponding GitHub PR numbers and authors' +complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions" -x -a checkout-prs -d 'Check out the head branch of the given pull requests (specified by number), also traverse chain of pull requests upwards, adding branches one by one to git-machete and check them out locally' +complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions" -f -a create-pr -d 'Create a PR for the current branch, using the upstream (parent) branch as the PR base' +complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions" -f -a restack-pr -d '(Force-)push and retarget the PR, without adding code owners as reviewers in the process' +complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions" -f -a retarget-pr -d 'Set the base of PR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)' +complete -c git-machete -n "__fish_seen_subcommand_from github; and not __fish_seen_subcommand_from anno-prs checkout-prs create-pr restack-pr retarget-pr sync update-pr-descriptions" -f -a update-pr-descriptions -d 'Update the generated sections of PR descriptions that lists the upstream and/or downstream PRs' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from anno-prs; and not __fish_seen_subcommand_from --with-urls" -f -l with-urls -d 'Include PR URLs in the annotations' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --all" -f -l all -d 'Checkout all open PRs' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --by" -x -l by -d "Checkout someone's open PRs" +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from checkout-prs; and not __fish_seen_subcommand_from --mine" -x -l mine -d 'Checkout open PRs for the current user associated with the GitHub token' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --draft" -f -l draft -d 'Create the new PR as a draft' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --title" -x -l title -d 'Set the title for new PR explicitly' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from create-pr; and not __fish_seen_subcommand_from --yes" -f -l yes -d 'Do not ask for confirmation whether to push the branch' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr; and not __fish_seen_subcommand_from --branch" -x -l branch -s b -a '(__machete_managed_branches)' -d 'Specify the branch for which the associated PR base will be set to its upstream (parent) branch' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from retarget-pr; and not __fish_seen_subcommand_from --ignore-if-missing" -f -l ignore-if-missing -d 'Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from update-pr-descriptions; and not __fish_seen_subcommand_from --all" -f -l all -d 'Update PR descriptions for all PRs in the repository' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from update-pr-descriptions; and not __fish_seen_subcommand_from --mine" -x -l mine -d 'Update PR descriptions for all PRs opened by the current user associated with the GitHub token' +complete -c git-machete -n "__fish_seen_subcommand_from github; and __fish_seen_subcommand_from update-pr-descriptions; and not __fish_seen_subcommand_from --related" -x -l related -d 'Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch' # git machete gitlab -complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a gitlab -d 'Create, check out and manage GitLab MRs while keeping them reflected in git machete' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync" -f -a anno-mrs -d 'Annotate the branches based on their corresponding GitLab MR numbers and authors' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync" -x -a checkout-mrs -d 'Check out the head branch of the given merge requests (specified by number), also traverse chain of merge requests upwards, adding branches one by one to git-machete and check them out locally' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync" -f -a create-mr -d 'Create a MR for the current branch, using the upstream (parent) branch as the MR source branch' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync" -f -a restack-mr -d '(Force-)pushes and retargets the MR, without adding code owners as reviewers in the process' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync" -f -a retarget-mr -d 'Sets the base of MR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from anno-mrs; and not __fish_seen_subcommand_from --with-urls" -f -l with-urls -d 'Include MR URLs in the annotations' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --all" -f -l all -d 'Checkout all open MRs' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --by" -x -l by -d "Checkout someone's open MRs" -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --mine" -x -l mine -d 'Checkout open MRs for the current user associated with the GitLab token' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr; and not __fish_seen_subcommand_from --draft" -f -l draft -d 'Create the new MR as a draft' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr; and not __fish_seen_subcommand_from --title" -x -l title -d 'Set the title for new MR explicitly' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr; and not __fish_seen_subcommand_from --yes" -f -l yes -d 'Do not ask for confirmation whether to push the branch' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr; and not __fish_seen_subcommand_from --branch" -x -l branch -s b -a '(__machete_managed_branches)' -d 'Specify the branch for which the associated MR source branch will be set to its upstream (parent) branch' -complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr; and not __fish_seen_subcommand_from --ignore-if-missing" -f -l ignore-if-missing -d 'Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch' +complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a gitlab -d 'Create, check out and manage GitLab MRs while keeping them reflected in git machete' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions" -f -a anno-mrs -d 'Annotate the branches based on their corresponding GitLab MR numbers and authors' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions" -x -a checkout-mrs -d 'Check out the head branch of the given merge requests (specified by number), also traverse chain of merge requests upwards, adding branches one by one to git-machete and check them out locally' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions" -f -a create-mr -d 'Create a MR for the current branch, using the upstream (parent) branch as the MR source branch' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions" -f -a restack-mr -d '(Force-)push and retarget the MR, without adding code owners as reviewers in the process' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions" -f -a retarget-mr -d 'Set the base of MR for the current branch to upstream (parent) branch, as seen by git machete (see git machete show up)' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and not __fish_seen_subcommand_from anno-mrs checkout-mrs create-mr restack-mr retarget-mr sync update-mr-descriptions" -f -a update-mr-descriptions -d 'Update the generated sections of MR descriptions that list the upstream and/or downstream MRs' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from anno-mrs; and not __fish_seen_subcommand_from --with-urls" -f -l with-urls -d 'Include MR URLs in the annotations' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --all" -f -l all -d 'Checkout all open MRs' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --by" -x -l by -d "Checkout someone's open MRs" +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from checkout-mrs; and not __fish_seen_subcommand_from --mine" -x -l mine -d 'Checkout open MRs for the current user associated with the GitLab token' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr; and not __fish_seen_subcommand_from --draft" -f -l draft -d 'Create the new MR as a draft' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr; and not __fish_seen_subcommand_from --title" -x -l title -d 'Set the title for new MR explicitly' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from create-mr; and not __fish_seen_subcommand_from --yes" -f -l yes -d 'Do not ask for confirmation whether to push the branch' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr; and not __fish_seen_subcommand_from --branch" -x -l branch -s b -a '(__machete_managed_branches)' -d 'Specify the branch for which the associated MR source branch will be set to its upstream (parent) branch' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from retarget-mr; and not __fish_seen_subcommand_from --ignore-if-missing" -f -l ignore-if-missing -d 'Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from update-mr-descriptions; and not __fish_seen_subcommand_from --all" -f -l all -d 'Update MR descriptions for all MRs in the project' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from update-mr-descriptions; and not __fish_seen_subcommand_from --mine" -x -l mine -d 'Update MR descriptions for all MRs opened by the current user associated with the GitHub token' +complete -c git-machete -n "__fish_seen_subcommand_from gitlab; and __fish_seen_subcommand_from update-mr-descriptions; and not __fish_seen_subcommand_from --related" -x -l related -d 'Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch' # git machete go complete -c git-machete -n "not __fish_seen_subcommand_from $__machete_commands" -f -a go -d 'Check out the branch relative to the position of the current branch, accepts down/first/last/next/root/prev/up argument' diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1 index e87a29666..21bee4e1b 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" "Oct 15, 2024" "" "git-machete" +.TH "GIT-MACHETE" "1" "Oct 26, 2024" "" "git-machete" .SH NAME -git-machete \- git-machete 3.30.1 +git-machete \- git-machete 3.31.0 .sp git machete is a robust tool that \fBsimplifies your git workflows\fP\&. .sp @@ -899,7 +899,7 @@ git machete github .UNINDENT .UNINDENT .sp -where \fB\fP is one of: \fBanno\-prs\fP, \fBcheckout\-prs\fP, \fBcreate\-pr\fP, \fBretarget\-pr\fP or \fBrestack\-pr\fP\&. +where \fB\fP is one of: \fBanno\-prs\fP, \fBcheckout\-prs\fP, \fBcreate\-pr\fP, \fBretarget\-pr\fP, \fBrestack\-pr\fP or \fBupdate\-pr\-descriptions\fP\&. .sp Creates, checks out and manages GitHub PRs while keeping them reflected in branch layout file. .sp @@ -998,7 +998,7 @@ Once the PR is successfully created, annotates the current branch with the new P If \fB\&.git/info/milestone\fP file is present, its contents (a single number \-\-\- milestone id) are used as milestone. If \fB\&.git/info/reviewers\fP file is present, its contents (one GitHub login per line) are used to set reviewers. .sp -The subject of the first unique commit of the branch is used as PR title. +Unless \fB\-\-title\fP is provided, the subject of the first unique commit of the branch is used as PR title. If \fB\&.git/info/description\fP or \fB\&.github/pull_request_template.md\fP template is present, its contents are used as PR description. Otherwise (or if \fBmachete.github.forceDescriptionFromCommitMessage\fP is set), PR description is taken from message body of the first unique commit of the branch. .sp @@ -1067,6 +1067,25 @@ deletes unmanaged branches, .IP 3. 3 deletes untracked managed branches that have no downstream branch. .UNINDENT +.TP +.B \fBupdate\-pr\-descriptions\fP: +Updates the generated sections (\(dqintros\(dq) of PR descriptions that list the upstream and/or downstream PRs +(depending on \fBmachete.github.prDescriptionIntroStyle\fP git config key). +.sp +\fBOptions:\fP +.INDENT 7.0 +.TP +.B \-\-all +Update PR descriptions for all PRs in the repository. +.TP +.B \-\-mine +Update PR descriptions for all PRs opened by the current user associated with the GitHub token. +.TP +.B \-\-related +Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch. +If \fBmachete.github.prDescriptionIntroStyle\fP is \fBup\-only\fP (default), then only downstream PR descriptions are updated. +If \fBmachete.github.prDescriptionIntroStyle\fP is \fBfull\fP, then both downstream and upstream PR descriptions are updated. +.UNINDENT .UNINDENT .sp \fBGit config keys:\fP @@ -1238,7 +1257,7 @@ Note that you need to use a global (not per\-project) milestone id. Look for som .sp If \fB\&.git/info/reviewers\fP file is present, its contents (one GitLab login per line) are used to set reviewers. .sp -The subject of the first unique commit of the branch is used as MR title. +Unless \fB\-\-title\fP is provided, the subject of the first unique commit of the branch is used as MR title. If \fB\&.git/info/description\fP or \fB\&.gitlab/merge_request_templates/Default.md\fP template is present, its contents are used as MR description. Otherwise (or if \fBmachete.gitlab.forceDescriptionFromCommitMessage\fP is set), MR description is taken from message body of the first unique commit of the branch. .sp @@ -1293,6 +1312,25 @@ Specify the branch for which the associated MR source branch will be set to its .B \-\-ignore\-if\-missing Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch. .UNINDENT +.TP +.B \fBupdate\-mr\-descriptions\fP: +Updates the generated sections (\(dqintros\(dq) of MR descriptions that list the upstream and/or downstream MRs +(depending on \fBmachete.gitlab.mrDescriptionIntroStyle\fP git config key). +.sp +\fBOptions:\fP +.INDENT 7.0 +.TP +.B \-\-all +Update MR descriptions for all MRs in the project. +.TP +.B \-\-mine +Update MR descriptions for all MRs opened by the current user associated with the GitLab token. +.TP +.B \-\-related +Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch. +If \fBmachete.gitlab.mrDescriptionIntroStyle\fP is \fBup\-only\fP (default), then only downstream MR descriptions are updated. +If \fBmachete.gitlab.mrDescriptionIntroStyle\fP is \fBfull\fP, then both downstream and upstream MR descriptions are updated. +.UNINDENT .UNINDENT .sp \fBGit config keys:\fP diff --git a/docs/source/cli/github.rst b/docs/source/cli/github.rst index 5e0667be2..bac036db7 100644 --- a/docs/source/cli/github.rst +++ b/docs/source/cli/github.rst @@ -8,7 +8,7 @@ github git machete github -where ```` is one of: ``anno-prs``, ``checkout-prs``, ``create-pr``, ``retarget-pr`` or ``restack-pr``. +where ```` is one of: ``anno-prs``, ``checkout-prs``, ``create-pr``, ``retarget-pr``, ``restack-pr`` or ``update-pr-descriptions``. Creates, checks out and manages GitHub PRs while keeping them reflected in branch layout file. @@ -83,7 +83,7 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc If ``.git/info/milestone`` file is present, its contents (a single number --- milestone id) are used as milestone. If ``.git/info/reviewers`` file is present, its contents (one GitHub login per line) are used to set reviewers. - The subject of the first unique commit of the branch is used as PR title. + Unless ``--title`` is provided, the subject of the first unique commit of the branch is used as PR title. If ``.git/info/description`` or ``.github/pull_request_template.md`` template is present, its contents are used as PR description. Otherwise (or if ``machete.github.forceDescriptionFromCommitMessage`` is set), PR description is taken from message body of the first unique commit of the branch. @@ -94,11 +94,11 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc **Options:** - --draft Create the new PR as a draft. + --draft Create the new PR as a draft. - --title= Set the PR title explicitly (the default is to use the first included commit's message as the title). + --title=<title> Set the PR title explicitly (the default is to use the first included commit's message as the title). - --yes Do not ask for confirmation whether to push the branch. + --yes Do not ask for confirmation whether to push the branch. ``restack-pr``: Perform the following sequence of actions: @@ -121,9 +121,9 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc **Options:** - -b, --branch=<branch> Specify the branch for which the associated PR base will be set to its upstream (parent) branch. The current branch is used if the option is absent. + -b, --branch=<branch> Specify the branch for which the associated PR base will be set to its upstream (parent) branch. The current branch is used if the option is absent. - --ignore-if-missing Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch. + --ignore-if-missing Ignore errors and quietly terminate execution if there is no PR opened for current (or specified) branch. ``sync``: **Deprecated.** Use ``github checkout-prs --mine``, ``delete-unmanaged`` and ``slide-out --removed-from-remote``. @@ -135,6 +135,20 @@ Creates, checks out and manages GitHub PRs while keeping them reflected in branc #. deletes unmanaged branches, #. deletes untracked managed branches that have no downstream branch. +``update-pr-descriptions``: + Updates the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs + (depending on ``machete.github.prDescriptionIntroStyle`` git config key). + + **Options:** + + --all Update PR descriptions for all PRs in the repository. + + --mine Update PR descriptions for all PRs opened by the current user associated with the GitHub token. + + --related Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch. + If ``machete.github.prDescriptionIntroStyle`` is ``up-only`` (default), then only downstream PR descriptions are updated. + If ``machete.github.prDescriptionIntroStyle`` is ``full``, then both downstream and upstream PR descriptions are updated. + **Git config keys:** ``machete.github.{domain,remote,organization,repository}`` (all subcommands): diff --git a/docs/source/cli/gitlab.rst b/docs/source/cli/gitlab.rst index a5d8bf4de..7d31d5fb0 100644 --- a/docs/source/cli/gitlab.rst +++ b/docs/source/cli/gitlab.rst @@ -84,7 +84,7 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc If ``.git/info/reviewers`` file is present, its contents (one GitLab login per line) are used to set reviewers. - The subject of the first unique commit of the branch is used as MR title. + Unless ``--title`` is provided, the subject of the first unique commit of the branch is used as MR title. If ``.git/info/description`` or ``.gitlab/merge_request_templates/Default.md`` template is present, its contents are used as MR description. Otherwise (or if ``machete.gitlab.forceDescriptionFromCommitMessage`` is set), MR description is taken from message body of the first unique commit of the branch. @@ -126,6 +126,20 @@ Creates, checks out and manages GitLab MRs while keeping them reflected in branc --ignore-if-missing Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch. +``update-mr-descriptions``: + Updates the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs + (depending on ``machete.gitlab.mrDescriptionIntroStyle`` git config key). + + **Options:** + + --all Update MR descriptions for all MRs in the project. + + --mine Update MR descriptions for all MRs opened by the current user associated with the GitLab token. + + --related Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch. + If ``machete.gitlab.mrDescriptionIntroStyle`` is ``up-only`` (default), then only downstream MR descriptions are updated. + If ``machete.gitlab.mrDescriptionIntroStyle`` is ``full``, then both downstream and upstream MR descriptions are updated. + **Git config keys:** ``machete.gitlab.{domain,remote,namespace,project}`` (all subcommands): diff --git a/git_machete/__init__.py b/git_machete/__init__.py index 7861b42ff..7aeb2458c 100644 --- a/git_machete/__init__.py +++ b/git_machete/__init__.py @@ -1 +1 @@ -__version__ = '3.30.1' +__version__ = '3.31.0' diff --git a/git_machete/cli.py b/git_machete/cli.py index 940f36673..d4486362f 100644 --- a/git_machete/cli.py +++ b/git_machete/cli.py @@ -240,7 +240,7 @@ def create_cli_parser() -> argparse.ArgumentParser: fork_point_exclusive_optional_args.add_argument('--override-to-parent', action='store_true') fork_point_exclusive_optional_args.add_argument('--unset-override', action='store_true') - def add_code_hosting_parser(command: str, subcommand_suffix: str, include_sync: bool) -> Any: + def add_code_hosting_parser(command: str, pr_or_mr: str, include_sync: bool) -> Any: parser = subparsers.add_parser( command, argument_default=argparse.SUPPRESS, @@ -248,11 +248,12 @@ def add_code_hosting_parser(command: str, subcommand_suffix: str, include_sync: add_help=False, parents=[common_args_parser]) parser.add_argument('subcommand', choices=[ - f'anno-{subcommand_suffix}s', - f'checkout-{subcommand_suffix}s', - f'create-{subcommand_suffix}', - f'restack-{subcommand_suffix}', - f'retarget-{subcommand_suffix}' + f'anno-{pr_or_mr}s', + f'checkout-{pr_or_mr}s', + f'create-{pr_or_mr}', + f'restack-{pr_or_mr}', + f'retarget-{pr_or_mr}', + f'update-{pr_or_mr}-descriptions' ] + (['sync'] if include_sync else [])) parser.add_argument('request_id', nargs='*', type=int) parser.add_argument('-b', '--branch') @@ -261,9 +262,11 @@ def add_code_hosting_parser(command: str, subcommand_suffix: str, include_sync: parser.add_argument('--draft', action='store_true') parser.add_argument('--ignore-if-missing', action='store_true') parser.add_argument('--mine', action='store_true') + parser.add_argument('--related', action='store_true') parser.add_argument('--title') parser.add_argument('--with-urls', action='store_true') parser.add_argument('--yes', action='store_true') + add_code_hosting_parser('github', 'pr', include_sync=True) add_code_hosting_parser('gitlab', 'mr', include_sync=False) @@ -461,9 +464,6 @@ def update_cli_options_using_parsed_args( elif opt == "no_detect_squash_merges": warn("`--no-detect-squash-merges` is deprecated, use `--squash-merge-detection=none` instead", end="\n\n") cli_opts.opt_squash_merge_detection_string = "none" - elif opt == "squash_merge_detection" and arg is not None: # if no arg is passed, argparse will fail anyway - cli_opts.opt_squash_merge_detection_string = arg - cli_opts.opt_squash_merge_detection_origin = "`--squash-merge-detection` flag" elif opt == "no_edit_merge": cli_opts.opt_no_edit_merge = True elif opt == "no_interactive_rebase": @@ -492,6 +492,9 @@ def update_cli_options_using_parsed_args( cli_opts.opt_return_to = arg elif opt == "roots": cli_opts.opt_roots = list(map(LocalBranchShortName.of, filter(None, arg.split(",")))) + elif opt == "squash_merge_detection" and arg is not None: # if no arg is passed, argparse will fail anyway + cli_opts.opt_squash_merge_detection_string = arg + cli_opts.opt_squash_merge_detection_origin = "`--squash-merge-detection` flag" elif opt == "start_from": cli_opts.opt_start_from = arg elif opt == "stat": @@ -727,61 +730,78 @@ def print_completion_resource(name: str) -> None: git.checkout(dest) elif cmd in ("github", "gitlab"): subcommand = parsed_cli.subcommand - config = GitHubClient.spec() if cmd == "github" else GitLabClient.spec() - subcommand_suffix = "pr" if cmd == "github" else "mr" + spec = GitHubClient.spec() if cmd == "github" else GitLabClient.spec() + pr_or_mr = spec.pr_short_name.lower() machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out) - if 'request_id' in parsed_cli and subcommand != f'checkout-{subcommand_suffix}s': - raise MacheteException(f"`request_id` option is only valid with `checkout-{subcommand_suffix}s` subcommand.") - for command in ('all', 'by', 'mine'): - if command in parsed_cli and subcommand != f"checkout-{subcommand_suffix}s": - raise MacheteException(f"`--{command}` option is only valid with `checkout-{subcommand_suffix}s` subcommand.") - if "branch" in parsed_cli and subcommand != f"retarget-{subcommand_suffix}": - raise MacheteException(f"`--branch` option is only valid with `retarget-{subcommand_suffix}` subcommand.") - if "draft" in parsed_cli and subcommand != f"create-{subcommand_suffix}": - raise MacheteException(f"`--draft` option is only valid with `create-{subcommand_suffix}` subcommand.") - if "ignore_if_missing" in parsed_cli and subcommand != f"retarget-{subcommand_suffix}": - raise MacheteException(f"`--ignore-if-missing` option is only valid with `retarget-{subcommand_suffix}` subcommand.") - if "title" in parsed_cli and subcommand != f"create-{subcommand_suffix}": - raise MacheteException(f"`--title` option is only valid with `create-{subcommand_suffix}` subcommand.") - if "with_urls" in parsed_cli and subcommand != f"anno-{subcommand_suffix}s": - raise MacheteException(f"`--with-urls` option is only valid with `anno-{subcommand_suffix}s` subcommand.") - if "yes" in parsed_cli and subcommand != f"create-{subcommand_suffix}": - raise MacheteException(f"`--yes` option is only valid with `create-{subcommand_suffix}` subcommand.") - - if subcommand == f"anno-{subcommand_suffix}s": - machete_client.sync_annotations_to_prs(config, include_urls=cli_opts.opt_with_urls) - elif subcommand == f"checkout-{subcommand_suffix}s": + if "request_id" in parsed_cli and subcommand != f"checkout-{pr_or_mr}s": + raise MacheteException(f"{spec.pr_short_name} number is only valid with `checkout-{pr_or_mr}s` subcommand.") + for option in ("all", "mine"): + if option in parsed_cli and subcommand not in (f"checkout-{pr_or_mr}s", f"update-{pr_or_mr}-descriptions"): + raise MacheteException(f"`--{option}` option is only valid with " + f"`checkout-{pr_or_mr}s` and `update-{pr_or_mr}-descriptions` subcommands.") + if "branch" in parsed_cli and subcommand != f"retarget-{pr_or_mr}": + raise MacheteException(f"`--branch` option is only valid with `retarget-{pr_or_mr}` subcommand.") + if "by" in parsed_cli and subcommand != f"checkout-{pr_or_mr}s": + raise MacheteException(f"`--by` option is only valid with `checkout-{pr_or_mr}s` subcommand.") + if "draft" in parsed_cli and subcommand != f"create-{pr_or_mr}": + raise MacheteException(f"`--draft` option is only valid with `create-{pr_or_mr}` subcommand.") + if "ignore_if_missing" in parsed_cli and subcommand != f"retarget-{pr_or_mr}": + raise MacheteException(f"`--ignore-if-missing` option is only valid with `retarget-{pr_or_mr}` subcommand.") + if "related" in parsed_cli and subcommand != f"update-{pr_or_mr}-descriptions": + raise MacheteException(f"`--related` option is only valid with `update-{pr_or_mr}-descriptions` subcommand.") + if "title" in parsed_cli and subcommand != f"create-{pr_or_mr}": + raise MacheteException(f"`--title` option is only valid with `create-{pr_or_mr}` subcommand.") + if "with_urls" in parsed_cli and subcommand != f"anno-{pr_or_mr}s": + raise MacheteException(f"`--with-urls` option is only valid with `anno-{pr_or_mr}s` subcommand.") + if "yes" in parsed_cli and subcommand != f"create-{pr_or_mr}": + raise MacheteException(f"`--yes` option is only valid with `create-{pr_or_mr}` subcommand.") + + if subcommand == f"anno-{pr_or_mr}s": + machete_client.sync_annotations_to_prs(spec, include_urls=cli_opts.opt_with_urls) + elif subcommand == f"checkout-{pr_or_mr}s": if len(set(parsed_cli_as_dict.keys()).intersection({'all', 'by', 'mine', 'request_id'})) != 1: - raise MacheteException(f"`checkout-{subcommand_suffix}s` subcommand must take exactly one of the following options: " + - ', '.join(['--all', '--by=...', '--mine', f'{subcommand_suffix}-number(s)'])) - machete_client.checkout_pull_requests(config, - pr_numbers=parsed_cli.request_id if 'request_id' in parsed_cli else [], - all=parsed_cli.all if 'all' in parsed_cli else False, - mine=parsed_cli.mine if 'mine' in parsed_cli else False, - by=parsed_cli.by if 'by' in parsed_cli else None, - fail_on_missing_current_user_for_my_opened_prs=True) - elif subcommand == f"create-{subcommand_suffix}": + raise MacheteException( + f"`checkout-{pr_or_mr}s` subcommand must take exactly one of the following options: " + f'`--all`, `--by=...`, `--mine`, `{pr_or_mr}-number(s)`') + machete_client.checkout_pull_requests( + spec, + pr_numbers=parsed_cli.request_id if 'request_id' in parsed_cli else [], + all=parsed_cli.all if 'all' in parsed_cli else False, + mine=parsed_cli.mine if 'mine' in parsed_cli else False, + by=parsed_cli.by if 'by' in parsed_cli else None, + fail_on_missing_current_user_for_my_opened_prs=True) + elif subcommand == f"create-{pr_or_mr}": current_branch = git.get_current_branch() machete_client.create_pull_request( - config, + spec, head=current_branch, opt_draft=cli_opts.opt_draft, opt_onto=cli_opts.opt_onto, opt_title=cli_opts.opt_title, opt_yes=cli_opts.opt_yes) - elif subcommand == f"restack-{subcommand_suffix}": - machete_client.restack_pull_request(config) - elif subcommand == f"retarget-{subcommand_suffix}": + elif subcommand == f"restack-{pr_or_mr}": + machete_client.restack_pull_request(spec) + elif subcommand == f"retarget-{pr_or_mr}": branch = parsed_cli.branch if 'branch' in parsed_cli else git.get_current_branch() machete_client.expect_in_managed_branches(branch) ignore_if_missing = parsed_cli.ignore_if_missing if 'ignore_if_missing' in parsed_cli else False - machete_client.retarget_pr(config, head=branch, ignore_if_missing=ignore_if_missing) + machete_client.retarget_pr(spec, head=branch, ignore_if_missing=ignore_if_missing) elif subcommand == "sync": # GitHub only - machete_client.checkout_pull_requests(config, pr_numbers=[], mine=True) + machete_client.checkout_pull_requests(spec, pr_numbers=[], mine=True) machete_client.delete_unmanaged(opt_squash_merge_detection=SquashMergeDetection.NONE, opt_yes=False) machete_client.delete_untracked(opt_yes=cli_opts.opt_yes) + elif subcommand == f"update-{pr_or_mr}-descriptions": + if len(set(parsed_cli_as_dict.keys()).intersection({'all', 'related', 'mine'})) != 1: + raise MacheteException( + f"`update-{pr_or_mr}-descriptions` subcommand must take exactly one of the following options: " + '`--all`, `--mine`, `--related`') + machete_client.update_pull_request_descriptions( + spec, + all=parsed_cli.all if 'all' in parsed_cli else False, + mine=parsed_cli.mine if 'mine' in parsed_cli else False, + related=parsed_cli.related if 'related' in parsed_cli else False) else: # an unknown subcommand is handled by argparse raise UnexpectedMacheteException(f"Unknown subcommand: `{subcommand}`") elif cmd == "is-managed": diff --git a/git_machete/client.py b/git_machete/client.py index ad4755fe8..46dd1fafc 100644 --- a/git_machete/client.py +++ b/git_machete/client.py @@ -28,8 +28,8 @@ LocalBranchShortName, RemoteBranchShortName, SyncToRemoteStatus) from .utils import (AnsiEscapeCodes, PopenResult, bold, colored, debug, dim, - excluding, flat_map, fmt, get_pretty_choices, get_second, - tupled, underline, warn) + excluding, flat_map, fmt, get_pretty_choices, + get_right_arrow, get_second, tupled, underline, warn) class SyncToParentStatus(Enum): @@ -2162,6 +2162,42 @@ def check_that_fork_point_is_ancestor_or_equal_to_tip_of_branch( f"Fork point {bold(fork_point_hash)} is not ancestor of or the tip " f"of the {bold(branch)} branch.") + def update_pull_request_descriptions(self, + spec: CodeHostingSpec, + *, + all: bool = False, + mine: bool = False, + related: bool = False, + ) -> None: + 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) + + current_user: Optional[str] = code_hosting_client.get_current_user_login() + if not current_user and mine: + msg = (f"Could not determine current user name, please check that the {spec.display_name} API token provided by one of the: " + f"{spec.token_providers_message}is valid.") + raise MacheteException(msg) + print(f'Checking for open {spec.display_name} {spec.pr_short_name}s... ', end='', flush=True) + all_open_prs: List[PullRequest] = code_hosting_client.get_open_pull_requests() + print(fmt('<green><b>OK</b></green>')) + + if related: + head = self.__git.get_current_branch() + current_pr = self.__get_sole_pull_request_for_head(code_hosting_client, head, ignore_if_missing=False) + else: + current_pr = None + applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests( + pr_numbers=[], all_opened_prs=all_open_prs, code_hosting_client=code_hosting_client, + all=all, mine=mine, by=None, related_to=current_pr, user=current_user) + + for pr in applicable_prs: + new_description = self.__get_updated_pull_request_description(code_hosting_client, pr, all_open_prs_preloaded=all_open_prs) + if pr.description != new_description: + code_hosting_client.set_description_of_pull_request(pr.number, description=new_description) + print(fmt(f'Description of {pr.display_text()} (<b>{pr.head} {get_right_arrow()} {pr.base}</b>) has been updated')) + def checkout_pull_requests(self, spec: CodeHostingSpec, pr_numbers: Optional[List[int]], @@ -2191,7 +2227,7 @@ def checkout_pull_requests(self, applicable_prs: List[PullRequest] = self.__get_applicable_pull_requests( pr_numbers, all_opened_prs=all_open_prs, code_hosting_client=code_hosting_client, - all=all, mine=mine, by=by, user=current_user) + all=all, mine=mine, by=by, related_to=None, user=current_user) debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}') self.__git.fetch_remote(org_repo_remote.remote) @@ -2296,13 +2332,14 @@ def __get_upwards_path_including_pr(spec: CodeHostingSpec, original_pr: PullRequ pr_base = pr.base if pr else None return path - @staticmethod def __get_applicable_pull_requests( + self, pr_numbers: Optional[List[int]], all_opened_prs: List[PullRequest], code_hosting_client: CodeHostingClient, all: bool, mine: bool, + related_to: Optional[PullRequest], by: Optional[str], user: Optional[str] ) -> List[PullRequest]: @@ -2343,6 +2380,14 @@ def __get_applicable_pull_requests( f"{bold(code_hosting_client.organization)}/{bold(code_hosting_client.repository)}") return [] return result + elif related_to: + style = self.__get_pr_description_into_style_from_config(spec) + result = [] + if style == PRDescriptionIntroStyle.FULL: + result += reversed(self.__get_upwards_path_including_pr(spec, related_to, all_opened_prs)) + result += [pr_ for pr_, _ in self.__get_downwards_tree_excluding_pr(related_to, all_opened_prs)] + return result + raise UnexpectedMacheteException("All params passed to __get_applicable_pull_requests are empty.") def __get_url_for_remote(self) -> Dict[str, str]: @@ -2367,15 +2412,8 @@ def restack_pull_request(self, spec: CodeHostingSpec) -> None: code_hosting_client = spec.create_client( domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository) - prs: List[PullRequest] = code_hosting_client.get_open_pull_requests_by_head(head) - if not prs: - raise MacheteException(f"No {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> " - f"have <b>{head}</b> as its {spec.head_branch_name} branch") - if len(prs) > 1: - raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> " - f"have <b>{head}</b> as its {spec.head_branch_name} branch: " + - ", ".join(_pr.short_display_text() for _pr in prs)) - pr = prs[0] + pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(code_hosting_client, head, ignore_if_missing=False) + assert pr is not None self.__git.fetch_remote(org_repo_remote.remote) @@ -2436,7 +2474,7 @@ def restack_pull_request(self, spec: CodeHostingSpec) -> None: self.__print_new_line(False) if converted_to_draft: - code_hosting_client.set_draft_status_of_pull_request(prs[0].number, target_draft_status=False) + code_hosting_client.set_draft_status_of_pull_request(pr.number, target_draft_status=False) print(f'{pr.display_text()} has been marked as ready for review again') else: @@ -2479,23 +2517,9 @@ def retarget_pr(self, spec: CodeHostingSpec, head: LocalBranchShortName, ignore_ code_hosting_client = spec.create_client( domain=domain, organization=org_repo_remote.organization, repository=org_repo_remote.repository) - debug(f'organization is {org_repo_remote.organization}, repository is {org_repo_remote.repository}') - - prs: List[PullRequest] = code_hosting_client.get_open_pull_requests_by_head(head) - if not prs: - if ignore_if_missing: - warn(f"no {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> " - f"have <b>{head}</b> as its {spec.head_branch_name} branch") - return - else: - raise MacheteException(f"No {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> " - f"have <b>{head}</b> as its {spec.head_branch_name} branch") - if len(prs) > 1: - raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{org_repo_remote.extract_org_and_repo()}</b> " - f"have <b>{head}</b> as its {spec.head_branch_name} branch: " + - ", ".join(_pr.short_display_text() for _pr in prs)) - pr = prs[0] - debug(f'found {pr}') + pr: Optional[PullRequest] = self.__get_sole_pull_request_for_head(code_hosting_client, head, ignore_if_missing=ignore_if_missing) + if pr is None: + return new_base: Optional[LocalBranchShortName] = self.__up_branch.get(LocalBranchShortName.of(head)) if not new_base: @@ -2620,6 +2644,30 @@ def __derive_org_repo_and_remote( START_GIT_MACHETE_GENERATED_COMMENT = '<!-- start git-machete generated -->' END_GIT_MACHETE_GENERATED_COMMENT = '<!-- end git-machete generated -->' + @staticmethod + def __get_sole_pull_request_for_head( + code_hosting_client: CodeHostingClient, + head: LocalBranchShortName, + ignore_if_missing: bool + ) -> Optional[PullRequest]: + prs: List[PullRequest] = code_hosting_client.get_open_pull_requests_by_head(head) + spec = code_hosting_client._spec + if not prs: + if ignore_if_missing: + warn(f"no {spec.pr_short_name}s in <b>{code_hosting_client.get_org_and_repo()}</b> " + f"have <b>{head}</b> as its {spec.head_branch_name} branch") + return None + else: + raise MacheteException(f"No {spec.pr_short_name}s in <b>{code_hosting_client.get_org_and_repo()}</b> " + f"have <b>{head}</b> as its {spec.head_branch_name} branch") + if len(prs) > 1: + raise MacheteException(f"Multiple {spec.pr_short_name}s in <b>{code_hosting_client.get_org_and_repo()}</b> " + f"have <b>{head}</b> as its {spec.head_branch_name} branch: " + + ", ".join(_pr.short_display_text() for _pr in prs)) + pr = prs[0] + debug(f'found {pr}') + return pr + 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( @@ -2663,7 +2711,7 @@ def __generate_pr_description_intro(self, code_hosting_client: CodeHostingClient pr_down_tree = [] prepend = f'{self.START_GIT_MACHETE_GENERATED_COMMENT}\n\n' - # In FULL mode, we're likely to generate the intro even when there are NO upstream PRs above + # In FULL mode, we're likely to generate a non-empty 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' diff --git a/git_machete/code_hosting.py b/git_machete/code_hosting.py index 6c138a3b8..7d2e6adc3 100644 --- a/git_machete/code_hosting.py +++ b/git_machete/code_hosting.py @@ -139,6 +139,9 @@ def __create_ssl_context() -> ssl.SSLContext: ctx.verify_mode = ssl.CERT_NONE return ctx + def get_org_and_repo(self) -> OrganizationAndRepository: + return OrganizationAndRepository(self.organization, self.repository) + @abstractmethod def create_pull_request(self, head: str, head_org_repo: OrganizationAndRepository, base: str, title: str, description: str, draft: bool) -> PullRequest: diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py index fb4ff9dac..798d697d0 100644 --- a/git_machete/generated_docs.py +++ b/git_machete/generated_docs.py @@ -564,7 +564,7 @@ <b>Usage:</b><b> git machete github <subcommand></b> - where `<subcommand>` is one of: `anno-prs`, `checkout-prs`, `create-pr`, `retarget-pr` or `restack-pr`. + where `<subcommand>` is one of: `anno-prs`, `checkout-prs`, `create-pr`, `retarget-pr`, `restack-pr` or `update-pr-descriptions`. Creates, checks out and manages GitHub PRs while keeping them reflected in branch layout file. @@ -638,7 +638,7 @@ If `.git/info/milestone` file is present, its contents (a single number — milestone id) are used as milestone. If `.git/info/reviewers` file is present, its contents (one GitHub login per line) are used to set reviewers. - The subject of the first unique commit of the branch is used as PR title. + Unless `--title` is provided, the subject of the first unique commit of the branch is used as PR title. If `.git/info/description` or `.github/pull_request_template.md` template is present, its contents are used as PR description. Otherwise (or if `machete.github.forceDescriptionFromCommitMessage` is set), PR description is taken from message body of the first unique commit of the branch. @@ -706,6 +706,24 @@ * deletes untracked managed branches that have no downstream branch. + `update-pr-descriptions`: + + Updates the generated sections ("intros") of PR descriptions that list the upstream and/or downstream PRs + (depending on `machete.github.prDescriptionIntroStyle` git config key). + + <b>Options:</b> + + <b>--all</b> + Update PR descriptions for all PRs in the repository. + + <b>--mine</b> + Update PR descriptions for all PRs opened by the current user associated with the GitHub token. + + <b>--related</b> + Update PR descriptions for all PRs that are upstream and/or downstream of the PR for the current branch. + If `machete.github.prDescriptionIntroStyle` is `up-only` (default), then only downstream PR descriptions are updated. + If `machete.github.prDescriptionIntroStyle` is `full`, then both downstream and upstream PR descriptions are updated. + <b>Git config keys:</b> `machete.github.{domain,remote,organization,repository}` (all subcommands): @@ -831,7 +849,7 @@ If `.git/info/reviewers` file is present, its contents (one GitLab login per line) are used to set reviewers. - The subject of the first unique commit of the branch is used as MR title. + Unless `--title` is provided, the subject of the first unique commit of the branch is used as MR title. If `.git/info/description` or `.gitlab/merge_request_templates/Default.md` template is present, its contents are used as MR description. Otherwise (or if `machete.gitlab.forceDescriptionFromCommitMessage` is set), MR description is taken from message body of the first unique commit of the branch. @@ -886,6 +904,24 @@ Ignore errors and quietly terminate execution if there is no MR opened for current (or specified) branch. + `update-mr-descriptions`: + + Updates the generated sections ("intros") of MR descriptions that list the upstream and/or downstream MRs + (depending on `machete.gitlab.mrDescriptionIntroStyle` git config key). + + <b>Options:</b> + + <b>--all</b> + Update MR descriptions for all MRs in the project. + + <b>--mine</b> + Update MR descriptions for all MRs opened by the current user associated with the GitLab token. + + <b>--related</b> + Update MR descriptions for all MRs that are upstream and/or downstream of the MR for the current branch. + If `machete.gitlab.mrDescriptionIntroStyle` is `up-only` (default), then only downstream MR descriptions are updated. + If `machete.gitlab.mrDescriptionIntroStyle` is `full`, then both downstream and upstream MR descriptions are updated. + <b>Git config keys:</b> `machete.gitlab.{domain,remote,namespace,project}` (all subcommands): diff --git a/tests/completion_e2e/test_completion_e2e.py b/tests/completion_e2e/test_completion_e2e.py index 5f94fc5d6..753f8d21d 100644 --- a/tests/completion_e2e/test_completion_e2e.py +++ b/tests/completion_e2e/test_completion_e2e.py @@ -59,7 +59,7 @@ "git machete fork-point --unset-override ": "develop", "git machete github ": - "anno-prs checkout-prs create-pr restack-pr retarget-pr", + "anno-prs checkout-prs create-pr restack-pr retarget-pr update-pr-descriptions", "git machete github anno-prs -": "--debug -h --help -v --verbose --with-urls", "git machete github checkout-prs -": @@ -70,8 +70,10 @@ "--branch --debug --help --ignore-if-missing --verbose", "git machete github retarget-pr -b ": "develop master", + "git machete github update-pr-descriptions -": + "--all --debug -h --help --mine --related -v --verbose", "git machete gitlab ": - "anno-mrs checkout-mrs create-mr restack-mr retarget-mr", + "anno-mrs checkout-mrs create-mr restack-mr retarget-mr update-mr-descriptions", "git machete gitlab anno-mrs -": "--debug -h --help -v --verbose --with-urls", "git machete gitlab checkout-mrs -": @@ -82,6 +84,8 @@ "--branch --debug --help --ignore-if-missing --verbose", "git machete gitlab retarget-mr -b ": "develop master", + "git machete gitlab update-mr-descriptions -": + "--all --debug -h --help --mine --related -v --verbose", "git machete g ": "down first last next prev root up", "git machete go ": diff --git a/tests/mockers_github.py b/tests/mockers_github.py index 8e95b05e6..24a3c7c0f 100644 --- a/tests/mockers_github.py +++ b/tests/mockers_github.py @@ -146,7 +146,7 @@ def handle_get() -> "MockAPIResponse": return MockAPIResponse(HTTPStatus.OK, pull) raise error_404() elif url_path_matches('/user'): - return MockAPIResponse(HTTPStatus.OK, {'login': 'github_user', 'type': 'User', 'company': 'VirtusLab'}) + return MockAPIResponse(HTTPStatus.OK, {'login': 'github_user', 'type': 'User'}) else: raise error_404() diff --git a/tests/test_github.py b/tests/test_github.py index b300cd8e6..946f31d40 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -335,9 +335,9 @@ def test_github_invalid_flag_combinations(self) -> None: assert_failure(["github", "anno-prs", "--draft"], "--draft option is only valid with create-pr subcommand.") assert_failure(["github", "anno-prs", "--mine"], - "--mine option is only valid with checkout-prs subcommand.") + "--mine option is only valid with checkout-prs and update-pr-descriptions subcommands.") assert_failure(["github", "retarget-pr", "123"], - "request_id option is only valid with checkout-prs subcommand.") + "PR number is only valid with checkout-prs subcommand.") assert_failure(["github", "checkout-prs", "-b", "foo"], "--branch option is only valid with retarget-pr subcommand.") assert_failure(["github", "create-pr", "--ignore-if-missing"], @@ -350,3 +350,9 @@ def test_github_invalid_flag_combinations(self) -> None: "--with-urls option is only valid with anno-prs subcommand.") assert_failure(["github", "restack-pr", "--yes"], "--yes option is only valid with create-pr subcommand.") + assert_failure(["github", "update-pr-descriptions", "--by=other-user"], + "--by option is only valid with checkout-prs subcommand.") + assert_failure(["github", "checkout-prs", "--related"], + "--related option is only valid with update-pr-descriptions subcommand.") + assert_failure(["github", "update-pr-descriptions", "--all", "--related"], + "update-pr-descriptions subcommand must take exactly one of the following options: --all, --mine, --related") diff --git a/tests/test_github_update_pr_descriptions.py b/tests/test_github_update_pr_descriptions.py new file mode 100644 index 000000000..7df98bc5d --- /dev/null +++ b/tests/test_github_update_pr_descriptions.py @@ -0,0 +1,169 @@ +from typing import Any, Dict, List + +from pytest_mock import MockerFixture + +from tests.base_test import BaseTest +from tests.mockers import (assert_failure, assert_success, + rewrite_branch_layout_file) +from tests.mockers_code_hosting import mock_from_url +from tests.mockers_github import (MockGitHubAPIState, + mock_github_token_for_domain_fake, + mock_github_token_for_domain_none, + mock_pr_json, mock_urlopen) + + +class TestGitHubUpdatePRDescriptions(BaseTest): + @staticmethod + def prs_for_test_update_pr_descriptions() -> List[Dict[str, Any]]: + return [ + mock_pr_json(head='chore/redundant_checks', base='restrict_access', number=18), + mock_pr_json(head='restrict_access', base='allow-ownership-link', number=17, user='github_user'), + mock_pr_json(head='allow-ownership-link', base='bugfix/feature', number=12), + mock_pr_json(head='bugfix/feature', base='enhance/feature', number=6), + mock_pr_json(head='enhance/add_user', base='develop', number=19), + mock_pr_json(head='testing/add_user', base='bugfix/add_user', number=22), + mock_pr_json(head='chore/comments', base='testing/add_user', number=24), + mock_pr_json(head='ignore-trailing', base='hotfix/add-trigger', number=3), + mock_pr_json(head='bugfix/remove-n-option', base='develop', number=5, state='closed', repo_id=2) + ] + + def test_github_update_pr_descriptions(self, mocker: MockerFixture) -> None: + self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url) + self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake) + github_api_state = MockGitHubAPIState.with_prs(*self.prs_for_test_update_pr_descriptions()) + self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state)) + + ( + self.repo_sandbox.new_branch("root") + .commit("initial commit") + .new_branch("develop") + .commit("first commit") + .push() + .new_branch("enhance/feature") + .commit("introduce feature") + .push() + .new_branch("bugfix/feature") + .commit("bugs removed") + .push() + .new_branch("allow-ownership-link") + .commit("fixes") + .push() + .new_branch('restrict_access') + .commit('authorized users only') + .push() + .new_branch("chore/redundant_checks") + .commit('remove some checks') + .push() + .check_out("root") + .new_branch("master") + .commit("Master commit") + .push() + .new_branch("hotfix/add-trigger") + .commit("HOTFIX Add the trigger") + .push() + .new_branch("ignore-trailing") + .commit("Ignore trailing data") + .push() + .delete_branch("root") + .new_branch('chore/fields') + .commit("remove outdated fields") + .push() + .check_out('develop') + .new_branch('enhance/add_user') + .commit('allow externals to add users') + .push() + .new_branch('bugfix/add_user') + .commit('first round of fixes') + .push() + .new_branch('testing/add_user') + .commit('add test set for add_user feature') + .push() + .new_branch('chore/comments') + .commit('code maintenance') + .push() + .check_out('allow-ownership-link') + ) + + body: str = \ + """ + develop + enhance/feature + bugfix/feature + allow-ownership-link + restrict_access + chore/redundant_checks + bugfix/add_user + testing/add_user + chore/comments + """ + rewrite_branch_layout_file(body) + + assert_success( + ['github', 'update-pr-descriptions', '--mine'], + """ + Checking for open GitHub PRs... OK + Description of PR #17 (restrict_access -> allow-ownership-link) has been updated + """ + ) + + assert_success( + ['github', 'update-pr-descriptions', '--related'], + """ + Checking for open GitHub PRs... OK + Description of PR #18 (chore/redundant_checks -> restrict_access) has been updated + """ + ) + self.repo_sandbox.set_git_config_key("machete.github.prDescriptionIntroStyle", "full") + assert_success( + ['github', 'update-pr-descriptions', '--related'], + """ + Checking for open GitHub PRs... OK + Description of PR #6 (bugfix/feature -> enhance/feature) has been updated + Description of PR #12 (allow-ownership-link -> bugfix/feature) has been updated + Description of PR #17 (restrict_access -> allow-ownership-link) has been updated + """ + ) + + assert_success( + ['github', 'update-pr-descriptions', '--all'], + """ + Checking for open GitHub PRs... OK + Description of PR #19 (enhance/add_user -> develop) has been updated + Description of PR #22 (testing/add_user -> bugfix/add_user) has been updated + Description of PR #24 (chore/comments -> testing/add_user) has been updated + Description of PR #3 (ignore-trailing -> hotfix/add-trigger) has been updated + """ + ) + + def test_github_update_pr_descriptions_misc_failures_and_warns(self, mocker: MockerFixture) -> None: + self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url) + self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitHubAPIState.with_prs())) + + self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none) + assert_success( + ["github", "update-pr-descriptions", "--all"], + """ + Checking for open GitHub PRs... OK + Warn: Currently there are no pull requests opened in repository example-org/example-repo + """ + ) + + assert_failure( + ["github", "update-pr-descriptions", "--mine"], + """ + Could not determine current user name, please check that the GitHub API token provided by one of the: + \t1. GITHUB_TOKEN environment variable + \t2. Content of the ~/.github-token file + \t3. Current auth token from the gh GitHub CLI + \t4. Current auth token from the hub GitHub CLI + is valid.""" + ) + + self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_fake) + assert_success( + ["github", "update-pr-descriptions", "--mine"], + """ + Checking for open GitHub PRs... OK + Warn: Current user github_user has no open pull request in repository example-org/example-repo + """ + ) diff --git a/tests/test_gitlab.py b/tests/test_gitlab.py index e426576df..3ac8e530d 100644 --- a/tests/test_gitlab.py +++ b/tests/test_gitlab.py @@ -285,9 +285,9 @@ def test_gitlab_invalid_flag_combinations(self) -> None: assert_failure(["gitlab", "anno-mrs", "--draft"], "--draft option is only valid with create-mr subcommand.") assert_failure(["gitlab", "anno-mrs", "--mine"], - "--mine option is only valid with checkout-mrs subcommand.") + "--mine option is only valid with checkout-mrs and update-mr-descriptions subcommands.") assert_failure(["gitlab", "retarget-mr", "123"], - "request_id option is only valid with checkout-mrs subcommand.") + "MR number is only valid with checkout-mrs subcommand.") assert_failure(["gitlab", "checkout-mrs", "-b", "foo"], "--branch option is only valid with retarget-mr subcommand.") assert_failure(["gitlab", "create-mr", "--ignore-if-missing"], @@ -300,3 +300,9 @@ def test_gitlab_invalid_flag_combinations(self) -> None: "--with-urls option is only valid with anno-mrs subcommand.") assert_failure(["gitlab", "restack-mr", "--yes"], "--yes option is only valid with create-mr subcommand.") + assert_failure(["gitlab", "update-mr-descriptions", "--by=other-user"], + "--by option is only valid with checkout-mrs subcommand.") + assert_failure(["gitlab", "checkout-mrs", "--related"], + "--related option is only valid with update-mr-descriptions subcommand.") + assert_failure(["gitlab", "update-mr-descriptions", "--all", "--related"], + "update-mr-descriptions subcommand must take exactly one of the following options: --all, --mine, --related") diff --git a/tests/test_gitlab_update_mr_descriptions.py b/tests/test_gitlab_update_mr_descriptions.py new file mode 100644 index 000000000..988a1d097 --- /dev/null +++ b/tests/test_gitlab_update_mr_descriptions.py @@ -0,0 +1,168 @@ +from typing import Any, Dict, List + +from pytest_mock import MockerFixture + +from tests.base_test import BaseTest +from tests.mockers import (assert_failure, assert_success, + rewrite_branch_layout_file) +from tests.mockers_code_hosting import mock_from_url +from tests.mockers_gitlab import (MockGitLabAPIState, + mock_gitlab_token_for_domain_fake, + mock_gitlab_token_for_domain_none, + mock_mr_json, mock_urlopen) + + +class TestGitLabUpdateMRDescriptions(BaseTest): + @staticmethod + def mrs_for_test_update_mr_descriptions() -> List[Dict[str, Any]]: + return [ + mock_mr_json(head='chore/redundant_checks', base='restrict_access', number=18), + mock_mr_json(head='restrict_access', base='allow-ownership-link', number=17, user='gitlab_user'), + mock_mr_json(head='allow-ownership-link', base='bugfix/feature', number=12), + mock_mr_json(head='bugfix/feature', base='enhance/feature', number=6), + mock_mr_json(head='enhance/add_user', base='develop', number=19), + mock_mr_json(head='testing/add_user', base='bugfix/add_user', number=22), + mock_mr_json(head='chore/comments', base='testing/add_user', number=24), + mock_mr_json(head='ignore-trailing', base='hotfix/add-trigger', number=3), + mock_mr_json(head='bugfix/remove-n-option', base='develop', number=5, state='closed', repo_id=2) + ] + + def test_gitlab_update_mr_descriptions(self, mocker: MockerFixture) -> None: + self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url) + self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_fake) + gitlab_api_state = MockGitLabAPIState.with_mrs(*self.mrs_for_test_update_mr_descriptions()) + self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(gitlab_api_state)) + + ( + self.repo_sandbox.new_branch("root") + .commit("initial commit") + .new_branch("develop") + .commit("first commit") + .push() + .new_branch("enhance/feature") + .commit("introduce feature") + .push() + .new_branch("bugfix/feature") + .commit("bugs removed") + .push() + .new_branch("allow-ownership-link") + .commit("fixes") + .push() + .new_branch('restrict_access') + .commit('authorized users only') + .push() + .new_branch("chore/redundant_checks") + .commit('remove some checks') + .push() + .check_out("root") + .new_branch("master") + .commit("Master commit") + .push() + .new_branch("hotfix/add-trigger") + .commit("HOTFIX Add the trigger") + .push() + .new_branch("ignore-trailing") + .commit("Ignore trailing data") + .push() + .delete_branch("root") + .new_branch('chore/fields') + .commit("remove outdated fields") + .push() + .check_out('develop') + .new_branch('enhance/add_user') + .commit('allow externals to add users') + .push() + .new_branch('bugfix/add_user') + .commit('first round of fixes') + .push() + .new_branch('testing/add_user') + .commit('add test set for add_user feature') + .push() + .new_branch('chore/comments') + .commit('code maintenance') + .push() + .check_out('allow-ownership-link') + ) + + body: str = \ + """ + develop + enhance/feature + bugfix/feature + allow-ownership-link + restrict_access + chore/redundant_checks + bugfix/add_user + testing/add_user + chore/comments + """ + rewrite_branch_layout_file(body) + + assert_success( + ['gitlab', 'update-mr-descriptions', '--mine'], + """ + Checking for open GitLab MRs... OK + Description of MR !17 (restrict_access -> allow-ownership-link) has been updated + """ + ) + + assert_success( + ['gitlab', 'update-mr-descriptions', '--related'], + """ + Checking for open GitLab MRs... OK + Description of MR !18 (chore/redundant_checks -> restrict_access) has been updated + """ + ) + self.repo_sandbox.set_git_config_key("machete.gitlab.mrDescriptionIntroStyle", "full") + assert_success( + ['gitlab', 'update-mr-descriptions', '--related'], + """ + Checking for open GitLab MRs... OK + Description of MR !6 (bugfix/feature -> enhance/feature) has been updated + Description of MR !12 (allow-ownership-link -> bugfix/feature) has been updated + Description of MR !17 (restrict_access -> allow-ownership-link) has been updated + """ + ) + + assert_success( + ['gitlab', 'update-mr-descriptions', '--all'], + """ + Checking for open GitLab MRs... OK + Description of MR !19 (enhance/add_user -> develop) has been updated + Description of MR !22 (testing/add_user -> bugfix/add_user) has been updated + Description of MR !24 (chore/comments -> testing/add_user) has been updated + Description of MR !3 (ignore-trailing -> hotfix/add-trigger) has been updated + """ + ) + + def test_gitlab_update_mr_descriptions_misc_failures_and_warns(self, mocker: MockerFixture) -> None: + self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url) + self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(MockGitLabAPIState.with_mrs())) + + self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_none) + assert_success( + ["gitlab", "update-mr-descriptions", "--all"], + """ + Checking for open GitLab MRs... OK + Warn: Currently there are no merge requests opened in project example-org/example-repo + """ + ) + + assert_failure( + ["gitlab", "update-mr-descriptions", "--mine"], + """ + Could not determine current user name, please check that the GitLab API token provided by one of the: + \t1. GITLAB_TOKEN environment variable + \t2. Content of the ~/.gitlab-token file + \t3. Current auth token from the glab GitLab CLI + is valid.""" + ) + + self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_fake) + assert_success( + ["gitlab", "update-mr-descriptions", "--mine"], + """ + Checking for open GitLab MRs... OK + Warn: Current user gitlab_user has no open merge request in project example-org/example-repo + """ + ) diff --git a/tox.ini b/tox.ini index ae93af50b..5adc8e00f 100644 --- a/tox.ini +++ b/tox.ini @@ -19,8 +19,9 @@ deps = allowlist_externals = mkdir commands = mkdir -p test-results/ - # `python -m pytest` so that the local plugin can be loaded, see https://stackoverflow.com/a/48306599 - python -m pytest -p tests.pytest_full_operands --numprocesses=auto --junitxml=test-results/testenv-{envname}.xml -m "not completion_e2e" {posargs} + # `python -m pytest` so that the local plugin for `--full-operands` can be loaded, see https://stackoverflow.com/a/48306599 + python -m pytest -p tests.pytest_full_operands \ + --numprocesses=auto --junitxml=test-results/testenv-{envname}.xml -m "not completion_e2e" {posargs} [testenv:test-completions] description = "Test shell completions" @@ -87,8 +88,9 @@ deps = passenv = PYTHON_VERSION allowlist_externals = cp commands = + # `python -m pytest` so that the local plugin for `--full-operands` can be loaded, see https://stackoverflow.com/a/48306599 # Generate (or append results to the existing) .coverage binary file (SQLite database) - pytest --cov=git_machete --cov-append --cov-branch \ + python -m pytest --cov=git_machete --cov-append --cov-branch -p tests.pytest_full_operands \ --numprocesses=auto --junitxml=test-results/testenv-{envname}.xml -m "not completion_e2e" {posargs} # Save a report to htmlcov/ coverage html --show-contexts