source: trunk/moap/vcs/svn.py @ 427

Revision 427, 9.4 KB checked in by thomas, 4 years ago (diff)
  • moap/test/test_vcs_svn.py:
  • moap/vcs/svn.py: Avoid Out of Date errors on moap ignore by first updating the directories on which properties will get changed. Fixes #268.
Line 
1# -*- Mode: Python; test-case-name: moap.test.test_vcs_svn -*-
2# vi:si:et:sw=4:sts=4:ts=4
3
4"""
5SVN functionality.
6"""
7
8import os
9import commands
10import re
11
12from moap.util import util, log
13from moap.vcs import vcs
14
15def detect(path):
16    """
17    Detect if the given source tree is using svn.
18
19    @return: True if the given path looks like a Subversion tree.
20    """
21    if not os.path.exists(os.path.join(path, '.svn')):
22        log.debug('svn', 'Did not find .svn directory under %s' % path)
23        return False
24
25    for n in ['props', 'text-base']:
26        if not os.path.exists(os.path.join(path, '.svn', n)):
27            log.debug('svn', 'Did not find .svn/%s under %s' % (n, path))
28            return False
29
30    return True
31
32class SVN(vcs.VCS):
33    name = 'Subversion'
34
35    meta = ['.svn']
36
37    def _getByStatus(self, path, status):
38        """
39        @param status: one character indicating the status we want to get
40                       all paths for.
41        """
42        ret = []
43
44        # ? should be escaped
45        if status == '?':
46            status = '\?'
47
48        oldPath = os.getcwd()
49
50        os.chdir(path)
51        cmd = "svn status --no-ignore"
52        output = commands.getoutput(cmd)
53        lines = output.split("\n")
54        matcher = re.compile('^' + status + '\s+(.*)')
55        for l in lines:
56            m = matcher.search(l)
57            if m:
58                relpath = m.expand("\\1")
59                ret.append(relpath)
60
61        # FIXME: would be nice to sort per directory
62        os.chdir(oldPath)
63        return ret
64
65    def getAdded(self, path):
66        return self._getByStatus(path, 'A')
67
68    def getDeleted(self, path):
69        return self._getByStatus(path, 'D')
70
71    def getIgnored(self, path):
72        return self._getByStatus(path, 'I')
73
74    def getUnknown(self, path):
75        return self._getByStatus(path, '?')
76
77    def ignore(self, paths, commit=True):
78        oldPath = os.getcwd()
79
80        os.chdir(self.path)
81        # svn ignores files by editing the svn:ignore property on the parent
82        tree = self.createTree(paths)
83        toCommit = []
84        paths = tree.keys()
85        paths.sort()
86        # update each of the directories, to avoid out-of-date directories
87        # see http://svnbook.red-bean.com/nightly/en/svn.basic.in-action.html#svn.basic.in-action.mixedrevs.limits
88        # See https://thomas.apestaart.org/moap/trac/ticket/268
89        cmd = "svn update -N %s" % " ".join(paths)
90        os.system(cmd)
91
92        for path in paths:
93            # read in old property
94            cmd = "svn propget svn:ignore %s" % path
95            (status, output) = commands.getstatusoutput(cmd)
96            lines = output.split("\n")
97            # svn 1.3.1 (r19032)
98            # $ svn propset svn:ignore --file - .
99            # svn: Reading from stdin is currently broken, so disabled
100            temp = util.writeTemp(lines + tree[path])
101            # svn needs to use "." for the base directory
102            if path == '':
103                path = '.'
104            toCommit.append(path)
105            cmd = "svn propset svn:ignore --file %s %s" % (temp, path)
106            os.system(cmd)
107            os.unlink(temp)
108
109        if commit and toCommit:
110            cmd = "svn commit -m 'moap ignore' -N %s" % " ".join(toCommit) 
111            (status, output) = commands.getstatusoutput(cmd)
112            if status != 0:
113                raise vcs.VCSException(output)
114        os.chdir(oldPath)
115
116    def commit(self, paths, message):
117        # get all the parents as well
118        parents = []
119        for p in paths:
120            while p:
121                p = os.path.dirname(p)
122                if p:
123                    parents.append(p)
124
125        paths.extend(parents)
126        temp = util.writeTemp([message, ])
127        paths = [os.path.join(self.path, p) for p in paths]
128        cmd = "svn commit --non-recursive --file %s %s" % (
129            temp, " ".join(paths))
130        log.debug('svn', 'Executing command: %s' % cmd)
131        status = os.system(cmd)
132        os.unlink(temp)
133        if status != 0:
134            return False
135
136        return True
137
138    def diff(self, path, revision1=None, revision2=None):
139        # the diff can also contain svn-specific property changes
140        # we need to filter them to be a normal unified diff
141        # These blocks are recognizable because they go
142        # newline - Property changes on: - stuff - newline
143        # We parse in the C locale so we need to set it - see ticket #266
144        oldPath = os.getcwd()
145        os.chdir(self.path)
146
147        rev = ''
148        if revision1 and revision2:
149            rev = '-r %s:%s' % (revision1, revision2)
150        cmd = "LANG=C svn diff %s %s" % (rev, path)
151        self.debug('Running %s', cmd)
152        output = commands.getoutput(cmd)
153        os.chdir(oldPath)
154
155        return self.scrubPropertyChanges(output)
156
157    def scrubPropertyChanges(self, output):
158        """
159        Scrub the given diff output from property changes.
160        """
161        reo = re.compile(
162            '^$\n'                        # starting empty line
163            '^Property changes on:.*?$\n' # Property changes line, non-greedy
164            '.*?'                         # all the other lines, non-greedy
165            '^$\n',                       # ending empty line
166             re.MULTILINE | re.DOTALL)    # make sure we do multi-line
167
168        return reo.sub('', output)
169
170    def getPropertyChanges(self, path):
171        ret = {}
172
173        cmd = "LANG=C svn diff %s" % path
174        # we add a newline so we can match each Property changes block by
175        # having it end on a newline, including the last block
176        output = commands.getoutput(cmd) + '\n'
177
178        # match Property changes blocks
179        reo = re.compile(
180            'Property changes on: (.*?)$\n' # Property changes line, non-greedy
181            '^_*$\n'                        # Divider line
182            '^(\w*: .*?'                    # Property name block, non-greedy
183            '(?:Property)?)'                # and stop at a possible next block
184            '^$\n'                          # and end on empty line
185            , re.MULTILINE | re.DOTALL)     # make sure we do multi-line
186
187        # match Name: blocks within a file's property changes
188        reop = re.compile(
189            '^\w*: (.*?)\n'                 # Property name block, non-greedy
190            , re.MULTILINE | re.DOTALL)     # make sure we do multi-line
191
192
193        fileMatches = reo.findall(output)
194        for path, properties in fileMatches:
195            ret[path] = reop.findall(properties)
196
197        return ret
198
199    def update(self, path, revision=None):
200        rev = None
201        if revision:
202            rev = '-r %s' % revision
203        if not path:
204            path = ''
205        cmd = "svn update %s %s" % (rev, path)
206        self.debug('Running %s', cmd)
207        status, output = commands.getstatusoutput(cmd)
208        if status != 0:
209            raise vcs.VCSException(output)
210        return output
211
212    def getRevision(self):
213        oldPath = os.getcwd()
214        os.chdir(self.path)
215
216        # FIXME: what if some files are at a different revision ?
217        cmd = "svn info %s | grep ^Revision: | cut -d: -f2" % self.path
218        status, output = commands.getstatusoutput(cmd)
219        output = output.strip()
220        if status != 0:
221            raise vcs.VCSException(output)
222
223        os.chdir(oldPath)
224
225        self.debug('SVN at revision %r' % output)
226        return output
227         
228    def getMiddleDifference(self, revision1, revision2):
229        delta = abs(int(revision2) - int(revision1))
230        middle = (int(revision1) + int(revision2)) / 2
231        if delta <= 1:
232            return str(middle), None
233
234        return str(middle), str(delta)
235
236    def getCheckoutCommands(self):
237        ret = []
238
239        oldPath = os.getcwd()
240        os.chdir(self.path)
241
242        # FIXME: what if some files are at a different revision ?
243        cmd = "svn info %s" % self.path
244        status, output = commands.getstatusoutput(cmd)
245        if status != 0:
246            raise vcs.VCSException(output)
247        lines = output.split('\n')
248        url = None
249        baseRevision = None
250        for line in lines:
251            if line.startswith('URL: '):
252                url = line[4:]
253            if line.startswith('Revision: '):
254                baseRevision = int(line[10:])
255
256        ret.append(
257            'svn checkout --non-interactive --revision %d %s checkout\n' % (
258                baseRevision, url))
259
260        # now check all paths that are at a different revision than the base
261        # one
262        cmd = "svn status -v"
263        status, output = commands.getstatusoutput(cmd)
264        if status != 0:
265            raise vcs.VCSException(output)
266        lines = output.split('\n')
267
268        matcher = re.compile('.{8}' # first 8 status columns
269            '\s+(\d+)'              # current revision
270            '\s+(\d+)'              # last commited version
271            '\s+(\w+)'              # last committer
272            '\s+(.*)'               # working copy path
273            )
274
275        # FIXME: group same revisions for a speedup
276        for line in lines:
277            m = matcher.search(line)
278            if m:
279                revision = int(m.expand("\\1"))
280                path = m.expand("\\4")
281                if revision != baseRevision:
282                    ret.append('svn update --non-interactive --non-recursive '
283                        '--revision %d %s' % (
284                        revision, os.path.join('checkout', path)))
285
286        os.chdir(oldPath)
287
288        return "\n".join(ret) + ('\n')
289
290VCSClass = SVN
Note: See TracBrowser for help on using the repository browser.