1
0
mirror of https://github.com/golang/go synced 2024-11-25 11:57:58 -07:00

codereview: add 'hg undo' command

R=adg, r
CC=golang-dev
https://golang.org/cl/4423045
This commit is contained in:
Russ Cox 2011-04-17 14:15:51 -04:00
parent a58fe3bd23
commit 0c6df25df1

View File

@ -111,6 +111,7 @@ server_url_base = None
defaultcc = None defaultcc = None
contributors = {} contributors = {}
missing_codereview = None missing_codereview = None
real_rollback = None
####################################################################### #######################################################################
# RE: UNICODE STRING HANDLING # RE: UNICODE STRING HANDLING
@ -196,12 +197,15 @@ class CL(object):
self.web = False self.web = False
self.copied_from = None # None means current user self.copied_from = None # None means current user
self.mailed = False self.mailed = False
self.private = False
def DiskText(self): def DiskText(self):
cl = self cl = self
s = "" s = ""
if cl.copied_from: if cl.copied_from:
s += "Author: " + cl.copied_from + "\n\n" s += "Author: " + cl.copied_from + "\n\n"
if cl.private:
s += "Private: " + str(self.private) + "\n"
s += "Mailed: " + str(self.mailed) + "\n" s += "Mailed: " + str(self.mailed) + "\n"
s += "Description:\n" s += "Description:\n"
s += Indent(cl.desc, "\t") s += Indent(cl.desc, "\t")
@ -219,6 +223,8 @@ class CL(object):
s += "Author: " + cl.copied_from + "\n" s += "Author: " + cl.copied_from + "\n"
if cl.url != '': if cl.url != '':
s += 'URL: ' + cl.url + ' # cannot edit\n\n' s += 'URL: ' + cl.url + ' # cannot edit\n\n'
if cl.private:
s += "Private: True\n"
s += "Reviewer: " + JoinComma(cl.reviewer) + "\n" s += "Reviewer: " + JoinComma(cl.reviewer) + "\n"
s += "CC: " + JoinComma(cl.cc) + "\n" s += "CC: " + JoinComma(cl.cc) + "\n"
s += "\n" s += "\n"
@ -264,7 +270,8 @@ class CL(object):
os.rename(path+'!', path) os.rename(path+'!', path)
if self.web and not self.copied_from: if self.web and not self.copied_from:
EditDesc(self.name, desc=self.desc, 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): def Delete(self, ui, repo):
dir = CodeReviewDir(ui, repo) dir = CodeReviewDir(ui, repo)
@ -389,6 +396,7 @@ def ParseCL(text, name):
'Reviewer': '', 'Reviewer': '',
'CC': '', 'CC': '',
'Mailed': '', 'Mailed': '',
'Private': '',
} }
for line in text.split('\n'): for line in text.split('\n'):
lineno += 1 lineno += 1
@ -435,6 +443,8 @@ def ParseCL(text, name):
# CLs created with this update will always have # CLs created with this update will always have
# Mailed: False on disk. # Mailed: False on disk.
cl.mailed = True cl.mailed = True
if sections['Private'] in ('True', 'true', 'Yes', 'yes'):
cl.private = True
if cl.desc == '<enter description here>': if cl.desc == '<enter description here>':
cl.desc = '' cl.desc = ''
return cl, 0, '' return cl, 0, ''
@ -779,7 +789,7 @@ def Incoming(ui, repo, opts):
_, incoming, _ = findcommonincoming(repo, getremote(ui, repo, opts)) _, incoming, _ = findcommonincoming(repo, getremote(ui, repo, opts))
return incoming 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. 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 clx.desc == '':
if promptyesno(ui, "change list should have a description\nre-edit (y/n)?"): if promptyesno(ui, "change list should have a description\nre-edit (y/n)?"):
continue 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]): elif not re.match(desc_re, clx.desc.split('\n')[0]):
if promptyesno(ui, desc_msg + "re-edit (y/n)?"): if promptyesno(ui, desc_msg + "re-edit (y/n)?"):
continue continue
@ -870,6 +883,7 @@ def EditCL(ui, repo, cl):
cl.reviewer = clx.reviewer cl.reviewer = clx.reviewer
cl.cc = clx.cc cl.cc = clx.cc
cl.files = clx.files cl.files = clx.files
cl.private = clx.private
break break
return "" return ""
@ -1066,7 +1080,7 @@ def change(ui, repo, *pats, **opts):
if cl.copied_from: if cl.copied_from:
return "original author must delete CL; hg change -D will remove locally" return "original author must delete CL; hg change -D will remove locally"
PostMessage(ui, cl.name, "*** Abandoned ***", send_mail=cl.mailed) 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) cl.Delete(ui, repo)
return return
@ -1087,6 +1101,9 @@ def change(ui, repo, *pats, **opts):
if clx.files is not None: if clx.files is not None:
cl.files = clx.files cl.files = clx.files
dirty[cl] = True dirty[cl] = True
if clx.private != cl.private:
cl.private = clx.private
dirty[cl] = True
if not opts["stdin"] and not opts["stdout"]: if not opts["stdin"] and not opts["stdout"]:
if name == "new": if name == "new":
@ -1104,6 +1121,8 @@ def change(ui, repo, *pats, **opts):
if opts["stdout"]: if opts["stdout"]:
ui.write(cl.EditorText()) ui.write(cl.EditorText())
elif opts["pending"]:
ui.write(cl.PendingText())
elif name == "new": elif name == "new":
if ui.quiet: if ui.quiet:
ui.write(cl.name) ui.write(cl.name)
@ -1132,17 +1151,90 @@ def clpatch(ui, repo, clname, **opts):
Submitting an imported patch will keep the original author's Submitting an imported patch will keep the original author's
name as the Author: line but add your own name to a Committer: line. 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: if missing_codereview:
return missing_codereview return missing_codereview
cl, vers, patch, err = DownloadCL(ui, repo, clname) if undo:
if err != "": if hgversion < '1.4':
return err # Don't have cmdutil.match (see implementation of sync command).
if patch == emptydiff: return "hg is too old to run hg undo - update to 1.4 or newer"
return "codereview issue %s has no diff" % clname
if not repo[vers]: # Find revision in Mercurial repository.
return "codereview issue %s is newer than the current repository; hg sync" % clname # 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())])
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) # find current hg version (hg identify)
ctx = repo[None] ctx = repo[None]
@ -1170,13 +1262,19 @@ def clpatch(ui, repo, clname, **opts):
cl.local = True cl.local = True
cl.files = out.strip().split() cl.files = out.strip().split()
if not cl.files: 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) files = ChangedFiles(ui, repo, [], opts)
extra = Sub(cl.files, files) extra = Sub(cl.files, files)
if extra: if extra:
ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n") ui.warn("warning: these files were listed in the patch but not changed:\n\t" + "\n\t".join(extra) + "\n")
cl.Flush(ui, repo) 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 # portPatch rewrites patch from being a patch against
# oldver to being a patch against newver. # oldver to being a patch against newver.
@ -1373,10 +1471,6 @@ def mail(ui, repo, *pats, **opts):
cl.Mail(ui, repo) 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): def pending(ui, repo, *pats, **opts):
"""show pending changes """show pending changes
@ -1533,7 +1627,7 @@ def submit(ui, repo, *pats, **opts):
if r == 0: if r == 0:
raise util.Abort("local repository out of date; must sync before submit") raise util.Abort("local repository out of date; must sync before submit")
except: except:
repo.rollback() real_rollback(repo)
raise raise
# we're committed. upload final patch, close review, add commit message # 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)) PostMessage(ui, cl.name, pmsg, reviewers="", cc=JoinComma(cl.reviewer+cl.cc))
if not cl.copied_from: if not cl.copied_from:
EditDesc(cl.name, closed="checked") EditDesc(cl.name, closed=True, private=cl.private)
cl.Delete(ui, repo) cl.Delete(ui, repo)
def sync(ui, repo, **opts): def sync(ui, repo, **opts):
@ -1601,7 +1695,7 @@ def sync_changes(ui, repo):
ui.warn("loading CL %s: %s\n" % (clname, err)) ui.warn("loading CL %s: %s\n" % (clname, err))
continue continue
if not cl.copied_from: if not cl.copied_from:
EditDesc(cl.name, closed="checked") EditDesc(cl.name, closed=True, private=cl.private)
cl.Delete(ui, repo) cl.Delete(ui, repo)
if hgversion < '1.4': if hgversion < '1.4':
@ -1675,6 +1769,7 @@ cmdtable = {
('D', 'deletelocal', None, 'delete locally, but do not change CL on server'), ('D', 'deletelocal', None, 'delete locally, but do not change CL on server'),
('i', 'stdin', None, 'read change list from standard input'), ('i', 'stdin', None, 'read change list from standard input'),
('o', 'stdout', None, 'print change list to standard output'), ('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 ..." "[-d | -D] [-i] [-o] change# or FILE ..."
), ),
@ -1739,6 +1834,14 @@ cmdtable = {
], ],
"[--local]", "[--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": (
upload, upload,
[], [],
@ -1992,7 +2095,7 @@ def GetSettings(issue):
f['description'] = MySend("/"+issue+"/description", force_auth=False) f['description'] = MySend("/"+issue+"/description", force_auth=False)
return f 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") set_status("uploading change to description")
form_fields = GetForm("/" + issue + "/edit") form_fields = GetForm("/" + issue + "/edit")
if subject is not None: 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 form_fields['reviewers'] = reviewers
if cc is not None: if cc is not None:
form_fields['cc'] = cc form_fields['cc'] = cc
if closed is not None: if closed:
form_fields['closed'] = closed form_fields['closed'] = "checked"
if private:
form_fields['private'] = "checked"
ctype, body = EncodeMultipartFormData(form_fields.items(), []) ctype, body = EncodeMultipartFormData(form_fields.items(), [])
response = MySend("/" + issue + "/edit", body, content_type=ctype) response = MySend("/" + issue + "/edit", body, content_type=ctype)
if response != "": if response != "":
@ -2039,8 +2144,17 @@ def PostMessage(ui, issue, message, reviewers=None, cc=None, send_mail=True, sub
class opt(object): class opt(object):
pass pass
def disabled(*opts, **kwopts): def nocommit(*pats, **opts):
raise util.Abort("commit is disabled when codereview is in use") """(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): def RietveldSetup(ui, repo):
global defaultcc, upload_options, rpc, server, server_url_base, force_google_account, verbosity, contributors 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. # Should only modify repository with hg submit.
# Disable the built-in Mercurial commands that might # Disable the built-in Mercurial commands that might
# trip things up. # 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: try:
f = open(repo.root + '/CONTRIBUTORS', 'r') f = open(repo.root + '/CONTRIBUTORS', 'r')