Exemplo n.º 1
0
def test_me_alias():
    """
    Only runs in env where you can get user alias.
    """
    # test @me alias
    text = get_az_devop_user_email()
    assert replace_user_aliases(text) == get_az_devop_user_email()
Exemplo n.º 2
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.º 3
0
def test_replace_user_aliases(tmp_path):
    """
    Test replacing user aliases.
    """
    # no aliases set just yet
    text = "john [email protected] [email protected]"
    assert replace_user_aliases(
        text) == "john [email protected] [email protected]"

    # Deduplication
    text = "john jane john"
    assert replace_user_aliases(text) == "john jane"
    text = "jane john jane john john"
    assert replace_user_aliases(text) == "jane john"

    config = {
        "user_aliases": {
            "john": "*****@*****.**",
            "jane": "*****@*****.**"
        }
    }

    with working_directory(tmp_path):
        with open(".doing-cli-config.yml", "w") as file:
            yaml.dump(config, file)

        text = "john [email protected] [email protected]"
        assert replace_user_aliases(
            text
        ) == "[email protected] [email protected] [email protected]"

        text = "john [email protected] [email protected]"
        assert replace_user_aliases(
            text
        ) == "[email protected] [email protected] [email protected]"

        text = "john jane john"
        assert replace_user_aliases(
            text) == "[email protected] [email protected]"

        text = "johnjane"
        assert replace_user_aliases(text) == "johnjane"
Exemplo n.º 4
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.º 5
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.º 6
0
def work_item_query(
    assignee: str,
    author: str,
    label: str,
    state: str,
    area: str,
    iteration: str,
    work_item_type: str,
    story_points: str,
):
    """Build query in wiql.

    # More on 'work item query language' syntax:
    # https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops
    """
    # ensure using user aliases
    assignee = replace_user_aliases(assignee)
    author = replace_user_aliases(author)

    # Get all workitems
    query = "SELECT [System.Id],[System.Title],[System.AssignedTo],"
    query += "[System.WorkItemType],[System.State],[System.CreatedDate], [System.State] "
    query += f"FROM WorkItems WHERE [System.AreaPath] = '{area}' "
    # Filter on iteration. Note we use UNDER so that user can choose to provide teams path for all sprints.
    query += f"AND [System.IterationPath] UNDER '{iteration}' "

    if assignee:
        query += f"AND [System.AssignedTo] = '{assignee}' "

    if author:
        query += f"AND [System.CreatedBy] = '{author}' "

    if label:
        for lab in label.split(","):
            query += f"AND [System.Tags] Contains '{lab.strip()}' "

    if state:
        custom_states = get_config("custom_states", {})
        if state in custom_states.keys():
            if type(custom_states[state]) is not list:
                custom_states[state] = [custom_states[state]]
            state_list = ",".join([f"'{x}'" for x in custom_states[state]])
            query += f"AND [System.State] IN ({state_list}) "
        elif state == "open":
            query += "AND [System.State] NOT IN ('Resolved','Closed','Done','Removed') "
        elif state == "closed":
            query += "AND [System.State] IN ('Resolved','Closed','Done') "
        elif state == "all":
            query += "AND [System.State] <> 'Removed' "
        elif state.startswith("'") and state.endswith("'"):
            query += f"AND [System.State] = {state} "
        else:
            raise ValueError(
                f"Invalid state: '{state}'. State should be:\n"
                "- one of the doing-cli default states: 'open', 'closed', 'all'\n"
                "- a custom state defined under 'custom_states' in the .doing-cli.config.yml file\n"
                "- a state available in this team, between quotes, e.g. \"'Active'\""
            )

    if work_item_type:
        validate_work_item_type(work_item_type)
        query += f"AND [System.WorkItemType] = '{work_item_type}' "

    if story_points:
        if story_points == "unassigned":
            query += "AND [Microsoft.VSTS.Scheduling.StoryPoints] = '' "
        elif story_points.startswith("<="):
            story_points = story_points.lstrip("<=")
            query += f"AND [Microsoft.VSTS.Scheduling.StoryPoints] <= '{story_points}' "
        elif story_points.startswith(">="):
            story_points = story_points.lstrip(">=")
            query += f"AND [Microsoft.VSTS.Scheduling.StoryPoints] >= '{story_points}' "
        elif story_points.startswith(">"):
            story_points = story_points.lstrip(">")
            query += f"AND [Microsoft.VSTS.Scheduling.StoryPoints] > '{story_points}' "
        elif story_points.startswith("<"):
            story_points = story_points.lstrip("<")
            query += f"AND [Microsoft.VSTS.Scheduling.StoryPoints] < '{story_points}' "
        else:
            query += f"AND [Microsoft.VSTS.Scheduling.StoryPoints] = '{story_points}' "

    # Ordering of results
    query += "ORDER BY [System.CreatedDate] asc"

    return query
Exemplo n.º 7
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,
            ))