34
34
import cookielib
35
35
import getpass
36
36
import logging
37
- import md5
38
37
import mimetypes
39
38
import optparse
40
39
import os
46
45
import urllib2
47
46
import urlparse
48
47
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
+
49
54
try :
50
55
import readline
51
56
except ImportError :
61
66
# Max size of patch or base file.
62
67
MAX_UPLOAD_SIZE = 900 * 1024
63
68
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
+
64
80
65
81
def GetEmail (prompt ):
66
82
"""Prompts the user for their email address and returns it.
@@ -248,8 +264,8 @@ def _Authenticate(self):
248
264
us to the URL we provided.
249
265
250
266
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.
253
269
"""
254
270
for i in range (3 ):
255
271
credentials = self .auth_function ()
@@ -330,7 +346,7 @@ def Send(self, request_path, payload=None,
330
346
except urllib2 .HTTPError , e :
331
347
if tries > 3 :
332
348
raise
333
- elif e .code == 401 :
349
+ elif e .code == 401 or e . code == 302 :
334
350
self ._Authenticate ()
335
351
## elif e.code >= 500 and e.code < 600:
336
352
## # Server Error - try again.
@@ -405,10 +421,10 @@ def _GetOpener(self):
405
421
# Review server
406
422
group = parser .add_option_group ("Review server options" )
407
423
group .add_option ("-s" , "--server" , action = "store" , dest = "server" ,
408
- default = "rietku .appspot.com" ,
424
+ default = "codereview .appspot.com" ,
409
425
metavar = "SERVER" ,
410
426
help = ("The server to upload to. The format is host[:port]. "
411
- "Defaults to 'rietku.appspot.com '." ))
427
+ "Defaults to '%default '." ))
412
428
group .add_option ("-e" , "--email" , action = "store" , dest = "email" ,
413
429
metavar = "EMAIL" , default = None ,
414
430
help = "The username to use. Will prompt if omitted." )
@@ -434,6 +450,9 @@ def _GetOpener(self):
434
450
group .add_option ("--cc" , action = "store" , dest = "cc" ,
435
451
metavar = "CC" , default = None ,
436
452
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" )
437
456
# Upload options
438
457
group = parser .add_option_group ("Patch options" )
439
458
group .add_option ("-m" , "--message" , action = "store" , dest = "message" ,
@@ -539,7 +558,8 @@ def GetContentType(filename):
539
558
use_shell = sys .platform .startswith ("win" )
540
559
541
560
def RunShellWithReturnCode (command , print_output = False ,
542
- universal_newlines = True ):
561
+ universal_newlines = True ,
562
+ env = os .environ ):
543
563
"""Executes a command and returns the output from stdout and the return code.
544
564
545
565
Args:
@@ -553,7 +573,8 @@ def RunShellWithReturnCode(command, print_output=False,
553
573
"""
554
574
logging .info ("Running %s" , command )
555
575
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 )
557
578
if print_output :
558
579
output_array = []
559
580
while True :
@@ -575,9 +596,9 @@ def RunShellWithReturnCode(command, print_output=False,
575
596
576
597
577
598
def RunShell (command , silent_ok = False , universal_newlines = True ,
578
- print_output = False ):
599
+ print_output = False , env = os . environ ):
579
600
data , retcode = RunShellWithReturnCode (command , print_output ,
580
- universal_newlines )
601
+ universal_newlines , env )
581
602
if retcode :
582
603
ErrorExit ("Got error status from %s:\n %s" % (command , data ))
583
604
if not silent_ok and not data :
@@ -674,7 +695,7 @@ def UploadFile(filename, file_id, content, is_binary, status, is_base):
674
695
(type , filename ))
675
696
file_too_large = True
676
697
content = ""
677
- checksum = md5 . new (content ).hexdigest ()
698
+ checksum = md5 (content ).hexdigest ()
678
699
if options .verbose > 0 and not file_too_large :
679
700
print "Uploading %s file for %s" % (type , filename )
680
701
url = "/%d/upload_content/%d/%d" % (int (issue ), int (patchset ), file_id )
@@ -717,6 +738,16 @@ def IsImage(self, filename):
717
738
return False
718
739
return mimetype .startswith ("image/" )
719
740
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
+
720
751
721
752
class SubversionVCS (VersionControlSystem ):
722
753
"""Implementation of the VersionControlSystem interface for Subversion."""
@@ -987,32 +1018,56 @@ class GitVCS(VersionControlSystem):
987
1018
988
1019
def __init__ (self , options ):
989
1020
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 = {}
992
1026
993
1027
def GenerateDiff (self , extra_args ):
994
1028
# This is more complicated than svn's GenerateDiff because we must convert
995
1029
# 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 [:]
997
1036
if self .options .revision :
998
1037
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 )
1000
1047
svndiff = []
1001
1048
filecount = 0
1002
1049
filename = None
1003
1050
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 )
1005
1052
if match :
1006
1053
filecount += 1
1007
- filename = match .group (1 )
1054
+ # Intentionally use the "after" filename so we can show renames.
1055
+ filename = match .group (2 )
1008
1056
svndiff .append ("Index: %s\n " % filename )
1057
+ if match .group (1 ) != match .group (2 ):
1058
+ self .renames [match .group (2 )] = match .group (1 )
1009
1059
else :
1010
1060
# The "index" line in a git diff looks like this (long hashes elided):
1011
1061
# index 82c0d44..b2cee3f 100755
1012
1062
# 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 )
1014
1064
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 )
1016
1071
svndiff .append (line + "\n " )
1017
1072
if not filecount :
1018
1073
ErrorExit ("No valid patches found in output from git diff" )
@@ -1023,19 +1078,47 @@ def GetUnknownFiles(self):
1023
1078
silent_ok = True )
1024
1079
return status .splitlines ()
1025
1080
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
+
1026
1089
def GetBaseFile (self , filename ):
1027
- hash = self .base_hashes [ filename ]
1090
+ hash_before , hash_after = self .hashes . get ( filename , ( None , None ))
1028
1091
base_content = None
1029
1092
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 :
1032
1102
status = "A"
1033
1103
base_content = ""
1104
+ elif not hash_after :
1105
+ status = "D"
1034
1106
else :
1035
1107
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
+
1039
1122
return (base_content , new_content , is_binary , status )
1040
1123
1041
1124
@@ -1121,16 +1204,20 @@ def GetBaseFile(self, filename):
1121
1204
status = "M"
1122
1205
else :
1123
1206
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
1124
1211
if status != "A" :
1125
- base_content = RunShell (["hg" , "cat" , "-r" , self . base_rev , oldrelpath ],
1212
+ base_content = RunShell (["hg" , "cat" , "-r" , base_rev , oldrelpath ],
1126
1213
silent_ok = True )
1127
1214
is_binary = "\0 " in base_content # Mercurial's heuristic
1128
1215
if status != "R" :
1129
1216
new_content = open (relpath , "rb" ).read ()
1130
1217
is_binary = is_binary or "\0 " in new_content
1131
1218
if is_binary and base_content :
1132
1219
# 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 ],
1134
1221
silent_ok = True , universal_newlines = False )
1135
1222
if not is_binary or not self .IsImage (relpath ):
1136
1223
new_content = None
@@ -1206,43 +1293,66 @@ def UploadSeparatePatches(issue, rpc_server, patchset, data, options):
1206
1293
return rv
1207
1294
1208
1295
1209
- def GuessVCS ( options ):
1296
+ def GuessVCSName ( ):
1210
1297
"""Helper to guess the version control system.
1211
1298
1212
1299
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.
1215
1301
1216
1302
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.
1218
1307
"""
1219
1308
# Mercurial has a command to get the base directory of a repository
1220
1309
# Try running it, but don't die if we don't have hg installed.
1221
1310
# NOTE: we try Mercurial first as it can sit on top of an SVN working copy.
1222
1311
try :
1223
1312
out , returncode = RunShellWithReturnCode (["hg" , "root" ])
1224
1313
if returncode == 0 :
1225
- return MercurialVCS ( options , out .strip ())
1314
+ return ( VCS_MERCURIAL , out .strip ())
1226
1315
except OSError , (errno , message ):
1227
1316
if errno != 2 : # ENOENT -- they don't have hg installed.
1228
1317
raise
1229
1318
1230
1319
# Subversion has a .svn in all working directories.
1231
1320
if os .path .isdir ('.svn' ):
1232
1321
logging .info ("Guessed VCS = Subversion" )
1233
- return SubversionVCS ( options )
1322
+ return ( VCS_SUBVERSION , None )
1234
1323
1235
1324
# Git has a command to test if you're in a git tree.
1236
1325
# Try running it, but don't die if we don't have git installed.
1237
1326
try :
1238
1327
out , returncode = RunShellWithReturnCode (["git" , "rev-parse" ,
1239
1328
"--is-inside-work-tree" ])
1240
1329
if returncode == 0 :
1241
- return GitVCS ( options )
1330
+ return ( VCS_GIT , None )
1242
1331
except OSError , (errno , message ):
1243
1332
if errno != 2 : # ENOENT -- they don't have git installed.
1244
1333
raise
1245
1334
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
+
1246
1356
ErrorExit (("Could not guess version control system. "
1247
1357
"Are you in a working copy directory?" ))
1248
1358
@@ -1326,11 +1436,16 @@ def RealMain(argv, data=None):
1326
1436
base_hashes = ""
1327
1437
for file , info in files .iteritems ():
1328
1438
if not info [0 ] is None :
1329
- checksum = md5 . new (info [0 ]).hexdigest ()
1439
+ checksum = md5 (info [0 ]).hexdigest ()
1330
1440
if base_hashes :
1331
1441
base_hashes += "|"
1332
1442
base_hashes += checksum + ":" + file
1333
1443
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" ))
1334
1449
# If we're uploading base files, don't send the email before the uploads, so
1335
1450
# that it contains the file status.
1336
1451
if options .send_mail and options .download_base :
0 commit comments