| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: vcpx -- tla (Arch 1.x) backend |
|---|
| 3 | # :Creato: sab 13 ago 2005 12:16:16 CEST |
|---|
| 4 | # :Autore: Robin Farine <robin.farine@terminus.org> |
|---|
| 5 | # :Licenza: GNU General Public License |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | # Current limitations and pitfalls. |
|---|
| 9 | # |
|---|
| 10 | # - Target backend not implemented. |
|---|
| 11 | # |
|---|
| 12 | # - In-version continuations not supported (raises an exception); this |
|---|
| 13 | # would probably require to compute a changeset with 'tla delta' |
|---|
| 14 | # instead of using update. |
|---|
| 15 | # |
|---|
| 16 | # - Pika escaped file names. This implementations requires a version |
|---|
| 17 | # of tla that supports pika escapes. For changesets created with a |
|---|
| 18 | # version of tla that did not support pika escapes, if one of these |
|---|
| 19 | # changeset contains a file name with a valid embedded pika escape |
|---|
| 20 | # sequence, things will break. |
|---|
| 21 | |
|---|
| 22 | """ |
|---|
| 23 | This module implements the backends for tla (Arch 1.x). |
|---|
| 24 | |
|---|
| 25 | This backend interprets tailor's repository, module and revision arguments |
|---|
| 26 | as follows: |
|---|
| 27 | |
|---|
| 28 | repository |
|---|
| 29 | a registered archive name |
|---|
| 30 | |
|---|
| 31 | module |
|---|
| 32 | <category>--<branch>--<version> |
|---|
| 33 | |
|---|
| 34 | revision |
|---|
| 35 | <revision> |
|---|
| 36 | """ |
|---|
| 37 | |
|---|
| 38 | __docformat__ = 'reStructuredText' |
|---|
| 39 | |
|---|
| 40 | import os |
|---|
| 41 | import re |
|---|
| 42 | from datetime import datetime |
|---|
| 43 | from time import strptime |
|---|
| 44 | from tempfile import mkdtemp |
|---|
| 45 | from cStringIO import StringIO |
|---|
| 46 | from email.Parser import Parser |
|---|
| 47 | from email.Errors import MessageParseError |
|---|
| 48 | |
|---|
| 49 | from changes import Changeset |
|---|
| 50 | from shwrap import ExternalCommand, PIPE |
|---|
| 51 | from source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \ |
|---|
| 52 | GetUpstreamChangesetsFailure, InvocationError |
|---|
| 53 | from target import TargetInitializationFailure |
|---|
| 54 | |
|---|
| 55 | |
|---|
| 56 | class TlaWorkingDir(UpdatableSourceWorkingDir): |
|---|
| 57 | """ |
|---|
| 58 | A working directory under ``tla``. |
|---|
| 59 | """ |
|---|
| 60 | |
|---|
| 61 | ## UpdatableSourceWorkingDir |
|---|
| 62 | |
|---|
| 63 | def _getUpstreamChangesets(self, sincerev): |
|---|
| 64 | """ |
|---|
| 65 | Build the list of upstream changesets missing in the working directory. |
|---|
| 66 | """ |
|---|
| 67 | |
|---|
| 68 | changesets = [] |
|---|
| 69 | self.fqversion = '/'.join([self.repository.repository, |
|---|
| 70 | self.repository.module]) |
|---|
| 71 | c = ExternalCommand(cwd=self.basedir, |
|---|
| 72 | command=self.repository.command("missing", "-f")) |
|---|
| 73 | out, err = c.execute(stdout=PIPE, stderr=PIPE) |
|---|
| 74 | if c.exit_status: |
|---|
| 75 | raise GetUpstreamChangesetsFailure( |
|---|
| 76 | "%s returned status %d saying\n%s" % |
|---|
| 77 | (str(c), c.exit_status, err.read())) |
|---|
| 78 | changesets = self.__parse_revision_logs(out.read().split()) |
|---|
| 79 | return changesets |
|---|
| 80 | |
|---|
| 81 | def _applyChangeset(self, changeset): |
|---|
| 82 | """ |
|---|
| 83 | Do the actual work of applying the changeset to the working copy and |
|---|
| 84 | record the changes in ``changeset``. Return a list of files involved |
|---|
| 85 | in conflicts. |
|---|
| 86 | """ |
|---|
| 87 | |
|---|
| 88 | if self.shared_basedirs: |
|---|
| 89 | tempdir = self.__hide_foreign_entries() |
|---|
| 90 | try: |
|---|
| 91 | conflicts = self.__apply_changeset(changeset) |
|---|
| 92 | finally: |
|---|
| 93 | if tempdir: |
|---|
| 94 | self.__restore_foreign_entries(tempdir) |
|---|
| 95 | else: |
|---|
| 96 | conflicts = self.__apply_changeset(changeset) |
|---|
| 97 | return conflicts |
|---|
| 98 | |
|---|
| 99 | def _checkoutUpstreamRevision(self, revision): |
|---|
| 100 | """ |
|---|
| 101 | Create the initial working directory during bootstrap. |
|---|
| 102 | """ |
|---|
| 103 | |
|---|
| 104 | fqrev = self.__initial_revision(revision) |
|---|
| 105 | if self.shared_basedirs: |
|---|
| 106 | tempdir = mkdtemp("", ",,tailor-", self.basedir) |
|---|
| 107 | try: |
|---|
| 108 | self.__checkout_initial_revision(fqrev, tempdir, "t") |
|---|
| 109 | finally: |
|---|
| 110 | newtree = os.path.join(tempdir, "t") |
|---|
| 111 | if os.path.exists(newtree): |
|---|
| 112 | for e in os.listdir(newtree): |
|---|
| 113 | os.rename(os.path.join(newtree, e), |
|---|
| 114 | os.path.join(self.basedir, e)) |
|---|
| 115 | os.rmdir(newtree) |
|---|
| 116 | os.rmdir(tempdir) |
|---|
| 117 | else: |
|---|
| 118 | root, destdir = os.path.split(self.basedir) |
|---|
| 119 | self.__checkout_initial_revision(fqrev, root, destdir) |
|---|
| 120 | return self.__parse_revision_logs([fqrev], False)[0] |
|---|
| 121 | |
|---|
| 122 | ## TlaWorkingDir private helper functions |
|---|
| 123 | |
|---|
| 124 | def __checkout_initial_revision(self, fqrev, root, destdir): |
|---|
| 125 | if not os.path.exists(root): |
|---|
| 126 | os.makedirs(root) |
|---|
| 127 | cmd = self.repository.command("get", "--no-pristine", fqrev, destdir) |
|---|
| 128 | c = ExternalCommand(cwd=root, command=cmd) |
|---|
| 129 | out, err = c.execute(stdout=PIPE, stderr=PIPE) |
|---|
| 130 | if c.exit_status: |
|---|
| 131 | raise TargetInitializationFailure( |
|---|
| 132 | "%s returned status %d saying\n%s" % |
|---|
| 133 | (str(c), c.exit_status, err.read())) |
|---|
| 134 | |
|---|
| 135 | def __apply_changeset(self, changeset): |
|---|
| 136 | c = ExternalCommand(cwd=self.basedir, |
|---|
| 137 | command=self.repository.command("update")) |
|---|
| 138 | out, err = c.execute(changeset.revision, stdout=PIPE, stderr=PIPE) |
|---|
| 139 | if not c.exit_status in [0, 1]: |
|---|
| 140 | raise ChangesetApplicationFailure( |
|---|
| 141 | "%s returned status %d saying\n%s" % |
|---|
| 142 | (str(c), c.exit_status, err.read())) |
|---|
| 143 | return self.__parse_apply_changeset_output(changeset, out) |
|---|
| 144 | |
|---|
| 145 | def __normalize_path(self, path): |
|---|
| 146 | if len(path) > 2: |
|---|
| 147 | if path[0:2] == "./": |
|---|
| 148 | path = path[2:] |
|---|
| 149 | if path.find("\(") != -1: |
|---|
| 150 | cmd = self.repository.command("escape", "--unescaped", path) |
|---|
| 151 | c = ExternalCommand(command=cmd) |
|---|
| 152 | out, err = c.execute(stdout=PIPE, stderr=PIPE) |
|---|
| 153 | if c.exit_status: |
|---|
| 154 | raise GetUpstreamChangesetsFailure( |
|---|
| 155 | "%s returned status %d saying\n%s" % |
|---|
| 156 | (str(c), c.exit_status, err.read())) |
|---|
| 157 | path = out.read() |
|---|
| 158 | return path |
|---|
| 159 | |
|---|
| 160 | def __initial_revision(self, revision): |
|---|
| 161 | fqversion = '/'.join([self.repository.repository, |
|---|
| 162 | self.repository.module]) |
|---|
| 163 | if revision in ['HEAD', 'INITIAL']: |
|---|
| 164 | cmd = self.repository.command("revisions") |
|---|
| 165 | if revision == 'HEAD': |
|---|
| 166 | cmd.append("-r") |
|---|
| 167 | cmd.append(fqversion) |
|---|
| 168 | c = ExternalCommand(command=cmd) |
|---|
| 169 | out, err = c.execute(stdout=PIPE, stderr=PIPE) |
|---|
| 170 | if c.exit_status: |
|---|
| 171 | raise TargetInitializationFailure( |
|---|
| 172 | "%s returned status %d saying\n%s" % |
|---|
| 173 | (str(c), c.exit_status, err.read())) |
|---|
| 174 | revision = out.readline().strip() |
|---|
| 175 | return '--'.join([fqversion, revision]) |
|---|
| 176 | |
|---|
| 177 | def __parse_revision_logs(self, fqrevlist, update=True): |
|---|
| 178 | changesets = [] |
|---|
| 179 | logparser = Parser() |
|---|
| 180 | c = ExternalCommand(cwd=self.basedir, |
|---|
| 181 | command=self.repository.command("cat-archive-log")) |
|---|
| 182 | for fqrev in fqrevlist: |
|---|
| 183 | out, err = c.execute(fqrev, stdout=PIPE, stderr=PIPE) |
|---|
| 184 | if c.exit_status: |
|---|
| 185 | raise GetUpstreamChangesetsFailure( |
|---|
| 186 | "%s returned status %d saying\n%s" % |
|---|
| 187 | (str(c), c.exit_status, err.read())) |
|---|
| 188 | err = None |
|---|
| 189 | try: |
|---|
| 190 | msg = logparser.parse(out) |
|---|
| 191 | except Exception, err: |
|---|
| 192 | pass |
|---|
| 193 | if not err and msg.is_multipart(): |
|---|
| 194 | err = "unable to parse log description" |
|---|
| 195 | if not err and update and msg.has_key('Continuation-of'): |
|---|
| 196 | err = "in-version continuations not supported" |
|---|
| 197 | if err: |
|---|
| 198 | raise GetUpstreamChangesetsFailure(str(err)) |
|---|
| 199 | y,m,d,hh,mm,ss,d1,d2,d3 = strptime(msg['Standard-date'], |
|---|
| 200 | "%Y-%m-%d %H:%M:%S %Z") |
|---|
| 201 | date = datetime(y,m,d,hh,mm,ss) |
|---|
| 202 | author = msg['Creator'] |
|---|
| 203 | revision = fqrev |
|---|
| 204 | logmsg = [msg['Summary']] |
|---|
| 205 | s = msg.get('Keywords', "").strip() |
|---|
| 206 | if s: |
|---|
| 207 | logmsg.append('Keywords: ' + s) |
|---|
| 208 | s = msg.get_payload().strip() |
|---|
| 209 | if s: |
|---|
| 210 | logmsg.append(s) |
|---|
| 211 | logmsg = '\n'.join(logmsg) |
|---|
| 212 | changesets.append(Changeset(revision, date, author, logmsg)) |
|---|
| 213 | return changesets |
|---|
| 214 | |
|---|
| 215 | def __hide_foreign_entries(self): |
|---|
| 216 | c = ExternalCommand(cwd=self.basedir, |
|---|
| 217 | command=self.repository.command("tree-lint", "-tu")) |
|---|
| 218 | out = c.execute(stdout=PIPE)[0] |
|---|
| 219 | tempdir = mkdtemp("", "++tailor-", self.basedir) |
|---|
| 220 | try: |
|---|
| 221 | for e in out: |
|---|
| 222 | e = e.strip() |
|---|
| 223 | ht = os.path.split(e) |
|---|
| 224 | # only move inventory violations at the root |
|---|
| 225 | if ht[0] and ht[1]: |
|---|
| 226 | continue |
|---|
| 227 | os.rename(os.path.join(self.basedir, e), |
|---|
| 228 | os.path.join(tempdir, e)) |
|---|
| 229 | except: |
|---|
| 230 | self.__restore_foreign_entries(tempdir) |
|---|
| 231 | raise |
|---|
| 232 | return tempdir |
|---|
| 233 | |
|---|
| 234 | def __restore_foreign_entries(self, tempdir): |
|---|
| 235 | for e in os.listdir(tempdir): |
|---|
| 236 | os.rename(os.path.join(tempdir, e), os.path.join(self.basedir, e)) |
|---|
| 237 | os.rmdir(tempdir) |
|---|
| 238 | |
|---|
| 239 | def __parse_apply_changeset_output(self, changeset, output): |
|---|
| 240 | conflicts = [] |
|---|
| 241 | skip = True |
|---|
| 242 | for line in output: |
|---|
| 243 | # skip comment lines, detect beginning and end of change list |
|---|
| 244 | if line[0] == '*': |
|---|
| 245 | if line.startswith("* applying changeset"): |
|---|
| 246 | skip = False |
|---|
| 247 | elif line.startswith("* reapplying local changes"): |
|---|
| 248 | break |
|---|
| 249 | continue |
|---|
| 250 | if skip: |
|---|
| 251 | continue |
|---|
| 252 | l = line.split() |
|---|
| 253 | l1 = self.__normalize_path(l[1]) |
|---|
| 254 | l2 = None |
|---|
| 255 | if len(l) > 2: |
|---|
| 256 | l2 = self.__normalize_path(l[2]) |
|---|
| 257 | |
|---|
| 258 | # ignore permission changes and changes in the {arch} directory |
|---|
| 259 | if l[0] in ['--', '-/'] or l1.startswith("{arch}"): |
|---|
| 260 | continue |
|---|
| 261 | if self.repository.IGNORE_IDS and l1.find('.arch-ids') >= 0: |
|---|
| 262 | continue |
|---|
| 263 | rev = changeset.revision |
|---|
| 264 | if l[0][0] == 'M' or l[0] in ['ch', 'cl']: |
|---|
| 265 | # 'ch': file <-> symlink, 'cl': ChangeLog updated |
|---|
| 266 | e = changeset.addEntry(l1, rev) |
|---|
| 267 | e.action_kind = e.UPDATED |
|---|
| 268 | elif l[0][0] == 'A': |
|---|
| 269 | e = changeset.addEntry(l1, rev) |
|---|
| 270 | e.action_kind = e.ADDED |
|---|
| 271 | elif l[0][0] == 'D': |
|---|
| 272 | e = changeset.addEntry(l1, rev) |
|---|
| 273 | e.action_kind = e.DELETED |
|---|
| 274 | elif l[0] in ['=>', '/>']: |
|---|
| 275 | e = changeset.addEntry(l2, rev) |
|---|
| 276 | e.old_name = l1 |
|---|
| 277 | e.action_kind = e.RENAMED |
|---|
| 278 | elif l[0] in ['C', '?']: |
|---|
| 279 | conflicts.append(l1) |
|---|
| 280 | if l2: |
|---|
| 281 | conflicts.append(l2) |
|---|
| 282 | else: |
|---|
| 283 | raise ChangesetApplicationFailure( |
|---|
| 284 | "unhandled changeset operation: \"%s\"" % |
|---|
| 285 | line.strip()) |
|---|
| 286 | return conflicts |
|---|