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 """A wrapper script to manage a set of client modules in different SCM. | 6 """A wrapper script to manage a set of client modules in different SCM. |
7 | 7 |
8 This script is intended to be used to help basic management of client | 8 This script is intended to be used to help basic management of client |
9 program sources residing in one or more Subversion modules and Git | 9 program sources residing in one or more Subversion modules and Git |
10 repositories, along with other modules it depends on, also in Subversion or Git, | 10 repositories, along with other modules it depends on, also in Subversion or Git, |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
51 Example: | 51 Example: |
52 hooks = [ | 52 hooks = [ |
53 { "pattern": "\\.(gif|jpe?g|pr0n|png)$", | 53 { "pattern": "\\.(gif|jpe?g|pr0n|png)$", |
54 "action": ["python", "image_indexer.py", "--all"]}, | 54 "action": ["python", "image_indexer.py", "--all"]}, |
55 ] | 55 ] |
56 """ | 56 """ |
57 | 57 |
58 __author__ = "darinf@gmail.com (Darin Fisher)" | 58 __author__ = "darinf@gmail.com (Darin Fisher)" |
59 __version__ = "0.3.4" | 59 __version__ = "0.3.4" |
60 | 60 |
61 import copy | |
62 import errno | 61 import errno |
63 import logging | 62 import logging |
64 import optparse | 63 import optparse |
65 import os | 64 import os |
66 import pprint | 65 import pprint |
67 import re | 66 import re |
68 import sys | 67 import sys |
69 import urlparse | 68 import urlparse |
70 import urllib | 69 import urllib |
71 | 70 |
(...skipping 620 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
692 return | 691 return |
693 | 692 |
694 # Run hooks on the basis of whether the files from the gclient operation | 693 # Run hooks on the basis of whether the files from the gclient operation |
695 # match each hook's pattern. | 694 # match each hook's pattern. |
696 for hook_dict in hooks: | 695 for hook_dict in hooks: |
697 pattern = re.compile(hook_dict['pattern']) | 696 pattern = re.compile(hook_dict['pattern']) |
698 matching_file_list = [f for f in file_list if pattern.search(f)] | 697 matching_file_list = [f for f in file_list if pattern.search(f)] |
699 if matching_file_list: | 698 if matching_file_list: |
700 self._RunHookAction(hook_dict, matching_file_list) | 699 self._RunHookAction(hook_dict, matching_file_list) |
701 | 700 |
702 def GetSCMCommandClosure(self, path, url, revision, command, args, file_list): | |
703 """Gets a closure that runs a SCM command on a particular dependency.""" | |
704 def _Closure(): | |
705 logging.debug("Running %s in %s to %s %s" % (command, path, url, | |
706 revision)) | |
707 options = copy.copy(self._options) | |
708 options.revision = revision | |
709 scm = gclient_scm.CreateSCM(url, self._root_dir, path) | |
710 scm.RunCommand(command, options, args, file_list) | |
711 return _Closure | |
712 | |
713 def RunOnDeps(self, command, args): | 701 def RunOnDeps(self, command, args): |
714 """Runs a command on each dependency in a client and its dependencies. | 702 """Runs a command on each dependency in a client and its dependencies. |
715 | 703 |
716 The module's dependencies are specified in its top-level DEPS files. | 704 The module's dependencies are specified in its top-level DEPS files. |
717 | 705 |
718 Args: | 706 Args: |
719 command: The command to use (e.g., 'status' or 'diff') | 707 command: The command to use (e.g., 'status' or 'diff') |
720 args: list of str - extra arguments to add to the command line. | 708 args: list of str - extra arguments to add to the command line. |
721 | 709 |
722 Raises: | 710 Raises: |
(...skipping 20 matching lines...) Expand all Loading... |
743 if not solutions: | 731 if not solutions: |
744 raise gclient_utils.Error("No solution specified") | 732 raise gclient_utils.Error("No solution specified") |
745 | 733 |
746 # When running runhooks --force, there's no need to consult the SCM. | 734 # When running runhooks --force, there's no need to consult the SCM. |
747 # All known hooks are expected to run unconditionally regardless of working | 735 # All known hooks are expected to run unconditionally regardless of working |
748 # copy state, so skip the SCM status check. | 736 # copy state, so skip the SCM status check. |
749 run_scm = not (command == 'runhooks' and self._options.force) | 737 run_scm = not (command == 'runhooks' and self._options.force) |
750 | 738 |
751 entries = {} | 739 entries = {} |
752 entries_deps_content = {} | 740 entries_deps_content = {} |
| 741 file_list = [] |
| 742 # Run on the base solutions first. |
| 743 for solution in solutions: |
| 744 name = solution["name"] |
| 745 deps_file = solution.get("deps_file", self._options.deps_file) |
| 746 if '/' in deps_file or '\\' in deps_file: |
| 747 raise gclient_utils.Error('deps_file name must not be a path, just a ' |
| 748 'filename.') |
| 749 if name in entries: |
| 750 raise gclient_utils.Error("solution %s specified more than once" % name) |
| 751 url = solution["url"] |
| 752 entries[name] = url |
| 753 if run_scm and url: |
| 754 self._options.revision = revision_overrides.get(name) |
| 755 scm = gclient_scm.CreateSCM(url, self._root_dir, name) |
| 756 scm.RunCommand(command, self._options, args, file_list) |
| 757 file_list = [os.path.join(name, f.strip()) for f in file_list] |
| 758 self._options.revision = None |
| 759 try: |
| 760 deps_content = gclient_utils.FileRead( |
| 761 os.path.join(self._root_dir, name, deps_file)) |
| 762 except IOError, e: |
| 763 if e.errno != errno.ENOENT: |
| 764 raise |
| 765 deps_content = "" |
| 766 entries_deps_content[name] = deps_content |
753 | 767 |
754 # To avoid threading issues, all file lists get constructed separately then | 768 # Process the dependencies next (sort alphanumerically to ensure that |
755 # gathered in a flattened list at the end. | 769 # containing directories get populated first and for readability) |
756 file_list_list = [] | 770 deps = self._ParseAllDeps(entries, entries_deps_content) |
757 file_list_dict = {} | 771 deps_to_process = deps.keys() |
| 772 deps_to_process.sort() |
758 | 773 |
759 thread_pool = gclient_utils.ThreadPool(self._options.jobs) | 774 # First pass for direct dependencies. |
760 thread_pool.Start() | 775 if command == 'update' and not self._options.verbose: |
| 776 pm = Progress('Syncing projects', len(deps_to_process)) |
| 777 for d in deps_to_process: |
| 778 if command == 'update' and not self._options.verbose: |
| 779 pm.update() |
| 780 if type(deps[d]) == str: |
| 781 url = deps[d] |
| 782 entries[d] = url |
| 783 if run_scm: |
| 784 self._options.revision = revision_overrides.get(d) |
| 785 scm = gclient_scm.CreateSCM(url, self._root_dir, d) |
| 786 scm.RunCommand(command, self._options, args, file_list) |
| 787 self._options.revision = None |
| 788 elif isinstance(deps[d], self.FileImpl): |
| 789 file = deps[d] |
| 790 self._options.revision = file.GetRevision() |
| 791 if run_scm: |
| 792 scm = gclient_scm.CreateSCM(file.GetPath(), self._root_dir, d) |
| 793 scm.RunCommand("updatesingle", self._options, |
| 794 args + [file.GetFilename()], file_list) |
| 795 |
| 796 if command == 'update' and not self._options.verbose: |
| 797 pm.end() |
761 | 798 |
762 try: | 799 # Second pass for inherited deps (via the From keyword) |
763 # Run on the base solutions first. | 800 for d in deps_to_process: |
764 for solution in solutions: | 801 if isinstance(deps[d], self.FromImpl): |
765 name = solution["name"] | 802 filename = os.path.join(self._root_dir, |
766 deps_file = solution.get("deps_file", self._options.deps_file) | 803 deps[d].module_name, |
767 if '/' in deps_file or '\\' in deps_file: | 804 self._options.deps_file) |
768 raise gclient_utils.Error('deps_file name must not be a path, just a ' | 805 content = gclient_utils.FileRead(filename) |
769 'filename.') | 806 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {}, |
770 if name in entries: | 807 False) |
771 raise gclient_utils.Error( | 808 # Getting the URL from the sub_deps file can involve having to resolve |
772 "solution %s specified more than once" % name) | 809 # a File() or having to resolve a relative URL. To resolve relative |
773 url = solution["url"] | 810 # URLs, we need to pass in the orignal sub deps URL. |
774 entries[name] = url | 811 sub_deps_base_url = deps[deps[d].module_name] |
775 if run_scm and url: | 812 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps) |
776 revision = revision_overrides.get(name) | 813 entries[d] = url |
777 file_list = [] | 814 if run_scm: |
778 file_list_dict[name] = file_list | 815 self._options.revision = revision_overrides.get(d) |
779 thread_pool.AddJob(self.GetSCMCommandClosure( | 816 scm = gclient_scm.CreateSCM(url, self._root_dir, d) |
780 name, url, revision, command, args, file_list)) | 817 scm.RunCommand(command, self._options, args, file_list) |
781 | 818 self._options.revision = None |
782 thread_pool.WaitJobs() | |
783 | |
784 for solution in solutions: | |
785 name = solution["name"] | |
786 deps_file = solution.get("deps_file", self._options.deps_file) | |
787 try: | |
788 deps_content = gclient_utils.FileRead( | |
789 os.path.join(self._root_dir, name, deps_file)) | |
790 except IOError, e: | |
791 if e.errno != errno.ENOENT: | |
792 raise | |
793 deps_content = "" | |
794 entries_deps_content[name] = deps_content | |
795 try: | |
796 file_list_list.append([os.path.join(name, f.strip()) | |
797 for f in file_list_dict[name]]) | |
798 except KeyError: | |
799 # We may not have added the file list to the dict, see tests above. | |
800 # Instead of duplicating the tests, it's less fragile to just ignore | |
801 # the exception. | |
802 pass | |
803 | |
804 # Process the dependencies next (sort alphanumerically to ensure that | |
805 # containing directories get populated first and for readability) | |
806 # TODO(piman): when using multiple threads, the ordering is not ensured. | |
807 # In many cases (e.g. updates to an existing checkout where DEPS don't | |
808 # move between directories), it'll still be correct but for completeness | |
809 # this should be fixed. | |
810 deps = self._ParseAllDeps(entries, entries_deps_content) | |
811 deps_to_process = deps.keys() | |
812 deps_to_process.sort() | |
813 | |
814 # First pass for direct dependencies. | |
815 if command == 'update' and not self._options.verbose: | |
816 pm = Progress('Syncing projects', len(deps_to_process)) | |
817 | |
818 for d in deps_to_process: | |
819 if command == 'update' and not self._options.verbose: | |
820 pm.update() | |
821 file_list = [] | |
822 file_list_list.append(file_list) | |
823 if type(deps[d]) == str: | |
824 url = deps[d] | |
825 entries[d] = url | |
826 if run_scm: | |
827 revision = revision_overrides.get(d) | |
828 thread_pool.AddJob(self.GetSCMCommandClosure(d, url, revision, | |
829 command, args, | |
830 file_list)) | |
831 elif isinstance(deps[d], self.FileImpl): | |
832 file = deps[d] | |
833 if run_scm: | |
834 revision = file.GetRevision() | |
835 thread_pool.AddJob(self.GetSCMCommandClosure( | |
836 d, url, revision, "updatesingle", args + [file.GetFilename()], | |
837 file_list)) | |
838 | |
839 thread_pool.WaitJobs() | |
840 | |
841 if command == 'update' and not self._options.verbose: | |
842 pm.end() | |
843 | |
844 # Second pass for inherited deps (via the From keyword) | |
845 for d in deps_to_process: | |
846 if isinstance(deps[d], self.FromImpl): | |
847 filename = os.path.join(self._root_dir, | |
848 deps[d].module_name, | |
849 self._options.deps_file) | |
850 content = gclient_utils.FileRead(filename) | |
851 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {}, | |
852 False) | |
853 # Getting the URL from the sub_deps file can involve having to resolve | |
854 # a File() or having to resolve a relative URL. To resolve relative | |
855 # URLs, we need to pass in the orignal sub deps URL. | |
856 sub_deps_base_url = deps[deps[d].module_name] | |
857 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps) | |
858 entries[d] = url | |
859 if run_scm: | |
860 revision = revision_overrides.get(d) | |
861 file_list = [] | |
862 file_list_list.append(file_list) | |
863 thread_pool.AddJob(self.GetSCMCommandClosure(d, url, revision, | |
864 command, args, | |
865 file_list)) | |
866 | |
867 thread_pool.WaitJobs() | |
868 finally: | |
869 thread_pool.Stop() | |
870 | |
871 file_list = sum(file_list_list, []) | |
872 | 819 |
873 # Convert all absolute paths to relative. | 820 # Convert all absolute paths to relative. |
874 for i in range(len(file_list)): | 821 for i in range(len(file_list)): |
875 # TODO(phajdan.jr): We should know exactly when the paths are absolute. | 822 # TODO(phajdan.jr): We should know exactly when the paths are absolute. |
876 # It depends on the command being executed (like runhooks vs sync). | 823 # It depends on the command being executed (like runhooks vs sync). |
877 if not os.path.isabs(file_list[i]): | 824 if not os.path.isabs(file_list[i]): |
878 continue | 825 continue |
879 | 826 |
880 prefix = os.path.commonprefix([self._root_dir.lower(), | 827 prefix = os.path.commonprefix([self._root_dir.lower(), |
881 file_list[i].lower()]) | 828 file_list[i].lower()]) |
(...skipping 448 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1330 help=("on update, delete any unexpected " | 1277 help=("on update, delete any unexpected " |
1331 "unversioned trees that are in the checkout")) | 1278 "unversioned trees that are in the checkout")) |
1332 option_parser.add_option("", "--snapshot", action="store_true", default=False, | 1279 option_parser.add_option("", "--snapshot", action="store_true", default=False, |
1333 help=("(revinfo only), create a snapshot file " | 1280 help=("(revinfo only), create a snapshot file " |
1334 "of the current version of all repositories")) | 1281 "of the current version of all repositories")) |
1335 option_parser.add_option("", "--name", | 1282 option_parser.add_option("", "--name", |
1336 help="specify alternate relative solution path") | 1283 help="specify alternate relative solution path") |
1337 option_parser.add_option("", "--gclientfile", default=None, | 1284 option_parser.add_option("", "--gclientfile", default=None, |
1338 metavar="FILENAME", | 1285 metavar="FILENAME", |
1339 help=("specify an alternate .gclient file")) | 1286 help=("specify an alternate .gclient file")) |
1340 option_parser.add_option("-j", "--jobs", default=1, type="int", | |
1341 help=("specify how many SCM commands can run in " | |
1342 "parallel")) | |
1343 | 1287 |
1344 if len(argv) < 2: | 1288 if len(argv) < 2: |
1345 # Users don't need to be told to use the 'help' command. | 1289 # Users don't need to be told to use the 'help' command. |
1346 option_parser.print_help() | 1290 option_parser.print_help() |
1347 return 1 | 1291 return 1 |
1348 # Add manual support for --version as first argument. | 1292 # Add manual support for --version as first argument. |
1349 if argv[1] == '--version': | 1293 if argv[1] == '--version': |
1350 option_parser.print_version() | 1294 option_parser.print_version() |
1351 return 0 | 1295 return 0 |
1352 | 1296 |
(...skipping 24 matching lines...) Expand all Loading... |
1377 | 1321 |
1378 if "__main__" == __name__: | 1322 if "__main__" == __name__: |
1379 try: | 1323 try: |
1380 result = Main(sys.argv) | 1324 result = Main(sys.argv) |
1381 except gclient_utils.Error, e: | 1325 except gclient_utils.Error, e: |
1382 print >> sys.stderr, "Error: %s" % str(e) | 1326 print >> sys.stderr, "Error: %s" % str(e) |
1383 result = 1 | 1327 result = 1 |
1384 sys.exit(result) | 1328 sys.exit(result) |
1385 | 1329 |
1386 # vim: ts=2:sw=2:tw=80:et: | 1330 # vim: ts=2:sw=2:tw=80:et: |
OLD | NEW |