class SageDev(object): def __init__(self, devrc='~/.sage/devrc', gitcmd='git', realm='sage.math.washington.edu', trac='http://trac.sagemath.org/experimental/'): devrc = os.path.expanduser(devrc) username, password = self.process_rc(devrc) self.UI = CmdLineInterface() self.git = GitInterface(self.UI, username, gitcmd) self.trac = TracInterface(self.UI, realm, trac, username, password) def process_rc(self, devrc): with open(devrc) as F: L = list(F) username = L[0].strip() passwd = L[1].strip() return username, passwd def start(self, ticketnum = None): curticket = self.git.current_ticket() if ticketnum is None: # User wants to create a ticket ticketnum = self.trac.create_ticket_interactive() if ticketnum is None: # They didn't succeed. return if curticket is not None: if self.UI.confirm("Should the new ticket depend on #%s?"%(curticket)): self.git.create_branch(self, ticketnum) self.trac.add_dependency(self, ticketnum, curticket) else: self.git.create_branch(self, ticketnum, at_unstable=True) dest = None if self.git.has_uncommitted_changes(): if curticket is None: options = ["#%s"%ticketnum, "stash"] else: options = ["#%s"%ticketnum, "#%s"%curticket, "stash"] dest = self.UI.get_input("Where do you want to commit your changes?", options) if dest == "stash": self.git.stash() elif dest == str(curticket): self.git.save() if self.git.exists(ticketnum): if dest == str(ticketnum): self.git.move_uncommited_changes(ticketnum) else: self.git.switch(ticketnum) else: self.git.fetch_branch(self, ticketnum) self.git.switch(ticketnum) def save(self): curticket = self.git.current_ticket() if self.UI.confirm("Are you sure you want to save your changes to ticket #%s?"%(curticket)): self.git.save() if self.UI.confirm("Would you like to upload the changes?"): self.git.upload() def upload(self, ticketnum=None): oldticket = self.git.current_ticket() if ticketnum is None or ticketnum == oldticket: oldticket = None ticketnum = self.git.current_ticket() if not self.UI.confirm("Are you sure you want to upload your changes to ticket #%s?"%(ticketnum)): return elif not self.git.exists(ticketnum): print "You don't have a branch for ticket %s"%(ticketnum) return elif not self.UI.confirm("Are you sure you want to upload your changes to ticket #%s?"%(ticketnum)): return else: self.start(ticketnum) self.git.upload() if oldticket is not None: self.git.switch(oldticket) def sync(self): curticket = self.git.current_ticket() if self.UI.confirm("Are you sure you want to save your changes and sync to the most recent development version of Sage?"): self.git.save() self.git.sync() if curticket is not None and curticket.isdigit(): dependencies = self.trac.dependencies(curticket) for dep in dependencies: if self.git.needs_update(dep) and self.UI.confirm("Do you want to sync to the latest version of #%s"%(dep)): self.git.sync(dep) def vanilla(self, release=False): if self.UI.confirm("Are you sure you want to revert to %s?"%(self.git.released_sage_ver() if release else "a plain development version")): if self.git.has_uncommitted_changes(): dest = self.UI.get_input("Where would you like to save your changes?",["current branch","stash"],"current branch") if dest == "stash": self.git.stash() else: self.git.save() self.git.vanilla(release) def review(self, ticketnum, user=None): if self.UI.confirm("Are you sure you want to download and review #%s"%(ticketnum)): self.git.review(ticketnum, user) if self.UI.confirm("Would you like to rebuild Sage?"): call("sage -b", shell=True) def status(self): self.git.execute("status") def list(self): self.git.execute("branch") def diff(self, vs_unstable=False): if vs_unstable: self.git.execute("diff", self.git._unstable) else: self.git.execute("diff") def prune(self): if self.UI.confirm("Are you sure you want to delete all branches that have been merged into unstable?"): self.git.prune() def abandon(self, ticketnum): if self.UI.confirm("Are you sure you want to delete your work on #%s?"%(ticketnum), default_yes=False): self.git.abandon(ticketnum) def help(self): raise NotImplementedError
class SageDev(object): def __init__( self, devrc=os.path.join(DOT_SAGE, "devrc"), gitcmd="git", realm="sage.math.washington.edu", trac="http://boxen.math.washington.edu:8888/sage_trac/", ssh_pubkey_file=None, ssh_passphrase="", ssh_comment=None, ): self.UI = CmdLineInterface() username, password, has_ssh_key = self._process_rc(devrc) self._username = username self.git = GitInterface(self.UI, username, gitcmd) self.trac = TracInterface(self.UI, realm, trac, username, password) self.tmp_dir = None if not has_ssh_key: self._send_ssh_key(username, passwd, devrc, ssh_pubkey_file, ssh_passphrase) def _get_tmp_dir(self): if self.tmp_dir is None: from tmpfile import mkdtemp self.tmp_dir = mkdtemp() atexit.register(lambda: shutil.rmtree(self.tmp_dir)) def _get_user_info(self): username = self.UI.get_input("Please enter your trac username: "******"Please enter your trac password (stored in plaintext on your filesystem): ") return username, passwd def _send_ssh_key(self, username, passwd, devrc, ssh_pubkey_file, ssh_passphrase, comment): if ssh_pubkey_file is None: ssh_pubkey_file = os.path.join(os.environ["HOME"], ".ssh", "id_rsa.pub") if not os.path.exists(ssh_pubkey_file): if not ssh_pubkey_file.endswith(".pub"): raise ValueError("public key filename must end with .pub") ssh_prikey_file = ssh_pubkey_file[:-4] cmd = ["ssh-keygen", "-q", "-t", "rsa", "-f", ssh_prikey_file, "-N", ssh_passphrase] if comment is not None: cmd.extend(["-C", comment]) call(cmd) with open(devrc, "w") as F: F.write("v0\n%s\n%s\nssh_sent" % (username, passwd)) def _process_rc(self, devrc): if not os.path.exists(devrc): username, passwd = self._get_user_info() has_ssh_key = False else: with open(devrc) as F: L = list(F) if len(L) < 3: username, passwd = self._get_user_info() else: username, passwd = L[1].strip(), L[2].strip() has_ssh_key = len(L) >= 4 return username, passwd, has_ssh_key def current_ticket(self): curbranch = self.git.current_branch() if curbranch is not None and curbranch.startswith("t/"): return curbranch[2:] else: return None def start(self, ticketnum=None): curticket = self.current_ticket() if ticketnum is None: # User wants to create a ticket ticketnum = self.trac.create_ticket_interactive() if ticketnum is None: # They didn't succeed. return if curticket is not None: if self.UI.confirm("Should the new ticket depend on #%s?" % (curticket)): self.git.create_branch(self, ticketnum) self.trac.add_dependency(self, ticketnum, curticket) else: self.git.create_branch(self, ticketnum, at_master=True) if not self.exists(ticketnum): self.git.fetch_ticket(ticketnum) self.git.switch("t/" + ticketnum) def save(self): curticket = self.git.current_ticket() if self.UI.confirm("Are you sure you want to save your changes to ticket #%s?" % (curticket)): self.git.save() if self.UI.confirm("Would you like to upload the changes?"): self.git.upload() def upload(self, ticketnum=None): oldticket = self.git.current_ticket() if ticketnum is None or ticketnum == oldticket: oldticket = None ticketnum = self.git.current_ticket() if not self.UI.confirm("Are you sure you want to upload your changes to ticket #%s?" % (ticketnum)): return elif not self.exists(ticketnum): self.UI.show("You don't have a branch for ticket %s" % (ticketnum)) return elif not self.UI.confirm("Are you sure you want to upload your changes to ticket #%s?" % (ticketnum)): return else: self.start(ticketnum) self.git.upload() if oldticket is not None: self.git.switch(oldticket) def sync(self): curticket = self.git.current_ticket() if self.UI.confirm( "Are you sure you want to save your changes and sync to the most recent development version of Sage?" ): self.git.save() self.git.sync() if curticket is not None and curticket.isdigit(): dependencies = self.trac.dependencies(curticket) for dep in dependencies: if self.git.needs_update(dep) and self.UI.confirm( "Do you want to sync to the latest version of #%s" % (dep) ): self.git.sync(dep) def vanilla(self, release=False): if self.UI.confirm( "Are you sure you want to revert to %s?" % (self.git.released_sage_ver() if release else "a plain development version") ): if self.git.has_uncommitted_changes(): dest = self.UI.get_input( "Where would you like to save your changes?", ["current branch", "stash"], "current branch" ) if dest == "stash": self.git.stash() else: self.git.save() self.git.vanilla(release) def review(self, ticketnum, user=None): if self.UI.confirm("Are you sure you want to download and review #%s" % (ticketnum)): raise NotImplementedError if self.UI.confirm("Would you like to rebuild Sage?"): call("sage -b", shell=True) # def status(self): # self.git.execute("status") # def list(self): # self.git.execute("branch") def diff(self, vs_unstable=False): if vs_unstable: self.git.execute("diff", self.git._unstable) else: self.git.execute("diff") def prune_merged(self): # gets rid of branches that have been merged into unstable # Do we need this confirmation? This is pretty harmless.... if self.UI.confirm("Are you sure you want to abandon all branches that have been merged into master?"): for branch in self.git.local_branches(): if self.git.is_ancestor_of(branch, "master"): self.UI.show("Abandoning %s"("#%s" % (branch[2:]) if branch.startswith("t/") else branch)) self.git.abandon(branch) def abandon(self, ticketnum): if self.UI.confirm("Are you sure you want to delete your work on #%s?" % (ticketnum), default_yes=False): self.git.abandon(ticketnum) def help(self): raise NotImplementedError def gather(self, branchname, *inputs): # Creates a join of inputs and stores that in a branch, switching to it. if len(inputs) == 0: self.UI.show("Please include at least one input branch") return if self.git.branch_exists(branchname): if not self.UI.confirm("The %s branch already exists; do you want to merge into it?", default_yes=False): return else: self.git.execute_silent("branch", branchname, inputs[0]) inputs = inputs[1:] # The following will deal with outstanding changes self.git.switch_branch(branchname) if len(inputs) > 1: self.git.execute( "merge", *inputs, q=True, m="Gathering %s into branch %s" % (", ".join(inputs), branchname) ) def show_dependencies(self, ticketnum=None, all=True): raise NotImplementedError def update_dependencies(self, ticketnum=None, dependencynum=None, all=False): # Merge in most recent changes from dependency(ies) raise NotImplementedError def add_dependency(self, ticketnum=None, dependencynum=None): # Do we want to do this? raise NotImplementedError def download_patch(self, ticketnum=None, patchname=None, url=None): """ Download a patch to a temporary directory. If only ``ticketnum`` is specified and the ticket has only one attachment, download the patch attached to ``ticketnum``. If ``ticketnum`` and ``patchname`` are specified, download the patch ``patchname`` attached to ``ticketnum``. If ``url`` is specified, download ``url``. Raise an error on any other combination of parameters. INPUT: - ``ticketnum`` -- an int or an Integer or ``None`` (default: ``None``) - ``patchname`` -- a string or ``None`` (default: ``None``) - ``url`` -- a string or ``None`` (default: ``None``) OUTPUT: Returns the absolute file name of the returned file. """ if url: if ticketnum or patchname: raise ValueError("If `url` is specifed, `ticketnum` and `patchname` must not be specified.") tmp_dir = self._get_tmp_dir() ret = os.path.join(tmp_dir, "patch") check_call("wget", "-r", "-O", ret) return ret elif ticketnum: if patchname: return self.download_patch( url=self.trac._tracsite + "/raw-attachment/ticket/%s/%s" % (ticketnum, patchname) ) else: attachments = self.trac.attachment_names() if len(attachments) == 0: raise ValueError("Ticket #%s has no attachments." % ticketnum) if len(attachments) == 1: return self.download_patch(ticketnum=ticketnum, patchname=attachments[0]) else: raise ValueError( "Ticket #%s has more than one attachment but parameter `patchname` is not present." % ticketnum ) else: raise ValueError("If `url` is not specified, `ticketnum` must be specified") def import_patch( self, ticketnum=None, patchname=None, url=None, local_file=None, diff_format=None, header_format=None, path_format=None, ): """ Import a patch to your working copy. If ``local_file`` is specified, apply the file it points to. Otherwise, apply the patch using :meth:`download_patch` and apply it. INPUT: - ``ticketnum`` -- an int or an Integer or ``None`` (default: ``None``) - ``patchname`` -- a string or ``None`` (default: ``None``) - ``url`` -- a string or ``None`` (default: ``None``) - ``local_file`` -- a string or ``None`` (default: ``None``) """ if not local_file: if ticketnum or patchname or url: raise ValueError( "If `local_file` is specified, `ticketnum`, `patchname`, and `url` must not be specified." ) return self.import_patch( local_file=self.download_patch(ticketnum=ticketnum, patchname=patchname, url=url), **kwargs ) else: lines = open(local_file).read().splitlines() lines = self._rewrite_patch( lines, to_format="git", from_diff_format=diff_format, from_header_format=header_format, from_path_format=path_format, ) # TODO: strip whitespace raise NotImplementedError def _detect_patch_diff_format(self, lines): """ Determine the format of the ``diff`` lines in ``lines``. INPUT: - ``lines`` -- a list of strings OUTPUT: Either ``git`` (for ``diff --git`` lines) or ``hg`` (for ``diff -r`` lines). EXAMPLES:: sage: s = SageDev() sage: s._detect_patch_diff_format(["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py"]) 'hg' sage: s._detect_patch_diff_format(["diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi"]) 'git' TESTS:: sage: s._detect_patch_diff_format(["# HG changeset patch"]) Traceback (most recent call last): ... NotImplementedError: Failed to detect diff format. sage: s._detect_patch_diff_format(["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py", "diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi"]) Traceback (most recent call last): ... ValueError: File appears to have mixed diff formats. """ format = None regexs = {"hg": HG_DIFF_REGEX, "git": GIT_DIFF_REGEX} for line in lines: for name, regex in regexs.items(): if regex.match(line): if format is None: format = name if format != name: raise ValueError("File appears to have mixed diff formats.") if format is None: raise NotImplementedError("Failed to detect diff format.") else: return format def _detect_patch_path_format(self, lines, diff_format=None): """ Determine the format of the paths in the patch given in ``lines``. INPUT: - ``lines`` -- a list of strings - ``diff_format`` -- ``'hg'``,``'git'``, or ``None`` (default: ``None``), the format of the ``diff`` lines in the patch. If ``None``, the format will be determined by :meth:`_detect_patch_diff_format`. OUTPUT: A string, ``'new'`` (new repository layout) or ``'old'`` (old repository layout). EXAMPLES:: sage: s = SageDev() sage: s._detect_patch_path_format(["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py"]) 'old' sage: s._detect_patch_path_format(["diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py"], diff_format="git") Traceback (most recent call last): ... NotImplementedError: Failed to detect path format. sage: s._detect_patch_path_format(["diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi"]) 'old' sage: s._detect_patch_path_format(["diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi"]) 'new' """ if diff_format is None: diff_format = self._detect_patch_diff_format(lines) path_format = None if diff_format == "git": regex = GIT_DIFF_REGEX elif diff_format == "hg": regex = HG_DIFF_REGEX else: raise NotImplementedError(diff_format) regexs = {"old": HG_PATH_REGEX, "new": GIT_PATH_REGEX} for line in lines: match = regex.match(line) if match: for group in match.groups(): for name, regex in regexs.items(): if regex.match(group): if path_format is None: path_format = name if path_format != name: raise ValueError("File appears to have mixed path formats.") if path_format is None: raise NotImplementedError("Failed to detect path format.") else: return path_format def _rewrite_patch_diff_paths(self, lines, to_format, from_format=None, diff_format=None): """ Rewrite the ``diff`` lines in ``lines`` to use ``to_format``. INPUT: - ``lines`` -- a list of strings - ``to_format`` -- ``'old'`` or ``'new'`` - ``from_format`` -- ``'old'``, ``'new'``, or ``None`` (default: ``None``), the current formatting of the paths; detected automatically if ``None`` - ``diff_format`` -- ``'git'``, ``'hg'``, or ``None`` (default: ``None``), the format of the ``diff`` lines; detected automatically if ``None`` OUTPUT: A list of string, ``lines`` rewritten to conform to ``lines``. EXAMPLES: Paths in the old format:: sage: s = SageDev() sage: s._rewrite_patch_diff_paths(['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py'], to_format="old") ['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py'] sage: s._rewrite_patch_diff_paths(['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi'], to_format="old") ['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi'] sage: s._rewrite_patch_diff_paths(['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py'], to_format="new") ['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py'] sage: s._rewrite_patch_diff_paths(['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi'], to_format="new") ['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi'] Paths in the new format:: sage: s._rewrite_patch_diff_paths(['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py'], to_format="old") ['diff -r 1492e39aff50 -r 5803166c5b11 sage/schemes/elliptic_curves/ell_rational_field.py'] sage: s._rewrite_patch_diff_paths(['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi'], to_format="old") ['diff --git a/sage/rings/padics/FM_template.pxi b/sage/rings/padics/FM_template.pxi'] sage: s._rewrite_patch_diff_paths(['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py'], to_format="new") ['diff -r 1492e39aff50 -r 5803166c5b11 src/sage/schemes/elliptic_curves/ell_rational_field.py'] sage: s._rewrite_patch_diff_paths(['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi'], to_format="new") ['diff --git a/src/sage/rings/padics/FM_template.pxi b/src/sage/rings/padics/FM_template.pxi'] """ if diff_format is None: diff_format = self._detect_patch_diff_format(lines) if from_format is None: from_format = self._detect_patch_path_format(lines) if to_format == from_format: return lines def hg_path_to_git_path(path): if any([path.startswith(p) for p in "module_list.py", "setup.py", "c_lib/", "sage/", "doc/"]): return "src/%s" % path