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
12591469b1e93f72172cef13a2f0675699994d7848epoger@google.comimport threading
1327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
1427442af2040e554a09785085463bfdcecb36ecb8epoger@google.comPROPERTY_MIMETYPE = 'svn:mime-type'
1527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
166dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com# Status types for GetFilesWithStatus()
176dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_ADDED                 = 0x01
186dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_DELETED               = 0x02
196dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_MODIFIED              = 0x04
206dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.comSTATUS_NOT_UNDER_SVN_CONTROL = 0x08
216dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com
22a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com
23a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.comif os.name == 'nt':
24a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com  SVN = 'svn.bat'
25a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.comelse:
26a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com  SVN = 'svn'
27a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com
28a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com
29a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.comdef Cat(svn_url):
30a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    """Returns the contents of the file at the given svn_url.
31a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com
32a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    @param svn_url URL of the file to read
33a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    """
34a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    proc = subprocess.Popen([SVN, 'cat', svn_url],
35a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com                            stdout=subprocess.PIPE,
36a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com                            stderr=subprocess.STDOUT)
37a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    exitcode = proc.wait()
38a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    if not exitcode == 0:
39a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        raise Exception('Could not retrieve %s. Verify that the URL is valid '
40a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com                        'and check your connection.' % svn_url)
41a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    return proc.communicate()[0]
42a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com
43a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com
4427442af2040e554a09785085463bfdcecb36ecb8epoger@google.comclass Svn:
4527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
4627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def __init__(self, directory):
4727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Set up to manipulate SVN control within the given directory.
4827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
49591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        The resulting object is thread-safe: access to all methods is
50591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        synchronized (if one thread is currently executing any of its methods,
51591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        all other threads must wait before executing any of its methods).
52591469b1e93f72172cef13a2f0675699994d7848epoger@google.com
5327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param directory
5427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
5527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        self._directory = directory
56591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        # This must be a reentrant lock, so that it can be held by both
57591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        # _RunCommand() and (some of) the methods that call it.
58591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        self._rlock = threading.RLock()
5927442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
6027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def _RunCommand(self, args):
6127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Run a command (from self._directory) and return stdout as a single
6227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        string.
6327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
6427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param args a list of arguments
6527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
66591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        with self._rlock:
67591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            print 'RunCommand: %s' % args
68591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            proc = subprocess.Popen(args, cwd=self._directory,
69591469b1e93f72172cef13a2f0675699994d7848epoger@google.com                                    stdout=subprocess.PIPE,
70591469b1e93f72172cef13a2f0675699994d7848epoger@google.com                                    stderr=subprocess.PIPE)
71591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            (stdout, stderr) = proc.communicate()
72591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            if proc.returncode is not 0:
73591469b1e93f72172cef13a2f0675699994d7848epoger@google.com              raise Exception('command "%s" failed in dir "%s": %s' %
74591469b1e93f72172cef13a2f0675699994d7848epoger@google.com                              (args, self._directory, stderr))
75591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            return stdout
7627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
77a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com    def GetInfo(self):
78a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        """Run "svn info" and return a dictionary containing its output.
79a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        """
80a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        output = self._RunCommand([SVN, 'info'])
81a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        svn_info = {}
82a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        for line in output.split('\n'):
83a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com          if ':' in line:
84a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com            (key, value) = line.split(':', 1)
85a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com            svn_info[key.strip()] = value.strip()
86a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        return svn_info
87a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com
8820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com    def Checkout(self, url, path):
8920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """Check out a working copy from a repository.
9020ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        Returns stdout as a single string.
9120ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
9220ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param url URL from which to check out the working copy
9320ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param path path (within self._directory) where the local copy will be
9420ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        written
9520ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """
96a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        return self._RunCommand([SVN, 'checkout', url, path])
9720ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
985865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org    def Update(self, path, revision='HEAD'):
99f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        """Update the working copy.
100f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        Returns stdout as a single string.
101f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
102f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        @param path path (within self._directory) within which to run
1035865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org          "svn update"
1045865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org        @param revision revision to update to
105f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        """
1065865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org        return self._RunCommand([SVN, 'update', path, '--revision', revision])
107f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
108f5ad077741427df305fadba9e6380103902bc266epoger@google.com    def ListSubdirs(self, url):
109f5ad077741427df305fadba9e6380103902bc266epoger@google.com        """Returns a list of all subdirectories (not files) within a given SVN
110f5ad077741427df305fadba9e6380103902bc266epoger@google.com        url.
111f5ad077741427df305fadba9e6380103902bc266epoger@google.com
112f5ad077741427df305fadba9e6380103902bc266epoger@google.com        @param url remote directory to list subdirectories of
113f5ad077741427df305fadba9e6380103902bc266epoger@google.com        """
114f5ad077741427df305fadba9e6380103902bc266epoger@google.com        subdirs = []
115a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        filenames = self._RunCommand([SVN, 'ls', url]).split('\n')
116f5ad077741427df305fadba9e6380103902bc266epoger@google.com        for filename in filenames:
117f5ad077741427df305fadba9e6380103902bc266epoger@google.com            if filename.endswith('/'):
118f5ad077741427df305fadba9e6380103902bc266epoger@google.com                subdirs.append(filename.strip('/'))
119f5ad077741427df305fadba9e6380103902bc266epoger@google.com        return subdirs
120f5ad077741427df305fadba9e6380103902bc266epoger@google.com
12127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def GetNewFiles(self):
12227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Return a list of files which are in this directory but NOT under
12327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        SVN control.
12427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
1256dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        return self.GetFilesWithStatus(STATUS_NOT_UNDER_SVN_CONTROL)
126d6256557cd99070e92693ebc6847d456d0579494epoger@google.com
127d6256557cd99070e92693ebc6847d456d0579494epoger@google.com    def GetNewAndModifiedFiles(self):
128d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        """Return a list of files in this dir which are newly added or modified,
129d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        including those that are not (yet) under SVN control.
130d6256557cd99070e92693ebc6847d456d0579494epoger@google.com        """
1316dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        return self.GetFilesWithStatus(
1326dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            STATUS_ADDED | STATUS_MODIFIED | STATUS_NOT_UNDER_SVN_CONTROL)
1336dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com
1346dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com    def GetFilesWithStatus(self, status):
1356dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        """Return a list of files in this dir with the given SVN status.
13627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
1376dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        @param status bitfield combining one or more STATUS_xxx values
1382e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """
1396dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_types_string = ''
1406dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_ADDED:
1416dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'A'
1426dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_DELETED:
1436dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'D'
1446dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_MODIFIED:
1456dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += 'M'
1466dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        if status & STATUS_NOT_UNDER_SVN_CONTROL:
1476dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com            status_types_string += '\?'
1486dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_regex_string = '^[%s].....\s+(.+)$' % status_types_string
1493c8d9cb8a7e78df44b2edb027085ec1647c82b61bungeman@google.com        stdout = self._RunCommand([SVN, 'status']).replace('\r', '')
1506dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        status_regex = re.compile(status_regex_string, re.MULTILINE)
1516dbf6cde3bb5bd49e74a2a881d816a0572c62dedepoger@google.com        files = status_regex.findall(stdout)
1522e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        return files
1532e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
15427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def AddFiles(self, filenames):
15527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Adds these files to SVN control.
15627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
15727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param filenames files to add to SVN control
15827442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
159a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com        self._RunCommand([SVN, 'add'] + filenames)
16027442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
16127442af2040e554a09785085463bfdcecb36ecb8epoger@google.com    def SetProperty(self, filenames, property_name, property_value):
16227442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """Sets a svn property for these files.
16327442af2040e554a09785085463bfdcecb36ecb8epoger@google.com
16427442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param filenames files to set property on
16527442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param property_name property_name to set for each file
16627442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        @param property_value what to set the property_name to
16727442af2040e554a09785085463bfdcecb36ecb8epoger@google.com        """
16820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        if filenames:
16920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com            self._RunCommand(
170a74302d628f48c7c1c3e14742b0bf293ccd633f7borenet@google.com                [SVN, 'propset', property_name, property_value] + filenames)
17120ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
17220ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com    def SetPropertyByFilenamePattern(self, filename_pattern,
17320ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com                                     property_name, property_value):
17420ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """Sets a svn property for all files matching filename_pattern.
17520ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com
17620ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param filename_pattern set the property for all files whose names match
17720ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com               this Unix-style filename pattern (e.g., '*.jpg')
17820ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param property_name property_name to set for each file
17920ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        @param property_value what to set the property_name to
18020ad5ac8f6e58390c0b511d00c66df61185af889epoger@google.com        """
181591469b1e93f72172cef13a2f0675699994d7848epoger@google.com        with self._rlock:
182591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            all_files = os.listdir(self._directory)
183591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            matching_files = sorted(fnmatch.filter(all_files, filename_pattern))
184591469b1e93f72172cef13a2f0675699994d7848epoger@google.com            self.SetProperty(matching_files, property_name, property_value)
1852e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
1862e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com    def ExportBaseVersionOfFile(self, file_within_repo, dest_path):
1872e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """Retrieves a copy of the base version (what you would get if you ran
1882e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        'svn revert') of a file within the repository.
1892e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com
1902e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        @param file_within_repo path to the file within the repo whose base
1912e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com               version you wish to obtain
1922e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        @param dest_path destination to which to write the base content
1932e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com        """
1943c8d9cb8a7e78df44b2edb027085ec1647c82b61bungeman@google.com        self._RunCommand([SVN, 'export', '--revision', 'BASE', '--force',
1952e0a061c091ae1f840267f8cb2e37c7817c8911fepoger@google.com                          file_within_repo, dest_path])
196