OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # | 2 # |
3 # Copyright 2008 Google Inc. All Rights Reserved. | 3 # Copyright 2008 Google Inc. All Rights Reserved. |
4 # | 4 # |
5 # Licensed under the Apache License, Version 2.0 (the "License"); | 5 # Licensed under the Apache License, Version 2.0 (the "License"); |
6 # you may not use this file except in compliance with the License. | 6 # you may not use this file except in compliance with the License. |
7 # You may obtain a copy of the License at | 7 # You may obtain a copy of the License at |
8 # | 8 # |
9 # http://www.apache.org/licenses/LICENSE-2.0 | 9 # http://www.apache.org/licenses/LICENSE-2.0 |
10 # | 10 # |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
88 DEFAULT_USAGE_TEXT = ( | 88 DEFAULT_USAGE_TEXT = ( |
89 """usage: %prog <subcommand> [options] [--] [svn options/args...] | 89 """usage: %prog <subcommand> [options] [--] [svn options/args...] |
90 a wrapper for managing a set of client modules in svn. | 90 a wrapper for managing a set of client modules in svn. |
91 Version """ + __version__ + """ | 91 Version """ + __version__ + """ |
92 | 92 |
93 subcommands: | 93 subcommands: |
94 cleanup | 94 cleanup |
95 config | 95 config |
96 diff | 96 diff |
97 export | 97 export |
98 pack | |
98 revert | 99 revert |
99 status | 100 status |
100 sync | 101 sync |
101 update | 102 update |
102 runhooks | 103 runhooks |
103 revinfo | 104 revinfo |
104 | 105 |
105 Options and extra arguments can be passed to invoked svn commands by | 106 Options and extra arguments can be passed to invoked svn commands by |
106 appending them to the command line. Note that if the first such | 107 appending them to the command line. Note that if the first such |
107 appended option starts with a dash (-) then the options must be | 108 appended option starts with a dash (-) then the options must be |
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
189 gclient diff | 190 gclient diff |
190 simple 'svn diff' for configured client and dependences | 191 simple 'svn diff' for configured client and dependences |
191 gclient diff -- -x -b | 192 gclient diff -- -x -b |
192 use 'svn diff -x -b' to suppress whitespace-only differences | 193 use 'svn diff -x -b' to suppress whitespace-only differences |
193 gclient diff -- -r HEAD -x -b | 194 gclient diff -- -r HEAD -x -b |
194 diff versus the latest version of each module | 195 diff versus the latest version of each module |
195 """, | 196 """, |
196 "export": | 197 "export": |
197 """Wrapper for svn export for all managed directories | 198 """Wrapper for svn export for all managed directories |
198 """, | 199 """, |
200 "pack": | |
201 | |
sgk
2009/09/04 00:25:11
No extra newline here (consistency with other entr
| |
202 """Generate a patch which can be applied at the root of the tree. | |
203 Internally, runs 'svn diff' on each checked out module and | |
204 dependencies, and performs minimal postprocessing of the output. The | |
205 resulting patch is printed to stdout and can be applied to a freshly | |
206 checked out tree via 'patch -p0 < patchfile'. Additional args and | |
207 options to 'svn diff' can be passed after gclient options. | |
208 | |
209 usage: pack [options] [--] [svn args/options] | |
210 | |
211 Valid options: | |
212 --verbose : output additional diagnostics | |
213 | |
214 Examples: | |
215 gclient pack > patch.txt | |
216 generate simple patch for configured client and dependences | |
217 gclient pack -- -x -b > patch.txt | |
218 generate patch using 'svn diff -x -b' to suppress | |
219 whitespace-only differences | |
220 gclient pack -- -r HEAD -x -b > patch.txt | |
221 generate patch, diffing each file versus the latest version of | |
222 each module | |
223 """, | |
199 "revert": | 224 "revert": |
200 """Revert every file in every managed directory in the client view. | 225 """Revert every file in every managed directory in the client view. |
201 | 226 |
202 usage: revert | 227 usage: revert |
203 """, | 228 """, |
204 "status": | 229 "status": |
205 """Show the status of client and dependent modules, using 'svn diff' | 230 """Show the status of client and dependent modules, using 'svn diff' |
206 for each module. Additional options and args may be passed to 'svn diff'. | 231 for each module. Additional options and args may be passed to 'svn diff'. |
207 | 232 |
208 usage: status [options] [--] [svn diff args/options] | 233 usage: status [options] [--] [svn diff args/options] |
(...skipping 192 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
401 if e.errno != errno.EACCES or sys.platform != 'win32': | 426 if e.errno != errno.EACCES or sys.platform != 'win32': |
402 raise | 427 raise |
403 print 'Failed to remove %s: trying again' % file_path | 428 print 'Failed to remove %s: trying again' % file_path |
404 time.sleep(0.1) | 429 time.sleep(0.1) |
405 os.rmdir(file_path) | 430 os.rmdir(file_path) |
406 | 431 |
407 | 432 |
408 def SubprocessCall(command, in_directory, fail_status=None): | 433 def SubprocessCall(command, in_directory, fail_status=None): |
409 """Runs command, a list, in directory in_directory. | 434 """Runs command, a list, in directory in_directory. |
410 | 435 |
411 This function wraps SubprocessCallAndCapture, but does not perform the | 436 This function wraps SubprocessCallAndFilter, but does not perform the |
412 capturing functions. See that function for a more complete usage | 437 filtering functions. See that function for a more complete usage |
413 description. | 438 description. |
414 """ | 439 """ |
415 # Call subprocess and capture nothing: | 440 # Call subprocess and capture nothing: |
416 SubprocessCallAndCapture(command, in_directory, fail_status) | 441 SubprocessCallAndFilter(command, in_directory, True, True, fail_status) |
417 | 442 |
418 | 443 |
419 def SubprocessCallAndCapture(command, in_directory, fail_status=None, | 444 def SubprocessCallAndFilter(command, |
420 pattern=None, capture_list=None): | 445 in_directory, |
446 print_messages, | |
447 print_stdout, | |
448 fail_status=None, filter=None): | |
421 """Runs command, a list, in directory in_directory. | 449 """Runs command, a list, in directory in_directory. |
422 | 450 |
423 A message indicating what is being done, as well as the command's stdout, | 451 If print_messages is true, a message indicating what is being done |
424 is printed to out. | 452 is printed to stdout. If print_stdout is true, the command's stdout |
453 is also forwarded to stdout. | |
425 | 454 |
426 If a pattern is specified, any line in the output matching pattern will have | 455 If a filter function is specified, it is expected to take a single |
427 its first match group appended to capture_list. | 456 string argument, and it will be called with each line of the |
457 subprocess's output. Each line has had the trailing newline character | |
458 trimmed. | |
428 | 459 |
429 If the command fails, as indicated by a nonzero exit status, gclient will | 460 If the command fails, as indicated by a nonzero exit status, gclient will |
430 exit with an exit status of fail_status. If fail_status is None (the | 461 exit with an exit status of fail_status. If fail_status is None (the |
431 default), gclient will raise an Error exception. | 462 default), gclient will raise an Error exception. |
432 """ | 463 """ |
433 | 464 |
434 print("\n________ running \'%s\' in \'%s\'" | 465 if print_messages: |
435 % (' '.join(command), in_directory)) | 466 print("\n________ running \'%s\' in \'%s\'" |
467 % (' '.join(command), in_directory)) | |
436 | 468 |
437 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the | 469 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the |
438 # executable, but shell=True makes subprocess on Linux fail when it's called | 470 # executable, but shell=True makes subprocess on Linux fail when it's called |
439 # with a list because it only tries to execute the first item in the list. | 471 # with a list because it only tries to execute the first item in the list. |
440 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory, | 472 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory, |
441 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE) | 473 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE) |
442 | 474 |
443 if pattern: | |
444 compiled_pattern = re.compile(pattern) | |
445 | |
446 # Also, we need to forward stdout to prevent weird re-ordering of output. | 475 # Also, we need to forward stdout to prevent weird re-ordering of output. |
447 # This has to be done on a per byte basis to make sure it is not buffered: | 476 # This has to be done on a per byte basis to make sure it is not buffered: |
448 # normally buffering is done for each line, but if svn requests input, no | 477 # normally buffering is done for each line, but if svn requests input, no |
449 # end-of-line character is output after the prompt and it would not show up. | 478 # end-of-line character is output after the prompt and it would not show up. |
450 in_byte = kid.stdout.read(1) | 479 in_byte = kid.stdout.read(1) |
451 in_line = "" | 480 in_line = "" |
452 while in_byte: | 481 while in_byte: |
453 if in_byte != "\r": | 482 if in_byte != "\r": |
454 sys.stdout.write(in_byte) | 483 if print_stdout: |
455 in_line += in_byte | 484 sys.stdout.write(in_byte) |
456 if in_byte == "\n" and pattern: | 485 if in_byte != "\n": |
457 match = compiled_pattern.search(in_line[:-1]) | 486 in_line += in_byte |
458 if match: | 487 if in_byte == "\n" and filter: |
459 capture_list.append(match.group(1)) | 488 filter(in_line) |
sgk
2009/09/04 00:25:11
Good, more flexible than just the pattern.
| |
460 in_line = "" | 489 in_line = "" |
461 in_byte = kid.stdout.read(1) | 490 in_byte = kid.stdout.read(1) |
462 rv = kid.wait() | 491 rv = kid.wait() |
463 | 492 |
464 if rv: | 493 if rv: |
465 msg = "failed to run command: %s" % " ".join(command) | 494 msg = "failed to run command: %s" % " ".join(command) |
466 | 495 |
467 if fail_status != None: | 496 if fail_status != None: |
468 print >>sys.stderr, msg | 497 print >>sys.stderr, msg |
469 sys.exit(fail_status) | 498 sys.exit(fail_status) |
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
559 # args[0] must be a supported command. This will blow up if it's something | 588 # args[0] must be a supported command. This will blow up if it's something |
560 # else, which is good. Note that the patterns are only effective when | 589 # else, which is good. Note that the patterns are only effective when |
561 # these commands are used in their ordinary forms, the patterns are invalid | 590 # these commands are used in their ordinary forms, the patterns are invalid |
562 # for "svn status --show-updates", for example. | 591 # for "svn status --show-updates", for example. |
563 pattern = { | 592 pattern = { |
564 'checkout': update_pattern, | 593 'checkout': update_pattern, |
565 'status': status_pattern, | 594 'status': status_pattern, |
566 'update': update_pattern, | 595 'update': update_pattern, |
567 }[args[0]] | 596 }[args[0]] |
568 | 597 |
569 SubprocessCallAndCapture(command, | 598 compiled_pattern = re.compile(pattern) |
570 in_directory, | |
571 pattern=pattern, | |
572 capture_list=file_list) | |
573 | 599 |
600 def CaptureMatchingLines(line): | |
601 match = compiled_pattern.search(line) | |
602 if match: | |
603 file_list.append(match.group(1)) | |
604 | |
605 RunSVNAndFilterOutput(args, | |
606 in_directory, | |
607 True, | |
608 True, | |
609 CaptureMatchingLines) | |
610 | |
611 def RunSVNAndFilterOutput(args, | |
612 in_directory, | |
613 print_messages, | |
614 print_stdout, | |
615 filter): | |
616 """Runs svn checkout, update, status, or diff, optionally outputting | |
617 to stdout. | |
618 | |
619 The first item in args must be either "checkout", "update", | |
620 "status", or "diff". | |
621 | |
622 svn's stdout is passed line-by-line to the given filter function. If | |
623 print_stdout is true, it is also printed to sys.stdout as in RunSVN. | |
624 | |
625 Args: | |
626 args: A sequence of command line parameters to be passed to svn. | |
627 in_directory: The directory where svn is to be run. | |
628 print_messages: Whether to print status messages to stdout about | |
629 which Subversion commands are being run. | |
630 print_stdout: Whether to forward Subversion's output to stdout. | |
631 filter: A function taking one argument (a string) which will be | |
632 passed each line (with the ending newline character removed) of | |
633 Subversion's output for filtering. | |
634 | |
635 Raises: | |
636 Error: An error occurred while running the svn command. | |
637 """ | |
638 command = [SVN_COMMAND] | |
639 command.extend(args) | |
640 | |
641 SubprocessCallAndFilter(command, | |
642 in_directory, | |
643 print_messages, | |
644 print_stdout, | |
645 filter=filter) | |
574 | 646 |
575 def CaptureSVNInfo(relpath, in_directory=None, print_error=True): | 647 def CaptureSVNInfo(relpath, in_directory=None, print_error=True): |
576 """Returns a dictionary from the svn info output for the given file. | 648 """Returns a dictionary from the svn info output for the given file. |
577 | 649 |
578 Args: | 650 Args: |
579 relpath: The directory where the working copy resides relative to | 651 relpath: The directory where the working copy resides relative to |
580 the directory given by in_directory. | 652 the directory given by in_directory. |
581 in_directory: The directory where svn is to be run. | 653 in_directory: The directory where svn is to be run. |
582 """ | 654 """ |
583 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error) | 655 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error) |
(...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
725 if file_list == None: | 797 if file_list == None: |
726 file_list = [] | 798 file_list = [] |
727 | 799 |
728 commands = { | 800 commands = { |
729 'cleanup': self.cleanup, | 801 'cleanup': self.cleanup, |
730 'export': self.export, | 802 'export': self.export, |
731 'update': self.update, | 803 'update': self.update, |
732 'revert': self.revert, | 804 'revert': self.revert, |
733 'status': self.status, | 805 'status': self.status, |
734 'diff': self.diff, | 806 'diff': self.diff, |
807 'pack': self.pack, | |
735 'runhooks': self.status, | 808 'runhooks': self.status, |
736 } | 809 } |
737 | 810 |
738 if not command in commands: | 811 if not command in commands: |
739 raise Error('Unknown command %s' % command) | 812 raise Error('Unknown command %s' % command) |
740 | 813 |
741 return commands[command](options, args, file_list) | 814 return commands[command](options, args, file_list) |
742 | 815 |
743 def cleanup(self, options, args, file_list): | 816 def cleanup(self, options, args, file_list): |
744 """Cleanup working copy.""" | 817 """Cleanup working copy.""" |
(...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
933 command.extend(args) | 1006 command.extend(args) |
934 if not os.path.isdir(path): | 1007 if not os.path.isdir(path): |
935 # svn status won't work if the directory doesn't exist. | 1008 # svn status won't work if the directory doesn't exist. |
936 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory " | 1009 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory " |
937 "does not exist." | 1010 "does not exist." |
938 % (' '.join(command), path)) | 1011 % (' '.join(command), path)) |
939 # There's no file list to retrieve. | 1012 # There's no file list to retrieve. |
940 else: | 1013 else: |
941 RunSVNAndGetFileList(command, path, file_list) | 1014 RunSVNAndGetFileList(command, path, file_list) |
942 | 1015 |
1016 def pack(self, options, args, file_list): | |
1017 """Generates a patch file which can be applied to the root of the | |
1018 repository.""" | |
1019 path = os.path.join(self._root_dir, self.relpath) | |
1020 command = ['diff'] | |
1021 command.extend(args) | |
1022 # Simple class which tracks which file is being diffed and | |
1023 # replaces instances of its file name in the original and | |
1024 # working copy lines of the svn diff output. | |
1025 class DiffFilterer(object): | |
1026 index_string = "Index: " | |
1027 original_prefix = "--- " | |
1028 working_prefix = "+++ " | |
1029 | |
1030 def __init__(self, relpath): | |
1031 # Note that we always use '/' as the path separator to be | |
1032 # consistent with svn's cygwin-style output on Windows | |
1033 self._relpath = relpath.replace("\\", "/") | |
1034 self._current_file = "" | |
1035 self._replacement_file = "" | |
1036 | |
1037 def SetCurrentFile(self, file): | |
1038 self._current_file = file | |
1039 # Note that we always use '/' as the path separator to be | |
1040 # consistent with svn's cygwin-style output on Windows | |
1041 self._replacement_file = self._relpath + '/' + file | |
1042 | |
1043 def ReplaceAndPrint(self, line): | |
1044 print(line.replace(self._current_file, self._replacement_file)) | |
1045 | |
1046 def Filter(self, line): | |
1047 if (line.startswith(self.index_string)): | |
1048 self.SetCurrentFile(line[len(self.index_string):]) | |
1049 self.ReplaceAndPrint(line) | |
1050 else: | |
1051 if (line.startswith(self.original_prefix) or | |
1052 line.startswith(self.working_prefix)): | |
1053 self.ReplaceAndPrint(line) | |
1054 else: | |
1055 print line | |
1056 | |
1057 filterer = DiffFilterer(self.relpath) | |
1058 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter) | |
943 | 1059 |
944 ## GClient implementation. | 1060 ## GClient implementation. |
945 | 1061 |
946 | 1062 |
947 class GClient(object): | 1063 class GClient(object): |
948 """Object that represent a gclient checkout.""" | 1064 """Object that represent a gclient checkout.""" |
949 | 1065 |
950 supported_commands = [ | 1066 supported_commands = [ |
951 'cleanup', 'diff', 'export', 'revert', 'status', 'update', 'runhooks' | 1067 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update', |
1068 'runhooks' | |
952 ] | 1069 ] |
953 | 1070 |
954 def __init__(self, root_dir, options): | 1071 def __init__(self, root_dir, options): |
955 self._root_dir = root_dir | 1072 self._root_dir = root_dir |
956 self._options = options | 1073 self._options = options |
957 self._config_content = None | 1074 self._config_content = None |
958 self._config_dict = {} | 1075 self._config_dict = {} |
959 self._deps_hooks = [] | 1076 self._deps_hooks = [] |
960 | 1077 |
961 def SetConfig(self, content): | 1078 def SetConfig(self, content): |
(...skipping 630 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1592 | 1709 |
1593 Raises: | 1710 Raises: |
1594 Error: if the command is unknown. | 1711 Error: if the command is unknown. |
1595 """ | 1712 """ |
1596 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT: | 1713 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT: |
1597 print(COMMAND_USAGE_TEXT[args[0]]) | 1714 print(COMMAND_USAGE_TEXT[args[0]]) |
1598 else: | 1715 else: |
1599 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0]) | 1716 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0]) |
1600 | 1717 |
1601 | 1718 |
1719 def DoPack(options, args): | |
1720 """Handle the pack subcommand. | |
1721 | |
1722 Raises: | |
1723 Error: if client isn't configured properly. | |
1724 """ | |
1725 client = GClient.LoadCurrentConfig(options) | |
1726 if not client: | |
1727 raise Error("client not configured; see 'gclient config'") | |
1728 if options.verbose: | |
1729 # Print out the .gclient file. This is longer than if we just printed the | |
1730 # client dict, but more legible, and it might contain helpful comments. | |
1731 print(client.ConfigContent()) | |
1732 options.verbose = True | |
1733 return client.RunOnDeps('pack', args) | |
1734 | |
1735 | |
1602 def DoStatus(options, args): | 1736 def DoStatus(options, args): |
1603 """Handle the status subcommand. | 1737 """Handle the status subcommand. |
1604 | 1738 |
1605 Raises: | 1739 Raises: |
1606 Error: if client isn't configured properly. | 1740 Error: if client isn't configured properly. |
1607 """ | 1741 """ |
1608 client = GClient.LoadCurrentConfig(options) | 1742 client = GClient.LoadCurrentConfig(options) |
1609 if not client: | 1743 if not client: |
1610 raise Error("client not configured; see 'gclient config'") | 1744 raise Error("client not configured; see 'gclient config'") |
1611 if options.verbose: | 1745 if options.verbose: |
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1711 raise Error("client not configured; see 'gclient config'") | 1845 raise Error("client not configured; see 'gclient config'") |
1712 client.PrintRevInfo() | 1846 client.PrintRevInfo() |
1713 | 1847 |
1714 | 1848 |
1715 gclient_command_map = { | 1849 gclient_command_map = { |
1716 "cleanup": DoCleanup, | 1850 "cleanup": DoCleanup, |
1717 "config": DoConfig, | 1851 "config": DoConfig, |
1718 "diff": DoDiff, | 1852 "diff": DoDiff, |
1719 "export": DoExport, | 1853 "export": DoExport, |
1720 "help": DoHelp, | 1854 "help": DoHelp, |
1855 "pack": DoPack, | |
1721 "status": DoStatus, | 1856 "status": DoStatus, |
1722 "sync": DoUpdate, | 1857 "sync": DoUpdate, |
1723 "update": DoUpdate, | 1858 "update": DoUpdate, |
1724 "revert": DoRevert, | 1859 "revert": DoRevert, |
1725 "runhooks": DoRunHooks, | 1860 "runhooks": DoRunHooks, |
1726 "revinfo" : DoRevInfo, | 1861 "revinfo" : DoRevInfo, |
1727 } | 1862 } |
1728 | 1863 |
1729 | 1864 |
1730 def DispatchCommand(command, options, args, command_map=None): | 1865 def DispatchCommand(command, options, args, command_map=None): |
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1809 | 1944 |
1810 if "__main__" == __name__: | 1945 if "__main__" == __name__: |
1811 try: | 1946 try: |
1812 result = Main(sys.argv) | 1947 result = Main(sys.argv) |
1813 except Error, e: | 1948 except Error, e: |
1814 print >> sys.stderr, "Error: %s" % str(e) | 1949 print >> sys.stderr, "Error: %s" % str(e) |
1815 result = 1 | 1950 result = 1 |
1816 sys.exit(result) | 1951 sys.exit(result) |
1817 | 1952 |
1818 # vim: ts=2:sw=2:tw=80:et: | 1953 # vim: ts=2:sw=2:tw=80:et: |
OLD | NEW |