Exemplo n.º 1
0
def cmd_list_pr(assignee, label, limit, state, project, organization) -> None:
    """
    Command for listing pull requests.

    devops: https://docs.microsoft.com/en-us/cli/azure/ext/azure-devops/repos/pr?view=azure-cli-latest#ext_azure_devops_az_repos_pr_list
    github cli: https://cli.github.com/manual/gh_pr_list
    """  # noqa
    assignee = replace_user_aliases(assignee)

    query = "az repos pr list "
    query += f'--status "{state}" '
    query += f"--top {limit} "
    query += f'--org "{organization}" '
    query += f'--project "{project}" '
    query += f'--repository "{get_repo_name()}" '

    # Generate a jmespath.org query
    # Example:
    # az repos pr list --status active --query "[*].{title: title, pullRequestID: pullRequestID, status: status, labels:labels[*].name, reviewers: reviewers[*].uniqueName} | [*].labels" -o jsonc # noqa
    jmespath_query = "[*].{title: title, pullRequestId: pullRequestId, creationDate: creationDate, status: status, labels:labels[*].name, reviewers: reviewers[*].uniqueName}"  # noqa
    if assignee and label:
        jmespath_query += " | [? reviewers!=\`null\` && labels!=\`null\`]"  # noqa
        jmespath_query += f" | [?{generate_jmespath(assignee, 'reviewers')} && {generate_jmespath(label, 'labels')}]"
    elif assignee:
        jmespath_query += " | [? reviewers!=\`null\`]"  # noqa
        jmespath_query += f" | [?{generate_jmespath(assignee, 'reviewers')}]"
    elif label:
        jmespath_query += " | [? labels!=\`null\`]"  # noqa
        jmespath_query += f" | [?{generate_jmespath(label, 'labels')}]"

    query += f'--query "{jmespath_query}" '

    prs = run_command(query)

    if len(prs) == 0:
        msg = "[dark_orange3]>[/dark_orange3] No pull requests given filters used"
        console.print(msg)
        return

    table = Table(title=f"{state} Pull requests")
    table.add_column("ID", justify="right", style="cyan", no_wrap=True)
    table.add_column("Title", justify="right", style="cyan", no_wrap=True)
    table.add_column("Reviewers", justify="right", style="cyan", no_wrap=True)
    table.add_column("Created", justify="right", style="cyan", no_wrap=True)
    table.add_column("Status", justify="right", style="cyan", no_wrap=True)

    for pr in prs:
        reviewers = ", ".join(pr.get("reviewers"))

        # Get created date
        # Example: '2021-06-18T09:57:56.653886+00:00'
        created = pr.get("creationDate")
        created = datetime.datetime.strptime(created, "%Y-%m-%dT%H:%M:%S.%f%z")
        now = datetime.datetime.now(timezone.utc)
        created = timeago.format(created, now)

        table.add_row(str(pr.get("pullRequestId")), pr.get("title"), reviewers,
                      created, pr.get("status"))

    console.print(table)
Exemplo n.º 2
0
def cmd_init(reference_issue: str = ""):
    """
    Create a .doing-cli-config file.

    Empty file if no reference_url is specified.

    Args:
        reference_issue: URL of work item to use as reference
    """
    if os.path.exists(".doing-cli-config.yml"):
        console.print("File '.doing-cli-config.yml' already exists.")
        return

    if not reference_issue:
        required_params = {
            "organization": "",
            "project": "",
            "team": "",
            "area": "",
            "iteration": "",
        }
        with open(".doing-cli-config.yml", "w") as file:
            file.write("# doing cli configuration file\n")
            file.write("# docs: https://github.com/ing-bank/doing-cli\n\n")
            yaml.dump(required_params, file)
            console.print(
                "[dark_orange3]>[/dark_orange3] Created new .doing-cli-config.yml file"
            )
            console.print(
                "\t[dark_orange3]>[/dark_orange3] Please fill in required parameters."
            )
        return

    organization, project, item_id = parse_reference(reference_issue)
    organization = "https://dev.azure.com/" + organization

    required_params = {"organization": organization, "project": project}

    cmd = f"az boards work-item show --id {item_id} "
    cmd += f'--org "{organization}" '
    workitem = run_command(cmd)
    workitem = workitem.get("fields")
    assert workitem is not None
    required_params["team"] = workitem.get("System.IterationLevel2")
    required_params["area"] = workitem.get("System.AreaPath")
    required_params["iteration"] = workitem.get("System.IterationPath")

    with open(".doing-cli-config.yml", "w") as file:
        file.write("# doing cli configuration file\n")
        file.write("# docs: https://github.com/ing-bank/doing-cli\n\n")
        yaml.dump(required_params, file)
        console.print(
            "[dark_orange3]>[/dark_orange3] Create new .doing-cli-config.yml file"
        )
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] Filled in required parameters using reference work item #{item_id}"
        )
Exemplo n.º 3
0
def close(work_item_id):
    """Close a specific WORK_ITEM_ID.

    A '#' prefix is allowed. You can specify multiple IDs by separating with a space.
    """
    organization = get_config("organization")
    state = "Closed"

    for id in work_item_id:
        id = str(id).lstrip("#")
        cmd = f"az boards work-item update --id {id} --state '{state}' "
        cmd += f"--org '{organization}'"
        result = run_command(cmd)
        assert result.get("fields").get("System.State") == state
        console.print(
            f"[dark_orange3]>[/dark_orange3] work item #{id} set to '{state}'")
Exemplo n.º 4
0
def pipe():
    """
    Open latest pipeline runs for repository view.
    """
    project = get_config("project")
    organization = get_config("organization")

    repo_pipes = run_command(
        f'az pipelines list --repository "{get_repo_name()}" --org "{organization}" -p "{project}"'
    )
    if len(repo_pipes) == 0:
        console.print(f"{get_repo_name()} has no pipelines defined currently")
        return None

    pipeline_id = repo_pipes[0].get("id")
    click.launch(f"{organization}/{project}/_build?definitionId={pipeline_id}")
Exemplo n.º 5
0
def close(pr_id):
    """
    Close a specific PR_ID.

    PR_ID is the ID number of a pull request. '!' prefix is allowed.
    You can specify multiple IDs by separating with a space.
    """
    organization = get_config("organization")
    state = "abandoned"

    for id in pr_id:
        id = str(id).lstrip("!")
        cmd = f'az repos pr update --id {id} --status "{state}" '
        cmd += f'--org "{organization}"'
        result = run_command(cmd)
        assert result.get("status") == state
        console.print(
            f"[dark_orange3]>[/dark_orange3] pullrequest !{id} set to '{state}'"
        )
Exemplo n.º 6
0
def policies():
    """
    Open repository policy settings.

    Will show the default branch policies by default.
    """
    project = get_config("project")
    organization = get_config("organization")

    repo_name = get_repo_name()
    repo = run_command(f'az repos show --repository "{repo_name}"')

    repo_id = repo.get("id")
    assert len(repo_id) > 0
    default_branch = repo.get("defaultBranch").split("/")[-1]
    assert len(default_branch) > 0

    url = f"{organization}/{project}/_settings/repositories?repo={repo_id}"
    url += f"&_a=policiesMid&refs=refs%2Fheads%2F{default_branch}"
    click.launch(url)
Exemplo n.º 7
0
def cmd_create_issue(
    title: str,
    mine: bool,
    assignee: str,
    body: str,
    type: str,
    label: str,
    parent: str,
    team: str,
    area: str,
    iteration: str,
    organization: str,
    project: str,
    story_points: str,
) -> int:
    """
    Create a new issue.

    Reference:

    - [`az boards work-item create]`(https://docs.microsoft.com/en-us/cli/azure/boards/work-item?view=azure-cli-latest#az-boards-work-item-create)
    """  # noqa
    if mine and assignee:
        raise InputError(
            "You cannot use --mine in combination with specifying --assigned-to."
        )

    if mine:
        assignee = get_az_devop_user_email()

    assignee = replace_user_aliases(assignee)
    validate_work_item_type(type)

    cmd = "az boards work-item create "
    cmd += f'--title "{title}" '
    cmd += f'--type "{type}" '
    if assignee:
        cmd += f'--assigned-to "{assignee}" '
    if body:
        cmd += f'--description "{body}" '

    if story_points != "" and label != "":
        cmd += f'--fields "Microsoft.VSTS.Scheduling.StoryPoints={story_points}" "System.Tags={label}" '
    if story_points != "" and label == "":
        cmd += f'--fields "Microsoft.VSTS.Scheduling.StoryPoints={story_points}" '
    if story_points == "" and label != "":
        cmd += f'--fields "System.Tags={label}" '

    cmd += f'--area "{area}" --iteration "{iteration}" --project "{project}" --organization "{organization}"'

    work_item = run_command(cmd)
    work_item_id = work_item.get("id")

    console.print(
        f"[dark_orange3]>[/dark_orange3] Created work item {work_item_id} '[cyan]{title}[/cyan]' ({type})"
    )
    console.print(f"\t[dark_orange3]>[/dark_orange3] added area-path '{area}'")
    console.print(
        f"\t[dark_orange3]>[/dark_orange3] added iteration-path '{iteration}'")
    if assignee:
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] added assignee '{assignee}'")
    if label:
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] added tags: '{label}'")
    if story_points:
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] assigned {story_points} storypoints"
        )

    if parent:
        cmd = "az boards work-item relation add "
        cmd += f"--id {work_item_id} "
        cmd += '--relation-type "parent" '
        cmd += f"--target-id {parent} "
        cmd += f'--org "{organization}" '
        run_command(cmd)
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] added work item #{parent} as a parent"
        )

    return work_item_id
Exemplo n.º 8
0
def set_merge_strategy_policy(merge_strategy: str, organization: str,
                              project: str) -> None:
    """
    Set merge strategy policy if needed.
    """
    if merge_strategy is None:
        return
    assert merge_strategy in [
        "basic merge", "squash merge", "rebase and fast-forward",
        "rebase with merge commit"
    ]

    merge_settings = ""
    merge_settings += f"--allow-no-fast-forward {str(merge_strategy == 'basic merge').lower()} "
    merge_settings += f"--allow-rebase {str(merge_strategy == 'rebase and fast-forward').lower()} "
    merge_settings += f"--allow-rebase-merge {str(merge_strategy == 'rebase with merge commit').lower()} "
    merge_settings += f"--allow-squash {str(merge_strategy == 'squash merge').lower()} "

    repo_name = get_repo_name()
    repo = run_command(f'az repos show --repository "{repo_name}"')

    repo_id = repo.get("id")
    assert len(repo_id) > 0

    default_branch = repo.get("defaultBranch").split("/")[-1]

    policies = run_command(
        f'az repos policy list --repository "{repo_id}" --branch "{default_branch}" -o json'
    )
    policies = [
        p for p in policies
        if p.get("type", {}).get("displayName") == "Require a merge strategy"
    ]

    enabled_policies = [p for p in policies if p.get("isEnabled") is True]

    # Create a new policies
    if len(enabled_policies) == 0:
        cmd = "az repos policy merge-strategy create --blocking true --enabled true "
        msg = "[dark_orange3]>[/dark_orange3] Set repository merge strategy "
        msg += f"on default branch '{default_branch}' to [cyan]'{merge_strategy}'[cyan]"

    # Update an existing policy, if needed
    if len(enabled_policies) == 1:
        policy_settings = enabled_policies[0].get("settings")
        policy_id = enabled_policies[0].get("id")

        # do we need to update the settings or are they correct already?
        if (policy_settings.get("allowNoFastForward") == (merge_strategy
                                                          == "basic merge")
                and policy_settings.get("allowRebase")
                == (merge_strategy == "rebase and fast-forward")
                and policy_settings.get("allowRebaseMerge")
                == (merge_strategy == "rebase with merge commit")
                and policy_settings.get("allowSquash") == (merge_strategy
                                                           == "squash merge")):
            # policy settings already OK. Don't do anything
            return
        cmd = f'az repos policy merge-strategy update --id "{policy_id}" --blocking true '
        msg = "[dark_orange3]>[/dark_orange3] Updated repository merge strategy "
        msg += f"on default branch '{default_branch}' to [cyan]'{merge_strategy}'[cyan]"

    if len(enabled_policies) > 2:
        raise AssertionError(
            "There are multiple merge strategy policies already enabled, not sure what to do / if this can happen."
        )

    # Add correct policy settings
    cmd += f'--branch {default_branch} --repository-id "{repo_id}" '
    cmd += f'{merge_settings} --project "{project}" --org "{organization}" '

    settings_url = f"{organization}/{project}/_settings/repositories?repo={repo_id}"
    settings_url += f"&_a=policiesMid&refs=refs%2Fheads%2F{default_branch}"

    # run command & report
    run_command(cmd)
    console.print(msg)
    console.print(f"\tView/edit policies manually: {settings_url}")
Exemplo n.º 9
0
def cmd_create_pr(
    work_item_id: str,
    draft: bool,
    auto_complete: bool,
    self_approve: bool,
    reviewers: str,
    checkout: bool,
    delete_source_branch: bool,
    team: str,
    area: str,
    iteration: str,
    organization: str,
    project: str,
) -> int:
    """
    Run command `doing create pr`.

    API doc:
    https://docs.microsoft.com/en-us/cli/azure/ext/azure-devops/repos/pr?view=azure-cli-latest#ext_azure_devops_az_repos_pr_create
    """
    work_item_id = str(work_item_id).lstrip("#")

    # add self to reviewers & replace user aliases
    reviewers = f"{reviewers} @me".strip()
    reviewers = replace_user_aliases(reviewers)

    if checkout:
        check_uncommitted_work()

    repo_name = get_repo_name()
    user_email = get_az_devop_user_email()

    # Info on related work item
    work_item = run_command(
        f'az boards work-item show --id {work_item_id} --org "{organization}"')
    work_item_title = work_item.get("fields").get("System.Title")

    # Info in other linked PRs to work item
    active_related_pr_ids = []
    relations = work_item.get("relations")
    if relations:
        for relation in relations:
            if relation.get("attributes").get("name") in ["Pull Request"]:
                # For example
                # url = 'vstfs:///Git/PullRequestId/bbd257b1-b8a9-1fc2-b123-1ea2cc23c333%2f4d2e1234-c1d0-1234-1f23-c1234d05d471%2f12345' # noqa
                # The bit after %2f is the pullrequestid (12345)
                related_pr_id = re.findall("^.*%2[fF]([0-9]+)$",
                                           relation.get("url"))[0]
                related_pr_id_status = run_command(
                    f'az repos pr show --id {related_pr_id} --query "status" --org "{organization}"'
                )
                if related_pr_id_status == "active":
                    active_related_pr_ids.append(related_pr_id)

    related_pr_ids = ",".join(active_related_pr_ids)

    # Info on remote branches
    remote_branches = run_command(
        f'az repos ref list --repository "{repo_name}" --query "[].name" --org "{organization}" -p "{project}"'
    )
    remote_branches = [
        x.rpartition("/")[2] for x in remote_branches
        if x.startswith("refs/heads")
    ]

    # Find the default branch from which to create a new branch and target the pull request to
    cmd = f'az repos show --repository "{repo_name}" --org "{organization}" -p "{project}"'
    default_branch = run_command(cmd).get("defaultBranch", "refs/heads/master")

    # Create a new remote branch, only if it does yet exist
    cmd = f'az repos ref list --repository "{repo_name}" --query "[?name==\'{default_branch}\'].objectId" '
    cmd += f'--org "{organization}" -p "{project}"'
    master_branch_object_id = run_command(cmd)[0]
    branch_name = f"{work_item_id}_{to_snake_case(remove_special_chars(work_item_title))}"
    if branch_name in remote_branches:
        console.print(
            f"[dark_orange3]>[/dark_orange3] Remote branch '[cyan]{branch_name}[/cyan]' already exists, using that one"
        )
        # Check if there is not already an existing PR for this branch
        prs = run_command(
            f'az repos pr list -r "{repo_name}" -s {branch_name} --org "{organization}" -p "{project}"'
        )
        if len(prs) >= 1:
            pr_id = prs[0].get("pullRequestId")
            console.print(
                f"[dark_orange3]>[/dark_orange3] Pull request {pr_id} already exists",
                f"for branch '[cyan]{branch_name}[/cyan]', aborting.",
            )
            if not checkout and (get_git_current_branch() != branch_name):
                explain_checkout(branch_name)
            if checkout and (get_git_current_branch() != branch_name):
                git_checkout(branch_name)
                # TODO:
                # Users might get a
                # fatal: A branch named '<work_item_id>_<issue_title>' already exists.
                # if local branch already exists.
                # We could test to see if it is setup to track the remote branch, and if not set that right
                # Might help some less experienced git users.

            return pr_id
    else:
        cmd = f'az repos ref create --name "heads/{branch_name}" --repository "{repo_name}" '
        cmd += f'--object-id "{master_branch_object_id}" -p "{project}" --org "{organization}"'
        branch = run_command(cmd)
        assert branch.get(
            "success"
        ), f"Could not create '{branch_name}'. Do you have contributor rights to the '{get_repo_name()}' repo?"  # noqa
        console.print(
            f"[dark_orange3]>[/dark_orange3] Created remote branch '[cyan]{branch_name}[/cyan]'"
        )

    # Check the PR merge strategy
    check_merge_strategy_policy()

    # Create the PR
    command = f'az repos pr create --repository "{repo_name}" '
    command += f'--draft "{str(draft).lower()}" '
    command += f'--work-items "{work_item_id}" '
    command += f'--source-branch "{branch_name}" '
    command += f'--title "{work_item_title}" '
    command += f'--project "{project}" --organization "{organization}" '

    # Some sensible defaults
    command += '--transition-work-items "true" '
    command += f'--delete-source-branch "{str(delete_source_branch).lower()}" '

    # auto-complete.
    command += f'--auto-complete "{str(auto_complete).lower()}" '

    if reviewers != "":
        # Azure wants the format --reviewers 'one' 'two' 'three'
        az_reviewers = " ".join([f'"{x}"' for x in reviewers.split(" ")])
        command += f"--reviewers {az_reviewers} "

    pr = run_command(command)

    # Report to user
    pr_id = pr.get("pullRequestId")
    console.print(
        f"[dark_orange3]>[/dark_orange3] Created pull request {pr_id} [cyan]'{work_item_title}'[cyan]"
    )
    console.print(
        f"\t[dark_orange3]>[/dark_orange3] linked work item {work_item_id}")
    if draft:
        console.print(
            "\t[dark_orange3]>[/dark_orange3] marked as draft pull request")
    if auto_complete:
        console.print(
            "\t[dark_orange3]>[/dark_orange3] set auto-complete to True'")
    if delete_source_branch:
        console.print(
            "\t[dark_orange3]>[/dark_orange3] set to delete remote source branch after PR completion"
        )
    if len(reviewers) > 0:
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] added reviewers: '{reviewers}'")
    if self_approve:
        run_command(
            f'az repos pr set-vote --id {pr_id} --vote "approve" --org "{organization}"'
        )
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] Approved PR {pr_id} for '{user_email}'"
        )
    if active_related_pr_ids:
        console.print(
            f"\t[dark_orange3]>[/dark_orange3] Note: work item has other active linked PRs: {related_pr_ids}"
        )

    if not checkout:
        explain_checkout(branch_name)
    else:
        git_checkout(branch_name)

    return pr_id
Exemplo n.º 10
0
def cmd_list(
    assignee: str,
    author: str,
    label: str,
    state: str,
    team: str,
    area: str,
    iteration: str,
    organization: str,
    project: str,
    work_item_type: str,
    show_state: bool,
    story_points: str = "",
    output_format: str = "table",
) -> None:
    """Run `doing list` command."""
    # Get config settings
    assignee = replace_user_aliases(assignee)
    author = replace_user_aliases(author)

    query = work_item_query(assignee, author, label, state, area, iteration,
                            work_item_type, story_points)
    work_items = run_command(
        f'az boards query --wiql "{query}" --org "{organization}" -p "{project}"'
    )

    if len(work_items) == 0:
        msg = f"[dark_orange3]>[/dark_orange3] No issues in sprint '{iteration}' given filters used"
        console.print(msg)
        return

    if output_format == "array":
        wi_array = [str(x.get("id")) for x in work_items]
        console.print(" ".join(wi_array))
        return

    workitem_prs = {}  # type: Dict

    # Now for each work item we could get linked PRs
    # However, APIs requests are slow, and most work items don't have a PR.
    # Instead, we'll retrieve all active PRs and see which items are linked (less API calls)
    repo_name = get_repo_name()
    query = f'az repos pr list --repository "{repo_name}" --org "{organization}" -p "{project}" '
    query += '--status active --query "[].pullRequestId"'
    active_pullrequest_ids = run_command(query)

    with Live(
            build_table(
                work_items=work_items,
                workitem_prs=workitem_prs,
                iteration=iteration,
                last_build=False,
                show_state=show_state,
            ),
            refresh_per_second=4,
            console=console,
    ) as live:

        # For each PR, get linked work items. Note that "az repos pr list --include-links" does not work :(
        # Posted issue on bug here: https://github.com/Azure/azure-cli-extensions/issues/2946
        for pr_id in track(active_pullrequest_ids,
                           description="Processing pull requests",
                           transient=False):
            linked_workitems = run_command(
                f'az repos pr work-item list --id {pr_id} --query "[].id" --org "{organization}"',
                allow_verbose=False)
            for work_item in linked_workitems:
                if work_item in workitem_prs.keys():
                    workitem_prs[work_item].append(str(pr_id))
                else:
                    workitem_prs[work_item] = [str(pr_id)]

            live.update(
                build_table(
                    work_items=work_items,
                    workitem_prs=workitem_prs,
                    iteration=iteration,
                    last_build=False,
                    show_state=show_state,
                ))

        live.update(
            build_table(
                work_items=work_items,
                workitem_prs=workitem_prs,
                iteration=iteration,
                last_build=False,
                show_state=show_state,
            ))