source: tailor/vcpx/changes.py @ 1637

Revision 1637, 8.0 KB checked in by lele@…, 5 years ago (diff)

Be backward compatible with existing statefiles
Using class attributes to initialize the default values is the right way
of extending the ChangesetEntry, without the risk of triggering
AttributeError on old instances loaded from a statefile built with
a previous version of tailor.

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Changesets
3# :Creato:   ven 11 giu 2004 15:31:18 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9Changesets are an object representation of a set of changes to some files.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from vcpx import TailorBug
15
16class ChangesetEntry(object):
17    """
18    Represent a changed entry in a Changeset.
19
20    For our scope, this simply means an entry ``name``, the original
21    ``old_revision``, the ``new_revision`` after this change, an
22    ``action_kind`` to denote the kind of change, and finally a ``status``
23    to indicate possible conflicts.
24    """
25
26    ADDED = 'ADD'
27    DELETED = 'DEL'
28    UPDATED = 'UPD'
29    RENAMED = 'REN'
30
31    APPLIED = 'APPLIED'
32    CONFLICT = 'CONFLICT'
33
34    old_name = None
35    old_revision = None
36    new_revision = None
37    action_kind = None
38    status = None
39    unidiff = None # This is the unidiff of this particular entry
40    is_directory = False # This usually makes sense only on ADDs and DELs
41    is_symlink = False
42
43    def __init__(self, name):
44        self.name = name
45
46    def __str__(self):
47        entry_kind = []
48        if self.is_directory:
49            entry_kind.append('DIR')
50        if self.is_symlink:
51            entry_kind.append('SYMLINK')
52        if entry_kind:
53            kind = '[' + ','.join(entry_kind) + ']'
54        else:
55            kind = ''
56        s = self.name + kind + '(' + self.action_kind
57        if self.action_kind == self.ADDED:
58            if self.new_revision:
59                s += ' at ' + self.new_revision
60        elif self.action_kind == self.UPDATED:
61            if self.new_revision:
62                s += ' to ' + self.new_revision
63        elif self.action_kind == self.DELETED:
64            if self.new_revision:
65                s += ' at ' + self.new_revision
66        elif self.action_kind == self.RENAMED:
67            s += ' from ' + self.old_name
68        else:
69            s += '??'
70        s += ')'
71        if isinstance(s, unicode):
72            s = s.encode('ascii', 'replace')
73        return s
74
75    def __eq__(self, other):
76        return (self.name == other.name and
77                self.old_name == other.old_name and
78                self.old_revision == other.old_revision and
79                self.new_revision == other.new_revision and
80                self.action_kind == other.action_kind)
81
82    def __ne__(self, other):
83        return (self.name != other.name or
84                self.old_name != other.old_name or
85                self.old_revision != other.old_revision or
86                self.new_revision != other.new_revision or
87                self.action_kind != other.action_kind)
88
89from textwrap import TextWrapper
90from re import compile, MULTILINE
91
92itemize_re = compile('^[ ]*[-*] ', MULTILINE)
93
94def refill(msg):
95    """
96    Refill a changelog message.
97
98    Normalize the message reducing multiple spaces and newlines to single
99    spaces, recognizing common form of ``bullet lists``, that is paragraphs
100    starting with either a dash "-" or an asterisk "*".
101    """
102
103    wrapper = TextWrapper()
104    res = []
105    items = itemize_re.split(msg.strip())
106
107    if len(items)>1:
108        # Remove possible first empty split, when the message immediately
109        # starts with a bullet
110        if not items[0]:
111            del items[0]
112
113        if len(items)>1:
114            wrapper.initial_indent = '- '
115            wrapper.subsequent_indent = ' '*2
116
117    for item in items:
118        if item:
119            words = filter(None, item.strip().replace('\n', ' ').split(' '))
120            normalized = ' '.join(words)
121            res.append(wrapper.fill(normalized))
122
123    return '\n\n'.join(res)
124
125
126class Changeset(object):
127    """
128    Represent a single upstream Changeset.
129
130    This is a container of each file affected by this revision of the tree.
131    """
132
133    ANONYMOUS_USER = "anonymous"
134    """Author name when it is not known"""
135
136    REFILL_MESSAGE = False
137    """Refill changelogs"""
138
139    def _get_date(self):
140        try:
141            return self.__date
142        except AttributeError, e:
143            # handle state-file Changesets created with previous versions of tailor
144            from vcpx.tzinfo import UTC
145            self.__date = self.__dict__['date'].replace(tzinfo=UTC)
146            return self.__date
147
148    def _set_date(self, date):
149        if date and date.tzinfo is None:
150            raise TailorBug("Changeset dates must have a timezone!")
151        self.__date = date
152
153    # date has to be a property because some backends (eg. monotone)
154    # update it after the constructor
155    date = property(_get_date, _set_date)
156
157    def __init__(self, revision, date, author, log, entries=None, **other):
158        """
159        Initialize a new Changeset.
160        """
161
162        self.revision = revision
163        self.date = date
164        # Author name may be missing, to mean a check in made by an
165        # anonymous user.
166        self.author = author or self.ANONYMOUS_USER
167        self.setLog(log)
168        self.entries = entries or []
169        self.unidiff = None        # This is the unidiff of the whole changeset
170        self.tags = other.get('tags', None)
171
172    # Don't take into account the entries, to compare changesets, because they
173    # may be loaded after changeset application: the not-yet-applied changeset
174    # will be different from the same-but-just-applied one.
175
176    def __eq__(self, other):
177        return (self.revision == other.revision and
178                self.date == other.date and
179                self.author == other.author)
180
181    def __ne__(self, other):
182        return (self.revision <> other.revision or
183                self.date <> other.date or
184                self.author <> other.author)
185
186    def setLog(self, log):
187        if self.REFILL_MESSAGE:
188            self.log = refill(log)
189        else:
190            self.log = log
191
192    def addEntry(self, entry, revision, before=None):
193        """
194        Facility to add an entry, eventually before another one.
195        """
196
197        e = ChangesetEntry(entry)
198        e.new_revision = revision
199        if before is None:
200            self.entries.append(e)
201        else:
202            self.entries.insert(self.entries.index(before), e)
203        return e
204
205    def __str__(self):
206        s = []
207        s.append('Revision: %s' % self.revision)
208        s.append('Date: %s' % str(self.date))
209        s.append('Author: %s' % self.author)
210        s.append('Entries: %s' % ', '.join([str(x) for x in self.entries]))
211        s.append('Log: %s' % self.log)
212        s = '\n'.join(s)
213        if isinstance(s, unicode):
214            s = s.encode('ascii', 'replace')
215        return s
216
217    def applyPatch(self, working_dir, patch_options="-p1"):
218        """
219        Apply the changeset using ``patch(1)`` to a given directory.
220        """
221
222        from shwrap import ExternalCommand
223        from source import ChangesetApplicationFailure
224
225        if self.unidiff:
226            cmd = ["patch"]
227            if patch_options:
228                if isinstance(patch_options, basestring):
229                    cmd.extend(patch_options.split(' '))
230                else:
231                    cmd.extend(patch_options)
232
233            patch = ExternalCommand(cwd=working_dir, command=cmd)
234            patch.execute(input=self.unidiff)
235
236            if patch.exit_status:
237                raise ChangesetApplicationFailure(
238                    "%s returned status %s" % (str(patch), patch.exit_status))
239
240    def addedEntries(self):
241        """
242        Facility to extract a list of added entries.
243        """
244
245        return [e for e in self.entries if e.action_kind == e.ADDED]
246
247    def modifiedEntries(self):
248        """
249        Facility to extract a list of modified entries.
250        """
251
252        return [e for e in self.entries if e.action_kind == e.UPDATED]
253
254    def removedEntries(self):
255        """
256        Facility to extract a list of deleted entries.
257        """
258
259        return [e for e in self.entries if e.action_kind == e.DELETED]
260
261    def renamedEntries(self):
262        """
263        Facility to extract a list of renamed entries.
264        """
265
266        return [e for e in self.entries if e.action_kind == e.RENAMED]
Note: See TracBrowser for help on using the repository browser.