source: tailor/vcpx/changes.py @ 1634

Revision 1634, 8.1 KB checked in by lele@…, 5 years ago (diff)

Move additional entry info after the name

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