OLD | NEW |
1 # Copyright 2009 Google Inc. All Rights Reserved. | 1 # Copyright 2009 Google Inc. All Rights Reserved. |
2 # | 2 # |
3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
4 # you may not use this file except in compliance with the License. | 4 # you may not use this file except in compliance with the License. |
5 # You may obtain a copy of the License at | 5 # You may obtain a copy of the License at |
6 # | 6 # |
7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
8 # | 8 # |
9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
13 # limitations under the License. | 13 # limitations under the License. |
14 | 14 |
| 15 """Gclient-specific SCM-specific operations.""" |
15 | 16 |
16 import logging | 17 import logging |
17 import os | 18 import os |
18 import re | 19 import re |
19 import subprocess | 20 import subprocess |
20 import sys | 21 import sys |
21 import xml.dom.minidom | 22 import xml.dom.minidom |
22 | 23 |
23 import gclient_utils | 24 import gclient_utils |
24 | 25 # TODO(maruel): Temporary. |
25 SVN_COMMAND = "svn" | 26 from scm import CaptureGit, CaptureGitStatus, CaptureSVN, |
26 GIT_COMMAND = "git" | 27 from scm import CaptureSVNHeadRevision, CaptureSVNInfo, CaptureSVNStatus, |
| 28 from scm import RunSVN, RunSVNAndFilterOutput, RunSVNAndGetFileList |
27 | 29 |
28 | 30 |
29 ### SCM abstraction layer | 31 ### SCM abstraction layer |
30 | 32 |
31 | 33 |
32 # Factory Method for SCM wrapper creation | 34 # Factory Method for SCM wrapper creation |
33 | 35 |
34 def CreateSCM(url=None, root_dir=None, relpath=None, scm_name='svn'): | 36 def CreateSCM(url=None, root_dir=None, relpath=None, scm_name='svn'): |
35 # TODO(maruel): Deduce the SCM from the url. | 37 # TODO(maruel): Deduce the SCM from the url. |
36 scm_map = { | 38 scm_map = { |
(...skipping 442 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
479 self.ReplaceAndPrint(line) | 481 self.ReplaceAndPrint(line) |
480 else: | 482 else: |
481 if (line.startswith(self.original_prefix) or | 483 if (line.startswith(self.original_prefix) or |
482 line.startswith(self.working_prefix)): | 484 line.startswith(self.working_prefix)): |
483 self.ReplaceAndPrint(line) | 485 self.ReplaceAndPrint(line) |
484 else: | 486 else: |
485 print line | 487 print line |
486 | 488 |
487 filterer = DiffFilterer(self.relpath) | 489 filterer = DiffFilterer(self.relpath) |
488 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter) | 490 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter) |
489 | |
490 | |
491 # ----------------------------------------------------------------------------- | |
492 # Git utils: | |
493 | |
494 | |
495 def CaptureGit(args, in_directory=None, print_error=True): | |
496 """Runs git, capturing output sent to stdout as a string. | |
497 | |
498 Args: | |
499 args: A sequence of command line parameters to be passed to git. | |
500 in_directory: The directory where git is to be run. | |
501 | |
502 Returns: | |
503 The output sent to stdout as a string. | |
504 """ | |
505 c = [GIT_COMMAND] | |
506 c.extend(args) | |
507 | |
508 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for | |
509 # the git.exe executable, but shell=True makes subprocess on Linux fail | |
510 # when it's called with a list because it only tries to execute the | |
511 # first string ("git"). | |
512 stderr = None | |
513 if not print_error: | |
514 stderr = subprocess.PIPE | |
515 return subprocess.Popen(c, | |
516 cwd=in_directory, | |
517 shell=sys.platform.startswith('win'), | |
518 stdout=subprocess.PIPE, | |
519 stderr=stderr).communicate()[0] | |
520 | |
521 | |
522 def CaptureGitStatus(files, upstream_branch='origin'): | |
523 """Returns git status. | |
524 | |
525 @files can be a string (one file) or a list of files. | |
526 | |
527 Returns an array of (status, file) tuples.""" | |
528 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch] | |
529 if not files: | |
530 pass | |
531 elif isinstance(files, basestring): | |
532 command.append(files) | |
533 else: | |
534 command.extend(files) | |
535 | |
536 status = CaptureGit(command).rstrip() | |
537 results = [] | |
538 if status: | |
539 for statusline in status.split('\n'): | |
540 m = re.match('^(\w)\t(.+)$', statusline) | |
541 if not m: | |
542 raise Exception("status currently unsupported: %s" % statusline) | |
543 results.append(('%s ' % m.group(1), m.group(2))) | |
544 return results | |
545 | |
546 | |
547 # ----------------------------------------------------------------------------- | |
548 # SVN utils: | |
549 | |
550 | |
551 def RunSVN(args, in_directory): | |
552 """Runs svn, sending output to stdout. | |
553 | |
554 Args: | |
555 args: A sequence of command line parameters to be passed to svn. | |
556 in_directory: The directory where svn is to be run. | |
557 | |
558 Raises: | |
559 Error: An error occurred while running the svn command. | |
560 """ | |
561 c = [SVN_COMMAND] | |
562 c.extend(args) | |
563 | |
564 gclient_utils.SubprocessCall(c, in_directory) | |
565 | |
566 | |
567 def CaptureSVN(args, in_directory=None, print_error=True): | |
568 """Runs svn, capturing output sent to stdout as a string. | |
569 | |
570 Args: | |
571 args: A sequence of command line parameters to be passed to svn. | |
572 in_directory: The directory where svn is to be run. | |
573 | |
574 Returns: | |
575 The output sent to stdout as a string. | |
576 """ | |
577 c = [SVN_COMMAND] | |
578 c.extend(args) | |
579 | |
580 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for | |
581 # the svn.exe executable, but shell=True makes subprocess on Linux fail | |
582 # when it's called with a list because it only tries to execute the | |
583 # first string ("svn"). | |
584 stderr = None | |
585 if not print_error: | |
586 stderr = subprocess.PIPE | |
587 return subprocess.Popen(c, | |
588 cwd=in_directory, | |
589 shell=(sys.platform == 'win32'), | |
590 stdout=subprocess.PIPE, | |
591 stderr=stderr).communicate()[0] | |
592 | |
593 | |
594 def RunSVNAndGetFileList(options, args, in_directory, file_list): | |
595 """Runs svn checkout, update, or status, output to stdout. | |
596 | |
597 The first item in args must be either "checkout", "update", or "status". | |
598 | |
599 svn's stdout is parsed to collect a list of files checked out or updated. | |
600 These files are appended to file_list. svn's stdout is also printed to | |
601 sys.stdout as in RunSVN. | |
602 | |
603 Args: | |
604 options: command line options to gclient | |
605 args: A sequence of command line parameters to be passed to svn. | |
606 in_directory: The directory where svn is to be run. | |
607 | |
608 Raises: | |
609 Error: An error occurred while running the svn command. | |
610 """ | |
611 command = [SVN_COMMAND] | |
612 command.extend(args) | |
613 | |
614 # svn update and svn checkout use the same pattern: the first three columns | |
615 # are for file status, property status, and lock status. This is followed | |
616 # by two spaces, and then the path to the file. | |
617 update_pattern = '^... (.*)$' | |
618 | |
619 # The first three columns of svn status are the same as for svn update and | |
620 # svn checkout. The next three columns indicate addition-with-history, | |
621 # switch, and remote lock status. This is followed by one space, and then | |
622 # the path to the file. | |
623 status_pattern = '^...... (.*)$' | |
624 | |
625 # args[0] must be a supported command. This will blow up if it's something | |
626 # else, which is good. Note that the patterns are only effective when | |
627 # these commands are used in their ordinary forms, the patterns are invalid | |
628 # for "svn status --show-updates", for example. | |
629 pattern = { | |
630 'checkout': update_pattern, | |
631 'status': status_pattern, | |
632 'update': update_pattern, | |
633 }[args[0]] | |
634 | |
635 compiled_pattern = re.compile(pattern) | |
636 | |
637 def CaptureMatchingLines(line): | |
638 match = compiled_pattern.search(line) | |
639 if match: | |
640 file_list.append(match.group(1)) | |
641 | |
642 RunSVNAndFilterOutput(args, | |
643 in_directory, | |
644 options.verbose, | |
645 True, | |
646 CaptureMatchingLines) | |
647 | |
648 def RunSVNAndFilterOutput(args, | |
649 in_directory, | |
650 print_messages, | |
651 print_stdout, | |
652 filter): | |
653 """Runs svn checkout, update, status, or diff, optionally outputting | |
654 to stdout. | |
655 | |
656 The first item in args must be either "checkout", "update", | |
657 "status", or "diff". | |
658 | |
659 svn's stdout is passed line-by-line to the given filter function. If | |
660 print_stdout is true, it is also printed to sys.stdout as in RunSVN. | |
661 | |
662 Args: | |
663 args: A sequence of command line parameters to be passed to svn. | |
664 in_directory: The directory where svn is to be run. | |
665 print_messages: Whether to print status messages to stdout about | |
666 which Subversion commands are being run. | |
667 print_stdout: Whether to forward Subversion's output to stdout. | |
668 filter: A function taking one argument (a string) which will be | |
669 passed each line (with the ending newline character removed) of | |
670 Subversion's output for filtering. | |
671 | |
672 Raises: | |
673 Error: An error occurred while running the svn command. | |
674 """ | |
675 command = [SVN_COMMAND] | |
676 command.extend(args) | |
677 | |
678 gclient_utils.SubprocessCallAndFilter(command, | |
679 in_directory, | |
680 print_messages, | |
681 print_stdout, | |
682 filter=filter) | |
683 | |
684 def CaptureSVNInfo(relpath, in_directory=None, print_error=True): | |
685 """Returns a dictionary from the svn info output for the given file. | |
686 | |
687 Args: | |
688 relpath: The directory where the working copy resides relative to | |
689 the directory given by in_directory. | |
690 in_directory: The directory where svn is to be run. | |
691 """ | |
692 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error) | |
693 dom = gclient_utils.ParseXML(output) | |
694 result = {} | |
695 if dom: | |
696 GetNamedNodeText = gclient_utils.GetNamedNodeText | |
697 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText | |
698 def C(item, f): | |
699 if item is not None: return f(item) | |
700 # /info/entry/ | |
701 # url | |
702 # reposityory/(root|uuid) | |
703 # wc-info/(schedule|depth) | |
704 # commit/(author|date) | |
705 # str() the results because they may be returned as Unicode, which | |
706 # interferes with the higher layers matching up things in the deps | |
707 # dictionary. | |
708 # TODO(maruel): Fix at higher level instead (!) | |
709 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str) | |
710 result['URL'] = C(GetNamedNodeText(dom, 'url'), str) | |
711 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str) | |
712 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'), | |
713 int) | |
714 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'), | |
715 str) | |
716 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str) | |
717 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str) | |
718 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str) | |
719 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str) | |
720 return result | |
721 | |
722 | |
723 def CaptureSVNHeadRevision(url): | |
724 """Get the head revision of a SVN repository. | |
725 | |
726 Returns: | |
727 Int head revision | |
728 """ | |
729 info = CaptureSVN(["info", "--xml", url], os.getcwd()) | |
730 dom = xml.dom.minidom.parseString(info) | |
731 return dom.getElementsByTagName('entry')[0].getAttribute('revision') | |
732 | |
733 | |
734 def CaptureSVNStatus(files): | |
735 """Returns the svn 1.5 svn status emulated output. | |
736 | |
737 @files can be a string (one file) or a list of files. | |
738 | |
739 Returns an array of (status, file) tuples.""" | |
740 command = ["status", "--xml"] | |
741 if not files: | |
742 pass | |
743 elif isinstance(files, basestring): | |
744 command.append(files) | |
745 else: | |
746 command.extend(files) | |
747 | |
748 status_letter = { | |
749 None: ' ', | |
750 '': ' ', | |
751 'added': 'A', | |
752 'conflicted': 'C', | |
753 'deleted': 'D', | |
754 'external': 'X', | |
755 'ignored': 'I', | |
756 'incomplete': '!', | |
757 'merged': 'G', | |
758 'missing': '!', | |
759 'modified': 'M', | |
760 'none': ' ', | |
761 'normal': ' ', | |
762 'obstructed': '~', | |
763 'replaced': 'R', | |
764 'unversioned': '?', | |
765 } | |
766 dom = gclient_utils.ParseXML(CaptureSVN(command)) | |
767 results = [] | |
768 if dom: | |
769 # /status/target/entry/(wc-status|commit|author|date) | |
770 for target in dom.getElementsByTagName('target'): | |
771 for entry in target.getElementsByTagName('entry'): | |
772 file_path = entry.getAttribute('path') | |
773 wc_status = entry.getElementsByTagName('wc-status') | |
774 assert len(wc_status) == 1 | |
775 # Emulate svn 1.5 status ouput... | |
776 statuses = [' '] * 7 | |
777 # Col 0 | |
778 xml_item_status = wc_status[0].getAttribute('item') | |
779 if xml_item_status in status_letter: | |
780 statuses[0] = status_letter[xml_item_status] | |
781 else: | |
782 raise Exception('Unknown item status "%s"; please implement me!' % | |
783 xml_item_status) | |
784 # Col 1 | |
785 xml_props_status = wc_status[0].getAttribute('props') | |
786 if xml_props_status == 'modified': | |
787 statuses[1] = 'M' | |
788 elif xml_props_status == 'conflicted': | |
789 statuses[1] = 'C' | |
790 elif (not xml_props_status or xml_props_status == 'none' or | |
791 xml_props_status == 'normal'): | |
792 pass | |
793 else: | |
794 raise Exception('Unknown props status "%s"; please implement me!' % | |
795 xml_props_status) | |
796 # Col 2 | |
797 if wc_status[0].getAttribute('wc-locked') == 'true': | |
798 statuses[2] = 'L' | |
799 # Col 3 | |
800 if wc_status[0].getAttribute('copied') == 'true': | |
801 statuses[3] = '+' | |
802 # Col 4 | |
803 if wc_status[0].getAttribute('switched') == 'true': | |
804 statuses[4] = 'S' | |
805 # TODO(maruel): Col 5 and 6 | |
806 item = (''.join(statuses), file_path) | |
807 results.append(item) | |
808 return results | |
OLD | NEW |