Skip to content

Commit cde2093

Browse files
author
andyster
committed
Updating the upload.py script
git-svn-id: http://jaikuengine.googlecode.com/svn/trunk@94 6315f19a-1029-11de-a3bf-55d8bd1d7a4a
1 parent 1cdbb5d commit cde2093

File tree

1 file changed

+150
-35
lines changed

1 file changed

+150
-35
lines changed

bin/upload.py

Lines changed: 150 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import cookielib
3535
import getpass
3636
import logging
37-
import md5
3837
import mimetypes
3938
import optparse
4039
import os
@@ -46,6 +45,12 @@
4645
import urllib2
4746
import urlparse
4847

48+
# The md5 module was deprecated in Python 2.5.
49+
try:
50+
from hashlib import md5
51+
except ImportError:
52+
from md5 import md5
53+
4954
try:
5055
import readline
5156
except ImportError:
@@ -61,6 +66,17 @@
6166
# Max size of patch or base file.
6267
MAX_UPLOAD_SIZE = 900 * 1024
6368

69+
# Constants for version control names. Used by GuessVCSName.
70+
VCS_GIT = "Git"
71+
VCS_MERCURIAL = "Mercurial"
72+
VCS_SUBVERSION = "Subversion"
73+
VCS_UNKNOWN = "Unknown"
74+
75+
# whitelist for non-binary filetypes which do not start with "text/"
76+
# .mm (Objective-C) shows up as application/x-freemind on my Linux box.
77+
TEXT_MIMETYPES = ['application/javascript', 'application/x-javascript',
78+
'application/x-freemind']
79+
6480

6581
def GetEmail(prompt):
6682
"""Prompts the user for their email address and returns it.
@@ -248,8 +264,8 @@ def _Authenticate(self):
248264
us to the URL we provided.
249265
250266
If we attempt to access the upload API without first obtaining an
251-
authentication cookie, it returns a 401 response and directs us to
252-
authenticate ourselves with ClientLogin.
267+
authentication cookie, it returns a 401 response (or a 302) and
268+
directs us to authenticate ourselves with ClientLogin.
253269
"""
254270
for i in range(3):
255271
credentials = self.auth_function()
@@ -330,7 +346,7 @@ def Send(self, request_path, payload=None,
330346
except urllib2.HTTPError, e:
331347
if tries > 3:
332348
raise
333-
elif e.code == 401:
349+
elif e.code == 401 or e.code == 302:
334350
self._Authenticate()
335351
## elif e.code >= 500 and e.code < 600:
336352
## # Server Error - try again.
@@ -405,10 +421,10 @@ def _GetOpener(self):
405421
# Review server
406422
group = parser.add_option_group("Review server options")
407423
group.add_option("-s", "--server", action="store", dest="server",
408-
default="rietku.appspot.com",
424+
default="codereview.appspot.com",
409425
metavar="SERVER",
410426
help=("The server to upload to. The format is host[:port]. "
411-
"Defaults to 'rietku.appspot.com'."))
427+
"Defaults to '%default'."))
412428
group.add_option("-e", "--email", action="store", dest="email",
413429
metavar="EMAIL", default=None,
414430
help="The username to use. Will prompt if omitted.")
@@ -434,6 +450,9 @@ def _GetOpener(self):
434450
group.add_option("--cc", action="store", dest="cc",
435451
metavar="CC", default=None,
436452
help="Add CC (comma separated email addresses).")
453+
group.add_option("--private", action="store_true", dest="private",
454+
default=False,
455+
help="Make the issue restricted to reviewers and those CCed")
437456
# Upload options
438457
group = parser.add_option_group("Patch options")
439458
group.add_option("-m", "--message", action="store", dest="message",
@@ -539,7 +558,8 @@ def GetContentType(filename):
539558
use_shell = sys.platform.startswith("win")
540559

541560
def RunShellWithReturnCode(command, print_output=False,
542-
universal_newlines=True):
561+
universal_newlines=True,
562+
env=os.environ):
543563
"""Executes a command and returns the output from stdout and the return code.
544564
545565
Args:
@@ -553,7 +573,8 @@ def RunShellWithReturnCode(command, print_output=False,
553573
"""
554574
logging.info("Running %s", command)
555575
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
556-
shell=use_shell, universal_newlines=universal_newlines)
576+
shell=use_shell, universal_newlines=universal_newlines,
577+
env=env)
557578
if print_output:
558579
output_array = []
559580
while True:
@@ -575,9 +596,9 @@ def RunShellWithReturnCode(command, print_output=False,
575596

576597

577598
def RunShell(command, silent_ok=False, universal_newlines=True,
578-
print_output=False):
599+
print_output=False, env=os.environ):
579600
data, retcode = RunShellWithReturnCode(command, print_output,
580-
universal_newlines)
601+
universal_newlines, env)
581602
if retcode:
582603
ErrorExit("Got error status from %s:\n%s" % (command, data))
583604
if not silent_ok and not data:
@@ -674,7 +695,7 @@ def UploadFile(filename, file_id, content, is_binary, status, is_base):
674695
(type, filename))
675696
file_too_large = True
676697
content = ""
677-
checksum = md5.new(content).hexdigest()
698+
checksum = md5(content).hexdigest()
678699
if options.verbose > 0 and not file_too_large:
679700
print "Uploading %s file for %s" % (type, filename)
680701
url = "/%d/upload_content/%d/%d" % (int(issue), int(patchset), file_id)
@@ -717,6 +738,16 @@ def IsImage(self, filename):
717738
return False
718739
return mimetype.startswith("image/")
719740

741+
def IsBinary(self, filename):
742+
"""Returns true if the guessed mimetyped isnt't in text group."""
743+
mimetype = mimetypes.guess_type(filename)[0]
744+
if not mimetype:
745+
return False # e.g. README, "real" binaries usually have an extension
746+
# special case for text files which don't start with text/
747+
if mimetype in TEXT_MIMETYPES:
748+
return False
749+
return not mimetype.startswith("text/")
750+
720751

721752
class SubversionVCS(VersionControlSystem):
722753
"""Implementation of the VersionControlSystem interface for Subversion."""
@@ -987,32 +1018,56 @@ class GitVCS(VersionControlSystem):
9871018

9881019
def __init__(self, options):
9891020
super(GitVCS, self).__init__(options)
990-
# Map of filename -> hash of base file.
991-
self.base_hashes = {}
1021+
# Map of filename -> (hash before, hash after) of base file.
1022+
# Hashes for "no such file" are represented as None.
1023+
self.hashes = {}
1024+
# Map of new filename -> old filename for renames.
1025+
self.renames = {}
9921026

9931027
def GenerateDiff(self, extra_args):
9941028
# This is more complicated than svn's GenerateDiff because we must convert
9951029
# the diff output to include an svn-style "Index:" line as well as record
996-
# the hashes of the base files, so we can upload them along with our diff.
1030+
# the hashes of the files, so we can upload them along with our diff.
1031+
1032+
# Special used by git to indicate "no such content".
1033+
NULL_HASH = "0"*40
1034+
1035+
extra_args = extra_args[:]
9971036
if self.options.revision:
9981037
extra_args = [self.options.revision] + extra_args
999-
gitdiff = RunShell(["git", "diff", "--full-index"] + extra_args)
1038+
extra_args.append('-M')
1039+
1040+
# --no-ext-diff is broken in some versions of Git, so try to work around
1041+
# this by overriding the environment (but there is still a problem if the
1042+
# git config key "diff.external" is used).
1043+
env = os.environ.copy()
1044+
if 'GIT_EXTERNAL_DIFF' in env: del env['GIT_EXTERNAL_DIFF']
1045+
gitdiff = RunShell(["git", "diff", "--no-ext-diff", "--full-index"]
1046+
+ extra_args, env=env)
10001047
svndiff = []
10011048
filecount = 0
10021049
filename = None
10031050
for line in gitdiff.splitlines():
1004-
match = re.match(r"diff --git a/(.*) b/.*$", line)
1051+
match = re.match(r"diff --git a/(.*) b/(.*)$", line)
10051052
if match:
10061053
filecount += 1
1007-
filename = match.group(1)
1054+
# Intentionally use the "after" filename so we can show renames.
1055+
filename = match.group(2)
10081056
svndiff.append("Index: %s\n" % filename)
1057+
if match.group(1) != match.group(2):
1058+
self.renames[match.group(2)] = match.group(1)
10091059
else:
10101060
# The "index" line in a git diff looks like this (long hashes elided):
10111061
# index 82c0d44..b2cee3f 100755
10121062
# We want to save the left hash, as that identifies the base file.
1013-
match = re.match(r"index (\w+)\.\.", line)
1063+
match = re.match(r"index (\w+)\.\.(\w+)", line)
10141064
if match:
1015-
self.base_hashes[filename] = match.group(1)
1065+
before, after = (match.group(1), match.group(2))
1066+
if before == NULL_HASH:
1067+
before = None
1068+
if after == NULL_HASH:
1069+
after = None
1070+
self.hashes[filename] = (before, after)
10161071
svndiff.append(line + "\n")
10171072
if not filecount:
10181073
ErrorExit("No valid patches found in output from git diff")
@@ -1023,19 +1078,47 @@ def GetUnknownFiles(self):
10231078
silent_ok=True)
10241079
return status.splitlines()
10251080

1081+
def GetFileContent(self, file_hash, is_binary):
1082+
"""Returns the content of a file identified by its git hash."""
1083+
data, retcode = RunShellWithReturnCode(["git", "show", file_hash],
1084+
universal_newlines=not is_binary)
1085+
if retcode:
1086+
ErrorExit("Got error status from 'git show %s'" % file_hash)
1087+
return data
1088+
10261089
def GetBaseFile(self, filename):
1027-
hash = self.base_hashes[filename]
1090+
hash_before, hash_after = self.hashes.get(filename, (None,None))
10281091
base_content = None
10291092
new_content = None
1030-
is_binary = False
1031-
if hash == "0" * 40: # All-zero hash indicates no base file.
1093+
is_binary = self.IsBinary(filename)
1094+
status = None
1095+
1096+
if filename in self.renames:
1097+
status = "A +" # Match svn attribute name for renames.
1098+
if filename not in self.hashes:
1099+
# If a rename doesn't change the content, we never get a hash.
1100+
base_content = RunShell(["git", "show", filename])
1101+
elif not hash_before:
10321102
status = "A"
10331103
base_content = ""
1104+
elif not hash_after:
1105+
status = "D"
10341106
else:
10351107
status = "M"
1036-
base_content, returncode = RunShellWithReturnCode(["git", "show", hash])
1037-
if returncode:
1038-
ErrorExit("Got error status from 'git show %s'" % hash)
1108+
1109+
is_image = self.IsImage(filename)
1110+
1111+
# Grab the before/after content if we need it.
1112+
# We should include file contents if it's text or it's an image.
1113+
if not is_binary or is_image:
1114+
# Grab the base content if we don't have it already.
1115+
if base_content is None and hash_before:
1116+
base_content = self.GetFileContent(hash_before, is_binary)
1117+
# Only include the "after" file if it's an image; otherwise it
1118+
# it is reconstructed from the diff.
1119+
if is_image and hash_after:
1120+
new_content = self.GetFileContent(hash_after, is_binary)
1121+
10391122
return (base_content, new_content, is_binary, status)
10401123

10411124

@@ -1121,16 +1204,20 @@ def GetBaseFile(self, filename):
11211204
status = "M"
11221205
else:
11231206
status, _ = out[0].split(' ', 1)
1207+
if ":" in self.base_rev:
1208+
base_rev = self.base_rev.split(":", 1)[0]
1209+
else:
1210+
base_rev = self.base_rev
11241211
if status != "A":
1125-
base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
1212+
base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
11261213
silent_ok=True)
11271214
is_binary = "\0" in base_content # Mercurial's heuristic
11281215
if status != "R":
11291216
new_content = open(relpath, "rb").read()
11301217
is_binary = is_binary or "\0" in new_content
11311218
if is_binary and base_content:
11321219
# Fetch again without converting newlines
1133-
base_content = RunShell(["hg", "cat", "-r", self.base_rev, oldrelpath],
1220+
base_content = RunShell(["hg", "cat", "-r", base_rev, oldrelpath],
11341221
silent_ok=True, universal_newlines=False)
11351222
if not is_binary or not self.IsImage(relpath):
11361223
new_content = None
@@ -1206,43 +1293,66 @@ def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
12061293
return rv
12071294

12081295

1209-
def GuessVCS(options):
1296+
def GuessVCSName():
12101297
"""Helper to guess the version control system.
12111298
12121299
This examines the current directory, guesses which VersionControlSystem
1213-
we're using, and returns an instance of the appropriate class. Exit with an
1214-
error if we can't figure it out.
1300+
we're using, and returns an string indicating which VCS is detected.
12151301
12161302
Returns:
1217-
A VersionControlSystem instance. Exits if the VCS can't be guessed.
1303+
A pair (vcs, output). vcs is a string indicating which VCS was detected
1304+
and is one of VCS_GIT, VCS_MERCURIAL, VCS_SUBVERSION, or VCS_UNKNOWN.
1305+
output is a string containing any interesting output from the vcs
1306+
detection routine, or None if there is nothing interesting.
12181307
"""
12191308
# Mercurial has a command to get the base directory of a repository
12201309
# Try running it, but don't die if we don't have hg installed.
12211310
# NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
12221311
try:
12231312
out, returncode = RunShellWithReturnCode(["hg", "root"])
12241313
if returncode == 0:
1225-
return MercurialVCS(options, out.strip())
1314+
return (VCS_MERCURIAL, out.strip())
12261315
except OSError, (errno, message):
12271316
if errno != 2: # ENOENT -- they don't have hg installed.
12281317
raise
12291318

12301319
# Subversion has a .svn in all working directories.
12311320
if os.path.isdir('.svn'):
12321321
logging.info("Guessed VCS = Subversion")
1233-
return SubversionVCS(options)
1322+
return (VCS_SUBVERSION, None)
12341323

12351324
# Git has a command to test if you're in a git tree.
12361325
# Try running it, but don't die if we don't have git installed.
12371326
try:
12381327
out, returncode = RunShellWithReturnCode(["git", "rev-parse",
12391328
"--is-inside-work-tree"])
12401329
if returncode == 0:
1241-
return GitVCS(options)
1330+
return (VCS_GIT, None)
12421331
except OSError, (errno, message):
12431332
if errno != 2: # ENOENT -- they don't have git installed.
12441333
raise
12451334

1335+
return (VCS_UNKNOWN, None)
1336+
1337+
1338+
def GuessVCS(options):
1339+
"""Helper to guess the version control system.
1340+
1341+
This examines the current directory, guesses which VersionControlSystem
1342+
we're using, and returns an instance of the appropriate class. Exit with an
1343+
error if we can't figure it out.
1344+
1345+
Returns:
1346+
A VersionControlSystem instance. Exits if the VCS can't be guessed.
1347+
"""
1348+
(vcs, extra_output) = GuessVCSName()
1349+
if vcs == VCS_MERCURIAL:
1350+
return MercurialVCS(options, extra_output)
1351+
elif vcs == VCS_SUBVERSION:
1352+
return SubversionVCS(options)
1353+
elif vcs == VCS_GIT:
1354+
return GitVCS(options)
1355+
12461356
ErrorExit(("Could not guess version control system. "
12471357
"Are you in a working copy directory?"))
12481358

@@ -1326,11 +1436,16 @@ def RealMain(argv, data=None):
13261436
base_hashes = ""
13271437
for file, info in files.iteritems():
13281438
if not info[0] is None:
1329-
checksum = md5.new(info[0]).hexdigest()
1439+
checksum = md5(info[0]).hexdigest()
13301440
if base_hashes:
13311441
base_hashes += "|"
13321442
base_hashes += checksum + ":" + file
13331443
form_fields.append(("base_hashes", base_hashes))
1444+
if options.private:
1445+
if options.issue:
1446+
print "Warning: Private flag ignored when updating an existing issue."
1447+
else:
1448+
form_fields.append(("private", "1"))
13341449
# If we're uploading base files, don't send the email before the uploads, so
13351450
# that it contains the file status.
13361451
if options.send_mail and options.download_base:

0 commit comments

Comments
 (0)