source: tailor/vcpx/repository/bzr.py @ 1671

Revision 1671, 15.6 KB checked in by lele@…, 3 years ago (diff)

Honor a preexisting target bazaar branch

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Bazaar support using the bzrlib instead of the frontend
3# :Creato:   Fri Aug 19 01:06:08 CEST 2005
4# :Autore:   Johan Rydberg <jrydberg@gnu.org>
5#            Jelmer Vernooij <jelmer@samba.org>
6#            Lalo Martins <lalo.martins@gmail.com>
7#            Olaf Conradi <olaf@conradi.org>
8# :Licenza:  GNU General Public License
9#
10
11"""
12This module implements the backends for Bazaar.
13"""
14
15__docformat__ = 'reStructuredText'
16
17
18from sys import version_info
19assert version_info >= (2,4), "Bazaar backend requires Python 2.4"
20del version_info
21
22from bzrlib import errors
23from bzrlib.branch import Branch
24from bzrlib.bzrdir import BzrDir
25from bzrlib.errors import NoSuchRevision, TagsNotSupported
26from bzrlib.missing import find_unmerged
27from bzrlib.osutils import normpath, pathjoin
28from bzrlib.plugin import load_plugins
29
30from vcpx.changes import Changeset, ChangesetEntry
31from vcpx.repository import Repository
32from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure
33from vcpx.target import SynchronizableTargetWorkingDir
34from vcpx.workdir import WorkingDir
35
36
37class BzrChangeset(Changeset):
38    """
39    Manage the particular reordering of the entries.
40
41    Apparently TreeDelta doesn't expose the entries in a sensible order,
42    they are grouped by kind.
43    """
44
45    def __init__(self, revision, date, author, log, entries=None, **other):
46        """
47        Initialize a new BzrChangeset, inserting the entries in a sensible order.
48        """
49
50        from os.path import split, join
51
52        super(BzrChangeset, self).__init__(revision, date, author, log, entries=None, **other)
53        if entries is not None:
54            for e in entries:
55                self.addEntry(e, revision)
56
57            # Adjust old_name on renamed entries: bzr tell us the *original*
58            # name of the rename...
59            # Consider this:
60            #
61            #     $ bzr mv newnamedir/subdir/a newnamedir/subdir/b
62            #     newnamedir/subdir/a => newnamedir/subdir/b
63            #     $ bzr mv newnamedir/subdir newnamedir/newsubdir
64            #     newnamedir/subdir => newnamedir/newsubdir
65            #     $ bzr mv newnamedir dir
66            #     newnamedir => dir
67            #     $ bzr st
68            #     renamed:
69            #       newnamedir => dir
70            #       newnamedir/subdir => dir/newsubdir
71            #       newnamedir/subdir/a => dir/newsubdir/b
72
73            renames = {}
74            for e in self.entries:
75                if e.action_kind == e.RENAMED:
76                    renames[e.old_name] = e.name
77                    d,f = split(e.old_name)
78                    while d:
79                        if d in renames:
80                            e.old_name = join(renames[d], e.old_name[len(d)+1:])
81                            break
82                        d,f = split(d)
83
84    def addEntry(self, entry, revision):
85        """
86        Fixup the ordering of the entries, by giving precedence to directories
87        """
88
89        if entry.action_kind in (entry.ADDED, entry.RENAMED) and entry.is_directory:
90            dirname = entry.name + '/' # does bzr on windows use this too?
91            for i,e in enumerate(self.entries):
92                if e.name.startswith(dirname):
93                    self.entries.insert(i, entry)
94                    return
95        elif entry.action_kind == entry.DELETED:
96            for i,e in enumerate(self.entries):
97                if e.action_kind == e.RENAMED and e.name == entry.name:
98                    # This is the following case:
99                    #  $ bzr rm A
100                    #  $ bzr mv B A
101                    self.entries.insert(i, entry)
102                    return
103                elif (e.action_kind == e.DELETED
104                      and e.is_directory
105                      and entry.name.startswith(e.name)):
106                    # Remove dir contents before dir itself
107                    self.entries.insert(i, entry)
108                    return
109                elif (e.action_kind == e.ADDED and e.name == entry.name):
110                    # put replacement (rm+add) in the right order
111                    self.entries.insert(i, entry)
112                    return
113
114        self.entries.append(entry)
115
116
117class BzrRepository(Repository):
118    METADIR = '.bzr'
119
120    def _load(self, project):
121        Repository._load(self, project)
122        ppath = project.config.get(self.name, 'python-path')
123        if ppath:
124            from sys import path
125
126            if ppath not in path:
127                path.insert(0, ppath)
128
129    def create(self):
130        """
131        Create a branch with a working tree at the base directory. If the base
132        directory is inside a Bazaar style "shared repository", it will use
133        that to create a branch and working tree (make sure it allows working
134        trees).
135        """
136
137        self.log.info('Initializing new repository in %r...', self.basedir)
138        try:
139            bzrdir = BzrDir.open(self.basedir)
140        except errors.NotBranchError:
141            # really a NotBzrDir error...
142            branch = BzrDir.create_branch_convenience(self.basedir, force_new_tree=True)
143            wtree = branch.bzrdir.open_workingtree()
144        else:
145            bzrdir.create_branch()
146            wtree = bzrdir.create_workingtree()
147
148        return wtree
149
150
151class BzrWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir):
152    def __init__(self, repository):
153        from os.path import split
154        from bzrlib import version_info, IGNORE_FILENAME
155
156        if version_info > (0,9):
157            from bzrlib.ignores import add_runtime_ignores, parse_ignore_file
158        else:
159            from bzrlib import DEFAULT_IGNORE
160
161        WorkingDir.__init__(self, repository)
162        # TODO: check if there is a "repository" in the configuration,
163        # and use it as a bzr repository
164        self.ignored = []
165        self._working_tree = None
166
167        # The bzr repository may have some plugins that needs to be activated
168        load_plugins()
169
170        try:
171            bzrdir = BzrDir.open(self.repository.basedir)
172            wt = self._working_tree = bzrdir.open_workingtree()
173
174            # read .bzrignore for _addSubtree()
175            if wt.has_filename(IGNORE_FILENAME):
176                f = wt.get_file_byname(IGNORE_FILENAME)
177                if version_info > (0,9):
178                    self.ignored.extend(parse_ignore_file(f))
179                else:
180                    self.ignored.extend([ line.rstrip("\n\r") for line in f.readlines() ])
181                f.close()
182        except (errors.NotBranchError, errors.NoWorkingTree):
183            pass
184
185        # Omit our own log...
186        logfile = self.repository.projectref().logfile
187        dir, file = split(logfile)
188        if dir == self.repository.basedir:
189            self.ignored.append(file)
190
191        # ... and state file
192        sfname = self.repository.projectref().state_file.filename
193        dir, file = split(sfname)
194        if dir == self.repository.basedir:
195            self.ignored.append(file)
196            self.ignored.append(file+'.old')
197            self.ignored.append(file+'.journal')
198
199        if version_info > (0,9):
200            add_runtime_ignores(self.ignored)
201        else:
202            DEFAULT_IGNORE.extend(self.ignored)
203
204
205    #############################
206    ## UpdatableSourceWorkingDir
207
208    def _changesetFromRevision(self, branch, revision_id):
209        """
210        Generate changeset for the given Bzr revision
211        """
212        from datetime import datetime
213        from vcpx.tzinfo import FixedOffset, UTC
214
215        revision = branch.repository.get_revision(revision_id)
216        deltatree = branch.get_revision_delta(branch.revision_id_to_revno(revision_id))
217        entries = []
218
219        for delta in deltatree.renamed:
220            e = ChangesetEntry(delta[1])
221            e.action_kind = ChangesetEntry.RENAMED
222            e.old_name = delta[0]
223            e.is_directory = delta[3] == 'directory'
224            entries.append(e)
225
226        for delta in deltatree.added:
227            e = ChangesetEntry(delta[0])
228            e.action_kind = ChangesetEntry.ADDED
229            e.is_directory = delta[2] == 'directory'
230            entries.append(e)
231
232        for delta in deltatree.removed:
233            e = ChangesetEntry(delta[0])
234            e.action_kind = ChangesetEntry.DELETED
235            e.is_directory = delta[2] == 'directory'
236            entries.append(e)
237
238        for delta in deltatree.modified:
239            e = ChangesetEntry(delta[0])
240            e.action_kind = ChangesetEntry.UPDATED
241            entries.append(e)
242
243        if revision.timezone is not None:
244            timezone = FixedOffset(revision.timezone / 60)
245        else:
246            timezone = UTC
247
248        return BzrChangeset(revision.revision_id,
249                            datetime.fromtimestamp(revision.timestamp, timezone),
250                            revision.get_apparent_author(),
251                            revision.message,
252                            entries)
253
254    def _getUpstreamChangesets(self, sincerev):
255        """
256        See what other revisions exist upstream and return them
257        """
258
259        from bzrlib import version_info
260
261        parent_branch = Branch.open(self.repository.repository)
262
263        branch = self._working_tree.branch
264        branch.lock_read()
265        try:
266            parent_branch.lock_read()
267            try:
268                if version_info > (1, 6):
269                    revisions = find_unmerged(branch, parent_branch, 'remote')[1]
270                else:
271                    revisions = find_unmerged(branch, parent_branch)[1]
272
273                self.log.info("Collecting %d missing changesets", len(revisions))
274
275                for id, revision in revisions:
276                    yield self._changesetFromRevision(parent_branch, revision)
277            except:
278                parent_branch.unlock()
279                raise
280            parent_branch.unlock()
281        except:
282            branch.unlock()
283            raise
284        branch.unlock()
285
286        self.log.info("Fetching concrete changesets")
287        branch.lock_write()
288        try:
289            branch.fetch(parent_branch)
290        finally:
291            branch.unlock()
292
293    def _applyChangeset(self, changeset):
294        """
295        Apply the given changeset to the working tree
296        """
297        parent_branch = BzrDir.open(self.repository.repository).open_branch()
298        self._working_tree.lock_write()
299        try:
300            count = self._working_tree.pull(parent_branch,
301                                            stop_revision=changeset.revision)
302            # XXX: this does not seem to return a true value on conflicts!
303            conflicts = self._working_tree.update()
304        finally:
305            self._working_tree.unlock()
306        try:
307            pulled_revnos = count.new_revno - count.old_revno
308        except AttributeError:
309            # Prior to 0.15 pull returned a simple integer instead of a result object
310            pulled_revnos = count
311        self.log.info('Updated to %r, applied %d changesets', changeset.revision, count)
312        if conflicts:
313            # No conflict handling yet
314            raise ChangesetApplicationFailure('Unsupported: conflicts')
315        return []
316
317    def _checkoutUpstreamRevision(self, revision):
318        """
319        Initial checkout of upstream branch, equivalent of 'bzr branch -r',
320        and return the last changeset.
321        """
322
323        from os.path import join, exists
324
325        if exists(join(self.repository.basedir, '.bzr')):
326            bzrdir = BzrDir.open(self.repository.basedir)
327            branch = bzrdir.open_branch()
328            self._working_tree = bzrdir.open_workingtree()
329            revid = self._working_tree.last_revision()
330            return self._changesetFromRevision(branch, revid)
331        else:
332            parent_bzrdir = BzrDir.open(self.repository.repository)
333            parent_branch = parent_bzrdir.open_branch()
334
335            if revision == "INITIAL":
336                try:
337                    revid = parent_branch.get_rev_id(1)
338                except NoSuchRevision:
339                    return None
340            elif revision == "HEAD":
341                revid = None
342            else:
343                revid = revision
344
345            self.log.info('Extracting %r out of %r in %r...',
346                          revid, parent_bzrdir.root_transport.base,
347                          self.repository.basedir)
348            bzrdir = parent_bzrdir.sprout(self.repository.basedir, revid)
349            self._working_tree = bzrdir.open_workingtree()
350
351            return self._changesetFromRevision(parent_branch, revid)
352
353    #################################
354    ## SynchronizableTargetWorkingDir
355
356    def _addPathnames(self, names):
357        if len(names):
358            names = [ pathjoin(self.repository.basedir, n) for n in names ]
359            self._working_tree.smart_add(names, recurse=False)
360
361    def _addSubtree(self, subdir):
362        subdir = pathjoin(self.repository.basedir, subdir)
363        added, ignored = self._working_tree.smart_add([subdir], recurse=True)
364
365        from vcpx.dualwd import IGNORED_METADIRS
366
367        for meta in IGNORED_METADIRS + self.ignored:
368            if ignored.has_key(meta):
369                del ignored[meta]
370
371        if len(ignored):
372            f = []
373            map(f.extend, ignored.values())
374            self._addPathnames(f)
375
376    def _commit(self, date, author, patchname, changelog=None, entries=None,
377                tags = [], isinitialcommit = False):
378        """
379        Commit the changeset.
380        """
381        from calendar import timegm  # like mktime(), but returns UTC timestamp
382        from binascii import hexlify
383        from re import search
384
385        logmessage = []
386        if patchname:
387            logmessage.append(patchname)
388        if changelog:
389            logmessage.append(changelog)
390        if logmessage:
391            self.log.info('Committing %s...', logmessage[0])
392            logmessage = '\n'.join(logmessage)
393        else:
394            self.log.info('Committing...')
395            logmessage = "Empty changelog"
396
397        timestamp = timegm(date.utctimetuple())
398        timezone  = date.utcoffset().seconds + date.utcoffset().days * 24 * 3600
399
400        # Normalize file names
401        if entries:
402            entries = [normpath(entry) for entry in entries]
403
404        self._working_tree.commit(logmessage, committer=author,
405                                  specific_files=entries,
406                                  verbose=self.repository.projectref().verbose,
407                                  timestamp=timestamp, timezone=timezone)
408
409    def _tag(self, tagname, date, author):
410        """
411        Tag the current version, if supported.
412        """
413        branch = self._working_tree.branch
414        try:
415            branch.tags.set_tag(tagname, branch.last_revision())
416        except TagsNotSupported:
417            pass
418
419    def _removePathnames(self, names):
420        """
421        Remove files from the tree.
422        """
423        self.log.info('Removing %s...', ', '.join(names))
424        names.sort(reverse=True) # remove files before the dir they're in
425        self._working_tree.remove(names)
426
427    def _renamePathname(self, oldname, newname):
428        """
429        Rename a file from oldname to newname.
430        """
431        self.log.info('Renaming %r to %r...', oldname, newname)
432        self._working_tree.rename_one(oldname, newname)
433
434    def _prepareTargetRepository(self):
435        from bzrlib import version_info
436        from vcpx.dualwd import IGNORED_METADIRS
437
438        if self._working_tree is None:
439            self._working_tree = self.repository.create()
440
441        if version_info > (0,9):
442            from bzrlib.ignores import add_runtime_ignores
443            add_runtime_ignores(IGNORED_METADIRS)
444        else:
445            from bzrlib import DEFAULT_IGNORE
446            DEFAULT_IGNORE.extend(IGNORED_METADIRS)
Note: See TracBrowser for help on using the repository browser.