git_wrapper.py 4.5 KB

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