Branch.py
1 ############################ Copyrights and license ############################ 2 # # 3 # Copyright 2012 Vincent Jacques <vincent@vincent-jacques.net> # 4 # Copyright 2012 Zearin <zearin@gonk.net> # 5 # Copyright 2013 AKFish <akfish@gmail.com> # 6 # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # 7 # Copyright 2013 martinqt <m.ki2@laposte.net> # 8 # Copyright 2014 Vincent Jacques <vincent@vincent-jacques.net> # 9 # Copyright 2015 Kyle Hornberg <khornberg@users.noreply.github.com> # 10 # Copyright 2016 Jannis Gebauer <ja.geb@me.com> # 11 # Copyright 2016 Peter Buckley <dx-pbuckley@users.noreply.github.com> # 12 # Copyright 2018 Steve Kowalik <steven@wedontsleep.org> # 13 # Copyright 2018 Wan Liuyang <tsfdye@gmail.com> # 14 # Copyright 2018 sfdye <tsfdye@gmail.com> # 15 # # 16 # This file is part of PyGithub. # 17 # http://pygithub.readthedocs.io/ # 18 # # 19 # PyGithub is free software: you can redistribute it and/or modify it under # 20 # the terms of the GNU Lesser General Public License as published by the Free # 21 # Software Foundation, either version 3 of the License, or (at your option) # 22 # any later version. # 23 # # 24 # PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # 25 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # 26 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # 27 # details. # 28 # # 29 # You should have received a copy of the GNU Lesser General Public License # 30 # along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # 31 # # 32 ################################################################################ 33 from __future__ import annotations 34 35 from typing import TYPE_CHECKING, Any 36 37 import github.BranchProtection 38 import github.Commit 39 import github.RequiredPullRequestReviews 40 import github.RequiredStatusChecks 41 from github import Consts 42 from github.GithubObject import ( 43 Attribute, 44 NonCompletableGithubObject, 45 NotSet, 46 Opt, 47 is_defined, 48 is_optional, 49 is_optional_list, 50 is_undefined, 51 ) 52 53 if TYPE_CHECKING: 54 from github.BranchProtection import BranchProtection 55 from github.Commit import Commit 56 from github.NamedUser import NamedUser 57 from github.PaginatedList import PaginatedList 58 from github.RequiredPullRequestReviews import RequiredPullRequestReviews 59 from github.RequiredStatusChecks import RequiredStatusChecks 60 from github.Team import Team 61 62 63 class Branch(NonCompletableGithubObject): 64 """ 65 This class represents Branches. The reference can be found here https://docs.github.com/en/rest/reference/repos#branches 66 """ 67 68 def __repr__(self) -> str: 69 return self.get__repr__({"name": self._name.value}) 70 71 @property 72 def commit(self) -> Commit: 73 return self._commit.value 74 75 @property 76 def name(self) -> str: 77 return self._name.value 78 79 @property 80 def protected(self) -> bool: 81 return self._protected.value 82 83 @property 84 def protection_url(self) -> str: 85 return self._protection_url.value 86 87 def _initAttributes(self) -> None: 88 self._commit: Attribute[Commit] = github.GithubObject.NotSet 89 self._name: Attribute[str] = github.GithubObject.NotSet 90 self._protection_url: Attribute[str] = github.GithubObject.NotSet 91 self._protected: Attribute[bool] = github.GithubObject.NotSet 92 93 def _useAttributes(self, attributes: dict[str, Any]) -> None: 94 if "commit" in attributes: # pragma no branch 95 self._commit = self._makeClassAttribute(github.Commit.Commit, attributes["commit"]) 96 if "name" in attributes: # pragma no branch 97 self._name = self._makeStringAttribute(attributes["name"]) 98 if "protection_url" in attributes: # pragma no branch 99 self._protection_url = self._makeStringAttribute(attributes["protection_url"]) 100 if "protected" in attributes: # pragma no branch 101 self._protected = self._makeBoolAttribute(attributes["protected"]) 102 103 def get_protection(self) -> BranchProtection: 104 """ 105 :calls: `GET /repos/{owner}/{repo}/branches/{branch}/protection <https://docs.github.com/en/rest/reference/repos#branches>`_ 106 """ 107 headers, data = self._requester.requestJsonAndCheck( 108 "GET", 109 self.protection_url, 110 headers={"Accept": Consts.mediaTypeRequireMultipleApprovingReviews}, 111 ) 112 return github.BranchProtection.BranchProtection(self._requester, headers, data, completed=True) 113 114 def edit_protection( 115 self, 116 strict: Opt[bool] = NotSet, 117 contexts: Opt[list[str]] = NotSet, 118 enforce_admins: Opt[bool] = NotSet, 119 dismissal_users: Opt[list[str]] = NotSet, 120 dismissal_teams: Opt[list[str]] = NotSet, 121 dismissal_apps: Opt[list[str]] = NotSet, 122 dismiss_stale_reviews: Opt[bool] = NotSet, 123 require_code_owner_reviews: Opt[bool] = NotSet, 124 required_approving_review_count: Opt[int] = NotSet, 125 user_push_restrictions: Opt[list[str]] = NotSet, 126 team_push_restrictions: Opt[list[str]] = NotSet, 127 app_push_restrictions: Opt[list[str]] = NotSet, 128 required_linear_history: Opt[bool] = NotSet, 129 allow_force_pushes: Opt[bool] = NotSet, 130 required_conversation_resolution: Opt[bool] = NotSet, 131 lock_branch: Opt[bool] = NotSet, 132 allow_fork_syncing: Opt[bool] = NotSet, 133 users_bypass_pull_request_allowances: Opt[list[str]] = NotSet, 134 teams_bypass_pull_request_allowances: Opt[list[str]] = NotSet, 135 apps_bypass_pull_request_allowances: Opt[list[str]] = NotSet, 136 block_creations: Opt[bool] = NotSet, 137 ) -> BranchProtection: 138 """ 139 :calls: `PUT /repos/{owner}/{repo}/branches/{branch}/protection <https://docs.github.com/en/rest/reference/repos#get-branch-protection>`_ 140 141 NOTE: The GitHub API groups strict and contexts together, both must 142 be submitted. Take care to pass both as arguments even if only one is 143 changing. Use edit_required_status_checks() to avoid this. 144 """ 145 assert is_optional(strict, bool), strict 146 assert is_optional_list(contexts, str), contexts 147 assert is_optional(enforce_admins, bool), enforce_admins 148 assert is_optional_list(dismissal_users, str), dismissal_users 149 assert is_optional_list(dismissal_teams, str), dismissal_teams 150 assert is_optional_list(dismissal_apps, str), dismissal_apps 151 assert is_optional(dismiss_stale_reviews, bool), dismiss_stale_reviews 152 assert is_optional(require_code_owner_reviews, bool), require_code_owner_reviews 153 assert is_optional(required_approving_review_count, int), required_approving_review_count 154 assert is_optional(required_linear_history, bool), required_linear_history 155 assert is_optional(allow_force_pushes, bool), allow_force_pushes 156 assert is_optional(required_conversation_resolution, bool), required_conversation_resolution 157 assert is_optional(lock_branch, bool), lock_branch 158 assert is_optional(allow_fork_syncing, bool), allow_fork_syncing 159 assert is_optional_list(users_bypass_pull_request_allowances, str), users_bypass_pull_request_allowances 160 assert is_optional_list(teams_bypass_pull_request_allowances, str), teams_bypass_pull_request_allowances 161 assert is_optional_list(apps_bypass_pull_request_allowances, str), apps_bypass_pull_request_allowances 162 163 post_parameters: dict[str, Any] = {} 164 if is_defined(strict) or is_defined(contexts): 165 if is_undefined(strict): 166 strict = False 167 if is_undefined(contexts): 168 contexts = [] 169 post_parameters["required_status_checks"] = { 170 "strict": strict, 171 "contexts": contexts, 172 } 173 else: 174 post_parameters["required_status_checks"] = None 175 176 if is_defined(enforce_admins): 177 post_parameters["enforce_admins"] = enforce_admins 178 else: 179 post_parameters["enforce_admins"] = None 180 181 if ( 182 is_defined(dismissal_users) 183 or is_defined(dismissal_teams) 184 or is_defined(dismissal_apps) 185 or is_defined(dismiss_stale_reviews) 186 or is_defined(require_code_owner_reviews) 187 or is_defined(required_approving_review_count) 188 or is_defined(users_bypass_pull_request_allowances) 189 or is_defined(teams_bypass_pull_request_allowances) 190 or is_defined(apps_bypass_pull_request_allowances) 191 ): 192 post_parameters["required_pull_request_reviews"] = {} 193 if is_defined(dismiss_stale_reviews): 194 post_parameters["required_pull_request_reviews"]["dismiss_stale_reviews"] = dismiss_stale_reviews 195 if is_defined(require_code_owner_reviews): 196 post_parameters["required_pull_request_reviews"][ 197 "require_code_owner_reviews" 198 ] = require_code_owner_reviews 199 if is_defined(required_approving_review_count): 200 post_parameters["required_pull_request_reviews"][ 201 "required_approving_review_count" 202 ] = required_approving_review_count 203 204 dismissal_restrictions = {} 205 if is_defined(dismissal_users): 206 dismissal_restrictions["users"] = dismissal_users 207 if is_defined(dismissal_teams): 208 dismissal_restrictions["teams"] = dismissal_teams 209 if is_defined(dismissal_apps): 210 dismissal_restrictions["apps"] = dismissal_apps 211 212 if dismissal_restrictions: 213 post_parameters["required_pull_request_reviews"]["dismissal_restrictions"] = dismissal_restrictions 214 215 bypass_pull_request_allowances = {} 216 if is_defined(users_bypass_pull_request_allowances): 217 bypass_pull_request_allowances["users"] = users_bypass_pull_request_allowances 218 if is_defined(teams_bypass_pull_request_allowances): 219 bypass_pull_request_allowances["teams"] = teams_bypass_pull_request_allowances 220 if is_defined(apps_bypass_pull_request_allowances): 221 bypass_pull_request_allowances["apps"] = apps_bypass_pull_request_allowances 222 223 if bypass_pull_request_allowances: 224 post_parameters["required_pull_request_reviews"][ 225 "bypass_pull_request_allowances" 226 ] = bypass_pull_request_allowances 227 else: 228 post_parameters["required_pull_request_reviews"] = None 229 if ( 230 is_defined(user_push_restrictions) 231 or is_defined(team_push_restrictions) 232 or is_defined(app_push_restrictions) 233 ): 234 if is_undefined(user_push_restrictions): 235 user_push_restrictions = [] 236 if is_undefined(team_push_restrictions): 237 team_push_restrictions = [] 238 if is_undefined(app_push_restrictions): 239 app_push_restrictions = [] 240 post_parameters["restrictions"] = { 241 "users": user_push_restrictions, 242 "teams": team_push_restrictions, 243 "apps": app_push_restrictions, 244 } 245 else: 246 post_parameters["restrictions"] = None 247 if is_defined(required_linear_history): 248 post_parameters["required_linear_history"] = required_linear_history 249 else: 250 post_parameters["required_linear_history"] = None 251 if is_defined(allow_force_pushes): 252 post_parameters["allow_force_pushes"] = allow_force_pushes 253 else: 254 post_parameters["allow_force_pushes"] = None 255 if is_defined(required_conversation_resolution): 256 post_parameters["required_conversation_resolution"] = required_conversation_resolution 257 else: 258 post_parameters["required_conversation_resolution"] = None 259 if is_defined(lock_branch): 260 post_parameters["lock_branch"] = lock_branch 261 else: 262 post_parameters["lock_branch"] = None 263 if is_defined(allow_fork_syncing): 264 post_parameters["allow_fork_syncing"] = allow_fork_syncing 265 else: 266 post_parameters["allow_fork_syncing"] = None 267 if is_defined(block_creations): 268 post_parameters["block_creations"] = block_creations 269 else: 270 post_parameters["block_creations"] = None 271 272 headers, data = self._requester.requestJsonAndCheck( 273 "PUT", 274 self.protection_url, 275 headers={"Accept": Consts.mediaTypeRequireMultipleApprovingReviews}, 276 input=post_parameters, 277 ) 278 279 return github.BranchProtection.BranchProtection(self._requester, headers, data, completed=True) 280 281 def remove_protection(self) -> None: 282 """ 283 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection <https://docs.github.com/en/rest/reference/repos#branches>`_ 284 """ 285 headers, data = self._requester.requestJsonAndCheck( 286 "DELETE", 287 self.protection_url, 288 ) 289 290 def get_required_status_checks(self) -> RequiredStatusChecks: 291 """ 292 :calls: `GET /repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks <https://docs.github.com/en/rest/reference/repos#branches>`_ 293 :rtype: :class:`github.RequiredStatusChecks.RequiredStatusChecks` 294 """ 295 headers, data = self._requester.requestJsonAndCheck("GET", f"{self.protection_url}/required_status_checks") 296 return github.RequiredStatusChecks.RequiredStatusChecks(self._requester, headers, data, completed=True) 297 298 def edit_required_status_checks( 299 self, 300 strict: Opt[bool] = NotSet, 301 contexts: Opt[list[str]] = NotSet, 302 ) -> RequiredStatusChecks: 303 """ 304 :calls: `PATCH /repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks <https://docs.github.com/en/rest/reference/repos#branches>`_ 305 """ 306 assert is_optional(strict, bool), strict 307 assert is_optional_list(contexts, str), contexts 308 309 post_parameters: dict[str, Any] = NotSet.remove_unset_items({"strict": strict, "contexts": contexts}) 310 headers, data = self._requester.requestJsonAndCheck( 311 "PATCH", 312 f"{self.protection_url}/required_status_checks", 313 input=post_parameters, 314 ) 315 316 return github.RequiredStatusChecks.RequiredStatusChecks(self._requester, headers, data, completed=True) 317 318 def remove_required_status_checks(self) -> None: 319 """ 320 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks <https://docs.github.com/en/rest/reference/repos#branches>`_ 321 """ 322 headers, data = self._requester.requestJsonAndCheck( 323 "DELETE", 324 f"{self.protection_url}/required_status_checks", 325 ) 326 327 def get_required_pull_request_reviews(self) -> RequiredPullRequestReviews: 328 """ 329 :calls: `GET /repos/{owner}/{repo}/branches/{branch}/protection/required_pull_request_reviews <https://docs.github.com/en/rest/reference/repos#branches>`_ 330 """ 331 headers, data = self._requester.requestJsonAndCheck( 332 "GET", 333 f"{self.protection_url}/required_pull_request_reviews", 334 headers={"Accept": Consts.mediaTypeRequireMultipleApprovingReviews}, 335 ) 336 return github.RequiredPullRequestReviews.RequiredPullRequestReviews( 337 self._requester, headers, data, completed=True 338 ) 339 340 def edit_required_pull_request_reviews( 341 self, 342 dismissal_users: Opt[list[str]] = NotSet, 343 dismissal_teams: Opt[list[str]] = NotSet, 344 dismissal_apps: Opt[list[str]] = NotSet, 345 dismiss_stale_reviews: Opt[bool] = NotSet, 346 require_code_owner_reviews: Opt[bool] = NotSet, 347 required_approving_review_count: Opt[int] = NotSet, 348 ) -> RequiredStatusChecks: 349 """ 350 :calls: `PATCH /repos/{owner}/{repo}/branches/{branch}/protection/required_pull_request_reviews <https://docs.github.com/en/rest/reference/repos#branches>`_ 351 """ 352 assert is_optional_list(dismissal_users, str), dismissal_users 353 assert is_optional_list(dismissal_teams, str), dismissal_teams 354 assert is_optional(dismiss_stale_reviews, bool), dismiss_stale_reviews 355 assert is_optional(require_code_owner_reviews, bool), require_code_owner_reviews 356 assert is_optional(required_approving_review_count, int), required_approving_review_count 357 358 post_parameters: dict[str, Any] = NotSet.remove_unset_items( 359 { 360 "dismiss_stale_reviews": dismiss_stale_reviews, 361 "require_code_owner_reviews": require_code_owner_reviews, 362 "required_approving_review_count": required_approving_review_count, 363 } 364 ) 365 366 dismissal_restrictions: dict[str, Any] = NotSet.remove_unset_items( 367 {"users": dismissal_users, "teams": dismissal_teams, "apps": dismissal_apps} 368 ) 369 370 if dismissal_restrictions: 371 post_parameters["dismissal_restrictions"] = dismissal_restrictions 372 373 headers, data = self._requester.requestJsonAndCheck( 374 "PATCH", 375 f"{self.protection_url}/required_pull_request_reviews", 376 headers={"Accept": Consts.mediaTypeRequireMultipleApprovingReviews}, 377 input=post_parameters, 378 ) 379 380 return github.RequiredStatusChecks.RequiredStatusChecks(self._requester, headers, data, completed=True) 381 382 def remove_required_pull_request_reviews(self) -> None: 383 """ 384 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection/required_pull_request_reviews <https://docs.github.com/en/rest/reference/repos#branches>`_ 385 """ 386 headers, data = self._requester.requestJsonAndCheck( 387 "DELETE", 388 f"{self.protection_url}/required_pull_request_reviews", 389 ) 390 391 def get_admin_enforcement(self) -> bool: 392 """ 393 :calls: `GET /repos/{owner}/{repo}/branches/{branch}/protection/enforce_admins <https://docs.github.com/en/rest/reference/repos#branches>`_ 394 """ 395 headers, data = self._requester.requestJsonAndCheck("GET", f"{self.protection_url}/enforce_admins") 396 return data["enabled"] 397 398 def set_admin_enforcement(self) -> None: 399 """ 400 :calls: `POST /repos/{owner}/{repo}/branches/{branch}/protection/enforce_admins <https://docs.github.com/en/rest/reference/repos#branches>`_ 401 """ 402 headers, data = self._requester.requestJsonAndCheck("POST", f"{self.protection_url}/enforce_admins") 403 404 def remove_admin_enforcement(self) -> None: 405 """ 406 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection/enforce_admins <https://docs.github.com/en/rest/reference/repos#branches>`_ 407 """ 408 headers, data = self._requester.requestJsonAndCheck("DELETE", f"{self.protection_url}/enforce_admins") 409 410 def get_user_push_restrictions(self) -> PaginatedList[NamedUser]: 411 """ 412 :calls: `GET /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/users <https://docs.github.com/en/rest/reference/repos#branches>`_ 413 """ 414 return github.PaginatedList.PaginatedList( 415 github.NamedUser.NamedUser, 416 self._requester, 417 f"{self.protection_url}/restrictions/users", 418 None, 419 ) 420 421 def get_team_push_restrictions(self) -> PaginatedList[Team]: 422 """ 423 :calls: `GET /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/teams <https://docs.github.com/en/rest/reference/repos#branches>`_ 424 """ 425 return github.PaginatedList.PaginatedList( 426 github.Team.Team, 427 self._requester, 428 f"{self.protection_url}/restrictions/teams", 429 None, 430 ) 431 432 def add_user_push_restrictions(self, *users: str) -> None: 433 """ 434 :calls: `POST /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/users <https://docs.github.com/en/rest/reference/repos#branches>`_ 435 :users: list of strings (user names) 436 """ 437 assert all(isinstance(element, str) for element in users), users 438 439 headers, data = self._requester.requestJsonAndCheck( 440 "POST", f"{self.protection_url}/restrictions/users", input=users 441 ) 442 443 def replace_user_push_restrictions(self, *users: str) -> None: 444 """ 445 :calls: `PUT /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/users <https://docs.github.com/en/rest/reference/repos#branches>`_ 446 :users: list of strings (user names) 447 """ 448 assert all(isinstance(element, str) for element in users), users 449 450 headers, data = self._requester.requestJsonAndCheck( 451 "PUT", f"{self.protection_url}/restrictions/users", input=users 452 ) 453 454 def remove_user_push_restrictions(self, *users: str) -> None: 455 """ 456 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/users <https://docs.github.com/en/rest/reference/repos#branches>`_ 457 :users: list of strings (user names) 458 """ 459 assert all(isinstance(element, str) for element in users), users 460 461 headers, data = self._requester.requestJsonAndCheck( 462 "DELETE", f"{self.protection_url}/restrictions/users", input=users 463 ) 464 465 def add_team_push_restrictions(self, *teams: str) -> None: 466 """ 467 :calls: `POST /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/teams <https://docs.github.com/en/rest/reference/repos#branches>`_ 468 :teams: list of strings (team slugs) 469 """ 470 assert all(isinstance(element, str) for element in teams), teams 471 472 headers, data = self._requester.requestJsonAndCheck( 473 "POST", f"{self.protection_url}/restrictions/teams", input=teams 474 ) 475 476 def replace_team_push_restrictions(self, *teams: str) -> None: 477 """ 478 :calls: `PUT /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/teams <https://docs.github.com/en/rest/reference/repos#branches>`_ 479 :teams: list of strings (team slugs) 480 """ 481 assert all(isinstance(element, str) for element in teams), teams 482 483 headers, data = self._requester.requestJsonAndCheck( 484 "PUT", f"{self.protection_url}/restrictions/teams", input=teams 485 ) 486 487 def remove_team_push_restrictions(self, *teams: str) -> None: 488 """ 489 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/teams <https://docs.github.com/en/rest/reference/repos#branches>`_ 490 :teams: list of strings (team slugs) 491 """ 492 assert all(isinstance(element, str) for element in teams), teams 493 494 headers, data = self._requester.requestJsonAndCheck( 495 "DELETE", f"{self.protection_url}/restrictions/teams", input=teams 496 ) 497 498 def remove_push_restrictions(self) -> None: 499 """ 500 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection/restrictions <https://docs.github.com/en/rest/reference/repos#branches>`_ 501 """ 502 headers, data = self._requester.requestJsonAndCheck("DELETE", f"{self.protection_url}/restrictions") 503 504 def get_required_signatures(self) -> bool: 505 """ 506 :calls: `GET /repos/{owner}/{repo}/branches/{branch}/protection/required_signatures <https://docs.github.com/en/rest/reference/repos#branches>`_ 507 """ 508 headers, data = self._requester.requestJsonAndCheck( 509 "GET", 510 f"{self.protection_url}/required_signatures", 511 headers={"Accept": Consts.signaturesProtectedBranchesPreview}, 512 ) 513 return data["enabled"] 514 515 def add_required_signatures(self) -> None: 516 """ 517 :calls: `POST /repos/{owner}/{repo}/branches/{branch}/protection/required_signatures <https://docs.github.com/en/rest/reference/repos#branches>`_ 518 """ 519 headers, data = self._requester.requestJsonAndCheck( 520 "POST", 521 f"{self.protection_url}/required_signatures", 522 headers={"Accept": Consts.signaturesProtectedBranchesPreview}, 523 ) 524 525 def remove_required_signatures(self) -> None: 526 """ 527 :calls: `DELETE /repos/{owner}/{repo}/branches/{branch}/protection/required_signatures <https://docs.github.com/en/rest/reference/repos#branches>`_ 528 """ 529 headers, data = self._requester.requestJsonAndCheck( 530 "DELETE", 531 f"{self.protection_url}/required_signatures", 532 headers={"Accept": Consts.signaturesProtectedBranchesPreview}, 533 )