# coding=utf-8# (The line above is necessary so that I can use 世界 in the# *comment* below without Python getting all bent out of shape.) # Copyright 2007-2009 Google Inc.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License. '''Mercurial interface to codereview.appspot.com. To configure, set the following options inyour repository's .hg/hgrc file. [extensions] codereview = path/to/codereview.py [codereview] server = codereview.appspot.com The server should be running Rietveld; see http://code.google.com/p/rietveld/. In addition to the new commands, this extension introducesthe file pattern syntax @nnnnnn, where nnnnnn is a change listnumber, to mean the files included in that change list, whichmust be associated with the current client. For example, if change 123456 contains the files x.go and y.go,"hg diff @123456" is equivalent to"hg diff x.go y.go".''' from mercurial import cmdutil, commands, hg, util, error, matchfrom mercurial.node import nullrev, hex, nullid, shortimport os, re, timeimport statimport subprocessimport threadingfrom HTMLParser import HTMLParser # The standard 'json' package is new in Python 2.6.# Before that it was an external package named simplejson.try: # Standard location in 2.6 and beyond. import jsonexcept Exception, e: try: # Conventional name for earlier package. import simplejson as json except: try: # Was also bundled with django, which is commonly installed. from django.utils import simplejson as json except: # We give up. raise e try: hgversion = util.version()except: from mercurial.version import version as v hgversion = v.get_version() try: from mercurial.discovery import findcommonincomingexcept: def findcommonincoming(repo, remote): return repo.findcommonincoming(remote) oldMessage = """The code review extension requires Mercurial 1.3 or newer. To install a new Mercurial, sudo easy_install mercurial works on most systems.""" linuxMessage = """You may need to clear your current Mercurial installation by running: sudo apt-get remove mercurial mercurial-common sudo rm -rf /etc/mercurial""" if hgversion < '1.3': msg = oldMessage if os.access("/etc/mercurial", 0): msg += linuxMessage raise util.Abort(msg) def promptyesno(ui, msg): # Arguments to ui.prompt changed between 1.3 and 1.3.1. # Even so, some 1.3.1 distributions seem to have the old prompt!?!? # What a terrible way to maintain software. try: return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0 except AttributeError: return ui.prompt(msg, ["&yes", "&no"], "y") != "n" # To experiment with Mercurial in the python interpreter:# >>> repo = hg.repository(ui.ui(), path = ".") ######################################################################## Normally I would split this into multiple files, but it simplifies# import path headaches to keep it all in one file. Sorry. import sysif __name__ == "__main__": print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly." sys.exit(2) server = "codereview.appspot.com"server_url_base = Nonedefaultcc = Nonecontributors = {}missing_codereview = Nonereal_rollback = NonereleaseBranch = None ######################################################################## RE: UNICODE STRING HANDLING## Python distinguishes between the str (string of bytes)# and unicode (string of code points) types. Most operations# work on either one just fine, but some (like regexp matching)# require unicode, and others (like write) require str.## As befits the language, Python hides the distinction between# unicode and str by converting between them silently, but# *only* if all the bytes/code points involved are 7-bit ASCII.# This means that if you're not careful, your program works# fine on "hello, world" and fails on "hello, 世界". And of course,# the obvious way to be careful - use static types - is unavailable.# So the only way is trial and error to find where to put explicit# conversions.## Because more functions do implicit conversion to str (string of bytes)# than do implicit conversion to unicode (string of code points),# the convention in this module is to represent all text as str,# converting to unicode only when calling a unicode-only function# and then converting back to str as soon as possible. def typecheck(s, t): if type(s) != t: raise util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t)) # If we have to pass unicode instead of str, ustr does that conversion clearly.def ustr(s): typecheck(s, str) return s.decode("utf-8") # Even with those, Mercurial still sometimes turns unicode into str# and then tries to use it as ascii. Change Mercurial's default.def set_mercurial_encoding_to_utf8(): from mercurial import encoding encoding.encoding = 'utf-8' set_mercurial_encoding_to_utf8() # Even with those we still run into problems.# I tried to do things by the book but could not convince# Mercurial to let me check in a change with UTF-8 in the# CL description or author field, no matter how many conversions# between str and unicode I inserted and despite changing the# default encoding. I'm tired of this game, so set the default# encoding for all of Python to 'utf-8', not 'ascii'.def default_to_utf8(): import sys reload(sys) # site.py deleted setdefaultencoding; get it back sys.setdefaultencoding('utf-8') default_to_utf8() ######################################################################## Change list parsing.## Change lists are stored in .hg/codereview/cl.nnnnnn# where nnnnnn is the number assigned by the code review server.# Most data about a change list is stored on the code review server# too: the description, reviewer, and cc list are all stored there.# The only thing in the cl.nnnnnn file is the list of relevant files.# Also, the existence of the cl.nnnnnn file marks this repository# as the one where the change list lives. emptydiff = """Index: ~rietveld~placeholder~===================================================================diff --git a/~rietveld~placeholder~ b/~rietveld~placeholder~new file mode 100644""" class CL(object): def __init__(self, name): typecheck(name, str) self.name = name self.desc = '' self.files = [] self.reviewer = [] self.cc = [] self.url = '' self.local = False 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") s += "Files:\n" for f in cl.files: s += "\t" + f + "\n" typecheck(s, str) return s def EditorText(self): cl = self s = _change_prolog s += "\n" if cl.copied_from: 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" s += "Description:\n" if cl.desc == '': s += "\t<enter description here>\n" else: s += Indent(cl.desc, "\t") s += "\n" if cl.local or cl.name == "new": s += "Files:\n" for f in cl.files: s += "\t" + f + "\n" s += "\n" typecheck(s, str) return s def PendingText(self): cl = self s = cl.name + ":" + "\n" s += Indent(cl.desc, "\t") s += "\n" if cl.copied_from: s += "\tAuthor: " + cl.copied_from + "\n" s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n" s += "\tCC: " + JoinComma(cl.cc) + "\n" s += "\tFiles:\n" for f in cl.files: s += "\t\t" + f + "\n" typecheck(s, str) return s def Flush(self, ui, repo): if self.name == "new": self.Upload(ui, repo, gofmt_just_warn=True, creating=True) dir = CodeReviewDir(ui, repo) path = dir + '/cl.' + self.name f = open(path+'!', "w") f.write(self.DiskText()) f.close() if sys.platform == "win32" and os.path.isfile(path): os.remove(path) 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), private=self.private) def Delete(self, ui, repo): dir = CodeReviewDir(ui, repo) os.unlink(dir + "/cl." + self.name) def Subject(self): s = line1(self.desc) if len(s) > 60: s = s[0:55] + "..." if self.name != "new": s = "code review %s: %s" % (self.name, s) typecheck(s, str) return s def Upload(self, ui, repo, send_mail=False, gofmt=True, gofmt_just_warn=False, creating=False, quiet=False): if not self.files and not creating: ui.warn("no files in change list\n") set_status("uploading CL metadata + diffs") os.chdir(repo.root) form_fields = [ ("content_upload", "1"), ("reviewers", JoinComma(self.reviewer)), ("cc", JoinComma(self.cc)), ("description", self.desc), ("base_hashes", ""), ] if self.name != "new": form_fields.append(("issue", self.name)) vcs = None # We do not include files when creating the issue, # because we want the patch sets to record the repository # and base revision they are diffs against. We use the patch # set message for that purpose, but there is no message with # the first patch set. Instead the message gets used as the # new CL's overall subject. So omit the diffs when creating # and then we'll run an immediate upload. # This has the effect that every CL begins with an empty "Patch set 1". if self.files and not creating: vcs = MercurialVCS(upload_options, ui, repo) data = vcs.GenerateDiff(self.files) files = vcs.GetBaseFiles(data) if len(data) > MAX_UPLOAD_SIZE: uploaded_diff_file = [] form_fields.append(("separate_patches", "1")) else: uploaded_diff_file = [("data", "data.diff", data)] else: uploaded_diff_file = [("data", "data.diff", emptydiff)] if vcs and self.name != "new": form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + getremote(ui, repo, {}).path)) else: # First upload sets the subject for the CL itself. form_fields.append(("subject", self.Subject())) ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) response_body = MySend("/upload", body, content_type=ctype) patchset = None msg = response_body lines = msg.splitlines() if len(lines) >= 2: msg = lines[0] patchset = lines[1].strip() patches = [x.split(" ", 1) for x in lines[2:]] if response_body.startswith("Issue updated.") and quiet: pass else: ui.status(msg + "\n") set_status("uploaded CL metadata + diffs") if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."): raise util.Abort("failed to update issue: " + response_body) issue = msg[msg.rfind("/")+1:] self.name = issue if not self.url: self.url = server_url_base + self.name if not uploaded_diff_file: set_status("uploading patches") patches = UploadSeparatePatches(issue, rpc, patchset, data, upload_options) if vcs: set_status("uploading base files") vcs.UploadBaseFiles(issue, rpc, patches, patchset, upload_options, files) if send_mail: set_status("sending mail") MySend("/" + issue + "/mail", payload="") self.web = True set_status("flushing changes to disk") self.Flush(ui, repo) return def Mail(self, ui, repo): pmsg = "Hello " + JoinComma(self.reviewer) if self.cc: pmsg += " (cc: %s)" % (', '.join(self.cc),) pmsg += ",\n" pmsg += "\n" repourl = getremote(ui, repo, {}).path if not self.mailed: pmsg += "I'd like you to review this change to\n" + repourl + "\n" else: pmsg += "Please take another look.\n" typecheck(pmsg, str) PostMessage(ui, self.name, pmsg, subject=self.Subject()) self.mailed = True self.Flush(ui, repo) def GoodCLName(name): typecheck(name, str) return re.match("^[0-9]+$", name) def ParseCL(text, name): typecheck(text, str) typecheck(name, str) sname = None lineno = 0 sections = { 'Author': '', 'Description': '', 'Files': '', 'URL': '', 'Reviewer': '', 'CC': '', 'Mailed': '', 'Private': '', } for line in text.split('\n'): lineno += 1 line = line.rstrip() if line != '' and line[0] == '#': continue if line == '' or line[0] == ' ' or line[0] == '\t': if sname == None and line != '': return None, lineno, 'text outside section' if sname != None: sections[sname] += line + '\n' continue p = line.find(':') if p >= 0: s, val = line[:p].strip(), line[p+1:].strip() if s in sections: sname = s if val != '': sections[sname] += val + '\n' continue return None, lineno, 'malformed section header' for k in sections: sections[k] = StripCommon(sections[k]).rstrip() cl = CL(name) if sections['Author']: cl.copied_from = sections['Author'] cl.desc = sections['Description'] for line in sections['Files'].split('\n'): i = line.find('#') if i >= 0: line = line[0:i].rstrip() line = line.strip() if line == '': continue cl.files.append(line) cl.reviewer = SplitCommaSpace(sections['Reviewer']) cl.cc = SplitCommaSpace(sections['CC']) cl.url = sections['URL'] if sections['Mailed'] != 'False': # Odd default, but avoids spurious mailings when # reading old CLs that do not have a Mailed: line. # 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, '' def SplitCommaSpace(s): typecheck(s, str) s = s.strip() if s == "": return [] return re.split(", *", s) def CutDomain(s): typecheck(s, str) i = s.find('@') if i >= 0: s = s[0:i] return s def JoinComma(l): for s in l: typecheck(s, str) return ", ".join(l) def ExceptionDetail(): s = str(sys.exc_info()[0]) if s.startswith("<type '") and s.endswith("'>"): s = s[7:-2] elif s.startswith("<class '") and s.endswith("'>"): s = s[8:-2] arg = str(sys.exc_info()[1]) if len(arg) > 0: s += ": " + arg return s def IsLocalCL(ui, repo, name): return GoodCLName(name) and os.access(CodeReviewDir(ui, repo) + "/cl." + name, 0) # Load CL from disk and/or the web.def LoadCL(ui, repo, name, web=True): typecheck(name, str)Lines 1–500 of 3217. Load more
# coding=utf-8 # (The line above is necessary so that I can use 世界 in the # *comment* below without Python getting all bent out of shape.) # Copyright 2007-2009 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. '''Mercurial interface to codereview.appspot.com. To configure, set the following options in your repository's .hg/hgrc file. [extensions] codereview = path/to/codereview.py [codereview] server = codereview.appspot.com The server should be running Rietveld; see http://code.google.com/p/rietveld/. In addition to the new commands, this extension introduces the file pattern syntax @nnnnnn, where nnnnnn is a change list number, to mean the files included in that change list, which must be associated with the current client. For example, if change 123456 contains the files x.go and y.go, "hg diff @123456" is equivalent to"hg diff x.go y.go". ''' from mercurial import cmdutil, commands, hg, util, error, match from mercurial.node import nullrev, hex, nullid, short import os, re, time import stat import subprocess import threading from HTMLParser import HTMLParser # The standard 'json' package is new in Python 2.6. # Before that it was an external package named simplejson. try: # Standard location in 2.6 and beyond. import json except Exception, e: try: # Conventional name for earlier package. import simplejson as json except: try: # Was also bundled with django, which is commonly installed. from django.utils import simplejson as json except: # We give up. raise e try: hgversion = util.version() except: from mercurial.version import version as v hgversion = v.get_version() try: from mercurial.discovery import findcommonincoming except: def findcommonincoming(repo, remote): return repo.findcommonincoming(remote) oldMessage = """ The code review extension requires Mercurial 1.3 or newer. To install a new Mercurial, sudo easy_install mercurial works on most systems. """ linuxMessage = """ You may need to clear your current Mercurial installation by running: sudo apt-get remove mercurial mercurial-common sudo rm -rf /etc/mercurial """ if hgversion < '1.3': msg = oldMessage if os.access("/etc/mercurial", 0): msg += linuxMessage raise util.Abort(msg) def promptyesno(ui, msg): # Arguments to ui.prompt changed between 1.3 and 1.3.1. # Even so, some 1.3.1 distributions seem to have the old prompt!?!? # What a terrible way to maintain software. try: return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0 except AttributeError: return ui.prompt(msg, ["&yes", "&no"], "y") != "n" # To experiment with Mercurial in the python interpreter: # >>> repo = hg.repository(ui.ui(), path = ".") ####################################################################### # Normally I would split this into multiple files, but it simplifies # import path headaches to keep it all in one file. Sorry. import sys if __name__ == "__main__": print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly." sys.exit(2) server = "codereview.appspot.com" server_url_base = None defaultcc = None contributors = {} missing_codereview = None real_rollback = None releaseBranch = None ####################################################################### # RE: UNICODE STRING HANDLING # # Python distinguishes between the str (string of bytes) # and unicode (string of code points) types. Most operations # work on either one just fine, but some (like regexp matching) # require unicode, and others (like write) require str. # # As befits the language, Python hides the distinction between # unicode and str by converting between them silently, but # *only* if all the bytes/code points involved are 7-bit ASCII. # This means that if you're not careful, your program works # fine on "hello, world" and fails on "hello, 世界". And of course, # the obvious way to be careful - use static types - is unavailable. # So the only way is trial and error to find where to put explicit # conversions. # # Because more functions do implicit conversion to str (string of bytes) # than do implicit conversion to unicode (string of code points), # the convention in this module is to represent all text as str, # converting to unicode only when calling a unicode-only function # and then converting back to str as soon as possible. def typecheck(s, t): if type(s) != t: raise util.Abort("type check failed: %s has type %s != %s" % (repr(s), type(s), t)) # If we have to pass unicode instead of str, ustr does that conversion clearly. def ustr(s): typecheck(s, str) return s.decode("utf-8") # Even with those, Mercurial still sometimes turns unicode into str # and then tries to use it as ascii. Change Mercurial's default. def set_mercurial_encoding_to_utf8(): from mercurial import encoding encoding.encoding = 'utf-8' set_mercurial_encoding_to_utf8() # Even with those we still run into problems. # I tried to do things by the book but could not convince # Mercurial to let me check in a change with UTF-8 in the # CL description or author field, no matter how many conversions # between str and unicode I inserted and despite changing the # default encoding. I'm tired of this game, so set the default # encoding for all of Python to 'utf-8', not 'ascii'. def default_to_utf8(): import sys reload(sys) # site.py deleted setdefaultencoding; get it back sys.setdefaultencoding('utf-8') default_to_utf8() ####################################################################### # Change list parsing. # # Change lists are stored in .hg/codereview/cl.nnnnnn # where nnnnnn is the number assigned by the code review server. # Most data about a change list is stored on the code review server # too: the description, reviewer, and cc list are all stored there. # The only thing in the cl.nnnnnn file is the list of relevant files. # Also, the existence of the cl.nnnnnn file marks this repository # as the one where the change list lives. emptydiff = """Index: ~rietveld~placeholder~ =================================================================== diff --git a/~rietveld~placeholder~ b/~rietveld~placeholder~ new file mode 100644 """ class CL(object): def __init__(self, name): typecheck(name, str) self.name = name self.desc = '' self.files = [] self.reviewer = [] self.cc = [] self.url = '' self.local = False 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") s += "Files:\n" for f in cl.files: s += "\t" + f + "\n" typecheck(s, str) return s def EditorText(self): cl = self s = _change_prolog s += "\n" if cl.copied_from: 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" s += "Description:\n" if cl.desc == '': s += "\t<enter description here>\n" else: s += Indent(cl.desc, "\t") s += "\n" if cl.local or cl.name == "new": s += "Files:\n" for f in cl.files: s += "\t" + f + "\n" s += "\n" typecheck(s, str) return s def PendingText(self): cl = self s = cl.name + ":" + "\n" s += Indent(cl.desc, "\t") s += "\n" if cl.copied_from: s += "\tAuthor: " + cl.copied_from + "\n" s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n" s += "\tCC: " + JoinComma(cl.cc) + "\n" s += "\tFiles:\n" for f in cl.files: s += "\t\t" + f + "\n" typecheck(s, str) return s def Flush(self, ui, repo): if self.name == "new": self.Upload(ui, repo, gofmt_just_warn=True, creating=True) dir = CodeReviewDir(ui, repo) path = dir + '/cl.' + self.name f = open(path+'!', "w") f.write(self.DiskText()) f.close() if sys.platform == "win32" and os.path.isfile(path): os.remove(path) 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), private=self.private) def Delete(self, ui, repo): dir = CodeReviewDir(ui, repo) os.unlink(dir + "/cl." + self.name) def Subject(self): s = line1(self.desc) if len(s) > 60: s = s[0:55] + "..." if self.name != "new": s = "code review %s: %s" % (self.name, s) typecheck(s, str) return s def Upload(self, ui, repo, send_mail=False, gofmt=True, gofmt_just_warn=False, creating=False, quiet=False): if not self.files and not creating: ui.warn("no files in change list\n") set_status("uploading CL metadata + diffs") os.chdir(repo.root) form_fields = [ ("content_upload", "1"), ("reviewers", JoinComma(self.reviewer)), ("cc", JoinComma(self.cc)), ("description", self.desc), ("base_hashes", ""), ] if self.name != "new": form_fields.append(("issue", self.name)) vcs = None # We do not include files when creating the issue, # because we want the patch sets to record the repository # and base revision they are diffs against. We use the patch # set message for that purpose, but there is no message with # the first patch set. Instead the message gets used as the # new CL's overall subject. So omit the diffs when creating # and then we'll run an immediate upload. # This has the effect that every CL begins with an empty "Patch set 1". if self.files and not creating: vcs = MercurialVCS(upload_options, ui, repo) data = vcs.GenerateDiff(self.files) files = vcs.GetBaseFiles(data) if len(data) > MAX_UPLOAD_SIZE: uploaded_diff_file = [] form_fields.append(("separate_patches", "1")) else: uploaded_diff_file = [("data", "data.diff", data)] else: uploaded_diff_file = [("data", "data.diff", emptydiff)] if vcs and self.name != "new": form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + getremote(ui, repo, {}).path)) else: # First upload sets the subject for the CL itself. form_fields.append(("subject", self.Subject())) ctype, body = EncodeMultipartFormData(form_fields, uploaded_diff_file) response_body = MySend("/upload", body, content_type=ctype) patchset = None msg = response_body lines = msg.splitlines() if len(lines) >= 2: msg = lines[0] patchset = lines[1].strip() patches = [x.split(" ", 1) for x in lines[2:]] if response_body.startswith("Issue updated.") and quiet: pass else: ui.status(msg + "\n") set_status("uploaded CL metadata + diffs") if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."): raise util.Abort("failed to update issue: " + response_body) issue = msg[msg.rfind("/")+1:] self.name = issue if not self.url: self.url = server_url_base + self.name if not uploaded_diff_file: set_status("uploading patches") patches = UploadSeparatePatches(issue, rpc, patchset, data, upload_options) if vcs: set_status("uploading base files") vcs.UploadBaseFiles(issue, rpc, patches, patchset, upload_options, files) if send_mail: set_status("sending mail") MySend("/" + issue + "/mail", payload="") self.web = True set_status("flushing changes to disk") self.Flush(ui, repo) return def Mail(self, ui, repo): pmsg = "Hello " + JoinComma(self.reviewer) if self.cc: pmsg += " (cc: %s)" % (', '.join(self.cc),) pmsg += ",\n" pmsg += "\n" repourl = getremote(ui, repo, {}).path if not self.mailed: pmsg += "I'd like you to review this change to\n" + repourl + "\n" else: pmsg += "Please take another look.\n" typecheck(pmsg, str) PostMessage(ui, self.name, pmsg, subject=self.Subject()) self.mailed = True self.Flush(ui, repo) def GoodCLName(name): typecheck(name, str) return re.match("^[0-9]+$", name) def ParseCL(text, name): typecheck(text, str) typecheck(name, str) sname = None lineno = 0 sections = { 'Author': '', 'Description': '', 'Files': '', 'URL': '', 'Reviewer': '', 'CC': '', 'Mailed': '', 'Private': '', } for line in text.split('\n'): lineno += 1 line = line.rstrip() if line != '' and line[0] == '#': continue if line == '' or line[0] == ' ' or line[0] == '\t': if sname == None and line != '': return None, lineno, 'text outside section' if sname != None: sections[sname] += line + '\n' continue p = line.find(':') if p >= 0: s, val = line[:p].strip(), line[p+1:].strip() if s in sections: sname = s if val != '': sections[sname] += val + '\n' continue return None, lineno, 'malformed section header' for k in sections: sections[k] = StripCommon(sections[k]).rstrip() cl = CL(name) if sections['Author']: cl.copied_from = sections['Author'] cl.desc = sections['Description'] for line in sections['Files'].split('\n'): i = line.find('#') if i >= 0: line = line[0:i].rstrip() line = line.strip() if line == '': continue cl.files.append(line) cl.reviewer = SplitCommaSpace(sections['Reviewer']) cl.cc = SplitCommaSpace(sections['CC']) cl.url = sections['URL'] if sections['Mailed'] != 'False': # Odd default, but avoids spurious mailings when # reading old CLs that do not have a Mailed: line. # 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, '' def SplitCommaSpace(s): typecheck(s, str) s = s.strip() if s == "": return [] return re.split(", *", s) def CutDomain(s): typecheck(s, str) i = s.find('@') if i >= 0: s = s[0:i] return s def JoinComma(l): for s in l: typecheck(s, str) return ", ".join(l) def ExceptionDetail(): s = str(sys.exc_info()[0]) if s.startswith("<type '") and s.endswith("'>"): s = s[7:-2] elif s.startswith("<class '") and s.endswith("'>"): s = s[8:-2] arg = str(sys.exc_info()[1]) if len(arg) > 0: s += ": " + arg return s def IsLocalCL(ui, repo, name): return GoodCLName(name) and os.access(CodeReviewDir(ui, repo) + "/cl." + name, 0) # Load CL from disk and/or the web. def LoadCL(ui, repo, name, web=True): typecheck(name, str)