OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Meta checkout manager supporting both Subversion and GIT. | 6 """Meta checkout manager supporting both Subversion and GIT. |
7 | 7 |
8 Files | 8 Files |
9 .gclient : Current client configuration, written by 'config' command. | 9 .gclient : Current client configuration, written by 'config' command. |
10 Format is a Python script defining 'solutions', a list whose | 10 Format is a Python script defining 'solutions', a list whose |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
44 | 44 |
45 Example: | 45 Example: |
46 hooks = [ | 46 hooks = [ |
47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$", | 47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$", |
48 "action": ["python", "image_indexer.py", "--all"]}, | 48 "action": ["python", "image_indexer.py", "--all"]}, |
49 ] | 49 ] |
50 """ | 50 """ |
51 | 51 |
52 __version__ = "0.4.1" | 52 __version__ = "0.4.1" |
53 | 53 |
| 54 import copy |
54 import errno | 55 import errno |
55 import logging | 56 import logging |
56 import optparse | 57 import optparse |
57 import os | 58 import os |
58 import pprint | 59 import pprint |
59 import re | 60 import re |
60 import subprocess | 61 import subprocess |
61 import sys | 62 import sys |
62 import urlparse | 63 import urlparse |
63 import urllib | 64 import urllib |
64 | 65 |
65 import breakpad | 66 import breakpad |
66 | 67 |
67 import gclient_scm | 68 import gclient_scm |
68 import gclient_utils | 69 import gclient_utils |
69 from third_party.repo.progress import Progress | |
70 | 70 |
71 | 71 |
72 def attr(attr, data): | 72 def attr(attr, data): |
73 """Sets an attribute on a function.""" | 73 """Sets an attribute on a function.""" |
74 def hook(fn): | 74 def hook(fn): |
75 setattr(fn, attr, data) | 75 setattr(fn, attr, data) |
76 return fn | 76 return fn |
77 return hook | 77 return hook |
78 | 78 |
79 | 79 |
(...skipping 481 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
561 sol, rev = revision.split('@', 1) | 561 sol, rev = revision.split('@', 1) |
562 if not sol in solutions_names: | 562 if not sol in solutions_names: |
563 #raise gclient_utils.Error('%s is not a valid solution.' % sol) | 563 #raise gclient_utils.Error('%s is not a valid solution.' % sol) |
564 print >> sys.stderr, ('Please fix your script, having invalid ' | 564 print >> sys.stderr, ('Please fix your script, having invalid ' |
565 '--revision flags will soon considered an error.') | 565 '--revision flags will soon considered an error.') |
566 else: | 566 else: |
567 revision_overrides[sol] = rev | 567 revision_overrides[sol] = rev |
568 index += 1 | 568 index += 1 |
569 return revision_overrides | 569 return revision_overrides |
570 | 570 |
| 571 def GetSCMCommandClosure(self, path, url, revision, command, args, file_list, |
| 572 continuation=None): |
| 573 """Gets a closure that runs a SCM command on a particular dependency.""" |
| 574 def _Closure(): |
| 575 logging.debug("Running %s in %s to %s %s" % (command, path, url, |
| 576 revision)) |
| 577 options = copy.copy(self._options) |
| 578 options.revision = revision |
| 579 scm = gclient_scm.CreateSCM(url, self.root_dir(), path) |
| 580 scm.RunCommand(command, options, args, file_list) |
| 581 if continuation: |
| 582 continuation() |
| 583 return _Closure |
| 584 |
571 def RunOnDeps(self, command, args): | 585 def RunOnDeps(self, command, args): |
572 """Runs a command on each dependency in a client and its dependencies. | 586 """Runs a command on each dependency in a client and its dependencies. |
573 | 587 |
574 Args: | 588 Args: |
575 command: The command to use (e.g., 'status' or 'diff') | 589 command: The command to use (e.g., 'status' or 'diff') |
576 args: list of str - extra arguments to add to the command line. | 590 args: list of str - extra arguments to add to the command line. |
577 """ | 591 """ |
578 if not self.dependencies: | 592 if not self.dependencies: |
579 raise gclient_utils.Error('No solution specified') | 593 raise gclient_utils.Error('No solution specified') |
580 revision_overrides = self._EnforceRevisions() | 594 revision_overrides = self._EnforceRevisions() |
581 | 595 |
582 # When running runhooks --force, there's no need to consult the SCM. | 596 # When running runhooks --force, there's no need to consult the SCM. |
583 # All known hooks are expected to run unconditionally regardless of working | 597 # All known hooks are expected to run unconditionally regardless of working |
584 # copy state, so skip the SCM status check. | 598 # copy state, so skip the SCM status check. |
585 run_scm = not (command == 'runhooks' and self._options.force) | 599 run_scm = not (command == 'runhooks' and self._options.force) |
586 | 600 |
587 entries = {} | 601 entries = {} |
588 file_list = [] | |
589 # Run on the base solutions first. | |
590 for solution in self.dependencies: | |
591 name = solution.name | |
592 if name in entries: | |
593 raise gclient_utils.Error("solution %s specified more than once" % name) | |
594 url = solution.url | |
595 entries[name] = url | |
596 if run_scm and url: | |
597 self._options.revision = revision_overrides.get(name) | |
598 scm = gclient_scm.CreateSCM(url, self.root_dir(), name) | |
599 scm.RunCommand(command, self._options, args, file_list) | |
600 file_list = [os.path.join(name, f.strip()) for f in file_list] | |
601 self._options.revision = None | |
602 | 602 |
603 # Process the dependencies next (sort alphanumerically to ensure that | 603 # To avoid threading issues, all file lists get constructed separately then |
604 # containing directories get populated first and for readability) | 604 # gathered in a flattened list at the end. |
605 deps = self._ParseAllDeps(entries) | 605 file_list_list = [] |
606 deps_to_process = deps.keys() | 606 file_list_dict = {} |
607 deps_to_process.sort() | |
608 | 607 |
609 # First pass for direct dependencies. | 608 thread_pool = gclient_utils.ThreadPool(self._options.jobs) |
610 if command == 'update' and not self._options.verbose: | 609 thread_pool.Start() |
611 pm = Progress('Syncing projects', len(deps_to_process)) | |
612 for d in deps_to_process: | |
613 if command == 'update' and not self._options.verbose: | |
614 pm.update() | |
615 if type(deps[d]) == str: | |
616 url = deps[d] | |
617 entries[d] = url | |
618 if run_scm: | |
619 self._options.revision = revision_overrides.get(d) | |
620 scm = gclient_scm.CreateSCM(url, self.root_dir(), d) | |
621 scm.RunCommand(command, self._options, args, file_list) | |
622 self._options.revision = None | |
623 elif isinstance(deps[d], self.FileImpl): | |
624 if command in (None, 'cleanup', 'diff', 'pack', 'status'): | |
625 continue | |
626 file_dep = deps[d] | |
627 self._options.revision = file_dep.GetRevision() | |
628 if run_scm: | |
629 scm = gclient_scm.SVNWrapper(file_dep.GetPath(), self.root_dir(), d) | |
630 scm.RunCommand('updatesingle', self._options, | |
631 args + [file_dep.GetFilename()], file_list) | |
632 | 610 |
633 if command == 'update' and not self._options.verbose: | 611 show_progress = command == 'update' and not self._options.verbose |
634 pm.end() | 612 pm_update = None |
635 | 613 |
636 # Second pass for inherited deps (via the From keyword) | 614 try: |
637 for d in deps_to_process: | 615 # Run on the base solutions first. |
638 if isinstance(deps[d], self.FromImpl): | 616 if show_progress: |
639 # Getting the URL from the sub_deps file can involve having to resolve | 617 pm = gclient_utils.ThreadSafeProgress('Syncing base solutions', |
640 # a File() or having to resolve a relative URL. To resolve relative | 618 len(self.dependencies)) |
641 # URLs, we need to pass in the orignal sub deps URL. | 619 pm_update = lambda: pm.update() |
642 sub_deps_base_url = deps[deps[d].module_name] | 620 |
643 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url | 621 for solution in self.dependencies: |
644 ).ParseDepsFile(False) | 622 name = solution.name |
645 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps) | 623 if name in entries: |
646 entries[d] = url | 624 raise gclient_utils.Error("solution %s specified more than once" |
647 if run_scm: | 625 % name) |
648 self._options.revision = revision_overrides.get(d) | 626 url = solution.url |
649 scm = gclient_scm.CreateSCM(url, self.root_dir(), d) | 627 entries[name] = url |
650 scm.RunCommand(command, self._options, args, file_list) | 628 if run_scm and url: |
651 self._options.revision = None | 629 revision = revision_overrides.get(name) |
| 630 file_list = [] |
| 631 file_list_dict[name] = file_list |
| 632 thread_pool.AddJob(self.GetSCMCommandClosure( |
| 633 name, url, revision, command, args, file_list, pm_update)) |
| 634 |
| 635 thread_pool.WaitJobs() |
| 636 |
| 637 if show_progress: |
| 638 pm.end() |
| 639 |
| 640 for solution in self.dependencies: |
| 641 name = solution.name |
| 642 try: |
| 643 file_list_list.append([os.path.join(name, f.strip()) |
| 644 for f in file_list_dict[name]]) |
| 645 except KeyError: |
| 646 # We may not have added the file list to the dict, see tests above. |
| 647 # Instead of duplicating the tests, it's less fragile to just ignore |
| 648 # the exception. |
| 649 pass |
| 650 try: |
| 651 deps_content = gclient_utils.FileRead( |
| 652 os.path.join(self.root_dir(), name, solution.deps_file)) |
| 653 except IOError, e: |
| 654 if e.errno != errno.ENOENT: |
| 655 raise |
| 656 deps_content = "" |
| 657 |
| 658 # Process the dependencies next (sort alphanumerically to ensure that |
| 659 # containing directories get populated first and for readability) |
| 660 # TODO(piman): when using multiple threads, the ordering is not ensured. |
| 661 # In many cases (e.g. updates to an existing checkout where DEPS don't |
| 662 # move between directories), it'll still be correct but for completeness |
| 663 # this should be fixed. |
| 664 deps = self._ParseAllDeps(entries) |
| 665 deps_to_process = deps.keys() |
| 666 deps_to_process.sort() |
| 667 |
| 668 # First pass for direct dependencies. |
| 669 if show_progress: |
| 670 pm = gclient_utils.ThreadSafeProgress('Syncing projects', |
| 671 len(deps_to_process)) |
| 672 pm_update = lambda: pm.update() |
| 673 for d in deps_to_process: |
| 674 file_list = [] |
| 675 file_list_list.append(file_list) |
| 676 |
| 677 if type(deps[d]) == str: |
| 678 url = deps[d] |
| 679 entries[d] = url |
| 680 if run_scm: |
| 681 revision = revision_overrides.get(d) |
| 682 thread_pool.AddJob(self.GetSCMCommandClosure(d, url, revision, |
| 683 command, args, |
| 684 file_list, pm_update)) |
| 685 elif isinstance(deps[d], self.FileImpl): |
| 686 if command in (None, 'cleanup', 'diff', 'pack', 'status'): |
| 687 continue |
| 688 file_dep = deps[d] |
| 689 revision = file_dep.GetRevision() |
| 690 if run_scm: |
| 691 thread_pool.AddJob(self.GetSCMCommandClosure( |
| 692 d, file_dep.GetPath(), revision, "updatesingle", |
| 693 args + [file_dep.GetFilename()], file_list)) |
| 694 |
| 695 thread_pool.WaitJobs() |
| 696 |
| 697 if show_progress: |
| 698 pm.end() |
| 699 |
| 700 # Second pass for inherited deps (via the From keyword) |
| 701 for d in deps_to_process: |
| 702 if isinstance(deps[d], self.FromImpl): |
| 703 # Getting the URL from the sub_deps file can involve having to resolve |
| 704 # a File() or having to resolve a relative URL. To resolve relative |
| 705 # URLs, we need to pass in the orignal sub deps URL. |
| 706 sub_deps_base_url = deps[deps[d].module_name] |
| 707 sub_deps = Dependency(self, deps[d].module_name, |
| 708 sub_deps_base_url).ParseDepsFile(False) |
| 709 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps) |
| 710 entries[d] = url |
| 711 if run_scm: |
| 712 revision = revision_overrides.get(d) |
| 713 file_list = [] |
| 714 file_list_list.append(file_list) |
| 715 thread_pool.AddJob(self.GetSCMCommandClosure(d, url, revision, |
| 716 command, args, |
| 717 file_list)) |
| 718 |
| 719 thread_pool.WaitJobs() |
| 720 |
| 721 finally: |
| 722 thread_pool.Stop() |
| 723 |
| 724 file_list = sum(file_list_list, []) |
652 | 725 |
653 # Convert all absolute paths to relative. | 726 # Convert all absolute paths to relative. |
654 for i in range(len(file_list)): | 727 for i in range(len(file_list)): |
655 # TODO(phajdan.jr): We should know exactly when the paths are absolute. | 728 # TODO(phajdan.jr): We should know exactly when the paths are absolute. |
656 # It depends on the command being executed (like runhooks vs sync). | 729 # It depends on the command being executed (like runhooks vs sync). |
657 if not os.path.isabs(file_list[i]): | 730 if not os.path.isabs(file_list[i]): |
658 continue | 731 continue |
659 | 732 |
660 prefix = os.path.commonprefix([self.root_dir().lower(), | 733 prefix = os.path.commonprefix([self.root_dir().lower(), |
661 file_list[i].lower()]) | 734 file_list[i].lower()]) |
(...skipping 490 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1152 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([ | 1225 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([ |
1153 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip()) | 1226 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip()) |
1154 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')])) | 1227 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')])) |
1155 parser = optparse.OptionParser(version='%prog ' + __version__) | 1228 parser = optparse.OptionParser(version='%prog ' + __version__) |
1156 parser.add_option('-v', '--verbose', action='count', default=0, | 1229 parser.add_option('-v', '--verbose', action='count', default=0, |
1157 help='Produces additional output for diagnostics. Can be ' | 1230 help='Produces additional output for diagnostics. Can be ' |
1158 'used up to three times for more logging info.') | 1231 'used up to three times for more logging info.') |
1159 parser.add_option('--gclientfile', dest='config_filename', | 1232 parser.add_option('--gclientfile', dest='config_filename', |
1160 default=os.environ.get('GCLIENT_FILE', '.gclient'), | 1233 default=os.environ.get('GCLIENT_FILE', '.gclient'), |
1161 help='Specify an alternate %default file') | 1234 help='Specify an alternate %default file') |
| 1235 parser.add_option("-j", "--jobs", default=0, type="int", |
| 1236 help=("specify how many SCM commands can run in " |
| 1237 "parallel")) |
| 1238 |
1162 # Integrate standard options processing. | 1239 # Integrate standard options processing. |
1163 old_parser = parser.parse_args | 1240 old_parser = parser.parse_args |
1164 def Parse(args): | 1241 def Parse(args): |
1165 (options, args) = old_parser(args) | 1242 (options, args) = old_parser(args) |
1166 level = None | 1243 level = None |
1167 if options.verbose == 2: | 1244 if options.verbose == 2: |
1168 level = logging.INFO | 1245 level = logging.INFO |
1169 elif options.verbose > 2: | 1246 elif options.verbose > 2: |
1170 level = logging.DEBUG | 1247 level = logging.DEBUG |
1171 logging.basicConfig(level=level, | 1248 logging.basicConfig(level=level, |
(...skipping 29 matching lines...) Expand all Loading... |
1201 return CMDhelp(parser, argv) | 1278 return CMDhelp(parser, argv) |
1202 except gclient_utils.Error, e: | 1279 except gclient_utils.Error, e: |
1203 print >> sys.stderr, 'Error: %s' % str(e) | 1280 print >> sys.stderr, 'Error: %s' % str(e) |
1204 return 1 | 1281 return 1 |
1205 | 1282 |
1206 | 1283 |
1207 if '__main__' == __name__: | 1284 if '__main__' == __name__: |
1208 sys.exit(Main(sys.argv[1:])) | 1285 sys.exit(Main(sys.argv[1:])) |
1209 | 1286 |
1210 # vim: ts=2:sw=2:tw=80:et: | 1287 # vim: ts=2:sw=2:tw=80:et: |
OLD | NEW |