| OLD | NEW |
| 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """SCM-specific utility classes.""" | 5 """SCM-specific utility classes.""" |
| 6 | 6 |
| 7 import cStringIO | 7 import cStringIO |
| 8 import glob | 8 import glob |
| 9 import logging | 9 import logging |
| 10 import os | 10 import os |
| (...skipping 380 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 391 return (False, cls.current_version) | 391 return (False, cls.current_version) |
| 392 elif ver > min_ver: | 392 elif ver > min_ver: |
| 393 return (True, cls.current_version) | 393 return (True, cls.current_version) |
| 394 return (True, cls.current_version) | 394 return (True, cls.current_version) |
| 395 | 395 |
| 396 | 396 |
| 397 class SVN(object): | 397 class SVN(object): |
| 398 current_version = None | 398 current_version = None |
| 399 | 399 |
| 400 @staticmethod | 400 @staticmethod |
| 401 def Capture(args, **kwargs): | 401 def Capture(args, cwd, **kwargs): |
| 402 """Always redirect stderr. | 402 """Always redirect stderr. |
| 403 | 403 |
| 404 Throws an exception if non-0 is returned. | 404 Throws an exception if non-0 is returned. |
| 405 """ | 405 """ |
| 406 return subprocess2.check_output( | 406 return subprocess2.check_output( |
| 407 ['svn'] + args, stderr=subprocess2.PIPE, **kwargs) | 407 ['svn'] + args, stderr=subprocess2.PIPE, cwd=cwd, **kwargs) |
| 408 | 408 |
| 409 @staticmethod | 409 @staticmethod |
| 410 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None): | 410 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None): |
| 411 """Runs svn checkout, update, or status, output to stdout. | 411 """Runs svn checkout, update, or status, output to stdout. |
| 412 | 412 |
| 413 The first item in args must be either "checkout", "update", or "status". | 413 The first item in args must be either "checkout", "update", or "status". |
| 414 | 414 |
| 415 svn's stdout is parsed to collect a list of files checked out or updated. | 415 svn's stdout is parsed to collect a list of files checked out or updated. |
| 416 These files are appended to file_list. svn's stdout is also printed to | 416 These files are appended to file_list. svn's stdout is also printed to |
| 417 sys.stdout as in Run. | 417 sys.stdout as in Run. |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 507 raise | 507 raise |
| 508 if retries == 10: | 508 if retries == 10: |
| 509 raise | 509 raise |
| 510 print "Sleeping %.1f seconds and retrying...." % backoff_time | 510 print "Sleeping %.1f seconds and retrying...." % backoff_time |
| 511 time.sleep(backoff_time) | 511 time.sleep(backoff_time) |
| 512 backoff_time *= 1.3 | 512 backoff_time *= 1.3 |
| 513 continue | 513 continue |
| 514 break | 514 break |
| 515 | 515 |
| 516 @staticmethod | 516 @staticmethod |
| 517 def CaptureInfo(cwd): | 517 def CaptureRemoteInfo(url): |
| 518 """Returns a dictionary from the svn info output for the given url. |
| 519 |
| 520 Throws an exception if svn info fails. |
| 521 """ |
| 522 assert isinstance(url, str) |
| 523 return SVN._CaptureInfo([url], None) |
| 524 |
| 525 @staticmethod |
| 526 def CaptureLocalInfo(files, cwd): |
| 527 """Returns a dictionary from the svn info output for the given files. |
| 528 |
| 529 Throws an exception if svn info fails. |
| 530 """ |
| 531 assert isinstance(files, (list, tuple)) |
| 532 return SVN._CaptureInfo(files, cwd) |
| 533 |
| 534 @staticmethod |
| 535 def _CaptureInfo(files, cwd): |
| 518 """Returns a dictionary from the svn info output for the given file. | 536 """Returns a dictionary from the svn info output for the given file. |
| 519 | 537 |
| 520 Throws an exception if svn info fails.""" | 538 Throws an exception if svn info fails.""" |
| 521 result = {} | 539 result = {} |
| 522 output = SVN.Capture(['info', '--xml', cwd]) | 540 info = ElementTree.XML(SVN.Capture(['info', '--xml'] + files, cwd)) |
| 523 info = ElementTree.XML(output) | |
| 524 if info is None: | 541 if info is None: |
| 525 return result | 542 return result |
| 526 entry = info.find('entry') | 543 entry = info.find('entry') |
| 527 if entry is None: | 544 if entry is None: |
| 528 return result | 545 return result |
| 529 | 546 |
| 530 # Use .text when the item is not optional. | 547 # Use .text when the item is not optional. |
| 531 result['Path'] = entry.attrib['path'] | 548 result['Path'] = entry.attrib['path'] |
| 532 result['Revision'] = int(entry.attrib['revision']) | 549 result['Revision'] = int(entry.attrib['revision']) |
| 533 result['Node Kind'] = entry.attrib['kind'] | 550 result['Node Kind'] = entry.attrib['kind'] |
| (...skipping 22 matching lines...) Expand all Loading... |
| 556 result[key] = getattr(result[key], 'text', result[key]) | 573 result[key] = getattr(result[key], 'text', result[key]) |
| 557 return result | 574 return result |
| 558 | 575 |
| 559 @staticmethod | 576 @staticmethod |
| 560 def CaptureRevision(cwd): | 577 def CaptureRevision(cwd): |
| 561 """Get the base revision of a SVN repository. | 578 """Get the base revision of a SVN repository. |
| 562 | 579 |
| 563 Returns: | 580 Returns: |
| 564 Int base revision | 581 Int base revision |
| 565 """ | 582 """ |
| 566 return SVN.CaptureInfo(cwd).get('Revision') | 583 return SVN.CaptureLocalInfo([], cwd).get('Revision') |
| 567 | 584 |
| 568 @staticmethod | 585 @staticmethod |
| 569 def CaptureStatus(files): | 586 def CaptureStatus(files, cwd): |
| 570 """Returns the svn 1.5 svn status emulated output. | 587 """Returns the svn 1.5 svn status emulated output. |
| 571 | 588 |
| 572 @files can be a string (one file) or a list of files. | 589 @files can be a string (one file) or a list of files. |
| 573 | 590 |
| 574 Returns an array of (status, file) tuples.""" | 591 Returns an array of (status, file) tuples.""" |
| 575 command = ["status", "--xml"] | 592 command = ["status", "--xml"] |
| 576 if not files: | 593 if not files: |
| 577 pass | 594 pass |
| 578 elif isinstance(files, basestring): | 595 elif isinstance(files, basestring): |
| 579 command.append(files) | 596 command.append(files) |
| (...skipping 11 matching lines...) Expand all Loading... |
| 591 'incomplete': '!', | 608 'incomplete': '!', |
| 592 'merged': 'G', | 609 'merged': 'G', |
| 593 'missing': '!', | 610 'missing': '!', |
| 594 'modified': 'M', | 611 'modified': 'M', |
| 595 'none': ' ', | 612 'none': ' ', |
| 596 'normal': ' ', | 613 'normal': ' ', |
| 597 'obstructed': '~', | 614 'obstructed': '~', |
| 598 'replaced': 'R', | 615 'replaced': 'R', |
| 599 'unversioned': '?', | 616 'unversioned': '?', |
| 600 } | 617 } |
| 601 dom = ElementTree.XML(SVN.Capture(command)) | 618 dom = ElementTree.XML(SVN.Capture(command, cwd)) |
| 602 results = [] | 619 results = [] |
| 603 if dom is None: | 620 if dom is None: |
| 604 return results | 621 return results |
| 605 # /status/target/entry/(wc-status|commit|author|date) | 622 # /status/target/entry/(wc-status|commit|author|date) |
| 606 for target in dom.findall('target'): | 623 for target in dom.findall('target'): |
| 607 for entry in target.findall('entry'): | 624 for entry in target.findall('entry'): |
| 608 file_path = entry.attrib['path'] | 625 file_path = entry.attrib['path'] |
| 609 wc_status = entry.find('wc-status') | 626 wc_status = entry.find('wc-status') |
| 610 # Emulate svn 1.5 status ouput... | 627 # Emulate svn 1.5 status ouput... |
| 611 statuses = [' '] * 7 | 628 statuses = [' '] * 7 |
| (...skipping 26 matching lines...) Expand all Loading... |
| 638 statuses[3] = '+' | 655 statuses[3] = '+' |
| 639 # Col 4 | 656 # Col 4 |
| 640 if wc_status.attrib.get('switched') == 'true': | 657 if wc_status.attrib.get('switched') == 'true': |
| 641 statuses[4] = 'S' | 658 statuses[4] = 'S' |
| 642 # TODO(maruel): Col 5 and 6 | 659 # TODO(maruel): Col 5 and 6 |
| 643 item = (''.join(statuses), file_path) | 660 item = (''.join(statuses), file_path) |
| 644 results.append(item) | 661 results.append(item) |
| 645 return results | 662 return results |
| 646 | 663 |
| 647 @staticmethod | 664 @staticmethod |
| 648 def IsMoved(filename): | 665 def IsMoved(filename, cwd): |
| 649 """Determine if a file has been added through svn mv""" | 666 """Determine if a file has been added through svn mv""" |
| 650 return SVN.IsMovedInfo(SVN.CaptureInfo(filename)) | 667 assert isinstance(filename, basestring) |
| 668 return SVN.IsMovedInfo(SVN.CaptureLocalInfo([filename], cwd)) |
| 651 | 669 |
| 652 @staticmethod | 670 @staticmethod |
| 653 def IsMovedInfo(info): | 671 def IsMovedInfo(info): |
| 654 """Determine if a file has been added through svn mv""" | 672 """Determine if a file has been added through svn mv""" |
| 655 return (info.get('Copied From URL') and | 673 return (info.get('Copied From URL') and |
| 656 info.get('Copied From Rev') and | 674 info.get('Copied From Rev') and |
| 657 info.get('Schedule') == 'add') | 675 info.get('Schedule') == 'add') |
| 658 | 676 |
| 659 @staticmethod | 677 @staticmethod |
| 660 def GetFileProperty(filename, property_name): | 678 def GetFileProperty(filename, property_name, cwd): |
| 661 """Returns the value of an SVN property for the given file. | 679 """Returns the value of an SVN property for the given file. |
| 662 | 680 |
| 663 Args: | 681 Args: |
| 664 filename: The file to check | 682 filename: The file to check |
| 665 property_name: The name of the SVN property, e.g. "svn:mime-type" | 683 property_name: The name of the SVN property, e.g. "svn:mime-type" |
| 666 | 684 |
| 667 Returns: | 685 Returns: |
| 668 The value of the property, which will be the empty string if the property | 686 The value of the property, which will be the empty string if the property |
| 669 is not set on the file. If the file is not under version control, the | 687 is not set on the file. If the file is not under version control, the |
| 670 empty string is also returned. | 688 empty string is also returned. |
| 671 """ | 689 """ |
| 672 try: | 690 try: |
| 673 return SVN.Capture(['propget', property_name, filename]) | 691 return SVN.Capture(['propget', property_name, filename], cwd) |
| 674 except subprocess2.CalledProcessError: | 692 except subprocess2.CalledProcessError: |
| 675 return '' | 693 return '' |
| 676 | 694 |
| 677 @staticmethod | 695 @staticmethod |
| 678 def DiffItem(filename, full_move=False, revision=None): | 696 def DiffItem(filename, cwd, full_move, revision): |
| 679 """Diffs a single file. | 697 """Diffs a single file. |
| 680 | 698 |
| 681 Should be simple, eh? No it isn't. | 699 Should be simple, eh? No it isn't. |
| 682 Be sure to be in the appropriate directory before calling to have the | 700 Be sure to be in the appropriate directory before calling to have the |
| 683 expected relative path. | 701 expected relative path. |
| 684 full_move means that move or copy operations should completely recreate the | 702 full_move means that move or copy operations should completely recreate the |
| 685 files, usually in the prospect to apply the patch for a try job.""" | 703 files, usually in the prospect to apply the patch for a try job.""" |
| 686 # If the user specified a custom diff command in their svn config file, | 704 # If the user specified a custom diff command in their svn config file, |
| 687 # then it'll be used when we do svn diff, which we don't want to happen | 705 # then it'll be used when we do svn diff, which we don't want to happen |
| 688 # since we want the unified diff. Using --diff-cmd=diff doesn't always | 706 # since we want the unified diff. Using --diff-cmd=diff doesn't always |
| 689 # work, since they can have another diff executable in their path that | 707 # work, since they can have another diff executable in their path that |
| 690 # gives different line endings. So we use a bogus temp directory as the | 708 # gives different line endings. So we use a bogus temp directory as the |
| 691 # config directory, which gets around these problems. | 709 # config directory, which gets around these problems. |
| 692 bogus_dir = tempfile.mkdtemp() | 710 bogus_dir = tempfile.mkdtemp() |
| 693 try: | 711 try: |
| 694 # Use "svn info" output instead of os.path.isdir because the latter fails | 712 # Use "svn info" output instead of os.path.isdir because the latter fails |
| 695 # when the file is deleted. | 713 # when the file is deleted. |
| 696 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename), | 714 return SVN._DiffItemInternal( |
| 697 bogus_dir, | 715 filename, |
| 698 full_move=full_move, revision=revision) | 716 cwd, |
| 717 SVN.CaptureLocalInfo([filename], cwd), |
| 718 bogus_dir, |
| 719 full_move, |
| 720 revision) |
| 699 finally: | 721 finally: |
| 700 gclient_utils.RemoveDirectory(bogus_dir) | 722 gclient_utils.RemoveDirectory(bogus_dir) |
| 701 | 723 |
| 702 @staticmethod | 724 @staticmethod |
| 703 def _DiffItemInternal(filename, info, bogus_dir, full_move=False, | 725 def _DiffItemInternal(filename, cwd, info, bogus_dir, full_move, revision): |
| 704 revision=None): | |
| 705 """Grabs the diff data.""" | 726 """Grabs the diff data.""" |
| 706 command = ["diff", "--config-dir", bogus_dir, filename] | 727 command = ["diff", "--config-dir", bogus_dir, filename] |
| 707 if revision: | 728 if revision: |
| 708 command.extend(['--revision', revision]) | 729 command.extend(['--revision', revision]) |
| 709 data = None | 730 data = None |
| 710 if SVN.IsMovedInfo(info): | 731 if SVN.IsMovedInfo(info): |
| 711 if full_move: | 732 if full_move: |
| 712 if info.get("Node Kind") == "directory": | 733 if info.get("Node Kind") == "directory": |
| 713 # Things become tricky here. It's a directory copy/move. We need to | 734 # Things become tricky here. It's a directory copy/move. We need to |
| 714 # diff all the files inside it. | 735 # diff all the files inside it. |
| (...skipping 15 matching lines...) Expand all Loading... |
| 730 data.write(GenFakeDiff(os.path.join(dirpath, f))) | 751 data.write(GenFakeDiff(os.path.join(dirpath, f))) |
| 731 if data: | 752 if data: |
| 732 tmp = data.getvalue() | 753 tmp = data.getvalue() |
| 733 data.close() | 754 data.close() |
| 734 data = tmp | 755 data = tmp |
| 735 else: | 756 else: |
| 736 data = GenFakeDiff(filename) | 757 data = GenFakeDiff(filename) |
| 737 else: | 758 else: |
| 738 if info.get("Node Kind") != "directory": | 759 if info.get("Node Kind") != "directory": |
| 739 # svn diff on a mv/cp'd file outputs nothing if there was no change. | 760 # svn diff on a mv/cp'd file outputs nothing if there was no change. |
| 740 data = SVN.Capture(command) | 761 data = SVN.Capture(command, cwd) |
| 741 if not data: | 762 if not data: |
| 742 # We put in an empty Index entry so upload.py knows about them. | 763 # We put in an empty Index entry so upload.py knows about them. |
| 743 data = "Index: %s\n" % filename.replace(os.sep, '/') | 764 data = "Index: %s\n" % filename.replace(os.sep, '/') |
| 744 # Otherwise silently ignore directories. | 765 # Otherwise silently ignore directories. |
| 745 else: | 766 else: |
| 746 if info.get("Node Kind") != "directory": | 767 if info.get("Node Kind") != "directory": |
| 747 # Normal simple case. | 768 # Normal simple case. |
| 748 try: | 769 try: |
| 749 data = SVN.Capture(command) | 770 data = SVN.Capture(command, cwd) |
| 750 except subprocess2.CalledProcessError: | 771 except subprocess2.CalledProcessError: |
| 751 if revision: | 772 if revision: |
| 752 data = GenFakeDiff(filename) | 773 data = GenFakeDiff(filename) |
| 753 else: | 774 else: |
| 754 raise | 775 raise |
| 755 # Otherwise silently ignore directories. | 776 # Otherwise silently ignore directories. |
| 756 return data | 777 return data |
| 757 | 778 |
| 758 @staticmethod | 779 @staticmethod |
| 759 def GenerateDiff(filenames, root=None, full_move=False, revision=None): | 780 def GenerateDiff(filenames, cwd, full_move, revision): |
| 760 """Returns a string containing the diff for the given file list. | 781 """Returns a string containing the diff for the given file list. |
| 761 | 782 |
| 762 The files in the list should either be absolute paths or relative to the | 783 The files in the list should either be absolute paths or relative to the |
| 763 given root. If no root directory is provided, the repository root will be | 784 given root. If no root directory is provided, the repository root will be |
| 764 used. | 785 used. |
| 765 The diff will always use relative paths. | 786 The diff will always use relative paths. |
| 766 """ | 787 """ |
| 767 assert isinstance(filenames, (list, tuple)) | 788 assert isinstance(filenames, (list, tuple)) |
| 768 previous_cwd = os.getcwd() | 789 root = os.path.normcase(os.path.join(cwd, '')) |
| 769 root = root or SVN.GetCheckoutRoot(previous_cwd) | |
| 770 root = os.path.normcase(os.path.join(root, '')) | |
| 771 def RelativePath(path, root): | 790 def RelativePath(path, root): |
| 772 """We must use relative paths.""" | 791 """We must use relative paths.""" |
| 773 if os.path.normcase(path).startswith(root): | 792 if os.path.normcase(path).startswith(root): |
| 774 return path[len(root):] | 793 return path[len(root):] |
| 775 return path | 794 return path |
| 776 # If the user specified a custom diff command in their svn config file, | 795 # If the user specified a custom diff command in their svn config file, |
| 777 # then it'll be used when we do svn diff, which we don't want to happen | 796 # then it'll be used when we do svn diff, which we don't want to happen |
| 778 # since we want the unified diff. Using --diff-cmd=diff doesn't always | 797 # since we want the unified diff. Using --diff-cmd=diff doesn't always |
| 779 # work, since they can have another diff executable in their path that | 798 # work, since they can have another diff executable in their path that |
| 780 # gives different line endings. So we use a bogus temp directory as the | 799 # gives different line endings. So we use a bogus temp directory as the |
| 781 # config directory, which gets around these problems. | 800 # config directory, which gets around these problems. |
| 782 bogus_dir = tempfile.mkdtemp() | 801 bogus_dir = tempfile.mkdtemp() |
| 783 try: | 802 try: |
| 784 os.chdir(root) | |
| 785 # Cleanup filenames | 803 # Cleanup filenames |
| 786 filenames = [RelativePath(f, root) for f in filenames] | 804 filenames = [RelativePath(f, root) for f in filenames] |
| 787 # Get information about the modified items (files and directories) | 805 # Get information about the modified items (files and directories) |
| 788 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames]) | 806 data = dict([(f, SVN.CaptureLocalInfo([f], root)) for f in filenames]) |
| 789 diffs = [] | 807 diffs = [] |
| 790 if full_move: | 808 if full_move: |
| 791 # Eliminate modified files inside moved/copied directory. | 809 # Eliminate modified files inside moved/copied directory. |
| 792 for (filename, info) in data.iteritems(): | 810 for (filename, info) in data.iteritems(): |
| 793 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory": | 811 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory": |
| 794 # Remove files inside the directory. | 812 # Remove files inside the directory. |
| 795 filenames = [f for f in filenames | 813 filenames = [f for f in filenames |
| 796 if not f.startswith(filename + os.path.sep)] | 814 if not f.startswith(filename + os.path.sep)] |
| 797 for filename in data.keys(): | 815 for filename in data.keys(): |
| 798 if not filename in filenames: | 816 if not filename in filenames: |
| 799 # Remove filtered out items. | 817 # Remove filtered out items. |
| 800 del data[filename] | 818 del data[filename] |
| 801 else: | 819 else: |
| 802 metaheaders = [] | 820 metaheaders = [] |
| 803 for (filename, info) in data.iteritems(): | 821 for (filename, info) in data.iteritems(): |
| 804 if SVN.IsMovedInfo(info): | 822 if SVN.IsMovedInfo(info): |
| 805 # for now, the most common case is a head copy, | 823 # for now, the most common case is a head copy, |
| 806 # so let's just encode that as a straight up cp. | 824 # so let's just encode that as a straight up cp. |
| 807 srcurl = info.get('Copied From URL') | 825 srcurl = info.get('Copied From URL') |
| 808 root = info.get('Repository Root') | 826 file_root = info.get('Repository Root') |
| 809 rev = int(info.get('Copied From Rev')) | 827 rev = int(info.get('Copied From Rev')) |
| 810 assert srcurl.startswith(root) | 828 assert srcurl.startswith(file_root) |
| 811 src = srcurl[len(root)+1:] | 829 src = srcurl[len(file_root)+1:] |
| 812 try: | 830 try: |
| 813 srcinfo = SVN.CaptureInfo(srcurl) | 831 srcinfo = SVN.CaptureRemoteInfo(srcurl) |
| 814 except subprocess2.CalledProcessError, e: | 832 except subprocess2.CalledProcessError, e: |
| 815 if not 'Not a valid URL' in e.stderr: | 833 if not 'Not a valid URL' in e.stderr: |
| 816 raise | 834 raise |
| 817 # Assume the file was deleted. No idea how to figure out at which | 835 # Assume the file was deleted. No idea how to figure out at which |
| 818 # revision the file was deleted. | 836 # revision the file was deleted. |
| 819 srcinfo = {'Revision': rev} | 837 srcinfo = {'Revision': rev} |
| 820 if (srcinfo.get('Revision') != rev and | 838 if (srcinfo.get('Revision') != rev and |
| 821 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])): | 839 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl], cwd)): |
| 822 metaheaders.append("#$ svn cp -r %d %s %s " | 840 metaheaders.append("#$ svn cp -r %d %s %s " |
| 823 "### WARNING: note non-trunk copy\n" % | 841 "### WARNING: note non-trunk copy\n" % |
| 824 (rev, src, filename)) | 842 (rev, src, filename)) |
| 825 else: | 843 else: |
| 826 metaheaders.append("#$ cp %s %s\n" % (src, | 844 metaheaders.append("#$ cp %s %s\n" % (src, |
| 827 filename)) | 845 filename)) |
| 828 | 846 |
| 829 if metaheaders: | 847 if metaheaders: |
| 830 diffs.append("### BEGIN SVN COPY METADATA\n") | 848 diffs.append("### BEGIN SVN COPY METADATA\n") |
| 831 diffs.extend(metaheaders) | 849 diffs.extend(metaheaders) |
| 832 diffs.append("### END SVN COPY METADATA\n") | 850 diffs.append("### END SVN COPY METADATA\n") |
| 833 # Now ready to do the actual diff. | 851 # Now ready to do the actual diff. |
| 834 for filename in sorted(data.iterkeys()): | 852 for filename in sorted(data.iterkeys()): |
| 835 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir, | 853 diffs.append(SVN._DiffItemInternal( |
| 836 full_move=full_move, | 854 filename, cwd, data[filename], bogus_dir, full_move, revision)) |
| 837 revision=revision)) | |
| 838 # Use StringIO since it can be messy when diffing a directory move with | 855 # Use StringIO since it can be messy when diffing a directory move with |
| 839 # full_move=True. | 856 # full_move=True. |
| 840 buf = cStringIO.StringIO() | 857 buf = cStringIO.StringIO() |
| 841 for d in filter(None, diffs): | 858 for d in filter(None, diffs): |
| 842 buf.write(d) | 859 buf.write(d) |
| 843 result = buf.getvalue() | 860 result = buf.getvalue() |
| 844 buf.close() | 861 buf.close() |
| 845 return result | 862 return result |
| 846 finally: | 863 finally: |
| 847 os.chdir(previous_cwd) | |
| 848 gclient_utils.RemoveDirectory(bogus_dir) | 864 gclient_utils.RemoveDirectory(bogus_dir) |
| 849 | 865 |
| 850 @staticmethod | 866 @staticmethod |
| 851 def GetEmail(repo_root): | 867 def GetEmail(cwd): |
| 852 """Retrieves the svn account which we assume is an email address.""" | 868 """Retrieves the svn account which we assume is an email address.""" |
| 853 try: | 869 try: |
| 854 infos = SVN.CaptureInfo(repo_root) | 870 infos = SVN.CaptureLocalInfo([], cwd) |
| 855 except subprocess2.CalledProcessError: | 871 except subprocess2.CalledProcessError: |
| 856 return None | 872 return None |
| 857 | 873 |
| 858 # Should check for uuid but it is incorrectly saved for https creds. | 874 # Should check for uuid but it is incorrectly saved for https creds. |
| 859 root = infos['Repository Root'] | 875 root = infos['Repository Root'] |
| 860 realm = root.rsplit('/', 1)[0] | 876 realm = root.rsplit('/', 1)[0] |
| 861 uuid = infos['UUID'] | 877 uuid = infos['UUID'] |
| 862 if root.startswith('https') or not uuid: | 878 if root.startswith('https') or not uuid: |
| 863 regexp = re.compile(r'<%s:\d+>.*' % realm) | 879 regexp = re.compile(r'<%s:\d+>.*' % realm) |
| 864 else: | 880 else: |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 897 key = ReadOneItem('K') | 913 key = ReadOneItem('K') |
| 898 if not key: | 914 if not key: |
| 899 break | 915 break |
| 900 value = ReadOneItem('V') | 916 value = ReadOneItem('V') |
| 901 if not value: | 917 if not value: |
| 902 break | 918 break |
| 903 values[key] = value | 919 values[key] = value |
| 904 return values | 920 return values |
| 905 | 921 |
| 906 @staticmethod | 922 @staticmethod |
| 907 def GetCheckoutRoot(directory): | 923 def GetCheckoutRoot(cwd): |
| 908 """Returns the top level directory of the current repository. | 924 """Returns the top level directory of the current repository. |
| 909 | 925 |
| 910 The directory is returned as an absolute path. | 926 The directory is returned as an absolute path. |
| 911 """ | 927 """ |
| 912 directory = os.path.abspath(directory) | 928 cwd = os.path.abspath(cwd) |
| 913 try: | 929 try: |
| 914 info = SVN.CaptureInfo(directory) | 930 info = SVN.CaptureLocalInfo([], cwd) |
| 915 cur_dir_repo_root = info['Repository Root'] | 931 cur_dir_repo_root = info['Repository Root'] |
| 916 url = info['URL'] | 932 url = info['URL'] |
| 917 except subprocess2.CalledProcessError: | 933 except subprocess2.CalledProcessError: |
| 918 return None | 934 return None |
| 919 while True: | 935 while True: |
| 920 parent = os.path.dirname(directory) | 936 parent = os.path.dirname(cwd) |
| 921 try: | 937 try: |
| 922 info = SVN.CaptureInfo(parent) | 938 info = SVN.CaptureLocalInfo([], parent) |
| 923 if (info['Repository Root'] != cur_dir_repo_root or | 939 if (info['Repository Root'] != cur_dir_repo_root or |
| 924 info['URL'] != os.path.dirname(url)): | 940 info['URL'] != os.path.dirname(url)): |
| 925 break | 941 break |
| 926 url = info['URL'] | 942 url = info['URL'] |
| 927 except subprocess2.CalledProcessError: | 943 except subprocess2.CalledProcessError: |
| 928 break | 944 break |
| 929 directory = parent | 945 cwd = parent |
| 930 return GetCasedPath(directory) | 946 return GetCasedPath(cwd) |
| 931 | 947 |
| 932 @classmethod | 948 @classmethod |
| 933 def AssertVersion(cls, min_version): | 949 def AssertVersion(cls, min_version): |
| 934 """Asserts svn's version is at least min_version.""" | 950 """Asserts svn's version is at least min_version.""" |
| 935 if cls.current_version is None: | 951 if cls.current_version is None: |
| 936 cls.current_version = cls.Capture(['--version']).split()[2] | 952 cls.current_version = cls.Capture(['--version'], None).split()[2] |
| 937 current_version_list = map(only_int, cls.current_version.split('.')) | 953 current_version_list = map(only_int, cls.current_version.split('.')) |
| 938 for min_ver in map(int, min_version.split('.')): | 954 for min_ver in map(int, min_version.split('.')): |
| 939 ver = current_version_list.pop(0) | 955 ver = current_version_list.pop(0) |
| 940 if ver < min_ver: | 956 if ver < min_ver: |
| 941 return (False, cls.current_version) | 957 return (False, cls.current_version) |
| 942 elif ver > min_ver: | 958 elif ver > min_ver: |
| 943 return (True, cls.current_version) | 959 return (True, cls.current_version) |
| 944 return (True, cls.current_version) | 960 return (True, cls.current_version) |
| 945 | 961 |
| 946 @staticmethod | 962 @staticmethod |
| 947 def Revert(repo_root, callback=None, ignore_externals=False): | 963 def Revert(cwd, callback=None, ignore_externals=False): |
| 948 """Reverts all svn modifications in repo_root, including properties. | 964 """Reverts all svn modifications in cwd, including properties. |
| 949 | 965 |
| 950 Deletes any modified files or directory. | 966 Deletes any modified files or directory. |
| 951 | 967 |
| 952 A "svn update --revision BASE" call is required after to revive deleted | 968 A "svn update --revision BASE" call is required after to revive deleted |
| 953 files. | 969 files. |
| 954 """ | 970 """ |
| 955 for file_status in SVN.CaptureStatus(repo_root): | 971 for file_status in SVN.CaptureStatus(None, cwd): |
| 956 file_path = os.path.join(repo_root, file_status[1]) | 972 file_path = os.path.join(cwd, file_status[1]) |
| 957 if (ignore_externals and | 973 if (ignore_externals and |
| 958 file_status[0][0] == 'X' and | 974 file_status[0][0] == 'X' and |
| 959 file_status[0][1:].isspace()): | 975 file_status[0][1:].isspace()): |
| 960 # Ignore externals. | 976 # Ignore externals. |
| 961 logging.info('Ignoring external %s' % file_status[1]) | 977 logging.info('Ignoring external %s' % file_status[1]) |
| 962 continue | 978 continue |
| 963 | 979 |
| 964 if callback: | 980 if callback: |
| 965 callback(file_status) | 981 callback(file_status) |
| 966 | 982 |
| (...skipping 11 matching lines...) Expand all Loading... |
| 978 gclient_utils.RemoveDirectory(file_path) | 994 gclient_utils.RemoveDirectory(file_path) |
| 979 else: | 995 else: |
| 980 logging.critical( | 996 logging.critical( |
| 981 ('No idea what is %s.\nYou just found a bug in gclient' | 997 ('No idea what is %s.\nYou just found a bug in gclient' |
| 982 ', please ping maruel@chromium.org ASAP!') % file_path) | 998 ', please ping maruel@chromium.org ASAP!') % file_path) |
| 983 | 999 |
| 984 if (file_status[0][0] in ('D', 'A', '!') or | 1000 if (file_status[0][0] in ('D', 'A', '!') or |
| 985 not file_status[0][1:].isspace()): | 1001 not file_status[0][1:].isspace()): |
| 986 # Added, deleted file requires manual intervention and require calling | 1002 # Added, deleted file requires manual intervention and require calling |
| 987 # revert, like for properties. | 1003 # revert, like for properties. |
| 988 if not os.path.isdir(repo_root): | 1004 if not os.path.isdir(cwd): |
| 989 # '.' was deleted. It's not worth continuing. | 1005 # '.' was deleted. It's not worth continuing. |
| 990 return | 1006 return |
| 991 try: | 1007 try: |
| 992 SVN.Capture(['revert', file_status[1]], cwd=repo_root) | 1008 SVN.Capture(['revert', file_status[1]], cwd=cwd) |
| 993 except subprocess2.CalledProcessError: | 1009 except subprocess2.CalledProcessError: |
| 994 if not os.path.exists(file_path): | 1010 if not os.path.exists(file_path): |
| 995 continue | 1011 continue |
| 996 raise | 1012 raise |
| OLD | NEW |