svn.py revision f5ad077741427df305fadba9e6380103902bc266
127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com'''
227442af2040e554a09785085463bfdcecb36ecb8epoger@google.comCopyright 2011 Google Inc.
327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
427442af2040e554a09785085463bfdcecb36ecb8epoger@google.comUse of this source code is governed by a BSD-style license that can be
527442af2040e554a09785085463bfdcecb36ecb8epoger@google.comfound in the LICENSE file.
627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com'''
727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.comimport fnmatch
920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.comimport os
1027442af2040e554a09785085463bfdcecb36ecb8epoger@google.comimport re
1127442af2040e554a09785085463bfdcecb36ecb8epoger@google.comimport subprocess
1227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
1327442af2040e554a09785085463bfdcecb36ecb8epoger@google.comPROPERTY_MIMETYPE = 'svn:mime-type'
1427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
156dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com# Status types for GetFilesWithStatus()
166dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_ADDED                 = 0x01
176dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_DELETED               = 0x02
186dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_MODIFIED              = 0x04
196dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_NOT_UNDER_SVN_CONTROL = 0x08
206dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com
2127442af2040e554a09785085463bfdcecb36ecb8epoger@google.comclass Svn:
2227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
2327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def __init__(self, directory):
2427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Set up to manipulate SVN control within the given directory.
2527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
2627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param directory
2727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
2827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        self._directory = directory
2927442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
3027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def _RunCommand(self, args):
3127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Run a command (from self._directory) and return stdout as a single
3227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        string.
3327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
3427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param args a list of arguments
3527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
3620ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        print 'RunCommand: %s' % args
3727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        proc = subprocess.Popen(args, cwd=self._directory,
3820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
3920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        (stdout, stderr) = proc.communicate()
4020ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        if proc.returncode is not 0:
4120ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com            raise Exception('command "%s" failed in dir "%s": %s' %
4220ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com                            (args, self._directory, stderr))
4327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        return stdout
4427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
4520ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com    def Checkout(self, url, path):
4620ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """Check out a working copy from a repository.
4720ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        Returns stdout as a single string.
4820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
4920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param url URL from which to check out the working copy
5020ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param path path (within self._directory) where the local copy will be
5120ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        written
5220ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """
5320ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        return self._RunCommand(['svn', 'checkout', url, path])
5420ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
55f5ad077741427df305fadba9e6380103902bc266epoger@google.com    def ListSubdirs(self, url):
56f5ad077741427df305fadba9e6380103902bc266epoger@google.com        """Returns a list of all subdirectories (not files) within a given SVN
57f5ad077741427df305fadba9e6380103902bc266epoger@google.com        url.
58f5ad077741427df305fadba9e6380103902bc266epoger@google.com
59f5ad077741427df305fadba9e6380103902bc266epoger@google.com        @param url remote directory to list subdirectories of
60f5ad077741427df305fadba9e6380103902bc266epoger@google.com        """
61f5ad077741427df305fadba9e6380103902bc266epoger@google.com        subdirs = []
62f5ad077741427df305fadba9e6380103902bc266epoger@google.com        filenames = self._RunCommand(['svn', 'ls', url]).split('\n')
63f5ad077741427df305fadba9e6380103902bc266epoger@google.com        for filename in filenames:
64f5ad077741427df305fadba9e6380103902bc266epoger@google.com            if filename.endswith('/'):
65f5ad077741427df305fadba9e6380103902bc266epoger@google.com                subdirs.append(filename.strip('/'))
66f5ad077741427df305fadba9e6380103902bc266epoger@google.com        return subdirs
67f5ad077741427df305fadba9e6380103902bc266epoger@google.com
6827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def GetNewFiles(self):
6927442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Return a list of files which are in this directory but NOT under
7027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        SVN control.
7127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
726dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        return self.GetFilesWithStatus(STATUS_NOT_UNDER_SVN_CONTROL)
73d6256557cd99070e92693ebc6847d456d0579494epoger@google.com
74d6256557cd99070e92693ebc6847d456d0579494epoger@google.com    def GetNewAndModifiedFiles(self):
75d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        """Return a list of files in this dir which are newly added or modified,
76d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        including those that are not (yet) under SVN control.
77d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        """
786dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        return self.GetFilesWithStatus(
796dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            STATUS_ADDED | STATUS_MODIFIED | STATUS_NOT_UNDER_SVN_CONTROL)
806dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com
816dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com    def GetFilesWithStatus(self, status):
826dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        """Return a list of files in this dir with the given SVN status.
8327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
846dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        @param status bitfield combining one or more STATUS_xxx values
852e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """
866dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_types_string = ''
876dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_ADDED:
886dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'A'
896dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_DELETED:
906dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'D'
916dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_MODIFIED:
926dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'M'
936dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_NOT_UNDER_SVN_CONTROL:
946dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += '\?'
956dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_regex_string = '^[%s].....\s+(.+)$' % status_types_string
962e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        stdout = self._RunCommand(['svn', 'status'])
976dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_regex = re.compile(status_regex_string, re.MULTILINE)
986dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        files = status_regex.findall(stdout)
992e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        return files
1002e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
10127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def AddFiles(self, filenames):
10227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Adds these files to SVN control.
10327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
10427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param filenames files to add to SVN control
10527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
10620ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        self._RunCommand(['svn', 'add'] + filenames)
10727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
10827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def SetProperty(self, filenames, property_name, property_value):
10927442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Sets a svn property for these files.
11027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
11127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param filenames files to set property on
11227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param property_name property_name to set for each file
11327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param property_value what to set the property_name to
11427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
11520ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        if filenames:
11620ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com            self._RunCommand(
11720ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com                ['svn', 'propset', property_name, property_value] + filenames)
11820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
11920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com    def SetPropertyByFilenamePattern(self, filename_pattern,
12020ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com                                     property_name, property_value):
12120ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """Sets a svn property for all files matching filename_pattern.
12220ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
12320ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param filename_pattern set the property for all files whose names match
12420ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com               this Unix-style filename pattern (e.g., '*.jpg')
12520ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param property_name property_name to set for each file
12620ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param property_value what to set the property_name to
12720ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """
12820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        all_files = os.listdir(self._directory)
1290f645b68a37ea0d494f578d1ff779f4a3ea6423aepoger@google.com        matching_files = sorted(fnmatch.filter(all_files, filename_pattern))
13020ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        self.SetProperty(matching_files, property_name, property_value)
1312e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
1322e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com    def ExportBaseVersionOfFile(self, file_within_repo, dest_path):
1332e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """Retrieves a copy of the base version (what you would get if you ran
1342e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        'svn revert') of a file within the repository.
1352e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
1362e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        @param file_within_repo path to the file within the repo whose base
1372e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com               version you wish to obtain
1382e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        @param dest_path destination to which to write the base content
1392e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """
1402e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        self._RunCommand(['svn', 'export', '--revision', 'BASE',
1412e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com                          file_within_repo, dest_path])
142