repo.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2015-2019 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import print_function, unicode_literals, absolute_import
  8. import logging
  9. import subprocess
  10. import pygit2
  11. import pagure
  12. import pagure.exceptions
  13. _log = logging.getLogger(__name__)
  14. def get_pygit2_version():
  15. """Return pygit2 version as a tuple of integers.
  16. This is needed for correct version comparison.
  17. """
  18. return tuple([int(i) for i in pygit2.__version__.split(".")])
  19. def run_command(command, cwd=None):
  20. _log.info("Running command: %s", command)
  21. try:
  22. out = subprocess.check_output(
  23. command, stderr=subprocess.STDOUT, cwd=cwd
  24. ).decode("utf-8")
  25. _log.info(" command ran successfully")
  26. _log.debug("Output: %s" % out)
  27. except subprocess.CalledProcessError as err:
  28. _log.debug(
  29. "Command FAILED: {cmd} returned code {code} with the "
  30. "following output: {output}".format(
  31. cmd=err.cmd, code=err.returncode, output=err.output
  32. )
  33. )
  34. raise pagure.exceptions.PagureException(
  35. "Did not manage to rebase this pull-request"
  36. )
  37. return out
  38. class PagureRepo(pygit2.Repository):
  39. """An utility class allowing to go around pygit2's inability to be
  40. stable.
  41. """
  42. @staticmethod
  43. def clone(path_from, path_to, checkout_branch=None, bare=False):
  44. """Clone the git repo at the specified path to the specified location.
  45. This method is meant to replace pygit2.clone_repository which for us
  46. leaks file descriptors on large project leading to "Too many open files
  47. error" which then prevent some tasks from completing.
  48. :arg path_from: the path or url of the git repository to clone
  49. :type path_from: str
  50. :arg path_to: the path where the git repository should be cloned
  51. :type path_to: str
  52. :
  53. """
  54. cmd = ["git", "clone", path_from, path_to]
  55. if checkout_branch:
  56. cmd.extend(["-b", checkout_branch])
  57. if bare:
  58. cmd.append("--bare")
  59. run_command(cmd)
  60. @staticmethod
  61. def push(remote, refname):
  62. """ Push the given reference to the specified remote. """
  63. pygit2_version = get_pygit2_version()
  64. if pygit2_version >= (0, 22):
  65. remote.push([refname])
  66. else:
  67. remote.push(refname)
  68. def pull(self, remote_name="origin", branch="master", force=False):
  69. """pull changes for the specified remote (defaults to origin).
  70. Code from MichaelBoselowitz at:
  71. https://github.com/MichaelBoselowitz/pygit2-examples/blob/
  72. 68e889e50a592d30ab4105a2e7b9f28fac7324c8/examples.py#L58
  73. licensed under the MIT license.
  74. """
  75. for remote in self.remotes:
  76. if remote.name == remote_name:
  77. remote.fetch()
  78. remote_master_id = self.lookup_reference(
  79. "refs/remotes/origin/%s" % branch
  80. ).target
  81. if force:
  82. repo_branch = self.lookup_reference(
  83. "refs/heads/%s" % branch
  84. )
  85. repo_branch.set_target(remote_master_id)
  86. merge_result, _ = self.merge_analysis(remote_master_id)
  87. # Up to date, do nothing
  88. if merge_result & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE:
  89. return
  90. # We can just fastforward
  91. elif merge_result & pygit2.GIT_MERGE_ANALYSIS_FASTFORWARD:
  92. self.checkout_tree(self.get(remote_master_id))
  93. master_ref = self.lookup_reference(
  94. "refs/heads/%s" % branch
  95. )
  96. master_ref.set_target(remote_master_id)
  97. self.head.set_target(remote_master_id)
  98. elif merge_result & pygit2.GIT_MERGE_ANALYSIS_NORMAL:
  99. raise pagure.exceptions.GitConflictsException(
  100. "Pulling remote changes leads to a conflict"
  101. )
  102. else:
  103. _log.debug(
  104. "Unexpected merge result: %s"
  105. % (pygit2.GIT_MERGE_ANALYSIS_NORMAL)
  106. )
  107. raise AssertionError("Unknown merge analysis result")
  108. @staticmethod
  109. def log(path, log_options=None, target=None, fromref=None):
  110. """Run git log with the specified options at the specified target.
  111. This method runs the system's `git log` command since pygit2 doesn't
  112. offer us the possibility to do this via them.
  113. :kwarg log_options: options to pass to git log
  114. :type log_options: list or None
  115. :kwarg target: the target of the git log command, can be a ref, a
  116. file or nothing
  117. :type path_to: str or None
  118. :kwarg fromref: a reference/commit to use when generating the log
  119. :type path_to: str or None
  120. :
  121. """
  122. cmd = ["git", "log"]
  123. if log_options:
  124. cmd.extend(log_options)
  125. if fromref:
  126. cmd.append(fromref)
  127. if target:
  128. cmd.extend(["--", target])
  129. return run_command(cmd, cwd=path)
  130. @staticmethod
  131. def get_active_branches(path, nbranch=8, catch_exception=False):
  132. """Returns the active branches in the git repo at the specified
  133. location.
  134. :arg path: the location of the git repo
  135. :type path: str
  136. :kwarg nbranch: the number of active branches to consider, defaults to
  137. 6
  138. :type nbranch: int
  139. :kwarg catch_exception: Whether or not this method should silently
  140. ignore all exception raised when running the git command and return
  141. an empty list in those situations. Defaults to False.
  142. :type catch_exception: boolean
  143. """
  144. cmd = [
  145. "git",
  146. "for-each-ref",
  147. "--count=%s" % nbranch,
  148. "--sort=-committerdate",
  149. "refs/heads/",
  150. ]
  151. output = []
  152. try:
  153. cmd_output = run_command(cmd, path)
  154. for row in cmd_output.split("\n"):
  155. output.append(
  156. row.replace("\t", " ")
  157. .rsplit(" ", 1)[-1]
  158. .replace("refs/heads/", "")
  159. )
  160. except Exception:
  161. if not catch_exception:
  162. raise
  163. return output