svn.py revision 6dbf6cde3bb5bd49e74a2a881d816a0572c62ded
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
5527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def GetNewFiles(self):
5627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Return a list of files which are in this directory but NOT under
5727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        SVN control.
5827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
596dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        return self.GetFilesWithStatus(STATUS_NOT_UNDER_SVN_CONTROL)
60d6256557cd99070e92693ebc6847d456d0579494epoger@google.com
61d6256557cd99070e92693ebc6847d456d0579494epoger@google.com    def GetNewAndModifiedFiles(self):
62d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        """Return a list of files in this dir which are newly added or modified,
63d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        including those that are not (yet) under SVN control.
64d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        """
656dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        return self.GetFilesWithStatus(
666dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            STATUS_ADDED | STATUS_MODIFIED | STATUS_NOT_UNDER_SVN_CONTROL)
676dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com
686dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com    def GetFilesWithStatus(self, status):
696dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        """Return a list of files in this dir with the given SVN status.
7027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
716dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        @param status bitfield combining one or more STATUS_xxx values
722e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """
736dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_types_string = ''
746dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_ADDED:
756dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'A'
766dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_DELETED:
776dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'D'
786dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_MODIFIED:
796dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'M'
806dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_NOT_UNDER_SVN_CONTROL:
816dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += '\?'
826dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_regex_string = '^[%s].....\s+(.+)$' % status_types_string
832e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        stdout = self._RunCommand(['svn', 'status'])
846dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_regex = re.compile(status_regex_string, re.MULTILINE)
856dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        files = status_regex.findall(stdout)
862e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        return files
872e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
8827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def AddFiles(self, filenames):
8927442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Adds these files to SVN control.
9027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
9127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param filenames files to add to SVN control
9227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
9320ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        self._RunCommand(['svn', 'add'] + filenames)
9427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
9527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def SetProperty(self, filenames, property_name, property_value):
9627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Sets a svn property for these files.
9727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
9827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param filenames files to set property on
9927442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param property_name property_name to set for each file
10027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param property_value what to set the property_name to
10127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
10220ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        if filenames:
10320ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com            self._RunCommand(
10420ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com                ['svn', 'propset', property_name, property_value] + filenames)
10520ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
10620ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com    def SetPropertyByFilenamePattern(self, filename_pattern,
10720ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com                                     property_name, property_value):
10820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """Sets a svn property for all files matching filename_pattern.
10920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
11020ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param filename_pattern set the property for all files whose names match
11120ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com               this Unix-style filename pattern (e.g., '*.jpg')
11220ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param property_name property_name to set for each file
11320ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param property_value what to set the property_name to
11420ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """
11520ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        all_files = os.listdir(self._directory)
1160f645b68a37ea0d494f578d1ff779f4a3ea6423aepoger@google.com        matching_files = sorted(fnmatch.filter(all_files, filename_pattern))
11720ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        self.SetProperty(matching_files, property_name, property_value)
1182e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
1192e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com    def ExportBaseVersionOfFile(self, file_within_repo, dest_path):
1202e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """Retrieves a copy of the base version (what you would get if you ran
1212e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        'svn revert') of a file within the repository.
1222e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
1232e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        @param file_within_repo path to the file within the repo whose base
1242e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com               version you wish to obtain
1252e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        @param dest_path destination to which to write the base content
1262e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """
1272e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        self._RunCommand(['svn', 'export', '--revision', 'BASE',
1282e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com                          file_within_repo, dest_path])
129