You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

318 lines
11KB

  1. import re
  2. import time
  3. from git.compat import (
  4. PY3,
  5. xrange,
  6. string_types,
  7. defenc
  8. )
  9. from git.objects.util import (
  10. parse_date,
  11. Serializable,
  12. altz_to_utctz_str,
  13. )
  14. from git.util import (
  15. Actor,
  16. LockedFD,
  17. LockFile,
  18. assure_directory_exists,
  19. to_native_path,
  20. bin_to_hex,
  21. file_contents_ro_filepath
  22. )
  23. import os.path as osp
  24. __all__ = ["RefLog", "RefLogEntry"]
  25. class RefLogEntry(tuple):
  26. """Named tuple allowing easy access to the revlog data fields"""
  27. _re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$')
  28. __slots__ = ()
  29. def __repr__(self):
  30. """Representation of ourselves in git reflog format"""
  31. res = self.format()
  32. if PY3:
  33. return res
  34. else:
  35. # repr must return a string, which it will auto-encode from unicode using the default encoding.
  36. # This usually fails, so we encode ourselves
  37. return res.encode(defenc)
  38. def format(self):
  39. """:return: a string suitable to be placed in a reflog file"""
  40. act = self.actor
  41. time = self.time
  42. return u"{} {} {} <{}> {!s} {}\t{}\n".format(self.oldhexsha,
  43. self.newhexsha,
  44. act.name,
  45. act.email,
  46. time[0],
  47. altz_to_utctz_str(time[1]),
  48. self.message)
  49. @property
  50. def oldhexsha(self):
  51. """The hexsha to the commit the ref pointed to before the change"""
  52. return self[0]
  53. @property
  54. def newhexsha(self):
  55. """The hexsha to the commit the ref now points to, after the change"""
  56. return self[1]
  57. @property
  58. def actor(self):
  59. """Actor instance, providing access"""
  60. return self[2]
  61. @property
  62. def time(self):
  63. """time as tuple:
  64. * [0] = int(time)
  65. * [1] = int(timezone_offset) in time.altzone format """
  66. return self[3]
  67. @property
  68. def message(self):
  69. """Message describing the operation that acted on the reference"""
  70. return self[4]
  71. @classmethod
  72. def new(self, oldhexsha, newhexsha, actor, time, tz_offset, message):
  73. """:return: New instance of a RefLogEntry"""
  74. if not isinstance(actor, Actor):
  75. raise ValueError("Need actor instance, got %s" % actor)
  76. # END check types
  77. return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message))
  78. @classmethod
  79. def from_line(cls, line):
  80. """:return: New RefLogEntry instance from the given revlog line.
  81. :param line: line bytes without trailing newline
  82. :raise ValueError: If line could not be parsed"""
  83. line = line.decode(defenc)
  84. fields = line.split('\t', 1)
  85. if len(fields) == 1:
  86. info, msg = fields[0], None
  87. elif len(fields) == 2:
  88. info, msg = fields
  89. else:
  90. raise ValueError("Line must have up to two TAB-separated fields."
  91. " Got %s" % repr(line))
  92. # END handle first split
  93. oldhexsha = info[:40]
  94. newhexsha = info[41:81]
  95. for hexsha in (oldhexsha, newhexsha):
  96. if not cls._re_hexsha_only.match(hexsha):
  97. raise ValueError("Invalid hexsha: %r" % (hexsha,))
  98. # END if hexsha re doesn't match
  99. # END for each hexsha
  100. email_end = info.find('>', 82)
  101. if email_end == -1:
  102. raise ValueError("Missing token: >")
  103. # END handle missing end brace
  104. actor = Actor._from_string(info[82:email_end + 1])
  105. time, tz_offset = parse_date(info[email_end + 2:])
  106. return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg))
  107. class RefLog(list, Serializable):
  108. """A reflog contains reflog entries, each of which defines a certain state
  109. of the head in question. Custom query methods allow to retrieve log entries
  110. by date or by other criteria.
  111. Reflog entries are ordered, the first added entry is first in the list, the last
  112. entry, i.e. the last change of the head or reference, is last in the list."""
  113. __slots__ = ('_path', )
  114. def __new__(cls, filepath=None):
  115. inst = super(RefLog, cls).__new__(cls)
  116. return inst
  117. def __init__(self, filepath=None):
  118. """Initialize this instance with an optional filepath, from which we will
  119. initialize our data. The path is also used to write changes back using
  120. the write() method"""
  121. self._path = filepath
  122. if filepath is not None:
  123. self._read_from_file()
  124. # END handle filepath
  125. def _read_from_file(self):
  126. try:
  127. fmap = file_contents_ro_filepath(self._path, stream=True, allow_mmap=True)
  128. except OSError:
  129. # it is possible and allowed that the file doesn't exist !
  130. return
  131. # END handle invalid log
  132. try:
  133. self._deserialize(fmap)
  134. finally:
  135. fmap.close()
  136. # END handle closing of handle
  137. #{ Interface
  138. @classmethod
  139. def from_file(cls, filepath):
  140. """
  141. :return: a new RefLog instance containing all entries from the reflog
  142. at the given filepath
  143. :param filepath: path to reflog
  144. :raise ValueError: If the file could not be read or was corrupted in some way"""
  145. return cls(filepath)
  146. @classmethod
  147. def path(cls, ref):
  148. """
  149. :return: string to absolute path at which the reflog of the given ref
  150. instance would be found. The path is not guaranteed to point to a valid
  151. file though.
  152. :param ref: SymbolicReference instance"""
  153. return osp.join(ref.repo.git_dir, "logs", to_native_path(ref.path))
  154. @classmethod
  155. def iter_entries(cls, stream):
  156. """
  157. :return: Iterator yielding RefLogEntry instances, one for each line read
  158. sfrom the given stream.
  159. :param stream: file-like object containing the revlog in its native format
  160. or basestring instance pointing to a file to read"""
  161. new_entry = RefLogEntry.from_line
  162. if isinstance(stream, string_types):
  163. stream = file_contents_ro_filepath(stream)
  164. # END handle stream type
  165. while True:
  166. line = stream.readline()
  167. if not line:
  168. return
  169. yield new_entry(line.strip())
  170. # END endless loop
  171. stream.close()
  172. @classmethod
  173. def entry_at(cls, filepath, index):
  174. """:return: RefLogEntry at the given index
  175. :param filepath: full path to the index file from which to read the entry
  176. :param index: python list compatible index, i.e. it may be negative to
  177. specify an entry counted from the end of the list
  178. :raise IndexError: If the entry didn't exist
  179. .. note:: This method is faster as it only parses the entry at index, skipping
  180. all other lines. Nonetheless, the whole file has to be read if
  181. the index is negative
  182. """
  183. fp = open(filepath, 'rb')
  184. if index < 0:
  185. return RefLogEntry.from_line(fp.readlines()[index].strip())
  186. else:
  187. # read until index is reached
  188. for i in xrange(index + 1):
  189. line = fp.readline()
  190. if not line:
  191. break
  192. # END abort on eof
  193. # END handle runup
  194. if i != index or not line:
  195. raise IndexError
  196. # END handle exception
  197. return RefLogEntry.from_line(line.strip())
  198. # END handle index
  199. def to_file(self, filepath):
  200. """Write the contents of the reflog instance to a file at the given filepath.
  201. :param filepath: path to file, parent directories are assumed to exist"""
  202. lfd = LockedFD(filepath)
  203. assure_directory_exists(filepath, is_file=True)
  204. fp = lfd.open(write=True, stream=True)
  205. try:
  206. self._serialize(fp)
  207. lfd.commit()
  208. except Exception:
  209. # on failure it rolls back automatically, but we make it clear
  210. lfd.rollback()
  211. raise
  212. # END handle change
  213. @classmethod
  214. def append_entry(cls, config_reader, filepath, oldbinsha, newbinsha, message):
  215. """Append a new log entry to the revlog at filepath.
  216. :param config_reader: configuration reader of the repository - used to obtain
  217. user information. May also be an Actor instance identifying the committer directly.
  218. May also be None
  219. :param filepath: full path to the log file
  220. :param oldbinsha: binary sha of the previous commit
  221. :param newbinsha: binary sha of the current commit
  222. :param message: message describing the change to the reference
  223. :param write: If True, the changes will be written right away. Otherwise
  224. the change will not be written
  225. :return: RefLogEntry objects which was appended to the log
  226. :note: As we are append-only, concurrent access is not a problem as we
  227. do not interfere with readers."""
  228. if len(oldbinsha) != 20 or len(newbinsha) != 20:
  229. raise ValueError("Shas need to be given in binary format")
  230. # END handle sha type
  231. assure_directory_exists(filepath, is_file=True)
  232. first_line = message.split('\n')[0]
  233. committer = isinstance(config_reader, Actor) and config_reader or Actor.committer(config_reader)
  234. entry = RefLogEntry((
  235. bin_to_hex(oldbinsha).decode('ascii'),
  236. bin_to_hex(newbinsha).decode('ascii'),
  237. committer, (int(time.time()), time.altzone), first_line
  238. ))
  239. lf = LockFile(filepath)
  240. lf._obtain_lock_or_raise()
  241. fd = open(filepath, 'ab')
  242. try:
  243. fd.write(entry.format().encode(defenc))
  244. finally:
  245. fd.close()
  246. lf._release_lock()
  247. # END handle write operation
  248. return entry
  249. def write(self):
  250. """Write this instance's data to the file we are originating from
  251. :return: self"""
  252. if self._path is None:
  253. raise ValueError("Instance was not initialized with a path, use to_file(...) instead")
  254. # END assert path
  255. self.to_file(self._path)
  256. return self
  257. #} END interface
  258. #{ Serializable Interface
  259. def _serialize(self, stream):
  260. write = stream.write
  261. # write all entries
  262. for e in self:
  263. write(e.format().encode(defenc))
  264. # END for each entry
  265. def _deserialize(self, stream):
  266. self.extend(self.iter_entries(stream))
  267. #} END serializable interface