def list(assignee, author, label, state, type, web, story_points, output_format, show_state): """List issues related to the project.""" if web: iteration = get_config("iteration") area = get_config("area") project = get_config("project") organization = get_config("organization") query = work_item_query( assignee=assignee, author=author, label=label, state=state, area=area, iteration=iteration, work_item_type=type, story_points=story_points, ) click.launch( f"{organization}/{project}/_workitems/?_a=query&wiql={quote(query)}" ) else: cmd_list( assignee=assignee, author=author, label=label, state=state, work_item_type=type, story_points=story_points, output_format=output_format, show_state=show_state, **get_common_options(), )
def test_get_config_fallback(): """ Test overrides via env vars. """ with pytest.raises(Exception): get_config("team1") assert get_config("team1", "foobar") == "foobar"
def check_merge_strategy_policy() -> None: """ Make sure merge strategy is set correctly. """ merge_strategy = get_config("merge_strategy", fallback="") if merge_strategy != "": set_merge_strategy_policy( merge_strategy=merge_strategy, organization=get_config("organization"), project=get_config("project"), )
def cmd_open_pr(pullrequest_id: Union[str, int]) -> None: """ Open a specific PULLREQUEST_ID. '!' prefix is allowed. """ pullrequest_id = str(pullrequest_id).lstrip("!").strip() project = get_config("project") organization = get_config("organization") click.launch( f"{organization}/{project}/_git/{get_repo_name()}/pullrequest/{pullrequest_id}" )
def get_common_options(): """ Retrieve common config options. Retrieves set of config settings from config file that are used in every command. """ return { "team": get_config("team"), "area": get_config("area"), "iteration": get_config("iteration"), "organization": get_config("organization"), "project": get_config("project"), }
def common_options(function): """ Custom decorator to avoid repeating commonly used options. To be used in Click commands. """ function = click.option( "--team", required=True, type=str, default=lambda: get_config("team"), help="The code of the team in azure", )(function) function = click.option( "--area", required=True, type=str, default=lambda: get_config("area"), help="The area code", )(function) function = click.option( "--iteration", required=True, type=str, default=lambda: get_config("iteration"), help="The current iteration (sprint)", )(function) function = click.option( "--organization", required=True, type=str, default=lambda: get_config("organization"), help="The organization in azure", )(function) function = click.option( "--project", required=True, type=str, default=lambda: get_config("project"), help="The project in azure", )(function) return function
def list(assignee, label, limit, state, web): """ List pull requests related to the project. """ project = get_config("project") organization = get_config("organization") # Translate github's {open|closed|merged|all} # To devops's {active|abandoned|completed|all} if state == "closed": state = "abandoned" if state == "open": state = "active" if state == "merged": state = "completed" if web: console.print( "[dark_orange3]>[/dark_orange3] Opening the pull requests web view." ) if state == "all": console.print( "\t You specified state='all' but ignoring because the web view does not support it." ) state = "active" if label: console.print( f"\t You specified label='{label}' but ignoring because the web view does not support it." ) if assignee: console.print( f"\t You need to manually filter on 'Assigned to' = '{assignee}'" ) click.launch( f"{organization}/{project}/_git/{get_repo_name()}/pullrequests?_a={state}" ) else: cmd_list_pr(assignee, label, limit, state, project, organization)
def test_create_file(tmp_path): """ Test reading a config file. """ config = { "user_aliases": { "john": "*****@*****.**", "jane": "*****@*****.**" } } with working_directory(tmp_path): with open(".doing-cli-config.yml", "w") as file: yaml.dump(config, file) assert get_config("user_aliases") == config["user_aliases"]
def cli(): """ CLI for repository/issue workflow on Azure Devops. """ # Set doing default as environment variables defaults = get_config("defaults", fallback="") if defaults: for setting, default in defaults.items(): if str(setting) not in os.environ: os.environ[setting] = str(default) else: if os.environ[setting] != default: console.print( f"Warning: Trying to set {setting} to '{default}' (specified in .doing-ing-config.yml)" ) console.print( f"\tbut {setting} has already been set to '{os.environ[setting]}' in the environment variables." )
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}'" )
show_envvar=True, ) @click.option( "--parent", "-p", required=False, default="", type=str, help="To create a child work item, specify the ID of the parent work item.", show_envvar=True, ) @click.option( "--reviewers", "-r", required=False, default=lambda: get_config("default_reviewers", ""), type=str, help=f"Space separated list of reviewer emails. Defaults to \"{get_config('default_reviewers','')}\"", show_envvar=True, ) @click.option( "--draft/--no-draft", required=False, default=True, help="Create draft/WIP pull request. Reviewers will not be notified until you publish. Default is --draft.", show_envvar=True, ) @click.option( "--auto-complete/--no-auto-complete", required=False, default=True,
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
def test_get_config_key(): """ Test overrides via env vars. """ os.environ["DOING_CONFIG_TEAM"] = "my team" assert get_config("team") == "my team"