1from __future__ import absolute_import
2
3import logging
4import os
5import re
6
7from pip9._vendor.six.moves.urllib import parse as urllib_parse
8
9from pip9.index import Link
10from pip9.utils import rmtree, display_path
11from pip9.utils.logging import indent_log
12from pip9.vcs import vcs, VersionControl
13
14_svn_xml_url_re = re.compile('url="([^"]+)"')
15_svn_rev_re = re.compile('committed-rev="(\d+)"')
16_svn_url_re = re.compile(r'URL: (.+)')
17_svn_revision_re = re.compile(r'Revision: (.+)')
18_svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"')
19_svn_info_xml_url_re = re.compile(r'<url>(.*)</url>')
20
21
22logger = logging.getLogger(__name__)
23
24
25class Subversion(VersionControl):
26    name = 'svn'
27    dirname = '.svn'
28    repo_name = 'checkout'
29    schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn')
30
31    def get_info(self, location):
32        """Returns (url, revision), where both are strings"""
33        assert not location.rstrip('/').endswith(self.dirname), \
34            'Bad directory: %s' % location
35        output = self.run_command(
36            ['info', location],
37            show_stdout=False,
38            extra_environ={'LANG': 'C'},
39        )
40        match = _svn_url_re.search(output)
41        if not match:
42            logger.warning(
43                'Cannot determine URL of svn checkout %s',
44                display_path(location),
45            )
46            logger.debug('Output that cannot be parsed: \n%s', output)
47            return None, None
48        url = match.group(1).strip()
49        match = _svn_revision_re.search(output)
50        if not match:
51            logger.warning(
52                'Cannot determine revision of svn checkout %s',
53                display_path(location),
54            )
55            logger.debug('Output that cannot be parsed: \n%s', output)
56            return url, None
57        return url, match.group(1)
58
59    def export(self, location):
60        """Export the svn repository at the url to the destination location"""
61        url, rev = self.get_url_rev()
62        rev_options = get_rev_options(url, rev)
63        url = self.remove_auth_from_url(url)
64        logger.info('Exporting svn repository %s to %s', url, location)
65        with indent_log():
66            if os.path.exists(location):
67                # Subversion doesn't like to check out over an existing
68                # directory --force fixes this, but was only added in svn 1.5
69                rmtree(location)
70            self.run_command(
71                ['export'] + rev_options + [url, location],
72                show_stdout=False)
73
74    def switch(self, dest, url, rev_options):
75        self.run_command(['switch'] + rev_options + [url, dest])
76
77    def update(self, dest, rev_options):
78        self.run_command(['update'] + rev_options + [dest])
79
80    def obtain(self, dest):
81        url, rev = self.get_url_rev()
82        rev_options = get_rev_options(url, rev)
83        url = self.remove_auth_from_url(url)
84        if rev:
85            rev_display = ' (to revision %s)' % rev
86        else:
87            rev_display = ''
88        if self.check_destination(dest, url, rev_options, rev_display):
89            logger.info(
90                'Checking out %s%s to %s',
91                url,
92                rev_display,
93                display_path(dest),
94            )
95            self.run_command(['checkout', '-q'] + rev_options + [url, dest])
96
97    def get_location(self, dist, dependency_links):
98        for url in dependency_links:
99            egg_fragment = Link(url).egg_fragment
100            if not egg_fragment:
101                continue
102            if '-' in egg_fragment:
103                # FIXME: will this work when a package has - in the name?
104                key = '-'.join(egg_fragment.split('-')[:-1]).lower()
105            else:
106                key = egg_fragment
107            if key == dist.key:
108                return url.split('#', 1)[0]
109        return None
110
111    def get_revision(self, location):
112        """
113        Return the maximum revision for all files under a given location
114        """
115        # Note: taken from setuptools.command.egg_info
116        revision = 0
117
118        for base, dirs, files in os.walk(location):
119            if self.dirname not in dirs:
120                dirs[:] = []
121                continue    # no sense walking uncontrolled subdirs
122            dirs.remove(self.dirname)
123            entries_fn = os.path.join(base, self.dirname, 'entries')
124            if not os.path.exists(entries_fn):
125                # FIXME: should we warn?
126                continue
127
128            dirurl, localrev = self._get_svn_url_rev(base)
129
130            if base == location:
131                base_url = dirurl + '/'   # save the root url
132            elif not dirurl or not dirurl.startswith(base_url):
133                dirs[:] = []
134                continue    # not part of the same svn tree, skip it
135            revision = max(revision, localrev)
136        return revision
137
138    def get_url_rev(self):
139        # hotfix the URL scheme after removing svn+ from svn+ssh:// readd it
140        url, rev = super(Subversion, self).get_url_rev()
141        if url.startswith('ssh://'):
142            url = 'svn+' + url
143        return url, rev
144
145    def get_url(self, location):
146        # In cases where the source is in a subdirectory, not alongside
147        # setup.py we have to look up in the location until we find a real
148        # setup.py
149        orig_location = location
150        while not os.path.exists(os.path.join(location, 'setup.py')):
151            last_location = location
152            location = os.path.dirname(location)
153            if location == last_location:
154                # We've traversed up to the root of the filesystem without
155                # finding setup.py
156                logger.warning(
157                    "Could not find setup.py for directory %s (tried all "
158                    "parent directories)",
159                    orig_location,
160                )
161                return None
162
163        return self._get_svn_url_rev(location)[0]
164
165    def _get_svn_url_rev(self, location):
166        from pip9.exceptions import InstallationError
167
168        entries_path = os.path.join(location, self.dirname, 'entries')
169        if os.path.exists(entries_path):
170            with open(entries_path) as f:
171                data = f.read()
172        else:  # subversion >= 1.7 does not have the 'entries' file
173            data = ''
174
175        if (data.startswith('8') or
176                data.startswith('9') or
177                data.startswith('10')):
178            data = list(map(str.splitlines, data.split('\n\x0c\n')))
179            del data[0][0]  # get rid of the '8'
180            url = data[0][3]
181            revs = [int(d[9]) for d in data if len(d) > 9 and d[9]] + [0]
182        elif data.startswith('<?xml'):
183            match = _svn_xml_url_re.search(data)
184            if not match:
185                raise ValueError('Badly formatted data: %r' % data)
186            url = match.group(1)    # get repository URL
187            revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0]
188        else:
189            try:
190                # subversion >= 1.7
191                xml = self.run_command(
192                    ['info', '--xml', location],
193                    show_stdout=False,
194                )
195                url = _svn_info_xml_url_re.search(xml).group(1)
196                revs = [
197                    int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml)
198                ]
199            except InstallationError:
200                url, revs = None, []
201
202        if revs:
203            rev = max(revs)
204        else:
205            rev = 0
206
207        return url, rev
208
209    def get_src_requirement(self, dist, location):
210        repo = self.get_url(location)
211        if repo is None:
212            return None
213        # FIXME: why not project name?
214        egg_project_name = dist.egg_name().split('-', 1)[0]
215        rev = self.get_revision(location)
216        return 'svn+%s@%s#egg=%s' % (repo, rev, egg_project_name)
217
218    def check_version(self, dest, rev_options):
219        """Always assume the versions don't match"""
220        return False
221
222    @staticmethod
223    def remove_auth_from_url(url):
224        # Return a copy of url with 'username:password@' removed.
225        # username/pass params are passed to subversion through flags
226        # and are not recognized in the url.
227
228        # parsed url
229        purl = urllib_parse.urlsplit(url)
230        stripped_netloc = \
231            purl.netloc.split('@')[-1]
232
233        # stripped url
234        url_pieces = (
235            purl.scheme, stripped_netloc, purl.path, purl.query, purl.fragment
236        )
237        surl = urllib_parse.urlunsplit(url_pieces)
238        return surl
239
240
241def get_rev_options(url, rev):
242    if rev:
243        rev_options = ['-r', rev]
244    else:
245        rev_options = []
246
247    r = urllib_parse.urlsplit(url)
248    if hasattr(r, 'username'):
249        # >= Python-2.5
250        username, password = r.username, r.password
251    else:
252        netloc = r[1]
253        if '@' in netloc:
254            auth = netloc.split('@')[0]
255            if ':' in auth:
256                username, password = auth.split(':', 1)
257            else:
258                username, password = auth, None
259        else:
260            username, password = None, None
261
262    if username:
263        rev_options += ['--username', username]
264    if password:
265        rev_options += ['--password', password]
266    return rev_options
267
268
269vcs.register(Subversion)
270