source: tailor/vcpx/tla.py @ 979

Revision 979, 10.8 KB checked in by Robin Farine <robin.farine@…>, 8 years ago (diff)

tla: fix update's output parsing
The 'tla update' command generates some warning messages when it
performs a tree lint (e.g. in presence of symbolic links which
point to non-existent files). This interferes with the parsing of
the changes so we stop to parse the command's output when we see
"* reapplying local changes" since there must be no local changes.

Line 
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"""
23This module implements the backends for tla (Arch 1.x).
24
25This backend interprets tailor's repository, module and revision arguments
26as follows:
27
28repository
29  a registered archive name
30
31module
32  <category>--<branch>--<version>
33
34revision
35  <revision>
36"""
37
38__docformat__ = 'reStructuredText'
39
40import os
41import re
42from datetime import datetime
43from time import strptime
44from tempfile import mkdtemp
45from cStringIO import StringIO
46from email.Parser import Parser
47from email.Errors import MessageParseError
48
49from changes import Changeset
50from shwrap import ExternalCommand, PIPE
51from source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
52     GetUpstreamChangesetsFailure, InvocationError
53from target import TargetInitializationFailure
54
55
56class 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 = None
220        if c.exit_status:
221            tempdir = mkdtemp("", "++tailor-", self.basedir)
222            try:
223                for e in out:
224                    e = e.strip()
225                    ht = os.path.split(e)
226                    # only accept inventory violations at the root
227                    if ht[0] and ht[1]:
228                        raise ChangesetApplicationFailure(
229                            "%s complains about \"%s\"" % (str(c), e))
230                    os.rename(os.path.join(self.basedir, e),
231                              os.path.join(tempdir, e))
232            except:
233                self.__restore_foreign_entries(tempdir)
234                raise
235        return tempdir
236
237    def __restore_foreign_entries(self, tempdir):
238        for e in os.listdir(tempdir):
239            os.rename(os.path.join(tempdir, e), os.path.join(self.basedir, e))
240        os.rmdir(tempdir)
241
242    def __parse_apply_changeset_output(self, changeset, output):
243        conflicts = []
244        skip = True
245        for line in output:
246            # skip comment lines, detect beginning and end of change list
247            if line[0] == '*':
248                if line.startswith("* applying changeset"):
249                    skip = False
250                elif line.startswith("* reapplying local changes"):
251                    break
252                continue
253            if skip:
254                continue
255            l = line.split()
256            l1 = self.__normalize_path(l[1])
257            l2 = None
258            if len(l) > 2:
259                l2 = self.__normalize_path(l[2])
260
261            # ignore permission changes and changes in the {arch} directory
262            if l[0] in ['--', '-/'] or l1.startswith("{arch}"):
263                continue
264            if self.repository.IGNORE_IDS and l1.find('.arch-ids') >= 0:
265                continue
266            rev = changeset.revision
267            if l[0][0] == 'M' or l[0] in ['ch', 'cl']:
268                # 'ch': file <-> symlink, 'cl': ChangeLog updated
269                e = changeset.addEntry(l1, rev)
270                e.action_kind = e.UPDATED
271            elif l[0][0] == 'A':
272                e = changeset.addEntry(l1, rev)
273                e.action_kind = e.ADDED
274            elif l[0][0] == 'D':
275                e = changeset.addEntry(l1, rev)
276                e.action_kind = e.DELETED
277            elif l[0] in ['=>', '/>']:
278                e = changeset.addEntry(l2, rev)
279                e.old_name = l1
280                e.action_kind = e.RENAMED
281            elif l[0] in ['C', '?']:
282                conflicts.append(l1)
283                if l2:
284                    conflicts.append(l2)
285            else:
286                raise ChangesetApplicationFailure(
287                        "unhandled changeset operation: \"%s\"" %
288                        line.strip())
289        return conflicts
Note: See TracBrowser for help on using the repository browser.