source: tailor/vcpx/changes.py @ 1648

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

Split the entries one per line

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' % '\n\t '.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.