66import time
77from enum import Enum
88from itertools import chain
9+ from pathlib import Path
10+ from urllib .parse import urlparse
911
12+ import git
1013import gitlab .const
1114from attrs import define
15+ from azure .devops .connection import Connection
16+ from azure .devops .released .client_factory import ClientFactory
17+ from azure .devops .released .core .core_client import CoreClient
18+ from azure .devops .released .git .git_client import GitClient
19+ from azure .devops .v7_1 .git .models import GitPullRequest , GitPullRequestSearchCriteria , TeamProjectReference , GitRepository
1220from github import Auth , Consts , Github , GithubException , PullRequest
1321from github .GithubException import UnknownObjectException
1422from gitlab import Gitlab , GitlabAuthenticationError , GitlabError
1523from gitlab .v4 .objects import ProjectMergeRequest
1624from giturlparse import GitUrlParsed , parse
25+ from msrest .authentication import BasicAuthentication
1726from typing_extensions import Protocol , TypedDict
1827
1928from patchwork .logger import logger
@@ -53,12 +62,13 @@ class PullRequestTexts(TypedDict):
5362
5463
5564class PullRequestState (Enum ):
56- OPEN = (["open" ], ["opened" ])
57- CLOSED = (["closed" ], ["closed" , "merged" ])
65+ OPEN = (["open" ], ["opened" ], [ "active" ] )
66+ CLOSED = (["closed" ], ["closed" , "merged" ], [ "completed" , "abandoned" , "notSet" ] )
5867
59- def __init__ (self , github_state : list [str ], gitlab_state : list [str ]):
68+ def __init__ (self , github_state : list [str ], gitlab_state : list [str ], azure_devops_state : list [ str ] ):
6069 self .github_state : list [str ] = github_state
6170 self .gitlab_state : list [str ] = gitlab_state
71+ self .azure_devops_state : list [str ] = azure_devops_state
6272
6373
6474_COMMENT_MARKER = "<!-- PatchWork comment marker -->"
@@ -112,6 +122,11 @@ def _apply_pr_template(pr: "PullRequestProtocol", body: str) -> str:
112122 # chunk_link_format = file_link_format + "_{start_line}_{end_line}"
113123 chunk_link_format = file_link_format + ""
114124 anchor_hash = hashlib .sha1
125+ elif isinstance (pr , AzureDevopsPullRequest ):
126+ backup_link_format = "{url}?_a=files"
127+ file_link_format = backup_link_format + "&path=/{path}"
128+ chunk_link_format = file_link_format + ""
129+ anchor_hash = hashlib .md5
115130 else :
116131 return pr .url ()
117132
@@ -135,7 +150,7 @@ def _apply_pr_template(pr: "PullRequestProtocol", body: str) -> str:
135150 format_to_use = chunk_link_format
136151
137152 replacement_value = format_to_use .format (
138- url = pr .url (), diff_anchor = diff_anchor , start_line = start , end_line = end
153+ url = pr .url (), path = path , diff_anchor = diff_anchor , start_line = start , end_line = end
139154 )
140155 template = template [:start_idx ] + replacement_value + template [end_idx + 2 :]
141156 start_idx , end_idx = PullRequestProtocol ._get_template_indexes (template )
@@ -352,6 +367,37 @@ def texts(self) -> PullRequestTexts:
352367 diffs = {file .filename : file .patch for file in self ._pr .get_files () if file .patch is not None },
353368 )
354369
370+ class AzureDevopsPullRequest (PullRequestProtocol ):
371+ def __init__ (self , pr : GitPullRequest , git_client : GitClient , pr_base_url : str ):
372+ self ._pr : GitPullRequest = pr
373+ self .git_client : GitClient = git_client
374+ self .pr_base_url = pr_base_url
375+
376+ @property
377+ def id (self ) -> int :
378+ return self ._pr .pull_request_id
379+
380+ def url (self ) -> str :
381+ final_pr_url = self .pr_base_url
382+ if not final_pr_url .endswith ("/" ):
383+ final_pr_url += "/"
384+ return final_pr_url + str (self .id )
385+
386+ def set_pr_description (self , body : str ) -> None :
387+ final_body = PullRequestProtocol ._apply_pr_template (self , body )
388+ 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 )
390+
391+ def create_comment (
392+ self , body : str , path : str | None = None , start_line : int | None = None , end_line : int | None = None
393+ ) -> str | None :
394+ ...
395+
396+ def reset_comments (self ) -> None :
397+ ...
398+
399+ def texts (self ) -> PullRequestTexts :
400+ ...
355401
356402class GithubClient (ScmPlatformClientProtocol ):
357403 DEFAULT_URL = Consts .DEFAULT_BASE_URL
@@ -608,3 +654,145 @@ def create_issue_comment(
608654
609655 obj = self .gitlab .projects .get (slug ).issues .create ({"title" : title , "description" : issue_text })
610656 return obj ["web_url" ]
657+
658+
659+ class AzureDevopsClient (ScmPlatformClientProtocol ):
660+ DEFAULT_URL = "https://dev.azure.com/"
661+
662+ def __init__ (self , access_token : str , url : str = DEFAULT_URL , remote : str = "origin" ):
663+ self .credentials = BasicAuthentication ('' , access_token )
664+ self .__url = url
665+ self .__remote = remote
666+ git_repo = git .Repo (Path .cwd (), search_parent_directories = True )
667+ original_remote_url = git_repo .remotes [remote ].url
668+ parsed_repo : GitUrlParsed = parse (original_remote_url )
669+ self .__org_name = parsed_repo .owner
670+ self .__project_name = parsed_repo .groups_path .replace ("/_git" , "" )
671+ self .__repo_name = parsed_repo .repo
672+
673+ def __pr_resource_html_url (self ):
674+ url = self .__url
675+ if not url .endswith ("/" ):
676+ url += "/"
677+ return f"{ url } { self .__org_name } /{ self .__project_name } /_git/{ self .__repo_name } /pullrequest/"
678+
679+
680+ @functools .cached_property
681+ def clients (self ) -> ClientFactory :
682+ url = self .__url
683+ if not url .endswith ("/" ):
684+ url += "/"
685+
686+ conn = Connection (base_url = f"{ url } { self .__org_name } " , creds = self .credentials )
687+ return conn .clients
688+
689+ @functools .cached_property
690+ def git_client (self ) -> GitClient :
691+ return self .clients .get_git_client ()
692+
693+ @functools .cached_property
694+ def core_client (self ) -> CoreClient :
695+ return self .clients .get_core_client ()
696+
697+ @functools .cached_property
698+ def project (self ) -> TeamProjectReference :
699+ projs = self .core_client .get_projects ()
700+ proj = next ((proj for proj in projs if proj .name == self .__project_name ), None )
701+ if proj is None :
702+ raise ValueError (f"Unable to determine project name from remote { self .__remote } url. Parsed project name: { self .__project_name } " )
703+ return proj
704+
705+ @functools .cached_property
706+ def repo (self ) -> GitRepository :
707+ repos = self .git_client .get_repositories (project = self .project .id )
708+ git_repo = next ((r for r in repos if r .name == self .__repo_name ), None )
709+ if git_repo is None :
710+ raise ValueError (f"Unable to determine repository name from remote { self .__remote } url. Parsed repository name: { self .__repo_name } " )
711+ return git_repo
712+
713+ def set_url (self , url : str ) -> None :
714+ self .__url = url
715+
716+ def test (self ) -> bool :
717+ response = self .core_client .get_projects ()
718+ return next (iter (response ), None ) is not None
719+
720+ def get_slug_and_id_from_url (self , url : str ) -> tuple [str , int ] | None :
721+ ...
722+
723+ def find_issue_by_url (self , url : str ) -> IssueText | None :
724+ ...
725+
726+ def find_issue_by_id (self , slug : str , issue_id : int ) -> IssueText | None :
727+ ...
728+
729+ def get_pr_by_url (self , url : str ) -> PullRequestProtocol | None :
730+ ...
731+
732+ def find_pr_by_id (self , slug : str , pr_id : int ) -> PullRequestProtocol | None :
733+ ...
734+
735+ def find_prs (
736+ self ,
737+ slug : str ,
738+ state : PullRequestState | None = None ,
739+ original_branch : str | None = None ,
740+ feature_branch : str | None = None ,
741+ limit : int | None = None ,
742+ ) -> list [PullRequestProtocol ]:
743+ kwargs_list = dict (status = [None ], target_ref_name = [None ], source_ref_name = [None ])
744+
745+ if state is not None :
746+ kwargs_list ["status" ] = state .gitlab_state # type: ignore
747+ if original_branch is not None :
748+ kwargs_list ["target_ref_name" ] = [f"refs/heads/{ original_branch } " ] # type: ignore
749+ if feature_branch is not None :
750+ kwargs_list ["source_ref_name" ] = [f"refs/heads/{ feature_branch } " ] # type: ignore
751+
752+ page_list = []
753+ keys = kwargs_list .keys ()
754+ for instance in itertools .product (* kwargs_list .values ()):
755+ kwargs = dict (((key , value ) for key , value in zip (keys , instance ) if value is not None ))
756+ git_pr_search = GitPullRequestSearchCriteria (
757+ repository_id = self .repo .id ,
758+ ** kwargs ,
759+ )
760+ pr_instances = self .git_client .get_pull_requests (
761+ project = self .project .id ,
762+ repository_id = self .repo .id ,
763+ search_criteria = git_pr_search
764+ )
765+ page_list .append (pr_instances )
766+
767+ rv_list = []
768+ for mr in itertools .islice (itertools .chain (* page_list ), limit ):
769+ rv_list .append (AzureDevopsPullRequest (mr , self .git_client , self .__pr_resource_html_url ()))
770+
771+ return rv_list
772+
773+ def create_pr (
774+ self ,
775+ slug : str ,
776+ title : str ,
777+ body : str ,
778+ original_branch : str ,
779+ feature_branch : str ,
780+ ) -> PullRequestProtocol :
781+ # before creating a PR, check if one already exists
782+ pr_body = GitPullRequest (
783+ source_ref_name = f"refs/heads/{ feature_branch } " ,
784+ target_ref_name = f"refs/heads/{ original_branch } " ,
785+ title = title ,
786+ description = body ,
787+ # should be web tag definition
788+ # labels="patchwork",
789+ )
790+ pr_instance = self .git_client .create_pull_request (pr_body , repository_id = self .repo .id , project = self .project .id )
791+ mr = AzureDevopsPullRequest (pr_instance , self .git_client , self .__pr_resource_html_url ()) # type: ignore
792+ return mr
793+
794+ def create_issue_comment (
795+ self , slug : str , issue_text : str , title : str | None = None , issue_id : int | None = None
796+ ) -> str :
797+ ...
798+
0 commit comments