44import hashlib
55import itertools
66import time
7+ from difflib import unified_diff
78from enum import Enum
89from itertools import chain
910from pathlib import Path
10- from urllib .parse import urlparse
1111
1212import git
1313import gitlab .const
14- from attrs import define
1514from azure .devops .connection import Connection
1615from azure .devops .released .client_factory import ClientFactory
1716from azure .devops .released .core .core_client import CoreClient
1817from azure .devops .released .git .git_client import GitClient
19- from azure .devops .v7_1 .git .models import GitPullRequest , GitPullRequestSearchCriteria , TeamProjectReference , GitRepository
18+ from azure .devops .v7_1 .git .models import GitPullRequest , GitPullRequestSearchCriteria , TeamProjectReference , \
19+ GitRepository , Comment , GitPullRequestCommentThread , GitTargetVersionDescriptor , GitBaseVersionDescriptor
2020from github import Auth , Consts , Github , GithubException , PullRequest
2121from github .GithubException import UnknownObjectException
2222from gitlab import Gitlab , GitlabAuthenticationError , GitlabError
2323from gitlab .v4 .objects import ProjectMergeRequest
2424from giturlparse import GitUrlParsed , parse
2525from msrest .authentication import BasicAuthentication
26- from typing_extensions import Protocol , TypedDict
26+ from typing_extensions import Protocol , TypedDict , Iterator
2727
2828from patchwork .logger import logger
2929
@@ -35,14 +35,6 @@ def get_slug_from_remote_url(remote_url: str) -> str:
3535 return slug
3636
3737
38- @define
39- class Comment :
40- path : str
41- body : str
42- start_line : int | None
43- end_line : int
44-
45-
4638class IssueText (TypedDict ):
4739 title : str
4840 body : str
@@ -386,18 +378,67 @@ def url(self) -> str:
386378 def set_pr_description (self , body : str ) -> None :
387379 final_body = PullRequestProtocol ._apply_pr_template (self , body )
388380 body = GitPullRequest (description = final_body )
389- self .git_client .update_pull_request (body , repository_id = self ._pr .repository .id , pull_request_id = self ._pr .pull_request_id , project = self ._pr .repository .project .id )
381+ self ._pr = self . git_client .update_pull_request (body , repository_id = self ._pr .repository .id , pull_request_id = self ._pr .pull_request_id , project = self ._pr .repository .project .id )
390382
391383 def create_comment (
392384 self , body : str , path : str | None = None , start_line : int | None = None , end_line : int | None = None
393385 ) -> str | None :
394- ...
386+ try :
387+ comment_body = Comment (content = body )
388+ comment_thread_body = GitPullRequestCommentThread (comments = [comment_body ])
389+ comment_thread = self .git_client .create_thread (comment_thread_body , repository_id = self ._pr .repository .id , pull_request_id = self .id , project = self ._pr .repository .project .id )
390+ return body
391+ except Exception as e :
392+ logger .error (e )
393+ return None
394+
395+ def __iter_comments (self ) -> Iterator [tuple [GitPullRequestCommentThread , list [Comment ]]]:
396+ threads = self .git_client .get_threads (repository_id = self ._pr .repository .id , pull_request_id = self .id , project = self ._pr .repository .project .id )
397+ for thread in threads :
398+ comments = self .git_client .get_comments (repository_id = self ._pr .repository .id , pull_request_id = self .id , thread_id = thread .id , project = self ._pr .repository .project .id )
399+ yield thread , comments
395400
396401 def reset_comments (self ) -> None :
397- ...
402+ for thread , comments in self .__iter_comments ():
403+ comment_ids_to_delete = []
404+ for comment in comments :
405+ if comment .content .startswith (_COMMENT_MARKER ):
406+ comment_ids_to_delete .append (comment .id )
407+ if len (comment_ids_to_delete ) == len (comments ):
408+ for comment_id in comment_ids_to_delete :
409+ self .git_client .delete_comment (repository_id = self ._pr .repository .id , pull_request_id = self .id , thread_id = thread .id , comment_id = comment_id , project = self ._pr .repository .project .id )
398410
399411 def texts (self ) -> PullRequestTexts :
400- ...
412+ target_branch = self ._pr .last_merge_source_commit .commit_id
413+ feature_branch = self ._pr .last_merge_target_commit .commit_id
414+
415+ repo = git .Repo (path = Path .cwd (), search_parent_directories = True )
416+ for remote in repo .remotes :
417+ remote .fetch ()
418+ target_commit = repo .commit (target_branch )
419+ feature_commit = repo .commit (feature_branch )
420+
421+ diff_index = feature_commit .diff (target_commit )
422+ diffs = dict ()
423+ for diff in diff_index :
424+ a_path = diff .a_path
425+ b_path = diff .b_path
426+ a_blob = diff .a_blob .data_stream .read ().decode ("utf-8" )
427+ b_blob = diff .b_blob .data_stream .read ().decode ("utf-8" )
428+ diff_lines = unified_diff (a_blob .splitlines (keepends = True ), b_blob .splitlines (keepends = True ), a_path , b_path )
429+ diff_content = "" .join (diff_lines )
430+ diffs [a_path ] = diff_content
431+
432+ comments : list [PullRequestComment ] = []
433+ for _ , raw_comments in self .__iter_comments ():
434+ for raw_comment in raw_comments :
435+ comments .append (dict (user = raw_comment .author .display_name , body = raw_comment .content ))
436+ return dict (
437+ title = self ._pr .title or "" ,
438+ body = self ._pr .description or "" ,
439+ comments = comments ,
440+ diffs = diffs ,
441+ )
401442
402443class GithubClient (ScmPlatformClientProtocol ):
403444 DEFAULT_URL = Consts .DEFAULT_BASE_URL
@@ -450,11 +491,11 @@ def find_issue_by_id(self, slug: str, issue_id: int) -> IssueText | None:
450491 logger .warn (f"Failed to get issue: { e } " )
451492 return None
452493
453- def get_pr_by_url (self , url : str ) -> PullRequestProtocol | None :
494+ def get_pr_by_url (self , url : str ) -> GithubPullRequest | None :
454495 slug , pr_id = self .get_slug_and_id_from_url (url )
455496 return self .find_pr_by_id (slug , pr_id )
456497
457- def find_pr_by_id (self , slug : str , pr_id : int ) -> PullRequestProtocol | None :
498+ def find_pr_by_id (self , slug : str , pr_id : int ) -> GithubPullRequest | None :
458499 repo = self .github .get_repo (slug )
459500 try :
460501 pr = repo .get_pull (pr_id )
@@ -508,7 +549,7 @@ def create_pr(
508549 body : str ,
509550 original_branch : str ,
510551 feature_branch : str ,
511- ) -> PullRequestProtocol :
552+ ) -> GithubPullRequest :
512553 # before creating a PR, check if one already exists
513554 repo = self .github .get_repo (slug )
514555 gh_pr = repo .create_pull (title = title , body = body , base = original_branch , head = feature_branch )
@@ -579,11 +620,11 @@ def find_issue_by_id(self, slug: str, issue_id: int) -> IssueText | None:
579620 logger .warn (f"Failed to get issue: { e } " )
580621 return None
581622
582- def get_pr_by_url (self , url : str ) -> PullRequestProtocol | None :
623+ def get_pr_by_url (self , url : str ) -> GitlabMergeRequest | None :
583624 slug , pr_id = self .get_slug_and_id_from_url (url )
584625 return self .find_pr_by_id (slug , pr_id )
585626
586- def find_pr_by_id (self , slug : str , pr_id : int ) -> PullRequestProtocol | None :
627+ def find_pr_by_id (self , slug : str , pr_id : int ) -> GitlabMergeRequest | None :
587628 project = self .gitlab .projects .get (slug )
588629 try :
589630 mr = project .mergerequests .get (pr_id )
@@ -599,7 +640,7 @@ def find_prs(
599640 original_branch : str | None = None ,
600641 feature_branch : str | None = None ,
601642 limit : int | None = None ,
602- ) -> list [PullRequestProtocol ]:
643+ ) -> list [GitlabMergeRequest ]:
603644 project = self .gitlab .projects .get (slug )
604645 kwargs_list = dict (iterator = [True ], state = [None ], target_branch = [None ], source_branch = [None ])
605646
@@ -630,7 +671,7 @@ def create_pr(
630671 body : str ,
631672 original_branch : str ,
632673 feature_branch : str ,
633- ) -> PullRequestProtocol :
674+ ) -> GitlabMergeRequest :
634675 # before creating a PR, check if one already exists
635676 project = self .gitlab .projects .get (slug )
636677 gl_mr = project .mergerequests .create (
@@ -714,23 +755,41 @@ def set_url(self, url: str) -> None:
714755 self .__url = url
715756
716757 def test (self ) -> bool :
717- response = self .core_client .get_projects ()
718- return next (iter (response ), None ) is not None
758+ try :
759+ proj = self .project
760+ return True
761+ except ValueError :
762+ return False
719763
720764 def get_slug_and_id_from_url (self , url : str ) -> tuple [str , int ] | None :
721- ...
765+ url_parts = url .split ("/" )
766+ if len (url_parts ) == 1 :
767+ logger .error (f"Invalid URL: { url } " )
768+ return None
769+
770+ try :
771+ resource_id = int (url_parts [- 1 ])
772+ except ValueError :
773+ logger .error (f"Invalid URL: { url } " )
774+ return None
775+
776+ slug = "/" .join (url_parts [- 6 :- 3 ])
777+
778+ return slug , resource_id
722779
723780 def find_issue_by_url (self , url : str ) -> IssueText | None :
724781 ...
725782
726783 def find_issue_by_id (self , slug : str , issue_id : int ) -> IssueText | None :
727784 ...
728785
729- def get_pr_by_url (self , url : str ) -> PullRequestProtocol | None :
730- ...
786+ def get_pr_by_url (self , url : str ) -> AzureDevopsPullRequest | None :
787+ slug , resource_id = self .get_slug_and_id_from_url (url )
788+ return self .find_pr_by_id (slug , resource_id )
731789
732- def find_pr_by_id (self , slug : str , pr_id : int ) -> PullRequestProtocol | None :
733- ...
790+ def find_pr_by_id (self , slug : str , pr_id : int ) -> AzureDevopsPullRequest | None :
791+ pr = self .git_client .get_pull_request (repository_id = self .repo .id , pull_request_id = pr_id , project = self .project .id )
792+ return AzureDevopsPullRequest (pr , self .git_client , self .__pr_resource_html_url ())
734793
735794 def find_prs (
736795 self ,
@@ -739,7 +798,7 @@ def find_prs(
739798 original_branch : str | None = None ,
740799 feature_branch : str | None = None ,
741800 limit : int | None = None ,
742- ) -> list [PullRequestProtocol ]:
801+ ) -> list [AzureDevopsPullRequest ]:
743802 kwargs_list = dict (status = [None ], target_ref_name = [None ], source_ref_name = [None ])
744803
745804 if state is not None :
@@ -777,7 +836,7 @@ def create_pr(
777836 body : str ,
778837 original_branch : str ,
779838 feature_branch : str ,
780- ) -> PullRequestProtocol :
839+ ) -> AzureDevopsPullRequest :
781840 # before creating a PR, check if one already exists
782841 pr_body = GitPullRequest (
783842 source_ref_name = f"refs/heads/{ feature_branch } " ,
0 commit comments