git_wrapper.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. # -*- coding: utf-8 -*-
  2. """
  3. Wrap python git interface for compatibility with older/newer version
  4. """
  5. try:
  6. from itertools import zip_longest
  7. except ImportError:
  8. from six.moves import zip_longest
  9. import logging
  10. import os
  11. from time import mktime
  12. from datetime import datetime
  13. from pelican.utils import set_date_tzinfo
  14. from git import Git, Repo
  15. DEV_LOGGER = logging.getLogger(__name__)
  16. def grouper(iterable, n, fillvalue=None):
  17. '''
  18. Collect data into fixed-length chunks or blocks
  19. '''
  20. # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
  21. args = [iter(iterable)] * n
  22. return zip_longest(fillvalue=fillvalue, *args)
  23. class _GitWrapperCommon(object):
  24. '''
  25. Wrap git module to provide a more stable interface across versions
  26. '''
  27. def __init__(self, repo_path):
  28. self.git = Git()
  29. self.repo = Repo(os.path.abspath('.'))
  30. def is_file_managed_by_git(self, path):
  31. '''
  32. :param path: Path to check
  33. :returns: True if path is managed by git
  34. '''
  35. status, _stdout, _stderr = self.git.execute(
  36. ['git', 'ls-files', path.encode('utf-8'), '--error-unmatch'],
  37. with_extended_output=True,
  38. with_exceptions=False)
  39. return status == 0
  40. def is_file_modified(self, path):
  41. '''
  42. Does a file have local changes not yet committed
  43. :returns: True if file has local changes
  44. '''
  45. status, _stdout, _stderr = self.git.execute(
  46. ['git', 'diff', '--quiet', 'HEAD', path.encode('utf-8')],
  47. with_extended_output=True,
  48. with_exceptions=False)
  49. return status != 0
  50. def get_commits_following(self, path):
  51. '''
  52. Get all commits including path following the file through
  53. renames
  54. :param path: Path which we will find commits for
  55. :returns: Sequence of commit objects. Newest to oldest
  56. '''
  57. return [
  58. commit for commit, _ in self.get_commits_and_names_iter(
  59. path)]
  60. def get_commits_and_names_iter(self, path):
  61. '''
  62. Get all commits including a given path following renames
  63. '''
  64. log_result = self.git.log(
  65. '--pretty=%H',
  66. '--follow',
  67. '--name-only',
  68. '--',
  69. path.encode('utf-8')).splitlines()
  70. for commit_sha, _, filename in grouper(log_result, 3):
  71. yield self.repo.commit(commit_sha), filename
  72. def get_commits(self, path, follow=False):
  73. '''
  74. Get all commits including path
  75. :param path: Path which we will find commits for
  76. :param bool follow: If True we will follow path through renames
  77. :returns: Sequence of commit objects. Newest to oldest
  78. '''
  79. if follow:
  80. return self.get_commits_following(path)
  81. else:
  82. return self._get_commits(path)
  83. class _GitWrapperLegacy(_GitWrapperCommon):
  84. def _get_commits(self, path):
  85. '''
  86. Get all commits including path without following renames
  87. :param path: Path which we will find commits for
  88. :returns: Sequence of commit objects. Newest to oldest
  89. '''
  90. return self.repo.commits(path=path.encode('utf-8'))
  91. @staticmethod
  92. def get_commit_date(commit, tz_name):
  93. '''
  94. Get datetime of commit comitted_date
  95. '''
  96. return set_date_tzinfo(
  97. datetime.fromtimestamp(mktime(commit.committed_date)),
  98. tz_name=tz_name)
  99. class _GitWrapper(_GitWrapperCommon):
  100. def _get_commits(self, path):
  101. '''
  102. Get all commits including path without following renames
  103. :param path: Path which we will find commits for
  104. :returns: Sequence of commit objects. Newest to oldest
  105. .. NOTE ::
  106. If this fails it could be that your gitpython version is out of sync with the git
  107. binary on your distro. Make sure you use the correct gitpython version.
  108. Alternatively enabling GIT_FILETIME_FOLLOW may also make your problem go away.
  109. '''
  110. return list(self.repo.iter_commits(paths=path.encode('utf-8')))
  111. @staticmethod
  112. def get_commit_date(commit, tz_name):
  113. '''
  114. Get datetime of commit comitted_date
  115. '''
  116. return set_date_tzinfo(
  117. datetime.fromtimestamp(commit.committed_date),
  118. tz_name=tz_name)
  119. _wrapper_cache = {}
  120. def git_wrapper(path):
  121. '''
  122. Get appropriate wrapper factory and cache instance for path
  123. '''
  124. path = os.path.abspath(path)
  125. if path not in _wrapper_cache:
  126. if hasattr(Repo, 'commits'):
  127. _wrapper_cache[path] = _GitWrapperLegacy(path)
  128. else:
  129. _wrapper_cache[path] = _GitWrapper(path)
  130. return _wrapper_cache[path]