| 1 | # -*- Mode: Python; test-case-name: moap.test.test_vcs_svn -*- |
|---|
| 2 | # vi:si:et:sw=4:sts=4:ts=4 |
|---|
| 3 | |
|---|
| 4 | """ |
|---|
| 5 | SVN functionality. |
|---|
| 6 | """ |
|---|
| 7 | |
|---|
| 8 | import os |
|---|
| 9 | import commands |
|---|
| 10 | import re |
|---|
| 11 | |
|---|
| 12 | from moap.util import util, log |
|---|
| 13 | from moap.vcs import vcs |
|---|
| 14 | |
|---|
| 15 | def 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 | |
|---|
| 32 | class 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 | |
|---|
| 290 | VCSClass = SVN |
|---|