source: tailor/vcpx/tla.py @ 1064

Revision 1064, 10.6 KB checked in by Cameron Patrick <cameron@…>, 7 years ago (diff)

Fix handling of tla archives with "untagged-source unrecognized" set
If a tla archive has the "untagged-source unrecognized" option in its
tagging-method file, tla will complain if there are any foreign files
present (e.g. the _darcs directory containing darcs metadata). The
tla backend was supposed to fix this but the code there didn't work
properly, as it was too fussy about tla exit codes and the output
formats of tla tree-lint.
This patch makes it work again (tested with tla 1.3.3).

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 = 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
Note: See TracBrowser for help on using the repository browser.