def _get_revision(self, revision): """ Given any revision identifier, returns a 40 char string with revision hash. :param revision: str or int or None """ if self._empty: raise EmptyRepositoryError("There are no changesets yet") if revision in [-1, None]: revision = b'tip' elif isinstance(revision, str): revision = safe_bytes(revision) try: if isinstance(revision, int): return ascii_str(self._repo[revision].hex()) return ascii_str( mercurial.scmutil.revsymbol(self._repo, revision).hex()) except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError): msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name) raise ChangesetDoesNotExistError(msg) except (LookupError, ): msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name) raise ChangesetDoesNotExistError(msg)
def get_nodes(self, path): """ Returns combined ``DirNode`` and ``FileNode`` objects list representing state of changeset at the given ``path``. If node at the given ``path`` is not instance of ``DirNode``, ChangesetError would be raised. """ if self._get_kind(path) != NodeKind.DIR: raise ChangesetError("Directory does not exist for revision %s at " " '%s'" % (self.revision, path)) path = path.rstrip('/') id = self._get_id_for_path(path) tree = self.repository._repo[id] dirnodes = [] filenodes = [] als = self.repository.alias for name, stat, id in tree.items(): obj_path = safe_str(name) if path != '': obj_path = '/'.join((path, obj_path)) if objects.S_ISGITLINK(stat): root_tree = self.repository._repo[self._tree_id] cf = ConfigFile.from_file( BytesIO( self.repository._repo.get_object( root_tree[b'.gitmodules'][1]).data)) url = ascii_str(cf.get(('submodule', obj_path), 'url')) dirnodes.append( SubModuleNode(obj_path, url=url, changeset=ascii_str(id), alias=als)) continue obj = self.repository._repo.get_object(id) if obj_path not in self._stat_modes: self._stat_modes[obj_path] = stat if isinstance(obj, objects.Tree): dirnodes.append(DirNode(obj_path, changeset=self)) elif isinstance(obj, objects.Blob): filenodes.append(FileNode(obj_path, changeset=self, mode=stat)) else: raise ChangesetError("Requested object should be Tree " "or Blob, is %r" % type(obj)) nodes = dirnodes + filenodes for node in nodes: if node.path not in self.nodes: self.nodes[node.path] = node nodes.sort() return nodes
def __init__(self, repository, revision): self.repository = repository assert isinstance(revision, str), repr(revision) self._ctx = repository._repo[ascii_bytes(revision)] self.raw_id = ascii_str(self._ctx.hex()) self.revision = self._ctx._rev self.nodes = {}
def _get_all_revisions2(self): # alternate implementation using dulwich includes = [ ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items() if type_ != b'T' ] return [c.commit.id for c in self._repo.get_walker(include=includes)]
def get_node(self, path): """ Returns ``Node`` object from the given ``path``. If there is no node at the given ``path``, ``ChangesetError`` would be raised. """ path = path.rstrip('/') if path not in self.nodes: try: id_ = self._get_id_for_path(path) except ChangesetError: raise NodeDoesNotExistError( "Cannot find one of parents' " "directories for a given path: %s" % path) stat = self._stat_modes.get(path) if stat and objects.S_ISGITLINK(stat): tree = self.repository._repo[self._tree_id] cf = ConfigFile.from_file( BytesIO( self.repository._repo.get_object( tree[b'.gitmodules'][1]).data)) url = ascii_str(cf.get(('submodule', path), 'url')) node = SubModuleNode(path, url=url, changeset=ascii_str(id_), alias=self.repository.alias) else: obj = self.repository._repo.get_object(id_) if isinstance(obj, objects.Tree): if path == '': node = RootNode(changeset=self) else: node = DirNode(path, changeset=self) node._tree = obj elif isinstance(obj, objects.Blob): node = FileNode(path, changeset=self) node._blob = obj else: raise NodeDoesNotExistError( "There is no file nor directory " "at the given path: '%s' at revision %s" % (path, self.short_id)) # cache node self.nodes[path] = node return self.nodes[path]
def _get_tags(self): if not self.revisions: return {} _tags = [(safe_str(key), ascii_str(sha)) for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T'] return OrderedDict( sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
def parents(self): """ Returns list of parents changesets. """ return [ self.repository.get_changeset(ascii_str(parent_id)) for parent_id in self._commit.parents ]
def branches(self): if not self.revisions: return {} _branches = [(safe_str(key), ascii_str(sha)) for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H'] return OrderedDict( sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
def _get_tags(self): if self._empty: return {} return OrderedDict( sorted( ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()), reverse=True, key=lambda x: x[0], # sort by name ))
def _get_bookmarks(self): if self._empty: return {} return OrderedDict( sorted( ((safe_str(n), ascii_str(h)) for n, h in self._repo._bookmarks.items()), reverse=True, key=lambda x: x[0], # sort by name ))
def get_file_annotate(self, path): """ Returns a generator of four element tuples with lineno, sha, changeset lazy loader and line """ annotations = self._get_filectx(path).annotate() annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations] for i, (fctx, line) in enumerate(annotation_lines): sha = ascii_str(fctx.hex()) yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line)
def generate_api_key(): """ Generates a random (presumably unique) API key. This value is used in URLs and "Bearer" HTTP Authorization headers, which in practice means it should only contain URL-safe characters (RFC 3986): unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" """ # Hexadecimal certainly qualifies as URL-safe. return ascii_str(binascii.hexlify(os.urandom(20)))
def _get_branches(self, normal=True, closed=False): """ Gets branches for this repository Returns only not closed branches by default :param closed: return also closed branches for mercurial :param normal: return also normal branches """ if self._empty: return {} bt = OrderedDict() for bn, _heads, node, isclosed in sorted( self._repo.branchmap().iterbranches()): if isclosed: if closed: bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node)) else: if normal: bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node)) return bt
def _get_revision(self, revision): """ Given any revision identifier, returns a 40 char string with revision hash. """ if self._empty: raise EmptyRepositoryError("There are no changesets yet") if revision in (None, '', 'tip', 'HEAD', 'head', -1): revision = -1 if isinstance(revision, int): try: return self.revisions[revision] except IndexError: msg = "Revision %r does not exist for %s" % (revision, self.name) raise ChangesetDoesNotExistError(msg) if isinstance(revision, str): if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')): try: return self.revisions[int(revision)] except IndexError: msg = "Revision %r does not exist for %s" % (revision, self) raise ChangesetDoesNotExistError(msg) # get by branch/tag name _ref_revision = self._parsed_refs.get(safe_bytes(revision)) if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']: return ascii_str(_ref_revision[0]) if revision in self.revisions: return revision # maybe it's a tag ? we don't have them in self.revisions if revision in self.tags.values(): return revision if SHA_PATTERN.match(revision): msg = "Revision %r does not exist for %s" % (revision, self.name) raise ChangesetDoesNotExistError(msg) raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
def get_file_history_2(self, path): """ Returns history of file as reversed list of ``Changeset`` objects for which file at given ``path`` has been modified. """ self._get_filectx(path) from dulwich.walk import Walker include = [self.raw_id] walker = Walker(self.repository._repo.object_store, include, paths=[path], max_entries=1) return [ self.repository.get_changeset(ascii_str(x.commit.id.decode)) for x in walker ]
def __init__(self, repository, revision): self._stat_modes = {} self.repository = repository try: commit = self.repository._repo[ascii_bytes(revision)] if isinstance(commit, objects.Tag): revision = safe_str(commit.object[1]) commit = self.repository._repo.get_object(commit.object[1]) except KeyError: raise RepositoryError("Cannot get object with id %s" % revision) self.raw_id = ascii_str(commit.id) self.short_id = self.raw_id[:12] self._commit = commit # a Dulwich Commmit with .id self._tree_id = commit.tree self._committer_property = 'committer' self._author_property = 'author' self._date_property = 'commit_time' self._date_tz_property = 'commit_timezone' self.revision = repository.revisions.index(self.raw_id) self.nodes = {} self._paths = {}
def _get_all_revisions(self): return [ ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs() ]
def get_changeset(self): wk_dir_id = ascii_str(self.repository._repo[None].parents()[0].hex()) return self.repository.get_changeset(wk_dir_id)
def commit(self, message, author, parents=None, branch=None, date=None, **kwargs): """ Performs in-memory commit (doesn't check workdir in any way) and returns newly created ``Changeset``. Updates repository's ``revisions``. :param message: message of the commit :param author: full username, i.e. "Joe Doe <*****@*****.**>" :param parents: single parent or sequence of parents from which commit would be derived :param date: ``datetime.datetime`` instance. Defaults to ``datetime.datetime.now()``. :param branch: branch name, as string. If none given, default backend's branch would be used. :raises ``CommitError``: if any error occurs while committing """ self.check_integrity(parents) if not isinstance(message, str): raise RepositoryError('message must be a str - got %r' % type(message)) if not isinstance(author, str): raise RepositoryError('author must be a str - got %r' % type(author)) from .repository import MercurialRepository if branch is None: branch = MercurialRepository.DEFAULT_BRANCH_NAME kwargs[b'branch'] = safe_bytes(branch) def filectxfn(_repo, memctx, bytes_path): """ Callback from Mercurial, returning ctx to commit for the given path. """ path = safe_str(bytes_path) # check if this path is removed if path in (node.path for node in self.removed): return None # check if this path is added for node in self.added: if node.path == path: return mercurial.context.memfilectx( _repo, memctx, path=bytes_path, data=node.content, islink=False, isexec=node.is_executable, copysource=False) # or changed for node in self.changed: if node.path == path: return mercurial.context.memfilectx( _repo, memctx, path=bytes_path, data=node.content, islink=False, isexec=node.is_executable, copysource=False) raise RepositoryError("Given path haven't been marked as added, " "changed or removed (%s)" % path) parents = [None, None] for i, parent in enumerate(self.parents): if parent is not None: parents[i] = parent._ctx.node() if date and isinstance(date, datetime.datetime): date = safe_bytes(date.strftime('%a, %d %b %Y %H:%M:%S')) commit_ctx = mercurial.context.memctx( repo=self.repository._repo, parents=parents, text=b'', files=[safe_bytes(x) for x in self.get_paths()], filectxfn=filectxfn, user=safe_bytes(author), date=date, extra=kwargs) # injecting given _repo params commit_ctx._text = safe_bytes(message) commit_ctx._user = safe_bytes(author) commit_ctx._date = date # TODO: Catch exceptions! n = self.repository._repo.commitctx(commit_ctx) # Returns mercurial node self._commit_ctx = commit_ctx # For reference # Update vcs repository object & recreate mercurial _repo # new_ctx = self.repository._repo[node] # new_tip = ascii_str(self.repository.get_changeset(new_ctx.hex())) self.repository.revisions.append(ascii_str(mercurial.node.hex(n))) self._repo = self.repository._get_repo(create=False) self.repository.branches = self.repository._get_branches() tip = self.repository.get_changeset() self.reset() return tip
def commit(self, message, author, parents=None, branch=None, date=None, **kwargs): """ Performs in-memory commit (doesn't check workdir in any way) and returns newly created ``Changeset``. Updates repository's ``revisions``. :param message: message of the commit :param author: full username, i.e. "Joe Doe <*****@*****.**>" :param parents: single parent or sequence of parents from which commit would be derived :param date: ``datetime.datetime`` instance. Defaults to ``datetime.datetime.now()``. :param branch: branch name, as string. If none given, default backend's branch would be used. :raises ``CommitError``: if any error occurs while committing """ self.check_integrity(parents) from .repository import GitRepository if branch is None: branch = GitRepository.DEFAULT_BRANCH_NAME repo = self.repository._repo object_store = repo.object_store ENCODING = b"UTF-8" # TODO: should probably be kept in sync with safe_str/safe_bytes and vcs/conf/settings.py DEFAULT_ENCODINGS # Create tree and populates it with blobs commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \ objects.Tree() for node in self.added + self.changed: # Compute subdirs if needed dirpath, nodename = posixpath.split(node.path) dirnames = safe_bytes(dirpath).split(b'/') if dirpath else [] parent = commit_tree ancestors = [('', parent)] # Tries to dig for the deepest existing tree while dirnames: curdir = dirnames.pop(0) try: dir_id = parent[curdir][1] except KeyError: # put curdir back into dirnames and stops dirnames.insert(0, curdir) break else: # If found, updates parent parent = self.repository._repo[dir_id] ancestors.append((curdir, parent)) # Now parent is deepest existing tree and we need to create subtrees # for dirnames (in reverse order) [this only applies for nodes from added] new_trees = [] blob = objects.Blob.from_string(node.content) node_path = safe_bytes(node.name) if dirnames: # If there are trees which should be created we need to build # them now (in reverse order) reversed_dirnames = list(reversed(dirnames)) curtree = objects.Tree() curtree[node_path] = node.mode, blob.id new_trees.append(curtree) for dirname in reversed_dirnames[:-1]: newtree = objects.Tree() #newtree.add(stat.S_IFDIR, dirname, curtree.id) newtree[dirname] = stat.S_IFDIR, curtree.id new_trees.append(newtree) curtree = newtree parent[reversed_dirnames[-1]] = stat.S_IFDIR, curtree.id else: parent.add(name=node_path, mode=node.mode, hexsha=blob.id) new_trees.append(parent) # Update ancestors for parent, tree, path in reversed([ (a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:]) ]): parent[path] = stat.S_IFDIR, tree.id object_store.add_object(tree) object_store.add_object(blob) for tree in new_trees: object_store.add_object(tree) for node in self.removed: paths = safe_bytes(node.path).split(b'/') tree = commit_tree trees = [tree] # Traverse deep into the forest... for path in paths: try: obj = self.repository._repo[tree[path][1]] if isinstance(obj, objects.Tree): trees.append(obj) tree = obj except KeyError: break # Cut down the blob and all rotten trees on the way back... for path, tree in reversed(list(zip(paths, trees))): del tree[path] if tree: # This tree still has elements - don't remove it or any # of it's parents break object_store.add_object(commit_tree) # Create commit commit = objects.Commit() commit.tree = commit_tree.id commit.parents = [p._commit.id for p in self.parents if p] commit.author = commit.committer = safe_bytes(author) commit.encoding = ENCODING commit.message = safe_bytes(message) # Compute date if date is None: date = time.time() elif isinstance(date, datetime.datetime): date = time.mktime(date.timetuple()) author_time = kwargs.pop('author_time', date) commit.commit_time = int(date) commit.author_time = int(author_time) tz = time.timezone author_tz = kwargs.pop('author_timezone', tz) commit.commit_timezone = tz commit.author_timezone = author_tz object_store.add_object(commit) # Update vcs repository object & recreate dulwich repo ref = b'refs/heads/%s' % safe_bytes(branch) repo.refs[ref] = commit.id self.repository.revisions.append(ascii_str(commit.id)) # invalidate parsed refs after commit self.repository._parsed_refs = self.repository._get_parsed_refs() tip = self.repository.get_changeset() self.reset() return tip