File: pull_request.py

package info (click to toggle)
azure-devops-cli-extension 1.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 20,384 kB
  • sloc: python: 160,782; xml: 198; makefile: 56; sh: 51
file content (695 lines) | stat: -rw-r--r-- 35,067 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import webbrowser

from knack.log import get_logger
from knack.util import CLIError
from azext_devops.devops_sdk.exceptions import AzureDevOpsClientRequestError
from azext_devops.devops_sdk.v5_0.git.models import (GitPullRequest, GitPullRequestCompletionOptions,
                                                     GitPullRequestSearchCriteria, IdentityRef, IdentityRefWithVote,
                                                     ResourceRef, GitRefFavorite, WebApiTagDefinition)
from azext_devops.devops_sdk.v5_0.work_item_tracking.models import JsonPatchOperation, WorkItemRelation
from azext_devops.dev.common.arguments import should_detect
from azext_devops.dev.common.git import get_current_branch_name, resolve_git_ref_heads, fetch_remote_and_checkout
from azext_devops.dev.common.identities import ME, resolve_identity_as_id
from azext_devops.dev.common.uri import uri_quote
from azext_devops.dev.common.uuid import EMPTY_UUID
from azext_devops.dev.common.services import (get_git_client,
                                              get_policy_client,
                                              get_work_item_tracking_client,
                                              resolve_instance,
                                              resolve_instance_project_and_repo,
                                              get_vsts_info_from_current_remote_url)

logger = get_logger(__name__)


def show_pull_request(id, open=False, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """Get the details of a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param open: Open the pull request in your web browser.
    :type open: bool
    :rtype: :class:`GitPullRequest <v5_0.git.models.GitPullRequest>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    pr = client.get_pull_request_by_id(id)
    pr = client.get_pull_request(project=pr.repository.project.id,
                                 repository_id=pr.repository.id,
                                 pull_request_id=id,
                                 include_commits=False,
                                 include_work_item_refs=True)
    if open:
        _open_pull_request(pr, organization)
    return pr


def list_pull_requests(repository=None, creator=None, include_links=False, reviewer=None,
                       source_branch=None, status=None, target_branch=None, project=None,
                       skip=None, top=None, organization=None, detect=None):
    """List pull requests.
    :param repository: Name or ID of the repository.
    :type repository: str
    :param creator: Limit results to pull requests created by this user.
    :type creator: str
    :param include_links: Include _links for each pull request.
    :type include_links: bool
    :param reviewer: Limit results to pull requests where this user is a reviewer.
    :type reviewer: str
    :param source_branch: Limit results to pull requests that originate from this source branch.
    :type source_branch: str
    :param status: Limit results to pull requests with this status.
    :type status: str
    :param target_branch: Limit results to pull requests that target this branch.
    :type target_branch: str
    :param skip: Number of pull requests to skip.
    :type skip: int
    :param top: Maximum number of pull requests to list.
    :type top: int
    :rtype: list of :class:`VssJsonCollectionWrapper <v5_0.git.models.VssJsonCollectionWrapper>`
    """
    organization, project, repository = resolve_instance_project_and_repo(
        detect=detect,
        organization=organization,
        project=project,
        repo=repository)
    search_criteria = GitPullRequestSearchCriteria(
        creator_id=resolve_identity_as_id(creator, organization),
        include_links=include_links,
        reviewer_id=resolve_identity_as_id(reviewer, organization),
        source_ref_name=resolve_git_ref_heads(source_branch),
        status=status,
        target_ref_name=resolve_git_ref_heads(target_branch))
    client = get_git_client(organization)
    if repository is None:
        pr_list = client.get_pull_requests_by_project(project=project, search_criteria=search_criteria,
                                                      skip=skip, top=top)
    else:
        pr_list = client.get_pull_requests(project=project, repository_id=repository,
                                           search_criteria=search_criteria,
                                           skip=skip, top=top)
    return pr_list


# pylint: disable=too-many-statements, too-many-locals, redefined-builtin
def create_pull_request(project=None, repository=None, source_branch=None, target_branch=None,
                        title=None, description=None, auto_complete=False, squash=False,
                        delete_source_branch=False, bypass_policy=False, bypass_policy_reason=None,
                        merge_commit_message=None, optional_reviewers=None, required_reviewers=None,
                        work_items=None, draft=None, open=False, organization=None, detect=None,
                        transition_work_items=False, labels=None):  # pylint: disable=redefined-builtin
    """Create a pull request.
    :param project: Name or ID of the team project.
    :type project: str
    :param repository: Name or ID of the repository to create the pull request in.
    :type repository: str
    :param source_branch: Name of the source branch. Example: "dev".
    :type source_branch: str
    :param target_branch: Name of the target branch. If not specified, defaults to the
                          default branch of the target repository.
    :type target_branch: str
    :param title: Title for the new pull request.
    :type title: str
    :param draft: Use this flag to create the pull request in draft/work in progress mode.
    :type draft: bool
    :param description: Description for the new pull request. Can include markdown.
                        Each value sent to this arg will be a new line.
                        For example: --description "First Line" "Second Line"
    :type description: list of str
    :param auto_complete: Set the pull request to complete automatically when all policies have passed and
                          the source branch can be merged into the target branch.
    :type auto_complete: bool
    :param squash: Squash the commits in the source branch when merging into the target branch.
    :type squash: bool
    :param delete_source_branch: Delete the source branch after the pull request has been completed
                                 and merged into the target branch.
    :type delete_source_branch: bool
    :param bypass_policy: Bypass required policies (if any) and completes the pull request once it
                          can be merged.
    :type bypass_policy: bool
    :param bypass_policy_reason: Reason for bypassing the required policies.
    :type bypass_policy_reason: str
    :param merge_commit_message: Message displayed when commits are merged.
    :type merge_commit_message: str
    :param optional_reviewers: Additional users or groups to include as optional reviewers on the new pull request.
                      Space separated.
    :type optional_reviewers: list of str
    :param required_reviewers: Additional users or groups to include as required reviewers on the new pull request.
                      Space separated.
    :type required_reviewers: list of str
    :param work_items: IDs of the work items to link to the new pull request. Space separated.
    :type work_items: list of str
    :param open: Open the pull request in your web browser.
    :type open: bool
    :param transition_work_items: Transition any work items linked to the pull request into the next logical state.
                   (e.g. Active -> Resolved)
    :type transition_work_items: bool
    :param labels: The labels associated with the pull request. Space separated.
    :type labels: list of str
    :rtype: :class:`GitPullRequest <v5_0.git.models.GitPullRequest>`
    """
    organization, project, repository = resolve_instance_project_and_repo(
        detect=detect,
        organization=organization,
        project=project,
        repo=repository)
    source_branch, target_branch = _get_branches_for_pull_request(
        organization, project, repository, source_branch, target_branch, detect)
    client = get_git_client(organization)
    multi_line_description = None
    if description is not None:
        multi_line_description = '\n'.join(description)
    if labels is not None:
        labels = [WebApiTagDefinition(name=label) for label in labels.split(' ')]
    pr = GitPullRequest(description=multi_line_description,
                        source_ref_name=source_branch,
                        target_ref_name=target_branch,
                        labels=labels)
    if draft is not None:
        pr.is_draft = draft

    pr.title = 'Merge ' + source_branch + ' to ' + target_branch
    if title is not None:
        pr.title = title

    pr.source_ref_name = resolve_git_ref_heads(source_branch)
    pr.target_ref_name = resolve_git_ref_heads(target_branch)
    if pr.source_ref_name == pr.target_ref_name:
        raise CLIError('The source branch, "{}", can not be the same as the target branch.'.format
                       (pr.source_ref_name))

    if optional_reviewers is not None:
        optional_reviewers = list(set(x.lower() for x in optional_reviewers))

    if required_reviewers is not None:
        required_reviewers = list(set(x.lower() for x in required_reviewers))

    reviewers = _resolve_reviewers_as_refs(optional_reviewers, required_reviewers, organization)

    pr.reviewers = reviewers

    if work_items is not None and work_items:
        resolved_work_items = []
        for work_item in work_items:
            resolved_work_items.append(ResourceRef(id=work_item))
        pr.work_item_refs = resolved_work_items
    pr = client.create_pull_request(git_pull_request_to_create=pr, project=project,
                                    repository_id=repository)
    # if title or description are not provided and there is a single commit, we will
    # use the its comment to populate the not-provided field(s).
    # the first line of the commit comment is used for title and all lines of the
    # comment are used for description.
    title_from_commit = None
    description_from_commit = None
    if (title is None) or (description is None):
        commits = client.get_pull_request_commits(repository_id=repository, pull_request_id=pr.pull_request_id,
                                                  project=project)
        if len(commits) == 1:
            commit_details = client.get_commit(commit_id=commits[0].commit_id, repository_id=repository,
                                               project=project, change_count=0)
            first_commit_comment = commit_details.comment

            if first_commit_comment:
                # when title is not specified, use the first line of first commit comment as PR title
                if title is None:
                    title_from_commit = first_commit_comment.split("\n")[0]
                # when description is not specified, use the entire commit comment as PR description
                if description is None:
                    description_from_commit = first_commit_comment
    set_completion_options = (bypass_policy or
                              bypass_policy_reason is not None or
                              squash or
                              merge_commit_message is not None or
                              delete_source_branch or
                              transition_work_items)
    if auto_complete or set_completion_options or title_from_commit is not None or description_from_commit is not None:
        pr_for_update = GitPullRequest()
        if auto_complete:
            # auto-complete will not get set on create, so a subsequent update is required.
            pr_for_update.auto_complete_set_by = IdentityRef(id=resolve_identity_as_id(ME, organization))
        if set_completion_options:
            completion_options = GitPullRequestCompletionOptions()
            completion_options.bypass_policy = bypass_policy
            completion_options.bypass_reason = bypass_policy_reason
            completion_options.delete_source_branch = delete_source_branch
            completion_options.squash_merge = squash
            completion_options.merge_commit_message = merge_commit_message
            completion_options.transition_work_items = transition_work_items
            pr_for_update.completion_options = completion_options
        if title_from_commit is not None:
            pr_for_update.title = title_from_commit
        if description_from_commit is not None:
            pr_for_update.description = description_from_commit
        pr = client.update_pull_request(git_pull_request_to_update=pr_for_update,
                                        project=pr.repository.project.id,
                                        repository_id=pr.repository.id,
                                        pull_request_id=pr.pull_request_id)
    if open:
        _open_pull_request(pr, organization)
    return pr


def _get_branches_for_pull_request(organization, project, repository, source_branch, target_branch, detect):
    if should_detect(detect):
        if source_branch is None:
            source_branch = get_current_branch_name()
            if source_branch is None:
                raise ValueError('The source branch could not be detected,'
                                 'please provide the --source-branch argument.')
    else:
        if source_branch is None:
            raise ValueError('--source-branch is a required argument.')
    if target_branch is None:
        if project is not None and repository is not None:
            target_branch = _get_default_branch(organization, project, repository)
        if target_branch is None:
            raise ValueError('The target branch could not be detected,'
                             'please provide the --target-branch argument.')
    return source_branch, target_branch


def update_pull_request(id, title=None, description=None, auto_complete=None,  # pylint: disable=redefined-builtin
                        squash=None, delete_source_branch=None, bypass_policy=None, draft=None,
                        bypass_policy_reason=None, merge_commit_message=None, organization=None, detect=None,
                        transition_work_items=None, status=None):
    """Update a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param title: New title for the pull request.
    :type title: str
    :param description: New description for the pull request.  Can include markdown.  Each value sent to this
                        arg will be a new line. For example: --description "First Line" "Second Line"
    :type description: list of str
    :param auto_complete: Set the pull request to complete automatically when all policies have passed and
                          the source branch can be merged into the target branch.
    :type auto_complete: bool
    :param squash: Squash the commits in the source branch when merging into the target branch.
    :type squash: bool
    :param delete_source_branch: Delete the source branch after the pull request has been completed
                                 and merged into the target branch.
    :type delete_source_branch: bool
    :param bypass_policy: Bypass required policies (if any) and completes the pull request once it
                          can be merged.
    :type bypass_policy: bool
    :param bypass_policy_reason: Reason for bypassing the required policies.
    :type bypass_policy_reason: str
    :param draft: Publish the PR or convert to draft mode.
    :type draft: bool
    :param merge_commit_message: Message displayed when commits are merged.
    :type merge_commit_message: str
    :param transition_work_items: Transition any work items linked to the pull request into the next logical state.
                   (e.g. Active -> Resolved)
    :type transition_work_items: bool
    :param status: Set the new state of pull request.
    :type status: str
    :rtype: :class:`GitPullRequest <v5_0.git.models.GitPullRequest>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    existing_pr = client.get_pull_request_by_id(id)
    if description is not None:
        multi_line_description = '\n'.join(description)
    else:
        multi_line_description = None
    pr = GitPullRequest(title=title, description=multi_line_description)
    if (bypass_policy is not None or   # pylint: disable=too-many-boolean-expressions
            bypass_policy_reason is not None or
            squash is not None or
            merge_commit_message is not None or
            delete_source_branch is not None or
            transition_work_items is not None):
        completion_options = existing_pr.completion_options
        if completion_options is None:
            completion_options = GitPullRequestCompletionOptions()
        if bypass_policy is not None:
            completion_options.bypass_policy = bypass_policy
        if bypass_policy_reason is not None:
            completion_options.bypass_reason = bypass_policy_reason
        if delete_source_branch is not None:
            completion_options.delete_source_branch = delete_source_branch
        if squash is not None:
            completion_options.squash_merge = squash
        if merge_commit_message is not None:
            completion_options.merge_commit_message = merge_commit_message
        if transition_work_items is not None:
            completion_options.transition_work_items = transition_work_items
        pr.completion_options = completion_options
    if auto_complete is not None:
        if auto_complete:
            pr.auto_complete_set_by = IdentityRef(id=resolve_identity_as_id(ME, organization))
        else:
            pr.auto_complete_set_by = IdentityRef(id=EMPTY_UUID)
    if draft is not None:
        pr.is_draft = draft
    if status is not None:
        pr.status = status
        if status == 'completed':
            pr.last_merge_source_commit = existing_pr.last_merge_source_commit
            if not pr.completion_options:
                pr.completion_options = existing_pr.completion_options
    pr = client.update_pull_request(git_pull_request_to_update=pr,
                                    project=existing_pr.repository.project.name,
                                    repository_id=existing_pr.repository.name,
                                    pull_request_id=id)
    return pr


def create_pull_request_reviewers(id, reviewers, organization=None, detect=None, required=None):  # pylint: disable=redefined-builtin
    """Add one or more reviewers to a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param reviewers: Users or groups to include as reviewers on a pull request. Space separated.
    :type reviewers: list of str
    :param required: Make the reviewers required.
    :type reviewers: bool
    :rtype: list of :class:`IdentityRefWithVote <v5_0.git.models.IdentityRefWithVote>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    pr = client.get_pull_request_by_id(id)
    resolved_reviewers = _resolve_reviewers_as_refs(reviewers, None, organization)

    if required:
        for reviewer in resolved_reviewers:
            reviewer.is_required = True

    identities = client.create_pull_request_reviewers(reviewers=resolved_reviewers,
                                                      project=pr.repository.project.id,
                                                      repository_id=pr.repository.id,
                                                      pull_request_id=id)
    return identities


def delete_pull_request_reviewers(id, reviewers, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """Remove one or more reviewers from a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param reviewers: Users or groups to remove as reviewers on a pull request. Space separated.
    :type reviewers: list of str
    :rtype: list of :class:`IdentityRefWithVote <v5_0.git.models.IdentityRefWithVote>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    pr = client.get_pull_request_by_id(id)
    resolved_reviewers = _resolve_reviewers_as_ids(reviewers, organization)
    for reviewer in resolved_reviewers:
        client.delete_pull_request_reviewer(project=pr.repository.project.id,
                                            repository_id=pr.repository.id,
                                            pull_request_id=id,
                                            reviewer_id=reviewer)
    return client.get_pull_request_reviewers(project=pr.repository.project.id,
                                             repository_id=pr.repository.id,
                                             pull_request_id=id)


def list_pull_request_reviewers(id, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """List reviewers of a pull request.
    :param id: ID of the pull request.
    :type id: int
    :rtype: list of :class:`IdentityRefWithVote <v5_0.git.models.IdentityRefWithVote>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    pr = client.get_pull_request_by_id(id)
    return client.get_pull_request_reviewers(project=pr.repository.project.id,
                                             repository_id=pr.repository.id,
                                             pull_request_id=id)


def add_pull_request_work_items(id, work_items, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """Link one or more work items to a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param work_items: IDs of the work items to link. Space separated.
    :type work_items: list of int
    :rtype: list of :class:`AssociatedWorkItem <v5_0.git.models.AssociatedWorkItem>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    existing_pr = client.get_pull_request_by_id(id)
    if work_items is not None and work_items:
        work_items = list(set(work_items))  # make distinct
        wit_client = get_work_item_tracking_client(organization)
        pr_url = 'vstfs:///Git/PullRequestId/{project}%2F{repo}%2F{id}'.format(
            project=existing_pr.repository.project.id, repo=existing_pr.repository.id, id=id)
        for work_item_id in work_items:
            patch_document = []
            patch_operation = JsonPatchOperation()
            patch_operation.op = 0
            patch_operation.path = '/relations/-'
            patch_operation.value = WorkItemRelation()
            patch_operation.value.attributes = {'name': 'Pull Request'}
            patch_operation.value.rel = 'ArtifactLink'
            patch_operation.value.url = pr_url
            patch_document.append(patch_operation)
            try:
                wit_client.update_work_item(document=patch_document, id=work_item_id)
            except AzureDevOpsClientRequestError as ex:
                logger.debug(ex, exc_info=True)
                message = ex.args[0]
                if message != 'Relation already exists.':
                    raise CLIError(ex)
        refs = client.get_pull_request_work_item_refs(project=existing_pr.repository.project.id,
                                                      repository_id=existing_pr.repository.id,
                                                      pull_request_id=id)
    ids = []
    for ref in refs:
        ids.append(ref.id)
    return wit_client.get_work_items(ids=ids)


def checkout(id, remote_name='origin'):  # pylint: disable=redefined-builtin
    """Checkout the PR source branch locally, if no local changes are present
    :param id: ID of the pull request.
    :type id: int
    :param remote_name: Name of git remote against which PR is raised
    :type remote_name: str
    """
    git_info = get_vsts_info_from_current_remote_url()
    organization = git_info.uri
    if not organization:
        raise CLIError('This command should be used from a valid Azure DevOps git repository only')

    client = get_git_client(organization)
    pr = client.get_pull_request_by_id(id)

    # favorite the ref
    refFavoriteRequest = GitRefFavorite(name=pr.source_ref_name, repository_id=pr.repository.id, type=2)
    try:
        client.create_favorite(favorite=refFavoriteRequest, project=pr.repository.project.id)
    except Exception as ex:  # pylint: disable=broad-except
        if 'is already a favorite for user' not in str(ex):
            raise ex

    fetch_remote_and_checkout(pr.source_ref_name, remote_name)


def remove_pull_request_work_items(id, work_items, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """Unlink one or more work items from a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param work_items: IDs of the work items to unlink. Space separated.
    :type work_items: list of int
    :rtype: list of :class:`AssociatedWorkItem <v5_0.git.models.AssociatedWorkItem>`
    """
    # pylint: disable=too-many-nested-blocks
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    existing_pr = client.get_pull_request_by_id(id)
    if work_items is not None and work_items:
        work_items = list(set(work_items))  # make distinct
        wit_client = get_work_item_tracking_client(organization)
        work_items_full = wit_client.get_work_items(ids=work_items, expand=1)
        if work_items_full:
            url = 'vstfs:///Git/PullRequestId/{project}%2F{repo}%2F{id}'.format(
                project=existing_pr.repository.project.id, repo=existing_pr.repository.id, id=id)
            for work_item in work_items_full:
                if work_item.relations is not None:
                    index = 0
                    for relation in work_item.relations:
                        if relation.url == url:
                            patch_document = []

                            patch_test_operation = JsonPatchOperation()
                            patch_test_operation.op = 'test'
                            patch_test_operation.path = '/rev'
                            patch_test_operation.value = work_item.rev
                            patch_document.append(patch_test_operation)

                            patch_operation = JsonPatchOperation()
                            patch_operation.op = 1
                            patch_operation.path = '/relations/{index}'.format(index=index)
                            patch_document.append(patch_operation)

                            wit_client.update_work_item(document=patch_document, id=work_item.id)
                        else:
                            index += 1
            refs = client.get_pull_request_work_item_refs(project=existing_pr.repository.project.id,
                                                          repository_id=existing_pr.repository.id,
                                                          pull_request_id=id)
            if refs:
                ids = []
                for ref in refs:
                    ids.append(ref.id)
                if ids:
                    return wit_client.get_work_items(ids=ids)
    return None


def list_pull_request_work_items(id, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """List linked work items for a pull request.
    :param id: ID of the pull request.
    :type id: int
    :rtype: list of :class:`AssociatedWorkItem <v5_0.git.models.AssociatedWorkItem>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    pr = client.get_pull_request_by_id(id)
    refs = client.get_pull_request_work_item_refs(project=pr.repository.project.id,
                                                  repository_id=pr.repository.id,
                                                  pull_request_id=id)
    if refs:
        ids = []
        for ref in refs:
            ids.append(ref.id)
        wit_client = get_work_item_tracking_client(organization)
        return wit_client.get_work_items(ids=ids)

    return refs


def vote_pull_request(id, vote, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """Vote on a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param vote: New vote value for the pull request.
    :type vote: int
    :rtype: :class:`IdentityRefWithVote <v5_0.git.models.IdentityRefWithVote>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    client = get_git_client(organization)
    pr = client.get_pull_request_by_id(id)
    resolved_reviewer = IdentityRefWithVote(id=resolve_identity_as_id(ME, organization))
    resolved_reviewer.vote = _convert_vote_to_int(vote)
    created_reviewer = client.create_pull_request_reviewer(project=pr.repository.project.id,
                                                           repository_id=pr.repository.id,
                                                           pull_request_id=id,
                                                           reviewer_id=resolved_reviewer.id,
                                                           reviewer=resolved_reviewer)
    return created_reviewer


def _convert_vote_to_int(vote):
    if vote.lower() == 'approve':
        return 10
    if vote.lower() == 'approve-with-suggestions':
        return 5
    if vote.lower() == 'reset':
        return 0
    if vote.lower() == 'wait-for-author':
        return -5
    if vote.lower() == 'reject':
        return -10
    raise CLIError('"{vote}" is an invalid value for a pull request vote.'.format(vote=vote))


def list_pr_policies(id, organization=None, detect=None, top=None, skip=None):  # pylint: disable=redefined-builtin
    """List policies of a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param top: Maximum number of policies to list.
    :type top: int
    :param skip: Number of policies to skip.
    :type skip: int
    :rtype: list of :class:`PolicyEvaluationRecord <policy.v4_0.models.PolicyEvaluationRecord>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    git_client = get_git_client(organization)
    pr = git_client.get_pull_request_by_id(id)
    policy_client = get_policy_client(organization)
    artifact_id = "vstfs:///CodeReview/CodeReviewId/{project_id}/{pull_request_id}".format(
        project_id=pr.repository.project.id, pull_request_id=id)
    return policy_client.get_policy_evaluations(project=pr.repository.project.id,
                                                artifact_id=artifact_id,
                                                top=top,
                                                skip=skip)


def queue_pr_policy(id, evaluation_id, organization=None, detect=None):  # pylint: disable=redefined-builtin
    """Queue an evaluation of a policy for a pull request.
    :param id: ID of the pull request.
    :type id: int
    :param evaluation_id: ID of the policy evaluation to queue.
    :type evaluation_id: str
    :rtype: :class:`PolicyEvaluationRecord <policy.v4_0.models.PolicyEvaluationRecord>`
    """
    organization = resolve_instance(detect=detect, organization=organization)
    git_client = get_git_client(organization)
    pr = git_client.get_pull_request_by_id(id)
    policy_client = get_policy_client(organization)
    return policy_client.requeue_policy_evaluation(project=pr.repository.project.id,
                                                   evaluation_id=evaluation_id)


def _resolve_reviewers_as_refs(optional_reviewers, required_reviewers, organization):
    """Takes a list containing identity names, emails, and ids,
    and return a list of IdentityRefWithVote objects. Reviewers found twice will be made required
    :param optional_reviewers: the list of optional reviewers
    :param required_reviewers: the list of required reviewers
    :rtype: list of :class:`IdentityRefWithVote <v5_0.git.models.IdentityRefWithVote>`
    """
    if optional_reviewers is None:
        optional_reviewers = []
    if required_reviewers is None:
        required_reviewers = []
    resolved_reviewers = []

    for reviewer in optional_reviewers:
        resolved_reviewers.append(IdentityRefWithVote(id=resolve_identity_as_id(reviewer, organization)))

    for reviewer in required_reviewers:
        resolved_reviewer = IdentityRefWithVote(id=resolve_identity_as_id(reviewer, organization))

        # is this id already in the list (make duplicate required)
        for optional_reviewer in resolved_reviewers:
            if optional_reviewer.id == resolved_reviewer.id:
                optional_reviewer.is_required = True
                continue

        resolved_reviewer.is_required = True
        resolved_reviewers.append(resolved_reviewer)

    return resolved_reviewers


def _resolve_reviewers_as_ids(reviewers, organization):
    """Takes a list containing identity names, emails, and ids,
    and returns a list of IdentityRefWithVote objects.
    """
    resolved_reviewers = None
    if reviewers is not None and reviewers:
        resolved_reviewers = []
        for reviewer in reviewers:
            resolved_reviewers.append(resolve_identity_as_id(reviewer, organization))
    return resolved_reviewers


def _open_pull_request(pull_request, organization):
    """Opens the pull request in the default browser.
    :param pull_request: The pull request to open.
    :type pull_request: str
    """
    url = organization.rstrip('/') + '/' + uri_quote(pull_request.repository.project.name)\
        + '/_git/' + uri_quote(pull_request.repository.name) + '/pullrequest/'\
        + str(pull_request.pull_request_id)
    webbrowser.open_new(url=url)


def _get_default_branch(organization, project, repository):
    client = get_git_client(organization)
    repo = client.get_repository(project=project, repository_id=repository)
    return repo.default_branch