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.

680 lines
26KB

  1. import os
  2. from git.compat import (
  3. string_types,
  4. defenc
  5. )
  6. from git.objects import Object, Commit
  7. from git.util import (
  8. join_path,
  9. join_path_native,
  10. to_native_path_linux,
  11. assure_directory_exists,
  12. hex_to_bin,
  13. LockedFD
  14. )
  15. from gitdb.exc import (
  16. BadObject,
  17. BadName
  18. )
  19. import os.path as osp
  20. from .log import RefLog
  21. __all__ = ["SymbolicReference"]
  22. def _git_dir(repo, path):
  23. """ Find the git dir that's appropriate for the path"""
  24. name = "%s" % (path,)
  25. if name in ['HEAD', 'ORIG_HEAD', 'FETCH_HEAD', 'index', 'logs']:
  26. return repo.git_dir
  27. return repo.common_dir
  28. class SymbolicReference(object):
  29. """Represents a special case of a reference such that this reference is symbolic.
  30. It does not point to a specific commit, but to another Head, which itself
  31. specifies a commit.
  32. A typical example for a symbolic reference is HEAD."""
  33. __slots__ = ("repo", "path")
  34. _resolve_ref_on_create = False
  35. _points_to_commits_only = True
  36. _common_path_default = ""
  37. _remote_common_path_default = "refs/remotes"
  38. _id_attribute_ = "name"
  39. def __init__(self, repo, path):
  40. self.repo = repo
  41. self.path = path
  42. def __str__(self):
  43. return self.path
  44. def __repr__(self):
  45. return '<git.%s "%s">' % (self.__class__.__name__, self.path)
  46. def __eq__(self, other):
  47. if hasattr(other, 'path'):
  48. return self.path == other.path
  49. return False
  50. def __ne__(self, other):
  51. return not (self == other)
  52. def __hash__(self):
  53. return hash(self.path)
  54. @property
  55. def name(self):
  56. """
  57. :return:
  58. In case of symbolic references, the shortest assumable name
  59. is the path itself."""
  60. return self.path
  61. @property
  62. def abspath(self):
  63. return join_path_native(_git_dir(self.repo, self.path), self.path)
  64. @classmethod
  65. def _get_packed_refs_path(cls, repo):
  66. return osp.join(repo.common_dir, 'packed-refs')
  67. @classmethod
  68. def _iter_packed_refs(cls, repo):
  69. """Returns an iterator yielding pairs of sha1/path pairs (as bytes) for the corresponding refs.
  70. :note: The packed refs file will be kept open as long as we iterate"""
  71. try:
  72. with open(cls._get_packed_refs_path(repo), 'rt') as fp:
  73. for line in fp:
  74. line = line.strip()
  75. if not line:
  76. continue
  77. if line.startswith('#'):
  78. # "# pack-refs with: peeled fully-peeled sorted"
  79. # the git source code shows "peeled",
  80. # "fully-peeled" and "sorted" as the keywords
  81. # that can go on this line, as per comments in git file
  82. # refs/packed-backend.c
  83. # I looked at master on 2017-10-11,
  84. # commit 111ef79afe, after tag v2.15.0-rc1
  85. # from repo https://github.com/git/git.git
  86. if line.startswith('# pack-refs with:') and 'peeled' not in line:
  87. raise TypeError("PackingType of packed-Refs not understood: %r" % line)
  88. # END abort if we do not understand the packing scheme
  89. continue
  90. # END parse comment
  91. # skip dereferenced tag object entries - previous line was actual
  92. # tag reference for it
  93. if line[0] == '^':
  94. continue
  95. yield tuple(line.split(' ', 1))
  96. # END for each line
  97. except (OSError, IOError):
  98. return
  99. # END no packed-refs file handling
  100. # NOTE: Had try-finally block around here to close the fp,
  101. # but some python version wouldn't allow yields within that.
  102. # I believe files are closing themselves on destruction, so it is
  103. # alright.
  104. @classmethod
  105. def dereference_recursive(cls, repo, ref_path):
  106. """
  107. :return: hexsha stored in the reference at the given ref_path, recursively dereferencing all
  108. intermediate references as required
  109. :param repo: the repository containing the reference at ref_path"""
  110. while True:
  111. hexsha, ref_path = cls._get_ref_info(repo, ref_path)
  112. if hexsha is not None:
  113. return hexsha
  114. # END recursive dereferencing
  115. @classmethod
  116. def _get_ref_info_helper(cls, repo, ref_path):
  117. """Return: (str(sha), str(target_ref_path)) if available, the sha the file at
  118. rela_path points to, or None. target_ref_path is the reference we
  119. point to, or None"""
  120. tokens = None
  121. repodir = _git_dir(repo, ref_path)
  122. try:
  123. with open(osp.join(repodir, ref_path), 'rt') as fp:
  124. value = fp.read().rstrip()
  125. # Don't only split on spaces, but on whitespace, which allows to parse lines like
  126. # 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo
  127. tokens = value.split()
  128. assert(len(tokens) != 0)
  129. except (OSError, IOError):
  130. # Probably we are just packed, find our entry in the packed refs file
  131. # NOTE: We are not a symbolic ref if we are in a packed file, as these
  132. # are excluded explicitly
  133. for sha, path in cls._iter_packed_refs(repo):
  134. if path != ref_path:
  135. continue
  136. # sha will be used
  137. tokens = sha, path
  138. break
  139. # END for each packed ref
  140. # END handle packed refs
  141. if tokens is None:
  142. raise ValueError("Reference at %r does not exist" % ref_path)
  143. # is it a reference ?
  144. if tokens[0] == 'ref:':
  145. return (None, tokens[1])
  146. # its a commit
  147. if repo.re_hexsha_only.match(tokens[0]):
  148. return (tokens[0], None)
  149. raise ValueError("Failed to parse reference information from %r" % ref_path)
  150. @classmethod
  151. def _get_ref_info(cls, repo, ref_path):
  152. """Return: (str(sha), str(target_ref_path)) if available, the sha the file at
  153. rela_path points to, or None. target_ref_path is the reference we
  154. point to, or None"""
  155. return cls._get_ref_info_helper(repo, ref_path)
  156. def _get_object(self):
  157. """
  158. :return:
  159. The object our ref currently refers to. Refs can be cached, they will
  160. always point to the actual object as it gets re-created on each query"""
  161. # have to be dynamic here as we may be a tag which can point to anything
  162. # Our path will be resolved to the hexsha which will be used accordingly
  163. return Object.new_from_sha(self.repo, hex_to_bin(self.dereference_recursive(self.repo, self.path)))
  164. def _get_commit(self):
  165. """
  166. :return:
  167. Commit object we point to, works for detached and non-detached
  168. SymbolicReferences. The symbolic reference will be dereferenced recursively."""
  169. obj = self._get_object()
  170. if obj.type == 'tag':
  171. obj = obj.object
  172. # END dereference tag
  173. if obj.type != Commit.type:
  174. raise TypeError("Symbolic Reference pointed to object %r, commit was required" % obj)
  175. # END handle type
  176. return obj
  177. def set_commit(self, commit, logmsg=None):
  178. """As set_object, but restricts the type of object to be a Commit
  179. :raise ValueError: If commit is not a Commit object or doesn't point to
  180. a commit
  181. :return: self"""
  182. # check the type - assume the best if it is a base-string
  183. invalid_type = False
  184. if isinstance(commit, Object):
  185. invalid_type = commit.type != Commit.type
  186. elif isinstance(commit, SymbolicReference):
  187. invalid_type = commit.object.type != Commit.type
  188. else:
  189. try:
  190. invalid_type = self.repo.rev_parse(commit).type != Commit.type
  191. except (BadObject, BadName):
  192. raise ValueError("Invalid object: %s" % commit)
  193. # END handle exception
  194. # END verify type
  195. if invalid_type:
  196. raise ValueError("Need commit, got %r" % commit)
  197. # END handle raise
  198. # we leave strings to the rev-parse method below
  199. self.set_object(commit, logmsg)
  200. return self
  201. def set_object(self, object, logmsg=None): # @ReservedAssignment
  202. """Set the object we point to, possibly dereference our symbolic reference first.
  203. If the reference does not exist, it will be created
  204. :param object: a refspec, a SymbolicReference or an Object instance. SymbolicReferences
  205. will be dereferenced beforehand to obtain the object they point to
  206. :param logmsg: If not None, the message will be used in the reflog entry to be
  207. written. Otherwise the reflog is not altered
  208. :note: plain SymbolicReferences may not actually point to objects by convention
  209. :return: self"""
  210. if isinstance(object, SymbolicReference):
  211. object = object.object # @ReservedAssignment
  212. # END resolve references
  213. is_detached = True
  214. try:
  215. is_detached = self.is_detached
  216. except ValueError:
  217. pass
  218. # END handle non-existing ones
  219. if is_detached:
  220. return self.set_reference(object, logmsg)
  221. # set the commit on our reference
  222. return self._get_reference().set_object(object, logmsg)
  223. commit = property(_get_commit, set_commit, doc="Query or set commits directly")
  224. object = property(_get_object, set_object, doc="Return the object our ref currently refers to")
  225. def _get_reference(self):
  226. """:return: Reference Object we point to
  227. :raise TypeError: If this symbolic reference is detached, hence it doesn't point
  228. to a reference, but to a commit"""
  229. sha, target_ref_path = self._get_ref_info(self.repo, self.path)
  230. if target_ref_path is None:
  231. raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha))
  232. return self.from_path(self.repo, target_ref_path)
  233. def set_reference(self, ref, logmsg=None):
  234. """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference.
  235. Otherwise an Object, given as Object instance or refspec, is assumed and if valid,
  236. will be set which effectively detaches the refererence if it was a purely
  237. symbolic one.
  238. :param ref: SymbolicReference instance, Object instance or refspec string
  239. Only if the ref is a SymbolicRef instance, we will point to it. Everything
  240. else is dereferenced to obtain the actual object.
  241. :param logmsg: If set to a string, the message will be used in the reflog.
  242. Otherwise, a reflog entry is not written for the changed reference.
  243. The previous commit of the entry will be the commit we point to now.
  244. See also: log_append()
  245. :return: self
  246. :note: This symbolic reference will not be dereferenced. For that, see
  247. ``set_object(...)``"""
  248. write_value = None
  249. obj = None
  250. if isinstance(ref, SymbolicReference):
  251. write_value = "ref: %s" % ref.path
  252. elif isinstance(ref, Object):
  253. obj = ref
  254. write_value = ref.hexsha
  255. elif isinstance(ref, string_types):
  256. try:
  257. obj = self.repo.rev_parse(ref + "^{}") # optionally deref tags
  258. write_value = obj.hexsha
  259. except (BadObject, BadName):
  260. raise ValueError("Could not extract object from %s" % ref)
  261. # END end try string
  262. else:
  263. raise ValueError("Unrecognized Value: %r" % ref)
  264. # END try commit attribute
  265. # typecheck
  266. if obj is not None and self._points_to_commits_only and obj.type != Commit.type:
  267. raise TypeError("Require commit, got %r" % obj)
  268. # END verify type
  269. oldbinsha = None
  270. if logmsg is not None:
  271. try:
  272. oldbinsha = self.commit.binsha
  273. except ValueError:
  274. oldbinsha = Commit.NULL_BIN_SHA
  275. # END handle non-existing
  276. # END retrieve old hexsha
  277. fpath = self.abspath
  278. assure_directory_exists(fpath, is_file=True)
  279. lfd = LockedFD(fpath)
  280. fd = lfd.open(write=True, stream=True)
  281. ok = True
  282. try:
  283. fd.write(write_value.encode('ascii') + b'\n')
  284. lfd.commit()
  285. ok = True
  286. finally:
  287. if not ok:
  288. lfd.rollback()
  289. # Adjust the reflog
  290. if logmsg is not None:
  291. self.log_append(oldbinsha, logmsg)
  292. return self
  293. # aliased reference
  294. reference = property(_get_reference, set_reference, doc="Returns the Reference we point to")
  295. ref = reference
  296. def is_valid(self):
  297. """
  298. :return:
  299. True if the reference is valid, hence it can be read and points to
  300. a valid object or reference."""
  301. try:
  302. self.object
  303. except (OSError, ValueError):
  304. return False
  305. else:
  306. return True
  307. @property
  308. def is_detached(self):
  309. """
  310. :return:
  311. True if we are a detached reference, hence we point to a specific commit
  312. instead to another reference"""
  313. try:
  314. self.ref
  315. return False
  316. except TypeError:
  317. return True
  318. def log(self):
  319. """
  320. :return: RefLog for this reference. Its last entry reflects the latest change
  321. applied to this reference
  322. .. note:: As the log is parsed every time, its recommended to cache it for use
  323. instead of calling this method repeatedly. It should be considered read-only."""
  324. return RefLog.from_file(RefLog.path(self))
  325. def log_append(self, oldbinsha, message, newbinsha=None):
  326. """Append a logentry to the logfile of this ref
  327. :param oldbinsha: binary sha this ref used to point to
  328. :param message: A message describing the change
  329. :param newbinsha: The sha the ref points to now. If None, our current commit sha
  330. will be used
  331. :return: added RefLogEntry instance"""
  332. # NOTE: we use the committer of the currently active commit - this should be
  333. # correct to allow overriding the committer on a per-commit level.
  334. # See https://github.com/gitpython-developers/GitPython/pull/146
  335. try:
  336. committer_or_reader = self.commit.committer
  337. except ValueError:
  338. committer_or_reader = self.repo.config_reader()
  339. # end handle newly cloned repositories
  340. return RefLog.append_entry(committer_or_reader, RefLog.path(self), oldbinsha,
  341. (newbinsha is None and self.commit.binsha) or newbinsha,
  342. message)
  343. def log_entry(self, index):
  344. """:return: RefLogEntry at the given index
  345. :param index: python list compatible positive or negative index
  346. .. note:: This method must read part of the reflog during execution, hence
  347. it should be used sparringly, or only if you need just one index.
  348. In that case, it will be faster than the ``log()`` method"""
  349. return RefLog.entry_at(RefLog.path(self), index)
  350. @classmethod
  351. def to_full_path(cls, path):
  352. """
  353. :return: string with a full repository-relative path which can be used to initialize
  354. a Reference instance, for instance by using ``Reference.from_path``"""
  355. if isinstance(path, SymbolicReference):
  356. path = path.path
  357. full_ref_path = path
  358. if not cls._common_path_default:
  359. return full_ref_path
  360. if not path.startswith(cls._common_path_default + "/"):
  361. full_ref_path = '%s/%s' % (cls._common_path_default, path)
  362. return full_ref_path
  363. @classmethod
  364. def delete(cls, repo, path):
  365. """Delete the reference at the given path
  366. :param repo:
  367. Repository to delete the reference from
  368. :param path:
  369. Short or full path pointing to the reference, i.e. refs/myreference
  370. or just "myreference", hence 'refs/' is implied.
  371. Alternatively the symbolic reference to be deleted"""
  372. full_ref_path = cls.to_full_path(path)
  373. abs_path = osp.join(repo.common_dir, full_ref_path)
  374. if osp.exists(abs_path):
  375. os.remove(abs_path)
  376. else:
  377. # check packed refs
  378. pack_file_path = cls._get_packed_refs_path(repo)
  379. try:
  380. with open(pack_file_path, 'rb') as reader:
  381. new_lines = []
  382. made_change = False
  383. dropped_last_line = False
  384. for line in reader:
  385. # keep line if it is a comment or if the ref to delete is not
  386. # in the line
  387. # If we deleted the last line and this one is a tag-reference object,
  388. # we drop it as well
  389. line = line.decode(defenc)
  390. if (line.startswith('#') or full_ref_path not in line) and \
  391. (not dropped_last_line or dropped_last_line and not line.startswith('^')):
  392. new_lines.append(line)
  393. dropped_last_line = False
  394. continue
  395. # END skip comments and lines without our path
  396. # drop this line
  397. made_change = True
  398. dropped_last_line = True
  399. # write the new lines
  400. if made_change:
  401. # write-binary is required, otherwise windows will
  402. # open the file in text mode and change LF to CRLF !
  403. with open(pack_file_path, 'wb') as fd:
  404. fd.writelines(l.encode(defenc) for l in new_lines)
  405. except (OSError, IOError):
  406. pass # it didn't exist at all
  407. # delete the reflog
  408. reflog_path = RefLog.path(cls(repo, full_ref_path))
  409. if osp.isfile(reflog_path):
  410. os.remove(reflog_path)
  411. # END remove reflog
  412. @classmethod
  413. def _create(cls, repo, path, resolve, reference, force, logmsg=None):
  414. """internal method used to create a new symbolic reference.
  415. If resolve is False, the reference will be taken as is, creating
  416. a proper symbolic reference. Otherwise it will be resolved to the
  417. corresponding object and a detached symbolic reference will be created
  418. instead"""
  419. git_dir = _git_dir(repo, path)
  420. full_ref_path = cls.to_full_path(path)
  421. abs_ref_path = osp.join(git_dir, full_ref_path)
  422. # figure out target data
  423. target = reference
  424. if resolve:
  425. target = repo.rev_parse(str(reference))
  426. if not force and osp.isfile(abs_ref_path):
  427. target_data = str(target)
  428. if isinstance(target, SymbolicReference):
  429. target_data = target.path
  430. if not resolve:
  431. target_data = "ref: " + target_data
  432. with open(abs_ref_path, 'rb') as fd:
  433. existing_data = fd.read().decode(defenc).strip()
  434. if existing_data != target_data:
  435. raise OSError("Reference at %r does already exist, pointing to %r, requested was %r" %
  436. (full_ref_path, existing_data, target_data))
  437. # END no force handling
  438. ref = cls(repo, full_ref_path)
  439. ref.set_reference(target, logmsg)
  440. return ref
  441. @classmethod
  442. def create(cls, repo, path, reference='HEAD', force=False, logmsg=None):
  443. """Create a new symbolic reference, hence a reference pointing to another reference.
  444. :param repo:
  445. Repository to create the reference in
  446. :param path:
  447. full path at which the new symbolic reference is supposed to be
  448. created at, i.e. "NEW_HEAD" or "symrefs/my_new_symref"
  449. :param reference:
  450. The reference to which the new symbolic reference should point to.
  451. If it is a commit'ish, the symbolic ref will be detached.
  452. :param force:
  453. if True, force creation even if a symbolic reference with that name already exists.
  454. Raise OSError otherwise
  455. :param logmsg:
  456. If not None, the message to append to the reflog. Otherwise no reflog
  457. entry is written.
  458. :return: Newly created symbolic Reference
  459. :raise OSError:
  460. If a (Symbolic)Reference with the same name but different contents
  461. already exists.
  462. :note: This does not alter the current HEAD, index or Working Tree"""
  463. return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, logmsg)
  464. def rename(self, new_path, force=False):
  465. """Rename self to a new path
  466. :param new_path:
  467. Either a simple name or a full path, i.e. new_name or features/new_name.
  468. The prefix refs/ is implied for references and will be set as needed.
  469. In case this is a symbolic ref, there is no implied prefix
  470. :param force:
  471. If True, the rename will succeed even if a head with the target name
  472. already exists. It will be overwritten in that case
  473. :return: self
  474. :raise OSError: In case a file at path but a different contents already exists """
  475. new_path = self.to_full_path(new_path)
  476. if self.path == new_path:
  477. return self
  478. new_abs_path = osp.join(_git_dir(self.repo, new_path), new_path)
  479. cur_abs_path = osp.join(_git_dir(self.repo, self.path), self.path)
  480. if osp.isfile(new_abs_path):
  481. if not force:
  482. # if they point to the same file, its not an error
  483. with open(new_abs_path, 'rb') as fd1:
  484. f1 = fd1.read().strip()
  485. with open(cur_abs_path, 'rb') as fd2:
  486. f2 = fd2.read().strip()
  487. if f1 != f2:
  488. raise OSError("File at path %r already exists" % new_abs_path)
  489. # else: we could remove ourselves and use the otherone, but
  490. # but clarity we just continue as usual
  491. # END not force handling
  492. os.remove(new_abs_path)
  493. # END handle existing target file
  494. dname = osp.dirname(new_abs_path)
  495. if not osp.isdir(dname):
  496. os.makedirs(dname)
  497. # END create directory
  498. os.rename(cur_abs_path, new_abs_path)
  499. self.path = new_path
  500. return self
  501. @classmethod
  502. def _iter_items(cls, repo, common_path=None):
  503. if common_path is None:
  504. common_path = cls._common_path_default
  505. rela_paths = set()
  506. # walk loose refs
  507. # Currently we do not follow links
  508. for root, dirs, files in os.walk(join_path_native(repo.common_dir, common_path)):
  509. if 'refs' not in root.split(os.sep): # skip non-refs subfolders
  510. refs_id = [d for d in dirs if d == 'refs']
  511. if refs_id:
  512. dirs[0:] = ['refs']
  513. # END prune non-refs folders
  514. for f in files:
  515. if f == 'packed-refs':
  516. continue
  517. abs_path = to_native_path_linux(join_path(root, f))
  518. rela_paths.add(abs_path.replace(to_native_path_linux(repo.common_dir) + '/', ""))
  519. # END for each file in root directory
  520. # END for each directory to walk
  521. # read packed refs
  522. for sha, rela_path in cls._iter_packed_refs(repo): # @UnusedVariable
  523. if rela_path.startswith(common_path):
  524. rela_paths.add(rela_path)
  525. # END relative path matches common path
  526. # END packed refs reading
  527. # return paths in sorted order
  528. for path in sorted(rela_paths):
  529. try:
  530. yield cls.from_path(repo, path)
  531. except ValueError:
  532. continue
  533. # END for each sorted relative refpath
  534. @classmethod
  535. def iter_items(cls, repo, common_path=None):
  536. """Find all refs in the repository
  537. :param repo: is the Repo
  538. :param common_path:
  539. Optional keyword argument to the path which is to be shared by all
  540. returned Ref objects.
  541. Defaults to class specific portion if None assuring that only
  542. refs suitable for the actual class are returned.
  543. :return:
  544. git.SymbolicReference[], each of them is guaranteed to be a symbolic
  545. ref which is not detached and pointing to a valid ref
  546. List is lexicographically sorted
  547. The returned objects represent actual subclasses, such as Head or TagReference"""
  548. return (r for r in cls._iter_items(repo, common_path) if r.__class__ == SymbolicReference or not r.is_detached)
  549. @classmethod
  550. def from_path(cls, repo, path):
  551. """
  552. :param path: full .git-directory-relative path name to the Reference to instantiate
  553. :note: use to_full_path() if you only have a partial path of a known Reference Type
  554. :return:
  555. Instance of type Reference, Head, or Tag
  556. depending on the given path"""
  557. if not path:
  558. raise ValueError("Cannot create Reference from %r" % path)
  559. # Names like HEAD are inserted after the refs module is imported - we have an import dependency
  560. # cycle and don't want to import these names in-function
  561. from . import HEAD, Head, RemoteReference, TagReference, Reference
  562. for ref_type in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference):
  563. try:
  564. instance = ref_type(repo, path)
  565. if instance.__class__ == SymbolicReference and instance.is_detached:
  566. raise ValueError("SymbolRef was detached, we drop it")
  567. return instance
  568. except ValueError:
  569. pass
  570. # END exception handling
  571. # END for each type to try
  572. raise ValueError("Could not find reference type suitable to handle path %r" % path)
  573. def is_remote(self):
  574. """:return: True if this symbolic reference points to a remote branch"""
  575. return self.path.startswith(self._remote_common_path_default + "/")