diff options
| author | Ondřej Surý <ondrej@sury.org> | 2011-04-28 10:35:15 +0200 | 
|---|---|---|
| committer | Ondřej Surý <ondrej@sury.org> | 2011-04-28 10:35:15 +0200 | 
| commit | c1ba1a0fec4aed430709030f98a3bdb90bfeea16 (patch) | |
| tree | 3df18657e50a0313ed6defcda30e4474cb28a467 /lib/codereview/codereview.py | |
| parent | 7b15ed9ef455b6b66c6b376898a88aef5d6a9970 (diff) | |
| download | golang-c1ba1a0fec4aed430709030f98a3bdb90bfeea16.tar.gz | |
Imported Upstream version 2011.04.27upstream/2011.04.27
Diffstat (limited to 'lib/codereview/codereview.py')
| -rw-r--r-- | lib/codereview/codereview.py | 187 | 
1 files changed, 160 insertions, 27 deletions
| diff --git a/lib/codereview/codereview.py b/lib/codereview/codereview.py index 766e827fc..bfa69fcc0 100644 --- a/lib/codereview/codereview.py +++ b/lib/codereview/codereview.py @@ -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 "" @@ -983,7 +997,10 @@ def CheckTabfmt(ui, repo, files, just_warn):  	for f in files:  		try:  			for line in open(f, 'r'): -				if line.startswith('    '): +				# Four leading spaces is enough to complain about, +				# except that some Plan 9 code uses four spaces as the label indent, +				# so allow that. +				if line.startswith('    ') and not re.match('    [A-Za-z0-9_]+:', line):  					badfiles.append(f)  					break  		except: @@ -1066,7 +1083,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 +1104,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 +1124,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 +1154,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 +1265,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 +1474,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 +1630,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()  		raise  	# we're committed. upload final patch, close review, add commit message @@ -1551,7 +1648,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 +1698,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 +1772,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 +1837,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,  		[], @@ -1813,6 +1919,16 @@ def IsRietveldSubmitted(ui, clname, hex):  			return True  	return False +def IsRietveldMailed(ui, clname): +	feed = XMLGet(ui, "/rss/issue/" + clname) +	if feed is None: +		return False +	for sum in feed.findall("{http://www.w3.org/2005/Atom}entry/{http://www.w3.org/2005/Atom}summary"): +		text = sum.text.strip() +		if re.match("I'd like you to review this change", text): +			return True +	return False +  def DownloadCL(ui, repo, clname):  	set_status("downloading CL " + clname)  	cl, err = LoadCL(ui, repo, clname) @@ -1875,7 +1991,9 @@ def DownloadCL(ui, repo, clname):  	# Print warning if email is not in CONTRIBUTORS file.  	him = FindContributor(ui, repo, email)  	me = FindContributor(ui, repo, None) -	if him != me: +	if him == me: +		cl.mailed = IsRietveldMailed(ui, clname) +	else:  		cl.copied_from = email  	return cl, vers, diffdata, "" @@ -1992,7 +2110,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 +2121,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 +2159,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 +2197,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') | 
