source: tailor/vcpx/hglib.py @ 1005

Revision 1005, 11.0 KB checked in by Brendan Cully <brendan@…>, 8 years ago (diff)

Don't apply duplicate tags to mercurial targets

I just noticed that a CVS source will replay all of the tags that apply
to HEAD on every run. This teaches mercurial to filter out equivalent tags
(by checking whether they already exist in the commit history up to the last
non-tag commit).

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Mercurial native backend
3# :Creato:   dom 11 set 2005 22:58:38 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5#            Brendan Cully <brendan@kublai.com>
6# :Licenza:  GNU General Public License
7#
8
9"""
10This module implements the backends for Mercurial, using its native API
11instead of thru the command line.
12"""
13
14__docformat__ = 'reStructuredText'
15
16from source import UpdatableSourceWorkingDir
17from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
18from mercurial import ui, hg, commands, util
19import os
20
21class HglibWorkingDir(UpdatableSourceWorkingDir, SyncronizableTargetWorkingDir):
22    # UpdatableSourceWorkingDir
23    def _checkoutUpstreamRevision(self, revision):
24        """
25        Initial checkout (hg clone)
26        """
27
28        self._getUI()
29        # We have to clone the entire repository to be able to pull from it
30        # later. So a partial checkout is a full clone followed by an update
31        # directly to the desired revision.
32
33        # Hg won't check out into an existing directory
34        checkoutdir = os.path.join(self.basedir,".hgtmp")
35        commands.clone(self._ui, self.repository.repository, checkoutdir,
36                       noupdate=True, ssh=None, remotecmd=None)
37        os.rename(os.path.join(checkoutdir, ".hg"),
38                  os.path.join(self.basedir,".hg"))
39        os.rmdir(checkoutdir)
40
41        repo = self._getRepo()
42        node = self._getNode(repo, revision)
43
44        self.log.info('Extracting revision %s from %s into %s',
45                      revision, self.repository.repository, self.basedir)
46        repo.update(node)
47
48        return self._changesetForRevision(repo, revision)
49
50    def _getUpstreamChangesets(self, sincerev):
51        """Fetch new changesets from the source"""
52        ui = self._getUI()
53        repo = self._getRepo()
54
55        commands.pull(ui, repo, "default", ssh=None, remotecmd=None, update=None)
56
57        from mercurial.node import bin
58        for rev in xrange(repo.changelog.rev(bin(sincerev)) + 1, repo.changelog.count()):
59            yield self._changesetForRevision(repo, str(rev))
60
61    def _applyChangeset(self, changeset):
62        repo = self._getRepo()
63        node = self._getNode(repo, changeset.revision)
64
65        return repo.update(node)
66
67    def _changesetForRevision(self, repo, revision):
68        from changes import Changeset, ChangesetEntry
69        from datetime import datetime
70
71        entries = []
72        node = self._getNode(repo, revision)
73        parents = repo.changelog.parents(node)
74        (manifest, user, date, files, message) = repo.changelog.read(node)
75
76        # Different targets seem to handle the TZ differently. It looks like
77        # darcs may be the most correct.
78        (dt, tz) = date.split(' ')
79        date = datetime.fromtimestamp(int(dt) + int(tz))
80
81        manifest = repo.manifest.read(manifest)
82
83        # To find adds, we get the manifests of any parents. If a file doesn't
84        # occur there, it's new.
85        pms = {}
86        for parent in repo.changelog.parents(node):
87            pms.update(repo.manifest.read(repo.changelog.read(parent)[0]))
88
89        # if files contains only '.hgtags', this is probably a tag cset.
90        # Tailor appears to only support tagging the current version, so only
91        # pass on tags that are for the immediate parents of the current node
92        tags = None
93        if files == ['.hgtags']:
94            tags = [tag for (tag, tagnode) in repo.tags().iteritems()
95                    if tagnode in parents]
96            # Since this is a tag, the parent manifest contains everything.
97            # The only question is whether or not .hgtags existed before
98            if pms.has_key('.hgtags'):
99                pms = {'.hgtags': pms['.hgtags']}
100            else:
101                pms = {}
102
103        for f in files:
104            e = ChangesetEntry(f)
105            # find renames
106            fl = repo.file(f)
107            oldname = fl.renamed(manifest[f])
108            if oldname:
109                e.action_kind = ChangesetEntry.RENAMED
110                e.old_name = oldname[0]
111                pms.pop(oldname[0])
112            else:
113                if pms.has_key(f):
114                    e.action_kind = ChangesetEntry.UPDATED
115                else:
116                    e.action_kind = ChangesetEntry.ADDED
117
118            entries.append(e)
119
120        for df in [file for file in pms.iterkeys() if not manifest.has_key(file)]:
121            e = ChangesetEntry(df)
122            e.action_kind = ChangesetEntry.DELETED
123            entries.append(e)
124
125        from mercurial.node import hex
126        revision = hex(node)
127        return Changeset(revision, date, user, message, entries, tags=tags)
128
129    def _getUI(self):
130        try:
131            return self._ui
132        except AttributeError:
133            project = self.repository.projectref()
134            self._ui = ui.ui(project.verbose,
135                             project.config.get(self.repository.name,
136                                                'debug', False),
137                             not project.verbose, False)
138            return self._ui
139
140    def _getRepo(self):
141        try:
142            return self._hg
143        except AttributeError:
144            ui = self._getUI()
145            self._hg = hg.repository(ui=ui, path=self.basedir, create=False)
146            return self._hg
147
148    def _getNode(self, repo, revision):
149        """Convert a tailor revision ID into an hg node"""
150        if revision == "HEAD":
151            node = repo.changelog.tip()
152        else:
153            if revision == "INITIAL":
154                rev = "0"
155            else:
156                rev = revision
157            node = repo.changelog.lookup(rev)
158
159        return node
160
161    def _normalizeEntryPaths(self, entry):
162        """
163        Normalize the name and old_name of an entry.
164
165        This implementation uses ``mercurial.util.normpath()``, since
166        at this level hg is expecting UNIX style pathnames, with
167        forward slash"/" as separator, also under insane operating systems.
168        """
169
170        entry.name = util.normpath(entry.name)
171        if entry.old_name:
172            entry.old_name = util.normpath(entry.old_name)
173
174    def _addPathnames(self, names):
175        from os.path import join, isdir, normpath
176
177        notdirs = [n for n in names
178                   if not isdir(join(self.basedir, normpath(n)))]
179        if notdirs:
180            self._hg.add(notdirs)
181
182    def _commit(self, date, author, patchname, changelog=None, names=None):
183        from time import mktime
184
185        encoding = self.repository.encoding
186
187        logmessage = []
188        if patchname:
189            logmessage.append(patchname)
190        if changelog:
191            logmessage.append(changelog)
192        if logmessage:
193            logmessage = '\n'.join(logmessage).encode(encoding)
194        else:
195            logmessage = "Empty changelog"
196        self._hg.commit(names and [n.encode(encoding) for n in names] or [],
197                        logmessage, author.encode(encoding),
198                        "%d 0" % mktime(date.timetuple()))
199
200    def _tag(self, tag):
201        """ Tag the tip with a given identifier """
202        # TODO: keep a handle on the changeset holding this tag? Then
203        # we can extract author, log, date from it.
204        opts = self._defaultOpts('tag')
205
206        # This seems gross. I don't get why I'm getting a unicode tag when
207        # it's just ascii underneath. Something weird is happening in CVS.
208        tag = tag.encode(self.repository.encoding)
209        # CVS can't tell when a tag was applied so it tends to pass around
210        # too many. We want to support retagging so we can't just ignore
211        # duplicates. But we can safely ignore a tag if it is contained
212        # in the commit history from tip back to the last non-tag commit.
213        repo = self._getRepo()
214        tagnodes = repo.tags().values()
215        try:
216            tagnode = repo.tags()[tag]
217            # tag commit can't be merge, right?
218            parent = repo.changelog.parents(repo.changelog.tip())[0]
219            while parent in tagnodes:
220                if tagnode == parent:
221                    return
222                parent = repo.changelog.parents(parent)[0]
223        except KeyError:
224            pass
225        commands.tag(self._getUI(), repo, tag, **opts)
226
227    def _defaultOpts(self, cmd):
228        # Not sure this is public. commands.parse might be, but this
229        # is easier, and while dispatch is easiest, you lose ui.
230        return dict([(f[1], f[2]) for f in commands.find(cmd)[1][1]])
231
232    def _removePathnames(self, names):
233        """Remove a sequence of entries"""
234
235        from os.path import join, isdir, normpath
236
237        notdirs = [n for n in names
238                   if not isdir(join(self.basedir, normpath(n)))]
239        if notdirs:
240            self._hg.remove(notdirs)
241
242    def _renamePathname(self, oldname, newname):
243        """Rename an entry"""
244
245        from os.path import join, isdir, normpath
246
247        if isdir(join(self.basedir, normpath(newname))):
248            # Given lack of support for directories in current HG,
249            # loop over all files under the old directory and
250            # do a copy on them.
251            for src, oldpath in self._hg.dirstate.walk(oldname):
252                tail = oldpath[len(oldname)+2:]
253                self._hg.copy(oldpath, join(newname, tail))
254                self._hg.remove([oldpath])
255        else:
256            self._hg.copy(oldname, newname)
257            self._hg.remove([oldname])
258
259    def _prepareTargetRepository(self):
260        """
261        Create the base directory if it doesn't exist, and the
262        repository as well in the new working directory.
263        """
264
265        from os.path import join, exists
266
267        self._getUI()
268
269        if exists(join(self.basedir, self.repository.METADIR)):
270            create = 0
271        else:
272            create = 1
273        self._hg = hg.repository(ui=self._ui, path=self.basedir, create=create)
274
275    def _prepareWorkingDirectory(self, source_repo):
276        """
277        Create the .hgignore.
278        """
279
280        from os.path import join
281        from re import escape
282        from dualwd import IGNORED_METADIRS
283
284        # Create the .hgignore file, that contains a regexp per line
285        # with all known VCs metadirs to be skipped.
286        ignore = open(join(self.basedir, '.hgignore'), 'w')
287        ignore.write('\n'.join(['(^|/)%s($|/)' % escape(md)
288                                for md in IGNORED_METADIRS]))
289        ignore.write('\n')
290        if self.logfile.startswith(self.basedir):
291            ignore.write('^')
292            ignore.write(self.logfile[len(self.basedir)+1:])
293            ignore.write('$\n')
294        if self.state_file.filename.startswith(self.basedir):
295            sfrelname = self.state_file.filename[len(self.basedir)+1:]
296            ignore.write('^')
297            ignore.write(sfrelname)
298            ignore.write('$\n')
299            ignore.write('^')
300            ignore.write(sfrelname+'.old')
301            ignore.write('$\n')
302            ignore.write('^')
303            ignore.write(sfrelname+'.journal')
304            ignore.write('$\n')
305        ignore.close()
306
307    def _initializeWorkingDir(self):
308        commands.add(self._ui, self._hg, self.basedir)
Note: See TracBrowser for help on using the repository browser.