| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 ''' | 2 ''' |
| 3 Copyright 2012 Google Inc. | 3 Copyright 2012 Google Inc. |
| 4 | 4 |
| 5 Use of this source code is governed by a BSD-style license that can be | 5 Use of this source code is governed by a BSD-style license that can be |
| 6 found in the LICENSE file. | 6 found in the LICENSE file. |
| 7 ''' | 7 ''' |
| 8 | 8 |
| 9 ''' | 9 ''' |
| 10 Generates a visual diff of all pending changes in the local SVN checkout. | 10 Generates a visual diff of all pending changes in the local SVN (or git!) |
| 11 checkout. |
| 11 | 12 |
| 12 Launch with --help to see more information. | 13 Launch with --help to see more information. |
| 13 | 14 |
| 15 TODO(epoger): Now that this tool supports either git or svn, rename it. |
| 14 TODO(epoger): Fix indentation in this file (2-space indents, not 4-space). | 16 TODO(epoger): Fix indentation in this file (2-space indents, not 4-space). |
| 15 ''' | 17 ''' |
| 16 | 18 |
| 17 # common Python modules | 19 # common Python modules |
| 18 import optparse | 20 import optparse |
| 19 import os | 21 import os |
| 20 import re | 22 import re |
| 21 import shutil | 23 import shutil |
| 24 import subprocess |
| 22 import sys | 25 import sys |
| 23 import tempfile | 26 import tempfile |
| 24 import urllib2 | 27 import urllib2 |
| 25 | 28 |
| 26 # Imports from within Skia | 29 # Imports from within Skia |
| 27 # | 30 # |
| 28 # We need to add the 'gm' directory, so that we can import gm_json.py within | 31 # We need to add the 'gm' directory, so that we can import gm_json.py within |
| 29 # that directory. That script allows us to parse the actual-results.json file | 32 # that directory. That script allows us to parse the actual-results.json file |
| 30 # written out by the GM tool. | 33 # written out by the GM tool. |
| 31 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | 34 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* |
| 32 # so any dirs that are already in the PYTHONPATH will be preferred. | 35 # so any dirs that are already in the PYTHONPATH will be preferred. |
| 33 # | 36 # |
| 34 # This assumes that the 'gm' directory has been checked out as a sibling of | 37 # This assumes that the 'gm' directory has been checked out as a sibling of |
| 35 # the 'tools' directory containing this script, which will be the case if | 38 # the 'tools' directory containing this script, which will be the case if |
| 36 # 'trunk' was checked out as a single unit. | 39 # 'trunk' was checked out as a single unit. |
| 37 GM_DIRECTORY = os.path.realpath( | 40 GM_DIRECTORY = os.path.realpath( |
| 38 os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm')) | 41 os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm')) |
| 39 if GM_DIRECTORY not in sys.path: | 42 if GM_DIRECTORY not in sys.path: |
| 40 sys.path.append(GM_DIRECTORY) | 43 sys.path.append(GM_DIRECTORY) |
| 41 import gm_json | 44 import gm_json |
| 42 import jsondiff | 45 import jsondiff |
| 43 import svn | 46 import svn |
| 44 | 47 |
| 45 USAGE_STRING = 'Usage: %s [options]' | 48 USAGE_STRING = 'Usage: %s [options]' |
| 46 HELP_STRING = ''' | 49 HELP_STRING = ''' |
| 47 | 50 |
| 48 Generates a visual diff of all pending changes in the local SVN checkout. | 51 Generates a visual diff of all pending changes in the local SVN/git checkout. |
| 49 | 52 |
| 50 This includes a list of all files that have been added, deleted, or modified | 53 This includes a list of all files that have been added, deleted, or modified |
| 51 (as far as SVN knows about). For any image modifications, pixel diffs will | 54 (as far as SVN/git knows about). For any image modifications, pixel diffs will |
| 52 be generated. | 55 be generated. |
| 53 | 56 |
| 54 ''' | 57 ''' |
| 55 | 58 |
| 56 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | 59 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) |
| 57 | 60 |
| 58 TRUNK_PATH = os.path.join(os.path.dirname(__file__), os.pardir) | 61 TRUNK_PATH = os.path.join(os.path.dirname(__file__), os.pardir) |
| 59 | 62 |
| 60 OPTION_DEST_DIR = '--dest-dir' | 63 OPTION_DEST_DIR = '--dest-dir' |
| 61 OPTION_PATH_TO_SKDIFF = '--path-to-skdiff' | 64 OPTION_PATH_TO_SKDIFF = '--path-to-skdiff' |
| (...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 153 if new_checksum: | 156 if new_checksum: |
| 154 new_image_url = _CreateGSUrl( | 157 new_image_url = _CreateGSUrl( |
| 155 imagename=imagename, | 158 imagename=imagename, |
| 156 hash_type=gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, | 159 hash_type=gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, |
| 157 hash_digest=new_checksum) | 160 hash_digest=new_checksum) |
| 158 _DownloadUrlToFile( | 161 _DownloadUrlToFile( |
| 159 source_url=new_image_url, | 162 source_url=new_image_url, |
| 160 dest_path=os.path.join(new_flattened_dir, | 163 dest_path=os.path.join(new_flattened_dir, |
| 161 filename_prefix + imagename)) | 164 filename_prefix + imagename)) |
| 162 | 165 |
| 166 def _RunCommand(args): |
| 167 """Run a command (from self._directory) and return stdout as a single |
| 168 string. |
| 169 |
| 170 @param args a list of arguments |
| 171 """ |
| 172 proc = subprocess.Popen(args, |
| 173 stdout=subprocess.PIPE, |
| 174 stderr=subprocess.PIPE) |
| 175 (stdout, stderr) = proc.communicate() |
| 176 if proc.returncode is not 0: |
| 177 raise Exception('command "%s" failed: %s' % (args, stderr)) |
| 178 return stdout |
| 179 |
| 180 def _GitGetModifiedFiles(): |
| 181 """Returns a list of locally modified files within the current working dir. |
| 182 |
| 183 TODO(epoger): Move this into a git utility package? |
| 184 """ |
| 185 return _RunCommand(['git', 'ls-files', '-m']).splitlines() |
| 186 |
| 187 def _GitExportBaseVersionOfFile(file_within_repo, dest_path): |
| 188 """Retrieves a copy of the base version of a file within the repository. |
| 189 |
| 190 @param file_within_repo path to the file within the repo whose base |
| 191 version you wish to obtain |
| 192 @param dest_path destination to which to write the base content |
| 193 |
| 194 TODO(epoger): Move this into a git utility package? |
| 195 """ |
| 196 # TODO(epoger): Replace use of "git show" command with lower-level git |
| 197 # commands? senorblanco points out that "git show" is a "porcelain" |
| 198 # command, intended for human use, as opposed to the "plumbing" commands |
| 199 # generally more suitable for scripting. (See |
| 200 # http://git-scm.com/book/en/Git-Internals-Plumbing-and-Porcelain ) |
| 201 # |
| 202 # For now, though, "git show" is the most straightforward implementation |
| 203 # I could come up with. I tried using "git cat-file", but I had trouble |
| 204 # getting it to work as desired. |
| 205 args = ['git', 'show', os.path.join('HEAD:.', file_within_repo)] |
| 206 with open(dest_path, 'wb') as outfile: |
| 207 proc = subprocess.Popen(args, stdout=outfile) |
| 208 proc.communicate() |
| 209 if proc.returncode is not 0: |
| 210 raise Exception('command "%s" failed' % args) |
| 211 |
| 163 def SvnDiff(path_to_skdiff, dest_dir, source_dir): | 212 def SvnDiff(path_to_skdiff, dest_dir, source_dir): |
| 164 """Generates a visual diff of all pending changes in source_dir. | 213 """Generates a visual diff of all pending changes in source_dir. |
| 165 | 214 |
| 166 @param path_to_skdiff | 215 @param path_to_skdiff |
| 167 @param dest_dir existing directory within which to write results | 216 @param dest_dir existing directory within which to write results |
| 168 @param source_dir | 217 @param source_dir |
| 169 """ | 218 """ |
| 170 # Validate parameters, filling in default values if necessary and possible. | 219 # Validate parameters, filling in default values if necessary and possible. |
| 171 path_to_skdiff = os.path.abspath(FindPathToSkDiff(path_to_skdiff)) | 220 path_to_skdiff = os.path.abspath(FindPathToSkDiff(path_to_skdiff)) |
| 172 if not dest_dir: | 221 if not dest_dir: |
| 173 dest_dir = tempfile.mkdtemp() | 222 dest_dir = tempfile.mkdtemp() |
| 174 dest_dir = os.path.abspath(dest_dir) | 223 dest_dir = os.path.abspath(dest_dir) |
| 175 | 224 |
| 176 os.chdir(source_dir) | 225 os.chdir(source_dir) |
| 226 using_svn = os.path.isdir('.svn') |
| 177 | 227 |
| 178 # Prepare temporary directories. | 228 # Prepare temporary directories. |
| 179 modified_flattened_dir = os.path.join(dest_dir, 'modified_flattened') | 229 modified_flattened_dir = os.path.join(dest_dir, 'modified_flattened') |
| 180 original_flattened_dir = os.path.join(dest_dir, 'original_flattened') | 230 original_flattened_dir = os.path.join(dest_dir, 'original_flattened') |
| 181 diff_dir = os.path.join(dest_dir, 'diffs') | 231 diff_dir = os.path.join(dest_dir, 'diffs') |
| 182 for dir in [modified_flattened_dir, original_flattened_dir, diff_dir] : | 232 for dir in [modified_flattened_dir, original_flattened_dir, diff_dir] : |
| 183 shutil.rmtree(dir, ignore_errors=True) | 233 shutil.rmtree(dir, ignore_errors=True) |
| 184 os.mkdir(dir) | 234 os.mkdir(dir) |
| 185 | 235 |
| 186 # Get a list of all locally modified (including added/deleted) files, | 236 # Get a list of all locally modified (including added/deleted) files, |
| 187 # descending subdirectories. | 237 # descending subdirectories. |
| 188 svn_repo = svn.Svn('.') | 238 if using_svn: |
| 189 modified_file_paths = svn_repo.GetFilesWithStatus( | 239 svn_repo = svn.Svn('.') |
| 190 svn.STATUS_ADDED | svn.STATUS_DELETED | svn.STATUS_MODIFIED) | 240 modified_file_paths = svn_repo.GetFilesWithStatus( |
| 241 svn.STATUS_ADDED | svn.STATUS_DELETED | svn.STATUS_MODIFIED) |
| 242 else: |
| 243 modified_file_paths = _GitGetModifiedFiles() |
| 191 | 244 |
| 192 # For each modified file: | 245 # For each modified file: |
| 193 # 1. copy its current contents into modified_flattened_dir | 246 # 1. copy its current contents into modified_flattened_dir |
| 194 # 2. copy its original contents into original_flattened_dir | 247 # 2. copy its original contents into original_flattened_dir |
| 195 for modified_file_path in modified_file_paths: | 248 for modified_file_path in modified_file_paths: |
| 196 if modified_file_path.endswith('.json'): | 249 if modified_file_path.endswith('.json'): |
| 197 # Special handling for JSON files, in the hopes that they | 250 # Special handling for JSON files, in the hopes that they |
| 198 # contain GM result summaries. | 251 # contain GM result summaries. |
| 199 (_unused, original_file_path) = tempfile.mkstemp() | 252 (_unused, original_file_path) = tempfile.mkstemp() |
| 200 svn_repo.ExportBaseVersionOfFile(modified_file_path, | 253 if using_svn: |
| 201 original_file_path) | 254 svn_repo.ExportBaseVersionOfFile( |
| 255 modified_file_path, original_file_path) |
| 256 else: |
| 257 _GitExportBaseVersionOfFile( |
| 258 modified_file_path, original_file_path) |
| 202 platform_prefix = re.sub(os.sep, '__', | 259 platform_prefix = re.sub(os.sep, '__', |
| 203 os.path.dirname(modified_file_path)) + '__' | 260 os.path.dirname(modified_file_path)) + '__' |
| 204 _CallJsonDiff(old_json_path=original_file_path, | 261 _CallJsonDiff(old_json_path=original_file_path, |
| 205 new_json_path=modified_file_path, | 262 new_json_path=modified_file_path, |
| 206 old_flattened_dir=original_flattened_dir, | 263 old_flattened_dir=original_flattened_dir, |
| 207 new_flattened_dir=modified_flattened_dir, | 264 new_flattened_dir=modified_flattened_dir, |
| 208 filename_prefix=platform_prefix) | 265 filename_prefix=platform_prefix) |
| 209 os.remove(original_file_path) | 266 os.remove(original_file_path) |
| 210 else: | 267 else: |
| 211 dest_filename = re.sub(os.sep, '__', modified_file_path) | 268 dest_filename = re.sub(os.sep, '__', modified_file_path) |
| 212 # If the file had STATUS_DELETED, it won't exist anymore... | 269 # If the file had STATUS_DELETED, it won't exist anymore... |
| 213 if os.path.isfile(modified_file_path): | 270 if os.path.isfile(modified_file_path): |
| 214 shutil.copyfile(modified_file_path, | 271 shutil.copyfile(modified_file_path, |
| 215 os.path.join(modified_flattened_dir, | 272 os.path.join(modified_flattened_dir, |
| 216 dest_filename)) | 273 dest_filename)) |
| 217 svn_repo.ExportBaseVersionOfFile( | 274 if using_svn: |
| 218 modified_file_path, | 275 svn_repo.ExportBaseVersionOfFile( |
| 219 os.path.join(original_flattened_dir, dest_filename)) | 276 modified_file_path, |
| 277 os.path.join(original_flattened_dir, dest_filename)) |
| 278 else: |
| 279 _GitExportBaseVersionOfFile( |
| 280 modified_file_path, |
| 281 os.path.join(original_flattened_dir, dest_filename)) |
| 220 | 282 |
| 221 # Run skdiff: compare original_flattened_dir against modified_flattened_dir | 283 # Run skdiff: compare original_flattened_dir against modified_flattened_dir |
| 222 RunCommand('%s %s %s %s' % (path_to_skdiff, original_flattened_dir, | 284 RunCommand('%s %s %s %s' % (path_to_skdiff, original_flattened_dir, |
| 223 modified_flattened_dir, diff_dir)) | 285 modified_flattened_dir, diff_dir)) |
| 224 print '\nskdiff results are ready in file://%s/index.html' % diff_dir | 286 print '\nskdiff results are ready in file://%s/index.html' % diff_dir |
| 225 | 287 |
| 226 def RaiseUsageException(): | 288 def RaiseUsageException(): |
| 227 raise Exception('%s\nRun with --help for more detail.' % ( | 289 raise Exception('%s\nRun with --help for more detail.' % ( |
| 228 USAGE_STRING % __file__)) | 290 USAGE_STRING % __file__)) |
| 229 | 291 |
| (...skipping 17 matching lines...) Expand all Loading... |
| 247 action='store', type='string', default=None, | 309 action='store', type='string', default=None, |
| 248 help='path to already-built skdiff tool; if not set, ' | 310 help='path to already-built skdiff tool; if not set, ' |
| 249 'will search for it in typical directories near this ' | 311 'will search for it in typical directories near this ' |
| 250 'script') | 312 'script') |
| 251 parser.add_option(OPTION_SOURCE_DIR, | 313 parser.add_option(OPTION_SOURCE_DIR, |
| 252 action='store', type='string', default='.', | 314 action='store', type='string', default='.', |
| 253 help='root directory within which to compare all ' + | 315 help='root directory within which to compare all ' + |
| 254 'files; defaults to "%default"') | 316 'files; defaults to "%default"') |
| 255 (options, args) = parser.parse_args() | 317 (options, args) = parser.parse_args() |
| 256 Main(options, args) | 318 Main(options, args) |
| OLD | NEW |