# 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".''' import sys if __name__ == "__main__": print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly." sys.exit(2) # We require Python 2.6 for the json package.if sys.version < '2.6': print >>sys.stderr, "The codereview extension requires Python 2.6 or newer." print >>sys.stderr, "You are running Python " + sys.version sys.exit(2) import jsonimport osimport reimport statimport subprocessimport threadingimport time from mercurial import commands as hg_commandsfrom mercurial import util as hg_util defaultcc = Nonecodereview_disabled = Nonereal_rollback = NonereleaseBranch = Noneserver = "codereview.appspot.com"server_url_base = None ######################################################################## Normally I would split this into multiple files, but it simplifies# import path headaches to keep it all in one file. Sorry.# The different parts of the file are separated by banners like this one. ######################################################################## Helpers def RelativePath(path, cwd): n = len(cwd) if path.startswith(cwd) and path[n] == '/': return path[n+1:] return path def Sub(l1, l2): return [l for l in l1 if l not in l2] def Add(l1, l2): l = l1 + Sub(l2, l1) l.sort() return l def Intersect(l1, l2): return [l for l in l1 if l in l2] ######################################################################## 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 hg_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 stdout, __stdout__ = sys.stdout, sys.__stdout__ reload(sys) # site.py deleted setdefaultencoding; get it back sys.stdout, sys.__stdout__ = stdout, __stdout__ sys.setdefaultencoding('utf-8') default_to_utf8() ######################################################################## Status printer for long-running commands global_status = None def set_status(s): # print >>sys.stderr, "\t", time.asctime(), s global global_status global_status = s class StatusThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): # pause a reasonable amount of time before # starting to display status messages, so that # most hg commands won't ever see them. time.sleep(30) # now show status every 15 seconds while True: time.sleep(15 - time.time() % 15) s = global_status if s is None: continue if s == "": s = "(unknown status)" print >>sys.stderr, time.asctime(), s def start_status_thread(): t = StatusThread() t.setDaemon(True) # allowed to exit if t is still running t.start() ######################################################################## 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 self.lgtm = [] 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, quick=False): cl = self s = cl.name + ":" + "\n" s += Indent(cl.desc, "\t") s += "\n" if cl.copied_from: s += "\tAuthor: " + cl.copied_from + "\n" if not quick: s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n" for (who, line) in cl.lgtm: s += "\t\t" + who + ": " + line + "\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") if ui.configbool("codereview", "force_gofmt", True) and gofmt: CheckFormat(ui, repo, self.files, just_warn=gofmt_just_warn) 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 + " " + ui.expandpath("default"))) 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 hg_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 = ui.expandpath("default") 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 sLines 1–500 of 3562. 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". ''' import sys if __name__ == "__main__": print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly." sys.exit(2) # We require Python 2.6 for the json package. if sys.version < '2.6': print >>sys.stderr, "The codereview extension requires Python 2.6 or newer." print >>sys.stderr, "You are running Python " + sys.version sys.exit(2) import json import os import re import stat import subprocess import threading import time from mercurial import commands as hg_commands from mercurial import util as hg_util defaultcc = None codereview_disabled = None real_rollback = None releaseBranch = None server = "codereview.appspot.com" server_url_base = None ####################################################################### # Normally I would split this into multiple files, but it simplifies # import path headaches to keep it all in one file. Sorry. # The different parts of the file are separated by banners like this one. ####################################################################### # Helpers def RelativePath(path, cwd): n = len(cwd) if path.startswith(cwd) and path[n] == '/': return path[n+1:] return path def Sub(l1, l2): return [l for l in l1 if l not in l2] def Add(l1, l2): l = l1 + Sub(l2, l1) l.sort() return l def Intersect(l1, l2): return [l for l in l1 if l in l2] ####################################################################### # 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 hg_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 stdout, __stdout__ = sys.stdout, sys.__stdout__ reload(sys) # site.py deleted setdefaultencoding; get it back sys.stdout, sys.__stdout__ = stdout, __stdout__ sys.setdefaultencoding('utf-8') default_to_utf8() ####################################################################### # Status printer for long-running commands global_status = None def set_status(s): # print >>sys.stderr, "\t", time.asctime(), s global global_status global_status = s class StatusThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): # pause a reasonable amount of time before # starting to display status messages, so that # most hg commands won't ever see them. time.sleep(30) # now show status every 15 seconds while True: time.sleep(15 - time.time() % 15) s = global_status if s is None: continue if s == "": s = "(unknown status)" print >>sys.stderr, time.asctime(), s def start_status_thread(): t = StatusThread() t.setDaemon(True) # allowed to exit if t is still running t.start() ####################################################################### # 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 self.lgtm = [] 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, quick=False): cl = self s = cl.name + ":" + "\n" s += Indent(cl.desc, "\t") s += "\n" if cl.copied_from: s += "\tAuthor: " + cl.copied_from + "\n" if not quick: s += "\tReviewer: " + JoinComma(cl.reviewer) + "\n" for (who, line) in cl.lgtm: s += "\t\t" + who + ": " + line + "\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") if ui.configbool("codereview", "force_gofmt", True) and gofmt: CheckFormat(ui, repo, self.files, just_warn=gofmt_just_warn) 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 + " " + ui.expandpath("default"))) 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 hg_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 = ui.expandpath("default") 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