repository.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import os
  4. from git import GitCommandError
  5. from git import Repo
  6. from github import Github
  7. from .const import NAME
  8. from .const import REPO_ROOT
  9. from .utils import branch_name
  10. from .utils import read_release_notes_from_changelog
  11. from .utils import ScriptError
  12. class Repository(object):
  13. def __init__(self, root=None, gh_name=None):
  14. if root is None:
  15. root = REPO_ROOT
  16. if gh_name is None:
  17. gh_name = NAME
  18. self.git_repo = Repo(root)
  19. self.gh_client = Github(os.environ['GITHUB_TOKEN'])
  20. self.gh_repo = self.gh_client.get_repo(gh_name)
  21. def create_release_branch(self, version, base=None):
  22. print('Creating release branch {} based on {}...'.format(version, base or 'master'))
  23. remote = self.find_remote(self.gh_repo.full_name)
  24. remote.fetch()
  25. if self.branch_exists(branch_name(version)):
  26. raise ScriptError(
  27. "Branch {} already exists locally. "
  28. "Please remove it before running the release script.".format(branch_name(version))
  29. )
  30. if base is not None:
  31. base = self.git_repo.tag('refs/tags/{}'.format(base))
  32. else:
  33. base = 'refs/remotes/{}/automated-releases'.format(remote.name)
  34. release_branch = self.git_repo.create_head(branch_name(version), commit=base)
  35. release_branch.checkout()
  36. self.git_repo.git.merge('--strategy=ours', '--no-edit', '{}/release'.format(remote.name))
  37. with release_branch.config_writer() as cfg:
  38. cfg.set_value('release', version)
  39. return release_branch
  40. def find_remote(self, remote_name=None):
  41. if not remote_name:
  42. remote_name = self.gh_repo.full_name
  43. for remote in self.git_repo.remotes:
  44. for url in remote.urls:
  45. if remote_name in url:
  46. return remote
  47. return None
  48. def create_bump_commit(self, bump_branch, version):
  49. print('Creating bump commit...')
  50. bump_branch.checkout()
  51. self.git_repo.git.commit('-a', '-s', '-m "Bump {}"'.format(version), '--no-verify')
  52. def diff(self):
  53. return self.git_repo.git.diff()
  54. def checkout_branch(self, name):
  55. return self.git_repo.branches[name].checkout()
  56. def push_branch_to_remote(self, branch, remote_name=None):
  57. print('Pushing branch {} to remote...'.format(branch.name))
  58. remote = self.find_remote(remote_name)
  59. remote.push(refspec=branch, force=True)
  60. def branch_exists(self, name):
  61. return name in [h.name for h in self.git_repo.heads]
  62. def create_release_pull_request(self, version):
  63. return self.gh_repo.create_pull(
  64. title='Bump {}'.format(version),
  65. body='Automated release for docker-compose {}\n\n{}'.format(
  66. version, read_release_notes_from_changelog()
  67. ),
  68. base='release',
  69. head=branch_name(version),
  70. )
  71. def create_release(self, version, release_notes, **kwargs):
  72. return self.gh_repo.create_git_release(
  73. tag=version, name=version, message=release_notes, **kwargs
  74. )
  75. def find_release(self, version):
  76. print('Retrieving release draft for {}'.format(version))
  77. releases = self.gh_repo.get_releases()
  78. for release in releases:
  79. if release.tag_name == version and release.title == version:
  80. return release
  81. return None
  82. def remove_release(self, version):
  83. print('Removing release draft for {}'.format(version))
  84. releases = self.gh_repo.get_releases()
  85. for release in releases:
  86. if release.tag_name == version and release.title == version:
  87. if not release.draft:
  88. print(
  89. 'The release at {} is no longer a draft. If you TRULY intend '
  90. 'to remove it, please do so manually.'
  91. )
  92. continue
  93. release.delete_release()
  94. def remove_bump_branch(self, version, remote_name=None):
  95. name = branch_name(version)
  96. if not self.branch_exists(name):
  97. return False
  98. print('Removing local branch "{}"'.format(name))
  99. if self.git_repo.active_branch.name == name:
  100. print('Active branch is about to be deleted. Checking out to master...')
  101. try:
  102. self.checkout_branch('master')
  103. except GitCommandError:
  104. raise ScriptError(
  105. 'Unable to checkout master. Try stashing local changes before proceeding.'
  106. )
  107. self.git_repo.branches[name].delete(self.git_repo, name, force=True)
  108. print('Removing remote branch "{}"'.format(name))
  109. remote = self.find_remote(remote_name)
  110. try:
  111. remote.push(name, delete=True)
  112. except GitCommandError as e:
  113. if 'remote ref does not exist' in str(e):
  114. return False
  115. raise ScriptError(
  116. 'Error trying to remove remote branch: {}'.format(e)
  117. )
  118. return True
  119. def find_release_pr(self, version):
  120. print('Retrieving release PR for {}'.format(version))
  121. name = branch_name(version)
  122. open_prs = self.gh_repo.get_pulls(state='open')
  123. for pr in open_prs:
  124. if pr.head.ref == name:
  125. print('Found matching PR #{}'.format(pr.number))
  126. return pr
  127. print('No open PR for this release branch.')
  128. return None
  129. def close_release_pr(self, version):
  130. print('Retrieving and closing release PR for {}'.format(version))
  131. name = branch_name(version)
  132. open_prs = self.gh_repo.get_pulls(state='open')
  133. count = 0
  134. for pr in open_prs:
  135. if pr.head.ref == name:
  136. print('Found matching PR #{}'.format(pr.number))
  137. pr.edit(state='closed')
  138. count += 1
  139. if count == 0:
  140. print('No open PR for this release branch.')
  141. return count
  142. def get_contributors(pr_data):
  143. commits = pr_data.get_commits()
  144. authors = {}
  145. for commit in commits:
  146. author = commit.author.login
  147. authors[author] = authors.get(author, 0) + 1
  148. return [x[0] for x in sorted(list(authors.items()), key=lambda x: x[1])]
  149. def upload_assets(gh_release, files):
  150. print('Uploading binaries and hash sums')
  151. for filename, filedata in files.items():
  152. print('Uploading {}...'.format(filename))
  153. gh_release.upload_asset(filedata[0], content_type='application/octet-stream')
  154. gh_release.upload_asset('{}.sha256'.format(filedata[0]), content_type='text/plain')
  155. gh_release.upload_asset(
  156. os.path.join(REPO_ROOT, 'script', 'run', 'run.sh'), content_type='text/plain'
  157. )