def get_changesets(self, start=None, end=None, start_date=None, end_date=None, branch_name=None, reverse=False, max_revisions=None): """ Returns iterator of ``MercurialChangeset`` objects from start to end (both are inclusive) :param start: None, str, int or mercurial lookup format :param end: None, str, int or mercurial lookup format :param start_date: :param end_date: :param branch_name: :param reversed: return changesets in reversed order """ start_raw_id = self._get_revision(start) start_pos = None if start is None else self.revisions.index( start_raw_id) end_raw_id = self._get_revision(end) end_pos = None if end is None else self.revisions.index(end_raw_id) if start_pos is not None and end_pos is not None and start_pos > end_pos: raise RepositoryError("Start revision '%s' cannot be " "after end revision '%s'" % (start, end)) if branch_name and branch_name not in self.allbranches: msg = "Branch %r not found in %s" % (branch_name, self.name) raise BranchDoesNotExistError(msg) if end_pos is not None: end_pos += 1 # filter branches filter_ = [] if branch_name: filter_.append(b'branch("%s")' % safe_bytes(branch_name)) if start_date: filter_.append(b'date(">%s")' % safe_bytes(str(start_date))) if end_date: filter_.append(b'date("<%s")' % safe_bytes(str(end_date))) if filter_ or max_revisions: if filter_: revspec = b' and '.join(filter_) else: revspec = b'all()' if max_revisions: revspec = b'limit(%s, %d)' % (revspec, max_revisions) revisions = mercurial.scmutil.revrange(self._repo, [revspec]) else: revisions = self.revisions # this is very much a hack to turn this into a list; a better solution # would be to get rid of this function entirely and use revsets revs = list(revisions)[start_pos:end_pos] if reverse: revs.reverse() return CollectionGenerator(self, revs)
def remove_tag(self, name, user, message=None, date=None): """ Removes tag with the given ``name``. :param name: name of the tag to be removed :param user: full username, i.e.: "Joe Doe <*****@*****.**>" :param message: message of the tag's removal commit :param date: date of tag's removal commit :raises TagDoesNotExistError: if tag with given name does not exists """ if name not in self.tags: raise TagDoesNotExistError("Tag %s does not exist" % name) if message is None: message = "Removed tag %s" % name if date is None: date = safe_bytes( datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')) local = False try: mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.commands.nullid, safe_bytes(message), local, safe_bytes(user), date) self.tags = self._get_tags() except mercurial.error.Abort as e: raise RepositoryError(e.args[0])
def _get_repo(self, create, src_url=None, update_after_clone=False): """ Function will check for mercurial repository in given path and return a localrepo object. If there is no repository in that path it will raise an exception unless ``create`` parameter is set to True - in that case repository would be created and returned. If ``src_url`` is given, would try to clone repository from the location at given clone_point. Additionally it'll make update to working copy accordingly to ``update_after_clone`` flag """ try: if src_url: url = safe_bytes(self._get_url(src_url)) opts = {} if not update_after_clone: opts.update({'noupdate': True}) MercurialRepository._check_url(url, self.baseui) mercurial.commands.clone(self.baseui, url, safe_bytes(self.path), **opts) # Don't try to create if we've already cloned repo create = False return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create) except (mercurial.error.Abort, mercurial.error.RepoError) as err: if create: msg = "Cannot create repository at %s. Original error was %s" \ % (self.name, err) else: msg = "Not valid repository at %s. Original error was %s" \ % (self.name, err) raise RepositoryError(msg)
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 tag(self, name, user, revision=None, message=None, date=None, **kwargs): """ Creates and returns a tag for the given ``revision``. :param name: name for new tag :param user: full username, i.e.: "Joe Doe <*****@*****.**>" :param revision: changeset id for which new tag would be created :param message: message of the tag's commit :param date: date of tag's commit :raises TagAlreadyExistError: if tag with same name already exists """ if name in self.tags: raise TagAlreadyExistError("Tag %s already exists" % name) changeset = self.get_changeset(revision) message = message or "Added tag %s for commit %s" % (name, changeset.raw_id) self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id self._parsed_refs = self._get_parsed_refs() self.tags = self._get_tags() return changeset
def _check_url(cls, url): """ Function will check given url and try to verify if it's a valid link. Sometimes it may happened that git will issue basic auth request that can cause whole API to hang when used from python or other external calls. On failures it'll raise urllib2.HTTPError, exception is also thrown when the return code is non 200 """ # check first if it's not an local url if os.path.isdir(url) or url.startswith('file:'): return True if url.startswith('git://'): return True if '+' in url[:url.find('://')]: url = url[url.find('+') + 1:] handlers = [] url_obj = mercurial.util.url(safe_bytes(url)) test_uri, authinfo = url_obj.authinfo() if not test_uri.endswith(b'info/refs'): test_uri = test_uri.rstrip(b'/') + b'/info/refs' url_obj.passwd = b'*****' cleaned_uri = str(url_obj) if authinfo: # create a password manager passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() passmgr.add_password(*authinfo) handlers.extend((mercurial.url.httpbasicauthhandler(passmgr), mercurial.url.httpdigestauthhandler(passmgr))) o = urllib.request.build_opener(*handlers) o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git req = urllib.request.Request( "%s?%s" % (safe_str(test_uri), urllib.parse.urlencode({"service": 'git-upload-pack'}))) try: resp = o.open(req) if resp.code != 200: raise Exception('Return Code is not 200') except Exception as e: # means it cannot be cloned raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e)) # now detect if it's proper git repo gitdata = resp.read() if b'service=git-upload-pack' not in gitdata: raise urllib.error.URLError("url [%s] does not look like an git" % cleaned_uri) return True
def tag(self, name, user, revision=None, message=None, date=None, **kwargs): """ Creates and returns a tag for the given ``revision``. :param name: name for new tag :param user: full username, i.e.: "Joe Doe <*****@*****.**>" :param revision: changeset id for which new tag would be created :param message: message of the tag's commit :param date: date of tag's commit :raises TagAlreadyExistError: if tag with same name already exists """ if name in self.tags: raise TagAlreadyExistError("Tag %s already exists" % name) changeset = self.get_changeset(revision) local = kwargs.setdefault('local', False) if message is None: message = "Added tag %s for changeset %s" % (name, changeset.short_id) if date is None: date = safe_bytes( datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')) try: mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date) except mercurial.error.Abort as e: raise RepositoryError(e.args[0]) # Reinitialize tags self.tags = self._get_tags() tag_id = self.tags[name] return self.get_changeset(revision=tag_id)
def pull(self, url): """ Tries to pull changes from external location. """ other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url))) try: mercurial.exchange.pull(self._repo, other, heads=None, force=None) except mercurial.error.Abort as err: # Propagate error but with vcs's type raise RepositoryError(str(err))
def fill_archive(self, stream=None, kind='tgz', prefix=None, subrepos=False): """ Fills up given stream. :param stream: file like object. :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``. Default: ``tgz``. :param prefix: name of root directory in archive. Default is repository name and changeset's raw_id joined with dash (``repo-tip.<KIND>``). :param subrepos: include subrepos in this archive. :raise ImproperArchiveTypeError: If given kind is wrong. :raise VcsError: If given stream is None """ allowed_kinds = settings.ARCHIVE_SPECS if kind not in allowed_kinds: raise ImproperArchiveTypeError('Archive kind not supported use one' 'of %s' % ' '.join(allowed_kinds)) if stream is None: raise VCSError('You need to pass in a valid stream for filling' ' with archival data') if prefix is None: prefix = '%s-%s' % (self.repository.name, self.short_id) elif prefix.startswith('/'): raise VCSError("Prefix cannot start with leading slash") elif prefix.strip() == '': raise VCSError("Prefix cannot be empty") mercurial.archival.archive(self.repository._repo, stream, ascii_bytes(self.raw_id), safe_bytes(kind), prefix=safe_bytes(prefix), subrepos=subrepos)
def get_config_value(self, section, name=None, config_file=None): """ Returns configuration value for a given [``section``] and ``name``. :param section: Section we want to retrieve value from :param name: Name of configuration we want to retrieve :param config_file: A path to file which should be used to retrieve configuration from (might also be a list of file paths) """ if config_file is None: config_file = [] elif isinstance(config_file, str): config_file = [config_file] config = self._repo.ui if config_file: config = mercurial.ui.ui() for path in config_file: config.readconfig(safe_bytes(path)) value = config.config(safe_bytes(section), safe_bytes(name)) return value if value is None else safe_str(value)
def _serve(self): # Note: we want a repo with config based on .hg/hgrc and can thus not use self.db_repo.scm_instance._repo.ui baseui = make_ui(repo_path=self.db_repo.repo_full_path) if not self.allow_push: baseui.setconfig(b'hooks', b'pretxnopen._ssh_reject', b'python:kallithea.lib.hooks.rejectpush') baseui.setconfig(b'hooks', b'prepushkey._ssh_reject', b'python:kallithea.lib.hooks.rejectpush') repo = mercurial.hg.repository(baseui, safe_bytes(self.db_repo.repo_full_path)) log.debug("Starting Mercurial sshserver for %s", self.db_repo.repo_full_path) mercurial.wireprotoserver.sshserver(baseui, repo).serve_forever()
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 __init__(self, path, content=None, changeset=None, mode=None): """ Only one of ``content`` and ``changeset`` may be given. Passing both would raise ``NodeError`` exception. :param path: relative path to the node :param content: content may be passed to constructor :param changeset: if given, will use it to lazily fetch content :param mode: octal representation of ST_MODE (i.e. 0100644) """ if content and changeset: raise NodeError("Cannot use both content and changeset") super(FileNode, self).__init__(path, kind=NodeKind.FILE) self.changeset = changeset if not isinstance(content, bytes) and content is not None: # File content is one thing that inherently must be bytes ... but # VCS module tries to be "user friendly" and support unicode ... content = safe_bytes(content) self._content = content self._mode = mode or 0o100644
def _get_filectx(self, path): path = path.rstrip('/') if self._get_kind(path) != NodeKind.FILE: raise ChangesetError("File does not exist for revision %s at " " '%s'" % (self.raw_id, path)) return self._ctx.filectx(safe_bytes(path))
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
def _check_url(cls, url, repoui=None): """ Function will check given url and try to verify if it's a valid link. Sometimes it may happened that mercurial will issue basic auth request that can cause whole API to hang when used from python or other external calls. On failures it'll raise urllib2.HTTPError, exception is also thrown when the return code is non 200 """ # check first if it's not an local url url = safe_bytes(url) if os.path.isdir(url) or url.startswith(b'file:'): return True if url.startswith(b'ssh:'): # in case of invalid uri or authentication issues, sshpeer will # throw an exception. mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip') return True url_prefix = None if b'+' in url[:url.find(b'://')]: url_prefix, url = url.split(b'+', 1) handlers = [] url_obj = mercurial.util.url(url) test_uri, authinfo = url_obj.authinfo() url_obj.passwd = b'*****' cleaned_uri = str(url_obj) if authinfo: # create a password manager passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() passmgr.add_password(*authinfo) handlers.extend((mercurial.url.httpbasicauthhandler(passmgr), mercurial.url.httpdigestauthhandler(passmgr))) o = urllib.request.build_opener(*handlers) o.addheaders = [('Content-Type', 'application/mercurial-0.1'), ('Accept', 'application/mercurial-0.1')] req = urllib.request.Request( "%s?%s" % (test_uri, urllib.parse.urlencode({ 'cmd': 'between', 'pairs': "%s-%s" % ('0' * 40, '0' * 40), }))) try: resp = o.open(req) if resp.code != 200: raise Exception('Return Code is not 200') except Exception as e: # means it cannot be cloned raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e)) if not url_prefix: # skip svn+http://... (and git+... too) # now check if it's a proper hg repo try: mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip') except Exception as e: raise urllib.error.URLError( "url [%s] does not look like an hg repo org_exc: %s" % (cleaned_uri, e)) return True