repository.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import os
  4. import tempfile
  5. import requests
  6. from git import GitCommandError
  7. from git import Repo
  8. from github import Github
  9. from .const import NAME
  10. from .const import REPO_ROOT
  11. from .utils import branch_name
  12. from .utils import read_release_notes_from_changelog
  13. from .utils import ScriptError
  14. class Repository(object):
  15. def __init__(self, root=None, gh_name=None):
  16. if root is None:
  17. root = REPO_ROOT
  18. if gh_name is None:
  19. gh_name = NAME
  20. self.git_repo = Repo(root)
  21. self.gh_client = Github(os.environ['GITHUB_TOKEN'])
  22. self.gh_repo = self.gh_client.get_repo(gh_name)
  23. def create_release_branch(self, version, base=None):
  24. print('Creating release branch {} based on {}...'.format(version, base or 'master'))
  25. remote = self.find_remote(self.gh_repo.full_name)
  26. br_name = branch_name(version)
  27. remote.fetch()
  28. if self.branch_exists(br_name):
  29. raise ScriptError(
  30. "Branch {} already exists locally. Please remove it before "
  31. "running the release script, or use `resume` instead.".format(
  32. br_name
  33. )
  34. )
  35. if base is not None:
  36. base = self.git_repo.tag('refs/tags/{}'.format(base))
  37. else:
  38. base = 'refs/remotes/{}/master'.format(remote.name)
  39. release_branch = self.git_repo.create_head(br_name, commit=base)
  40. release_branch.checkout()
  41. self.git_repo.git.merge('--strategy=ours', '--no-edit', '{}/release'.format(remote.name))
  42. with release_branch.config_writer() as cfg:
  43. cfg.set_value('release', version)
  44. return release_branch
  45. def find_remote(self, remote_name=None):
  46. if not remote_name:
  47. remote_name = self.gh_repo.full_name
  48. for remote in self.git_repo.remotes:
  49. for url in remote.urls:
  50. if remote_name in url:
  51. return remote
  52. return None
  53. def create_bump_commit(self, bump_branch, version):
  54. print('Creating bump commit...')
  55. bump_branch.checkout()
  56. self.git_repo.git.commit('-a', '-s', '-m "Bump {}"'.format(version), '--no-verify')
  57. def diff(self):
  58. return self.git_repo.git.diff()
  59. def checkout_branch(self, name):
  60. return self.git_repo.branches[name].checkout()
  61. def push_branch_to_remote(self, branch, remote_name=None):
  62. print('Pushing branch {} to remote...'.format(branch.name))
  63. remote = self.find_remote(remote_name)
  64. remote.push(refspec=branch, force=True)
  65. def branch_exists(self, name):
  66. return name in [h.name for h in self.git_repo.heads]
  67. def create_release_pull_request(self, version):
  68. return self.gh_repo.create_pull(
  69. title='Bump {}'.format(version),
  70. body='Automated release for docker-compose {}\n\n{}'.format(
  71. version, read_release_notes_from_changelog()
  72. ),
  73. base='release',
  74. head=branch_name(version),
  75. )
  76. def create_release(self, version, release_notes, **kwargs):
  77. return self.gh_repo.create_git_release(
  78. tag=version, name=version, message=release_notes, **kwargs
  79. )
  80. def find_release(self, version):
  81. print('Retrieving release draft for {}'.format(version))
  82. releases = self.gh_repo.get_releases()
  83. for release in releases:
  84. if release.tag_name == version and release.title == version:
  85. return release
  86. return None
  87. def publish_release(self, release):
  88. release.update_release(
  89. name=release.title,
  90. message=release.body,
  91. draft=False,
  92. prerelease=release.prerelease
  93. )
  94. def remove_release(self, version):
  95. print('Removing release draft for {}'.format(version))
  96. releases = self.gh_repo.get_releases()
  97. for release in releases:
  98. if release.tag_name == version and release.title == version:
  99. if not release.draft:
  100. print(
  101. 'The release at {} is no longer a draft. If you TRULY intend '
  102. 'to remove it, please do so manually.'.format(release.url)
  103. )
  104. continue
  105. release.delete_release()
  106. def remove_bump_branch(self, version, remote_name=None):
  107. name = branch_name(version)
  108. if not self.branch_exists(name):
  109. return False
  110. print('Removing local branch "{}"'.format(name))
  111. if self.git_repo.active_branch.name == name:
  112. print('Active branch is about to be deleted. Checking out to master...')
  113. try:
  114. self.checkout_branch('master')
  115. except GitCommandError:
  116. raise ScriptError(
  117. 'Unable to checkout master. Try stashing local changes before proceeding.'
  118. )
  119. self.git_repo.branches[name].delete(self.git_repo, name, force=True)
  120. print('Removing remote branch "{}"'.format(name))
  121. remote = self.find_remote(remote_name)
  122. try:
  123. remote.push(name, delete=True)
  124. except GitCommandError as e:
  125. if 'remote ref does not exist' in str(e):
  126. return False
  127. raise ScriptError(
  128. 'Error trying to remove remote branch: {}'.format(e)
  129. )
  130. return True
  131. def find_release_pr(self, version):
  132. print('Retrieving release PR for {}'.format(version))
  133. name = branch_name(version)
  134. open_prs = self.gh_repo.get_pulls(state='open')
  135. for pr in open_prs:
  136. if pr.head.ref == name:
  137. print('Found matching PR #{}'.format(pr.number))
  138. return pr
  139. print('No open PR for this release branch.')
  140. return None
  141. def close_release_pr(self, version):
  142. print('Retrieving and closing release PR for {}'.format(version))
  143. name = branch_name(version)
  144. open_prs = self.gh_repo.get_pulls(state='open')
  145. count = 0
  146. for pr in open_prs:
  147. if pr.head.ref == name:
  148. print('Found matching PR #{}'.format(pr.number))
  149. pr.edit(state='closed')
  150. count += 1
  151. if count == 0:
  152. print('No open PR for this release branch.')
  153. return count
  154. def write_git_sha(self):
  155. with open(os.path.join(REPO_ROOT, 'compose', 'GITSHA'), 'w') as f:
  156. f.write(self.git_repo.head.commit.hexsha[:7])
  157. def cherry_pick_prs(self, release_branch, ids):
  158. if not ids:
  159. return
  160. release_branch.checkout()
  161. for i in ids:
  162. try:
  163. i = int(i)
  164. except ValueError as e:
  165. raise ScriptError('Invalid PR id: {}'.format(e))
  166. print('Retrieving PR#{}'.format(i))
  167. pr = self.gh_repo.get_pull(i)
  168. patch_data = requests.get(pr.patch_url).text
  169. self.apply_patch(patch_data)
  170. def apply_patch(self, patch_data):
  171. with tempfile.NamedTemporaryFile(mode='w', prefix='_compose_cherry', encoding='utf-8') as f:
  172. f.write(patch_data)
  173. f.flush()
  174. self.git_repo.git.am('--3way', f.name)
  175. def get_prs_in_milestone(self, version):
  176. milestones = self.gh_repo.get_milestones(state='open')
  177. milestone = None
  178. for ms in milestones:
  179. if ms.title == version:
  180. milestone = ms
  181. break
  182. if not milestone:
  183. print('Didn\'t find a milestone matching "{}"'.format(version))
  184. return None
  185. issues = self.gh_repo.get_issues(milestone=milestone, state='all')
  186. prs = []
  187. for issue in issues:
  188. if issue.pull_request is not None:
  189. prs.append(issue.number)
  190. return sorted(prs)
  191. def get_contributors(pr_data):
  192. commits = pr_data.get_commits()
  193. authors = {}
  194. for commit in commits:
  195. if not commit.author:
  196. continue
  197. author = commit.author.login
  198. authors[author] = authors.get(author, 0) + 1
  199. return [x[0] for x in sorted(list(authors.items()), key=lambda x: x[1])]
  200. def upload_assets(gh_release, files):
  201. print('Uploading binaries and hash sums')
  202. for filename, filedata in files.items():
  203. print('Uploading {}...'.format(filename))
  204. gh_release.upload_asset(filedata[0], content_type='application/octet-stream')
  205. gh_release.upload_asset('{}.sha256'.format(filedata[0]), content_type='text/plain')
  206. print('Uploading run.sh...')
  207. gh_release.upload_asset(
  208. os.path.join(REPO_ROOT, 'script', 'run', 'run.sh'), content_type='text/plain'
  209. )
  210. def delete_assets(gh_release):
  211. print('Removing previously uploaded assets')
  212. for asset in gh_release.get_assets():
  213. print('Deleting asset {}'.format(asset.name))
  214. asset.delete_asset()