]> Cypherpunks repositories - gostls13.git/commitdiff
codereview: add 'hg undo' command
authorRuss Cox <rsc@golang.org>
Sun, 17 Apr 2011 18:15:51 +0000 (14:15 -0400)
committerRuss Cox <rsc@golang.org>
Sun, 17 Apr 2011 18:15:51 +0000 (14:15 -0400)
R=adg, r
CC=golang-dev
https://golang.org/cl/4423045

lib/codereview/codereview.py

index 766e827fc38b63050319408df2cb0756eec593d2..6e1d0b0d34c78d66fe25b61b0a457f4418f7637c 100644 (file)
@@ -111,6 +111,7 @@ server_url_base = None
 defaultcc = None
 contributors = {}
 missing_codereview = None
+real_rollback = None
 
 #######################################################################
 # RE: UNICODE STRING HANDLING
@@ -196,12 +197,15 @@ class CL(object):
                self.web = False
                self.copied_from = None # None means current user
                self.mailed = False
+               self.private = False
 
        def DiskText(self):
                cl = self
                s = ""
                if cl.copied_from:
                        s += "Author: " + cl.copied_from + "\n\n"
+               if cl.private:
+                       s += "Private: " + str(self.private) + "\n"
                s += "Mailed: " + str(self.mailed) + "\n"
                s += "Description:\n"
                s += Indent(cl.desc, "\t")
@@ -219,6 +223,8 @@ class CL(object):
                        s += "Author: " + cl.copied_from + "\n"
                if cl.url != '':
                        s += 'URL: ' + cl.url + '       # cannot edit\n\n'
+               if cl.private:
+                       s += "Private: True\n"
                s += "Reviewer: " + JoinComma(cl.reviewer) + "\n"
                s += "CC: " + JoinComma(cl.cc) + "\n"
                s += "\n"
@@ -264,7 +270,8 @@ class CL(object):
                os.rename(path+'!', path)
                if self.web and not self.copied_from:
                        EditDesc(self.name, desc=self.desc,
-                               reviewers=JoinComma(self.reviewer), cc=JoinComma(self.cc))
+                               reviewers=JoinComma(self.reviewer), cc=JoinComma(self.cc),
+                               private=self.private)
 
        def Delete(self, ui, repo):
                dir = CodeReviewDir(ui, repo)
@@ -389,6 +396,7 @@ def ParseCL(text, name):
                'Reviewer': '',
                'CC': '',
                'Mailed': '',
+               'Private': '',
        }
        for line in text.split('\n'):
                lineno += 1
@@ -435,6 +443,8 @@ def ParseCL(text, name):
                # CLs created with this update will always have 
                # Mailed: False on disk.
                cl.mailed = True
+       if sections['Private'] in ('True', 'true', 'Yes', 'yes'):
+               cl.private = True
        if cl.desc == '<enter description here>':
                cl.desc = ''
        return cl, 0, ''
@@ -779,7 +789,7 @@ def Incoming(ui, repo, opts):
        _, incoming, _ = findcommonincoming(repo, getremote(ui, repo, opts))
        return incoming
 
-desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build)'
+desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build|undo CL)'
 
 desc_msg = '''Your CL description appears not to use the standard form.
 
@@ -827,6 +837,9 @@ def EditCL(ui, repo, cl):
                if clx.desc == '':
                        if promptyesno(ui, "change list should have a description\nre-edit (y/n)?"):
                                continue
+               elif re.search('<enter reason for undo>', clx.desc):
+                       if promptyesno(ui, "change list description omits reason for undo\nre-edit (y/n)?"):
+                               continue
                elif not re.match(desc_re, clx.desc.split('\n')[0]):
                        if promptyesno(ui, desc_msg + "re-edit (y/n)?"):
                                continue
@@ -870,6 +883,7 @@ def EditCL(ui, repo, cl):
                cl.reviewer = clx.reviewer
                cl.cc = clx.cc
                cl.files = clx.files
+               cl.private = clx.private
                break
        return ""
 
@@ -1066,7 +1080,7 @@ def change(ui, repo, *pats, **opts):
                        if cl.copied_from:
                                return "original author must delete CL; hg change -D will remove locally"
                        PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed)
-                       EditDesc(cl.name, closed="checked")
+                       EditDesc(cl.name, closed=True, private=cl.private)
                cl.Delete(ui, repo)
                return
 
@@ -1087,6 +1101,9 @@ def change(ui, repo, *pats, **opts):
                if clx.files is not None:
                        cl.files = clx.files
                        dirty[cl] = True
+               if clx.private != cl.private:
+                       cl.private = clx.private
+                       dirty[cl] = True
 
        if not opts["stdin"] and not opts["stdout"]:
                if name == "new":
@@ -1104,6 +1121,8 @@ def change(ui, repo, *pats, **opts):
 
        if opts["stdout"]:
                ui.write(cl.EditorText())
+       elif opts["pending"]:
+               ui.write(cl.PendingText())
        elif name == "new":
                if ui.quiet:
                        ui.write(cl.name)
@@ -1132,17 +1151,90 @@ def clpatch(ui, repo, clname, **opts):
        Submitting an imported patch will keep the original author's
        name as the Author: line but add your own name to a Committer: line.
        """
+       return clpatch_or_undo(ui, repo, clname, opts)
+
+def undo(ui, repo, clname, **opts):
+       """undo the effect of a CL
+       
+       Creates a new CL that undoes an earlier CL.
+       After creating the CL, opens the CL text for editing so that
+       you can add the reason for the undo to the description.
+       """
+       return clpatch_or_undo(ui, repo, clname, opts, undo=True)
+
+def rev2clname(rev):
+       # Extract CL name from revision description.
+       # The last line in the description that is a codereview URL is the real one.
+       # Earlier lines might be part of the user-written description.
+       all = re.findall('(?m)^http://codereview.appspot.com/([0-9]+)$', rev.description())
+       if len(all) > 0:
+               return all[-1]
+       return ""
+
+undoHeader = """undo CL %s / %s
+
+<enter reason for undo>
+
+««« original CL description
+"""
+
+undoFooter = """
+»»»
+"""
+
+# Implementation of clpatch/undo.
+def clpatch_or_undo(ui, repo, clname, opts, undo=False):
        if missing_codereview:
                return missing_codereview
 
-       cl, vers, patch, err = DownloadCL(ui, repo, clname)
-       if err != "":
-               return err
-       if patch == emptydiff:
-               return "codereview issue %s has no diff" % clname
+       if undo:
+               if hgversion < '1.4':
+                       # Don't have cmdutil.match (see implementation of sync command).
+                       return "hg is too old to run hg undo - update to 1.4 or newer"
+
+               # Find revision in Mercurial repository.
+               # Assume CL number is 7+ decimal digits.
+               # Otherwise is either change log sequence number (fewer decimal digits),
+               # hexadecimal hash, or tag name.
+               # Mercurial will fall over long before the change log
+               # sequence numbers get to be 7 digits long.
+               if re.match('^[0-9]{7,}$', clname):
+                       found = False
+                       matchfn = cmdutil.match(repo, [], {'rev': None})
+                       def prep(ctx, fns):
+                               pass
+                       for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev': None}, prep):
+                               rev = repo[ctx.rev()]
+                               # Last line with a code review URL is the actual review URL.
+                               # Earlier ones might be part of the CL description.
+                               n = rev2clname(rev)
+                               if n == clname:
+                                       found = True
+                                       break
+                       if not found:
+                               return "cannot find CL %s in local repository" % clname
+               else:
+                       rev = repo[clname]
+                       if not rev:
+                               return "unknown revision %s" % clname
+                       clname = rev2clname(rev)
+                       if clname == "":
+                               return "cannot find CL name in revision description"
+               
+               # Create fresh CL and start with patch that would reverse the change.
+               vers = short(rev.node())
+               cl = CL("new")
+               cl.desc = (undoHeader % (clname, vers)) + rev.description() + undoFooter
+               patch = RunShell(["hg", "diff", "--git", "-r", vers + ":" + short(rev.parents()[0].node())])
 
-       if not repo[vers]:
-               return "codereview issue %s is newer than the current repository; hg sync" % clname
+       else:  # clpatch
+               cl, vers, patch, err = DownloadCL(ui, repo, clname)
+               if err != "":
+                       return err
+               if patch == emptydiff:
+                       return "codereview issue %s has no diff" % clname
+               if not repo[vers]:
+                       return "codereview issue %s is newer than the current repository; hg sync" % clname
 
        # find current hg version (hg identify)
        ctx = repo[None]
@@ -1170,13 +1262,19 @@ def clpatch(ui, repo, clname, **opts):
        cl.local = True
        cl.files = out.strip().split()
        if not cl.files:
-               return "codereview issue %s has no diff" % clname
+               return "codereview issue %s has no changed files" % clname
        files = ChangedFiles(ui, repo, [], opts)
        extra = Sub(cl.files, files)
        if extra:
                ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n")
        cl.Flush(ui, repo)
-       ui.write(cl.PendingText() + "\n")
+       if undo:
+               err = EditCL(ui, repo, cl)
+               if err != "":
+                       return "CL created, but error editing: " + err
+               cl.Flush(ui, repo)
+       else:
+               ui.write(cl.PendingText() + "\n")
 
 # portPatch rewrites patch from being a patch against
 # oldver to being a patch against newver.
@@ -1373,10 +1471,6 @@ def mail(ui, repo, *pats, **opts):
 
        cl.Mail(ui, repo)               
 
-def nocommit(ui, repo, *pats, **opts):
-       """(disabled when using this extension)"""
-       return "The codereview extension is enabled; do not use commit."
-
 def pending(ui, repo, *pats, **opts):
        """show pending changes
 
@@ -1533,7 +1627,7 @@ def submit(ui, repo, *pats, **opts):
                if r == 0:
                        raise util.Abort("local repository out of date; must sync before submit")
        except:
-               repo.rollback()
+               real_rollback(repo)
                raise
 
        # we're committed. upload final patch, close review, add commit message
@@ -1551,7 +1645,7 @@ def submit(ui, repo, *pats, **opts):
        PostMessage(ui, cl.name, pmsg, reviewers="", cc=JoinComma(cl.reviewer+cl.cc))
 
        if not cl.copied_from:
-               EditDesc(cl.name, closed="checked")
+               EditDesc(cl.name, closed=True, private=cl.private)
        cl.Delete(ui, repo)
 
 def sync(ui, repo, **opts):
@@ -1601,7 +1695,7 @@ def sync_changes(ui, repo):
                                        ui.warn("loading CL %s: %s\n" % (clname, err))
                                        continue
                                if not cl.copied_from:
-                                       EditDesc(cl.name, closed="checked")
+                                       EditDesc(cl.name, closed=True, private=cl.private)
                                cl.Delete(ui, repo)
 
        if hgversion < '1.4':
@@ -1675,6 +1769,7 @@ cmdtable = {
                        ('D', 'deletelocal', None, 'delete locally, but do not change CL on server'),
                        ('i', 'stdin', None, 'read change list from standard input'),
                        ('o', 'stdout', None, 'print change list to standard output'),
+                       ('p', 'pending', None, 'print pending summary to standard output'),
                ],
                "[-d | -D] [-i] [-o] change# or FILE ..."
        ),
@@ -1739,6 +1834,14 @@ cmdtable = {
                ],
                "[--local]",
        ),
+       "^undo": (
+               undo,
+               [
+                       ('', 'ignore_hgpatch_failure', None, 'create CL metadata even if hgpatch fails'),
+                       ('', 'no_incoming', None, 'disable check for incoming changes'),
+               ],
+               "change#"
+       ),
        "^upload": (
                upload,
                [],
@@ -1992,7 +2095,7 @@ def GetSettings(issue):
                f['description'] = MySend("/"+issue+"/description", force_auth=False)
        return f
 
-def EditDesc(issue, subject=None, desc=None, reviewers=None, cc=None, closed=None):
+def EditDesc(issue, subject=None, desc=None, reviewers=None, cc=None, closed=False, private=False):
        set_status("uploading change to description")
        form_fields = GetForm("/" + issue + "/edit")
        if subject is not None:
@@ -2003,8 +2106,10 @@ def EditDesc(issue, subject=None, desc=None, reviewers=None, cc=None, closed=Non
                form_fields['reviewers'] = reviewers
        if cc is not None:
                form_fields['cc'] = cc
-       if closed is not None:
-               form_fields['closed'] = closed
+       if closed:
+               form_fields['closed'] = "checked"
+       if private:
+               form_fields['private'] = "checked"
        ctype, body = EncodeMultipartFormData(form_fields.items(), [])
        response = MySend("/" + issue + "/edit", body, content_type=ctype)
        if response != "":
@@ -2039,8 +2144,17 @@ def PostMessage(ui, issue, message, reviewers=None, cc=None, send_mail=True, sub
 class opt(object):
        pass
 
-def disabled(*opts, **kwopts):
-       raise util.Abort("commit is disabled when codereview is in use")
+def nocommit(*pats, **opts):
+       """(disabled when using this extension)"""
+       raise util.Abort("codereview extension enabled; use mail, upload, or submit instead of commit")
+
+def nobackout(*pats, **opts):
+       """(disabled when using this extension)"""
+       raise util.Abort("codereview extension enabled; use undo instead of backout")
+
+def norollback(*pats, **opts):
+       """(disabled when using this extension)"""
+       raise util.Abort("codereview extension enabled; use undo instead of rollback")
 
 def RietveldSetup(ui, repo):
        global defaultcc, upload_options, rpc, server, server_url_base, force_google_account, verbosity, contributors
@@ -2068,7 +2182,11 @@ def RietveldSetup(ui, repo):
        # Should only modify repository with hg submit.
        # Disable the built-in Mercurial commands that might
        # trip things up.
-       cmdutil.commit = disabled
+       cmdutil.commit = nocommit
+       global real_rollback
+       real_rollback = repo.rollback
+       repo.rollback = norollback
+       # would install nobackout if we could; oh well
 
        try:
                f = open(repo.root + '/CONTRIBUTORS', 'r')