code review fixes

* clean up error handling: show Exception info
* white space fixes
* clean up output when creating CL
* simplify hg change command; add hg file
* fix stale cookie bug (thanks iant)
* in LoadAllCL, load each CL in a different thread,
  to parallelize the slow web fetches
* throw away support for Mercurial before version 1.3
* add @CL-number file pattern for commands like diff
* make hg sync show files being sync'ed

R=r
http://go/go-review/1016016
diff --git a/lib/codereview/codereview.py b/lib/codereview/codereview.py
index 8ee0a6b..7bb7e6b 100644
--- a/lib/codereview/codereview.py
+++ b/lib/codereview/codereview.py
@@ -23,27 +23,32 @@
     codereview = path/to/codereview.py
 
     [codereview]
-    project = project-url        # optional
+	server = codereview.appspot.com
 
-If the project URL is specified, codereview will fetch
-default the reviewer and cc list from that URL each time
-it runs an "upload" command.
+The server should be running Rietveld; see http://code.google.com/p/rietveld/.
 '''
 
+# TODO(rsc):
+#	fix utf-8 upload bug
+#	look for and clear submitted CLs during sync / add "adopt" command?
+#	creating an issue prints the URL twice
+#	better documentation
+
 from mercurial import cmdutil, commands, hg, util, error, match
 from mercurial.node import nullrev, hex, nullid, short
 import os, re
 import stat
+import threading
 from HTMLParser import HTMLParser
 
 try:
 	hgversion = util.version()
-except Exception, e:
+except:
 	from mercurial.version import version as v
 	hgversion = v.get_version()
 
 
-# To experiment with Mercurial in the python interpreter: 
+# To experiment with Mercurial in the python interpreter:
 #    >>> repo = hg.repository(ui.ui(), path = ".")
 
 #######################################################################
@@ -108,7 +113,7 @@
 			s += "\t" + f + "\n"
 		s += "\n"
 		return s
-	
+
 	def PendingText(self):
 		cl = self
 		s = cl.name + ":" + "\n"
@@ -120,7 +125,7 @@
 		for f in cl.files:
 			s += "\t\t" + f + "\n"
 		return s
-	
+
 	def Flush(self, ui, repo):
 		if self.name == "new":
 			self.Upload(ui, repo)
@@ -133,7 +138,7 @@
 		if self.web:
 			EditDesc(self.name, subject=line1(self.desc), desc=self.desc,
 				reviewers=JoinComma(self.reviewer), cc=JoinComma(self.cc))
-	
+
 	def Delete(self, ui, repo):
 		dir = CodeReviewDir(ui, repo)
 		os.unlink(dir + "/cl." + self.name)
@@ -150,7 +155,7 @@
 		]
 
 		# NOTE(rsc): This duplicates too much of RealMain,
-		# but RealMain doesn't have the nicest interface in the world.
+		# but RealMain doesn't have the most reusable interface.
 		if self.name != "new":
 			form_fields.append(("issue", self.name))
 		vcs = GuessVCS(upload_options)
@@ -170,12 +175,14 @@
 			msg = lines[0]
 			patchset = lines[1].strip()
 			patches = [x.split(" ", 1) for x in lines[2:]]
-		ui.status("uploaded: " + msg + "\n")
+		ui.status(msg + "\n")
 		if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."):
 			print response_body
 			raise "failed to update issue"
 		issue = msg[msg.rfind("/")+1:]
 		self.name = issue
+		if not self.url:
+			self.url = server_url_base + self.name
 		if not uploaded_diff_file:
 			patches = UploadSeparatePatches(issue, rpc, patchset, data, upload_options)
 		vcs.UploadBaseFiles(issue, rpc, patches, patchset, upload_options, files)
@@ -186,7 +193,7 @@
 		return
 
 def GoodCLName(name):
-	return re.match("^[0-9]+$", name)	
+	return re.match("^[0-9]+$", name)
 
 def ParseCL(text, name):
 	sname = None
@@ -244,27 +251,40 @@
 def JoinComma(l):
 	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
+
 # Load CL from disk and/or the web.
 def LoadCL(ui, repo, name, web=True):
 	if not GoodCLName(name):
 		return None, "invalid CL name"
 	dir = CodeReviewDir(ui, repo)
 	path = dir + "cl." + name
-	try:
+	if os.access(path, 0):
 		ff = open(path)
 		text = ff.read()
 		ff.close()
 		cl, lineno, err = ParseCL(text, name)
 		if err != "":
-			return None, "malformed CL data"
+			return None, "malformed CL data: "+err
 		cl.local = True
-	except Exception, e:
+	else:
 		cl = CL(name)
 	if web:
 		try:
 			f = GetSettings(name)
-		except Exception, e:
-			return None, "cannot load CL data from code review server"
+		except:
+			return None, "cannot load CL data from code review server: "+ExceptionDetail()
+		if 'reviewers' not in f:
+			return None, "malformed response loading CL data from code review server"
 		cl.reviewer = SplitCommaSpace(f['reviewers'])
 		cl.cc = SplitCommaSpace(f['cc'])
 		cl.desc = f['description']
@@ -272,17 +292,40 @@
 		cl.web = True
 	return cl, ''
 
+class LoadCLThread(threading.Thread):
+	def __init__(self, ui, repo, dir, f, web):
+		threading.Thread.__init__(self)
+		self.ui = ui
+		self.repo = repo
+		self.f = f
+		self.web = web
+		self.cl = None
+	def run(self):
+		cl, err = LoadCL(self.ui, self.repo, self.f[3:], web=self.web)
+		if err != '':
+			self.ui.warn("loading "+self.dir+self.f+": " + err + "\n")
+			return
+		self.cl = cl
+
 # Load all the CLs from this repository.
 def LoadAllCL(ui, repo, web=True):
 	dir = CodeReviewDir(ui, repo)
 	m = {}
-	for f in os.listdir(dir):
-		if f.startswith('cl.'):
-			cl, err = LoadCL(ui, repo, f[3:], web=web)
-			if err != '':
-				ui.warn("loading "+dir+f+": " + err + "\n")
-				continue
-			m[cl.name] = cl
+	files = [f for f in os.listdir(dir) if f.startswith('cl.')]
+	if not files:
+		return m
+	if web:
+		# Authenticate now, so we can use threads below
+		MySend(None)
+	active = []
+	for f in files:
+		t = LoadCLThread(ui, repo, dir, f, web)
+		t.start()
+		active.append(t)
+	for t in active:
+		t.join()
+		if t.cl:
+			m[t.cl.name] = t.cl
 	return m
 
 # Find repository root.  On error, ui.warn and return None
@@ -305,8 +348,8 @@
 	if not os.path.isdir(dir):
 		try:
 			os.mkdir(dir, 0700)
-		except Exception, e:
-			ui.warn('cannot mkdir %s: %s\n' % (dir, e))
+		except:
+			ui.warn('cannot mkdir %s: %s\n' % (dir, ExceptionDetail()))
 			return None
 	return dir
 
@@ -364,21 +407,18 @@
 # Return list of changed files in repository that match pats.
 def ChangedFiles(ui, repo, pats, opts):
 	# Find list of files being operated on.
-	# TODO(rsc): The cutoff might not be 1.3.
-	# Definitely after 1.0.2.
-	try:
-		matcher = cmdutil.match(repo, pats, opts)
-		node1, node2 = cmdutil.revpair(repo, None)
-		modified, added, removed = repo.status(node1, node2, matcher)[:3]
-	except AttributeError, e:
-		# Probably in earlier Mercurial, say 1.0.2.
-		_, matcher, _ = cmdutil.matchpats(repo, pats, opts)
-		node1, node2 = cmdutil.revpair(repo, None)
-		modified, added, removed = repo.status(node1, node2, match=matcher)[:3]
-	return modified + added + removed
+	matcher = cmdutil.match(repo, pats, opts)
+	node1, node2 = cmdutil.revpair(repo, None)
+	modified, added, removed = repo.status(node1, node2, matcher)[:3]
+	l = modified + added + removed
+	l.sort()
+	return l
 
 # Return list of files claimed by existing CLs
 def TakenFiles(ui, repo):
+	return Taken(ui, repo).keys()
+
+def Taken(ui, repo):
 	all = LoadAllCL(ui, repo, web=False)
 	taken = {}
 	for _, cl in all.items():
@@ -394,19 +434,17 @@
 	return [l for l in l1 if l not in l2]
 
 def Add(l1, l2):
-	return l1 + Sub(l2, l1)
+	l = l1 + Sub(l2, l1)
+	l.sort()
+	return l
 
 def Intersect(l1, l2):
 	return [l for l in l1 if l in l2]
 
 def Incoming(ui, repo, opts, op):
 	source, _, _ = hg.parseurl(ui.expandpath("default"), None)
-	try:
-		other = hg.repository(cmdutil.remoteui(repo, opts), source)
-		_, incoming, _ = repo.findcommonincoming(other)
-	except AttributeError, e:
-		other = hg.repository(ui, source)
-		incoming = repo.findincoming(other)
+	other = hg.repository(cmdutil.remoteui(repo, opts), source)
+	_, incoming, _ = repo.findcommonincoming(other)
 	return incoming
 
 def EditCL(ui, repo, cl):
@@ -415,7 +453,6 @@
 		s = ui.edit(s, ui.username())
 		clx, line, err = ParseCL(s, cl.name)
 		if err != '':
-			# TODO(rsc): another 1.3 inconsistency
 			if ui.prompt("error parsing change list: line %d: %s\nre-edit (y/n)?" % (line, err), ["&yes", "&no"], "y") == "n":
 				return "change list not modified"
 			continue
@@ -458,6 +495,26 @@
 				return None, err
 	return cl, ""
 
+# reposetup replaces cmdutil.match with this wrapper,
+# which expands the syntax @clnumber to mean the files
+# in that CL.
+original_match = None
+def ReplacementForCmdutilMatch(repo, pats=[], opts={}, globbed=False, default='relpath'):
+	taken = []
+	files = []
+	for p in pats:
+		if p.startswith('@'):
+			taken.append(p)
+			clname = p[1:]
+			if not GoodCLName(clname):
+				raise util.Abort("invalid CL name " + clname)
+			cl, err = LoadCL(repo.ui, repo, clname, web=False)
+			if err != '':
+				raise util.Abort("loading CL " + clname + ": " + err)
+			files = Add(files, cl.files)
+	pats = Sub(pats, taken)	+ ['path:'+f for f in files]	
+	return original_match(repo, pats=pats, opts=opts, globbed=globbed, default=default)
+
 #######################################################################
 # Mercurial commands
 
@@ -473,48 +530,52 @@
 # Other parameters are taken in order from items on the command line that
 # don't start with a dash.  If no default value is given in the parameter list,
 # they are required.
-# 
+#
 
-# Change command.
 def change(ui, repo, *pats, **opts):
 	"""create or edit a change list
-	
+
 	Create or edit a change list.
 	A change list is a group of files to be reviewed and submitted together,
 	plus a textual description of the change.
 	Change lists are referred to by simple alphanumeric names.
 
 	Changes must be reviewed before they can be submitted.
-	
-	In the absence of options, the change command opens the
-	change list for editing in the default editor.  
-	"""
-	
-	if opts["add"] and opts["delete"]:
-		return "cannot use -a with -d"
 
-	if (opts["add"] or opts["delete"]) and (opts["stdin"] or opts["stdout"]):
-		return "cannot use -a/-d with -i/-o"
+	In the absence of options, the change command opens the
+	change list for editing in the default editor.
+	"""
 
 	dirty = {}
 	if len(pats) > 0 and GoodCLName(pats[0]):
 		name = pats[0]
+		if len(pats) != 1:
+			return "cannot specify CL name and file patterns"
 		pats = pats[1:]
 		cl, err = LoadCL(ui, repo, name, web=True)
 		if err != '':
 			return err
-		if not cl.local and (opts["add"] or opts["delete"] or opts["stdin"] or not opts["stdout"]):
+		if not cl.local and (opts["stdin"] or not opts["stdout"]):
 			return "cannot change non-local CL " + name
 	else:
-		if opts["add"] or opts["delete"]:
-			return "cannot use -a/-d when creating CL"
 		name = "new"
 		cl = CL("new")
 		dirty[cl] = True
+		files = ChangedFiles(ui, repo, pats, opts)
+		taken = TakenFiles(ui, repo)
+		files = Sub(files, taken)
 	
-	files = ChangedFiles(ui, repo, pats, opts)
-	taken = TakenFiles(ui, repo)
-	files = Sub(files, taken)
+	if opts["delete"]:
+		if name == "new":
+			return "cannot use -d with file patterns"
+		if opts["stdin"] or opts["stdout"]:
+			return "cannot use -d with -i or -o"
+		if not cl.local:
+			return "cannot change non-local CL " + name
+		PostMessage(cl.name, "*** Abandoned ***", send_mail="checked")
+		EditDesc(cl.name, closed="checked")
+		cl.Delete(ui, repo)
+		return
 
 	if opts["stdin"]:
 		s = sys.stdin.read()
@@ -534,35 +595,7 @@
 			cl.files = clx.files
 			dirty[cl] = True
 
-	if opts["add"]:
-		newfiles = Sub(files, cl.files)
-		stolen = Intersect(newfiles, taken)
-		if stolen:
-			ui.status("# Taking files from other CLs.  To undo:\n")
-			for f in stolen:
-				ocl = taken[f]
-				ui.status("#	hg change -a %s %s\n" % (ocl.name, f))
-				ocl.files = Sub(ocl.files, [f])
-				dirty[ocl] = True
-		not_stolen = Sub(newfiles, stolen)
-		if not_stolen:
-			ui.status("# Add files to CL.  To undo:\n")
-			for f in not_stolen:
-				ui.status("#	hg change -d %s %s\n" % (cl.name, f))
-		if newfiles:
-			cl.files += newfiles
-			dirty[cl] = True
-
-	if opts["delete"]:
-		oldfiles = Intersect(files, cl.files)
-		if oldfiles:
-			ui.status("# Removing files from CL.  To undo:\n")
-			for f in oldfiles:
-				ui.status("#	hg change -a %s %s\n" % (cl.name, f))
-			cl.files = Sub(cl.files, oldfiles)
-			dirty[cl] = True
-
-	if not opts["add"] and not opts["delete"] and not opts["stdin"] and not opts["stdout"]:
+	if not opts["stdin"] and not opts["stdout"]:
 		if name == "new":
 			cl.files = files
 		err = EditCL(ui, repo, cl)
@@ -572,16 +605,99 @@
 
 	for d, _ in dirty.items():
 		d.Flush(ui, repo)
-	
+
 	if opts["stdout"]:
 		ui.write(cl.EditorText())
 	elif name == "new":
 		if ui.quiet:
 			ui.write(cl.name)
 		else:
-			ui.write("URL: " + cl.url)
+			ui.write("CL created: " + cl.url + "\n")
 	return
 
+def codereview_login(ui, repo, **opts):
+	"""log in to code review server
+
+	Logs in to the code review server, saving a cookie in
+	a file in your home directory.
+	"""
+	MySend(None)
+
+def file(ui, repo, clname, pat, *pats, **opts):
+	"""assign files to or remove files from a change list
+	
+	Assign files to or (with -d) remove files from a change list.
+	
+	The -d option only removes files from the change list.
+	It does not edit them or remove them from the repository.
+	"""
+	pats = tuple([pat] + list(pats))
+	if not GoodCLName(clname):
+		return "invalid CL name " + clname
+	
+	dirty = {}
+	cl, err = LoadCL(ui, repo, clname, web=False)
+	if err != '':
+		return err
+	if not cl.local:
+		return "cannot change non-local CL " + clname
+
+	files = ChangedFiles(ui, repo, pats, opts)
+
+	if opts["delete"]:
+		oldfiles = Intersect(files, cl.files)
+		if oldfiles:
+			if not ui.quiet:
+				ui.status("# Removing files from CL.  To undo:\n")
+				ui.status("#	cd %s\n" % (repo.root))
+				for f in oldfiles:
+					ui.status("#	hg file %s %s\n" % (cl.name, f))
+			cl.files = Sub(cl.files, oldfiles)
+			cl.Flush(ui, repo)
+		else:
+			ui.status("no such files in CL")
+		return
+
+	if not files:
+		return "no such modified files"
+
+	files = Sub(files, cl.files)
+	taken = Taken(ui, repo)
+	warned = False
+	for f in files:
+		if f in taken:
+			if not warned and not ui.quiet:
+				ui.status("# Taking files from other CLs.  To undo:\n")
+				ui.status("#	cd %s\n" % (repo.root))
+				warned = True
+			ocl = taken[f]
+			if not ui.quiet:
+				ui.status("#	hg file %s %s\n" % (ocl.name, f))
+			if ocl not in dirty:
+				ocl.files = Sub(ocl.files, files)
+				dirty[ocl] = True
+	cl.files = Add(cl.files, files)
+	dirty[cl] = True
+	for d, _ in dirty.items():
+		d.Flush(ui, repo)
+	return
+	
+def mail(ui, repo, *pats, **opts):
+	cl, err = CommandLineCL(ui, repo, pats, opts)
+	if err != "":
+		return err
+	if not cl.reviewer:
+		return "no reviewers listed in CL"
+	cl.Upload(ui, repo)
+	pmsg = "Hello " + JoinComma(cl.reviewer) + ",\n"
+	pmsg += "\n"
+	pmsg += "I'd like you to review the following change.\n"
+	subject = "code review %s: %s" % (cl.name, line1(cl.desc))
+	PostMessage(cl.name, pmsg, send_mail="checked", subject=subject)
+
+def nocommit(ui, repo, *pats, **opts):
+	return "The codereview extension is enabled; do not use commit."
+
 def pending(ui, repo, *pats, **opts):
 	m = LoadAllCL(ui, repo, web=True)
 	names = m.keys()
@@ -597,33 +713,15 @@
 			s += "\t" + f + "\n"
 		ui.write(s)
 
-def upload(ui, repo, name, **opts):
-	repo.ui.quiet = True
-	cl, err = LoadCL(ui, repo, name, web=True)
-	if err != "":
-		return err
-	if not cl.local:
-		return "cannot upload non-local change"
-	cl.Upload(ui, repo)
-	print "%s%s\n" % (server_url_base, cl.name)
-	return
+def reposetup(ui, repo):
+	global original_match
+	original_match = cmdutil.match
+	cmdutil.match = ReplacementForCmdutilMatch
+	RietveldSetup(ui, repo)
 
-def mail(ui, repo, *pats, **opts):
-	cl, err = CommandLineCL(ui, repo, pats, opts)
-	if err != "":
-		return err
-	if not cl.reviewer:
-		return "no reviewers listed in CL"
-	cl.Upload(ui, repo)
-	pmsg = "Hello " + JoinComma(cl.reviewer) + ",\n"
-	pmsg += "\n"
-	pmsg += "I'd like you to review the following change.\n"
-	subject = "code review %s: %s" % (cl.name, line1(cl.desc))
-	PostMessage(cl.name, pmsg, send_mail="checked", subject=subject)
-	
 def submit(ui, repo, *pats, **opts):
 	"""submit change to remote repository
-	
+
 	Submits change to remote repository.
 	Bails out if the local repository is not in sync with the remote one.
 	"""
@@ -634,7 +732,7 @@
 	cl, err = CommandLineCL(ui, repo, pats, opts)
 	if err != "":
 		return err
-	
+
 	about = ""
 	if cl.reviewer:
 		about += "R=" + JoinComma(cl.reviewer) + "\n"
@@ -660,12 +758,8 @@
 	if date:
 		opts['date'] = util.parsedate(date)
 	opts['message'] = cl.desc.rstrip() + "\n\n" + about
-	try:
-		m = match.exact(repo.root, repo.getcwd(), cl.files)
-		node = repo.commit(opts['message'], opts.get('user'), opts.get('date'), m)
-	except Exception, e:
-		_, m, _ = util._matcher(repo.root, repo.getcwd(), cl.files, None, None, 'path', None)
-		node = repo.commit(text=opts['message'], user=opts.get('user'), date=opts.get('date'), match=m)
+	m = match.exact(repo.root, repo.getcwd(), cl.files)
+	node = repo.commit(opts['message'], opts.get('user'), opts.get('date'), m)
 	if not node:
 		return "nothing changed"
 
@@ -683,10 +777,7 @@
 	# if it works, we're committed.
 	# if not, roll back
 	dest, _, _ = hg.parseurl(ui.expandpath("default"), None)
-	try:
-		other = hg.repository(cmdutil.remoteui(repo, opts), dest)
-	except AttributeError, e:
-		other = hg.repository(ui, dest)
+	other = hg.repository(cmdutil.remoteui(repo, opts), dest)
 	r = repo.push(other, False, None)
 	if r == 0:
 		repo.rollback()
@@ -707,37 +798,42 @@
 
 def sync(ui, repo, **opts):
 	"""synchronize with remote repository
-	
+
 	Incorporates recent changes from the remote repository
 	into the local repository.
-	
-	Equivalent to the Mercurial command "hg pull -u".
 	"""
-	repo.ui.quiet = True
+	ui.status = sync_note
+	ui.note = sync_note
 	source, _, _ = hg.parseurl(ui.expandpath("default"), None)
-	try:
-		other = hg.repository(cmdutil.remoteui(repo, opts), source)
-	except AttributeError, e:
-		other = hg.repository(ui, source)
+	other = hg.repository(cmdutil.remoteui(repo, opts), source)
 	modheads = repo.pull(other)
-	return commands.postincoming(ui, repo, modheads, True, "tip")
+	err = commands.postincoming(ui, repo, modheads, True, "tip")
+	if err:
+		return err
+	sync_changes(ui, repo)
 
-def dologin(ui, repo, **opts):
-	"""log in to code review server
-	
-	Logs in to the code review server, saving a cookie in
-	a file in your home directory.
-	"""
-	MySend("/")
+def sync_note(msg):
+	if msg == 'resolving manifests\n' or msg == 'searching for changes\n':
+		return
+	sys.stdout.write(msg)
 
+def sync_changes(ui, repo):
+	pass
 
 def uisetup(ui):
 	if "^commit|ci" in commands.table:
 		commands.table["^commit|ci"] = (nocommit, [], "")
-	RietveldSetup(ui)
 
-def nocommit(ui, repo, *pats, **opts):
-	return "The codereview extension is enabled; do not use commit."
+def upload(ui, repo, name, **opts):
+	repo.ui.quiet = True
+	cl, err = LoadCL(ui, repo, name, web=True)
+	if err != "":
+		return err
+	if not cl.local:
+		return "cannot upload non-local change"
+	cl.Upload(ui, repo)
+	print "%s%s\n" % (server_url_base, cl.name)
+	return
 
 review_opts = [
 	('r', 'reviewer', '', 'add reviewer'),
@@ -749,39 +845,43 @@
 cmdtable = {
 	# The ^ means to show this command in the help text that
 	# is printed when running hg with no arguments.
-
-	# TODO: Should change upload?
 	"^change": (
 		change,
 		[
-			('a', 'add', None, 'add files to change list'),
-			('d', 'delete', None, 'remove files from change list'),
-			('o', 'stdout', None, 'print change list to standard output'),
+			('d', 'delete', None, 'delete existing change list'),
 			('i', 'stdin', None, 'read change list from standard input'),
+			('o', 'stdout', None, 'print change list to standard output'),
 		],
-		"[-a | -d | [-i] [-o]] [change#] [FILE ...]"
+		"[-i] [-o] change# or FILE ..."
+	),
+	"codereview-login": (
+		codereview_login,
+		[],
+		"",
+	),
+	"commit|ci": (
+		nocommit,
+		[],
+		"",
+	),
+	"^file": (
+		file,
+		[
+			('d', 'delete', None, 'delete files from change list (but not repository)'),
+		],
+		"[-d] change# FILE ..."
 	),
 	"^pending|p": (
 		pending,
 		[],
 		"[FILE ...]"
 	),
-
-	# TODO: cdiff - steal diff options and command line
-
-	"^upload": (
-		upload,
-		[],
-		"change#"
-	),
-	
 	"^mail": (
 		mail,
 		review_opts + [
 		] + commands.walkopts,
 		"[-r reviewer] [--cc cc] [change# | file ...]"
 	),
-
 	"^submit": (
 		submit,
 		review_opts + [
@@ -789,23 +889,15 @@
 		] + commands.walkopts + commands.commitopts + commands.commitopts2,
 		"[-r reviewer] [--cc cc] [change# | file ...]"
 	),
-	
 	"^sync": (
 		sync,
 		[],
 		"",
 	),
-	
-	"commit|ci": (
-		nocommit,
+	"^upload": (
+		upload,
 		[],
-		"",
-	),
-	
-	"codereview-login": (
-		dologin,
-		[],
-		"",
+		"change#"
 	),
 }
 
@@ -862,7 +954,7 @@
 		if self.curdata is not None:
 			self.curdata += data
 
-# Like upload.py Send but only authenticates when the 
+# Like upload.py Send but only authenticates when the
 # redirect is to www.google.com/accounts.  This keeps
 # unnecessary redirects from happening during testing.
 def MySend(request_path, payload=None,
@@ -890,6 +982,8 @@
     self = rpc
     if not self.authenticated:
       self._Authenticate()
+    if request_path is None:
+      return
 
     old_timeout = socket.getdefaulttimeout()
     socket.setdefaulttimeout(timeout)
@@ -915,7 +1009,7 @@
             self._Authenticate()
           elif e.code == 302:
             loc = e.info()["location"]
-            if not loc.startswith('https://www.google.com/accounts/ServiceLogin'):
+            if not loc.startswith('https://www.google.com/a') or loc.find('/ServiceLogin') < 0:
               return ''
             self._Authenticate()
           else:
@@ -933,8 +1027,7 @@
 
 def GetSettings(issue):
 	f = GetForm("/" + issue + "/edit")
-	if not f:
-		print "PUB"
+	if not f or 'reviewers' not in f:
 		f = GetForm("/" + issue + "/publish")
 	return f
 
@@ -996,8 +1089,8 @@
 class opt(object):
 	pass
 
-def RietveldSetup(ui):
-	global upload_options, rpc, server, server_url_base
+def RietveldSetup(ui, repo):
+	global upload_options, rpc, server, server_url_base, force_google_account, verbosity
 
 	# TODO(rsc): If the repository config has no codereview section,
 	# do not enable the extension.  This allows users to
@@ -1006,12 +1099,15 @@
 	# if not ui.has_section("codereview"):
 	# 	cmdtable = {}
 	# 	return
+	
+	if not ui.verbose:
+		verbosity = 0
 
 	# Config options.
 	x = ui.config("codereview", "server")
 	if x is not None:
 		server = x
-	
+
 	# TODO(rsc): Take from ui.username?
 	email = None
 	x = ui.config("codereview", "email")
@@ -1022,7 +1118,7 @@
 	x = ui.config("codereview", "cc")
 	if x is not None:
 		cc = x
-	
+
 	server_url_base = "http://" + server + "/"
 	x = ui.config("codereview", "server_url_base")
 	if x is not None:
@@ -1031,6 +1127,7 @@
 		server_url_base += "/"
 
 	testing = ui.config("codereview", "testing")
+	force_google_account = ui.configbool("codereview", "force_google_account", False)
 
 	upload_options = opt()
 	upload_options.email = email
@@ -1048,7 +1145,7 @@
 	upload_options.vcs = None
 	upload_options.server = server
 	upload_options.save_cookies = True
-	
+
 	if testing:
 		upload_options.save_cookies = False
 		upload_options.email = "test@example.com"
@@ -1272,7 +1369,7 @@
       The authentication token returned by ClientLogin.
     """
     account_type = "GOOGLE"
-    if self.host.endswith(".google.com"):
+    if self.host.endswith(".google.com") and not force_google_account:
       # Needed for use inside Google.
       account_type = "HOSTED"
     req = self._CreateRequest(
@@ -1420,9 +1517,6 @@
             raise
           elif e.code == 401 or e.code == 302:
             self._Authenticate()
-##           elif e.code >= 500 and e.code < 600:
-##             # Server Error - try again.
-##             continue
           else:
             raise
     finally:
@@ -2561,9 +2655,9 @@
       msg = response_body
   else:
     msg = response_body
-  StatusUpdate(msg)
   if not response_body.startswith("Issue created.") and \
   not response_body.startswith("Issue updated."):
+    print >>sys.stderr, msg
     sys.exit(0)
   issue = msg[msg.rfind("/")+1:]