source: tailor/vcpx/repository/hg.py @ 1220

Revision 1220, 15.4 KB checked in by lele@…, 7 years ago (diff)

Support both commands.find() and commands.findcmd() as the former was recently renamed

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 mercurial import ui, hg, commands
17
18from vcpx.repository import Repository
19from vcpx.source import UpdatableSourceWorkingDir
20from vcpx.target import SynchronizableTargetWorkingDir
21
22
23class HgRepository(Repository):
24    METADIR = '.hg'
25
26    def _load(self, project):
27        Repository._load(self, project)
28        ppath = project.config.get(self.name, 'python-path')
29        if ppath:
30            from sys import path
31
32            if ppath not in path:
33                path.insert(0, ppath)
34        self.EXTRA_METADIRS = ['.hgtags']
35
36    def _validateConfiguration(self):
37        """
38        Mercurial expects all data to be in utf-8, so we disallow other encodings
39        """
40        Repository._validateConfiguration(self)
41
42        if self.encoding.upper() != 'UTF-8':
43            self.log.warning("Forcing UTF-8 encoding instead of " + self.encoding)
44            self.encoding = 'UTF-8'
45
46
47class HgWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir):
48    # UpdatableSourceWorkingDir
49    def _checkoutUpstreamRevision(self, revision):
50        """
51        Initial checkout (hg clone)
52        """
53
54        from os import mkdir, rename, rmdir
55        from os.path import exists, join
56
57        self._getUI()
58        # We have to clone the entire repository to be able to pull from it
59        # later. So a partial checkout is a full clone followed by an update
60        # directly to the desired revision.
61
62        # If the basedir does not exist, create it
63        if not exists(self.repository.basedir):
64            mkdir(self.repository.basedir)
65
66        # clone it only if .hg does not exist
67        if not exists(join(self.repository.basedir, ".hg")):
68            # Hg won't check out into an existing directory
69            checkoutdir = join(self.repository.basedir,".hgtmp")
70            opts = self._defaultOpts('clone')
71            opts['noupdate'] = True
72            commands.clone(self._ui, self.repository.repository, checkoutdir,
73                           **opts)
74            rename(join(checkoutdir, ".hg"), join(self.repository.basedir,".hg"))
75            rmdir(checkoutdir)
76        else:
77            # Does hgrc exist? If not, we write one
78            hgrc = join(self.repository.basedir, ".hg", "hgrc")
79            if not exists(hgrc):
80                hgrc = file(hgrc, "w")
81                hgrc.write("[paths]\ndefault = %s\ndefault-push = %s\n" %
82                           (self.repository.repository,
83                            self.repository.repository))
84                hgrc.close()
85
86        repo = self._getRepo()
87        node = self._getNode(repo, revision)
88
89        self.log.info('Extracting revision %r from %r into %r',
90                      revision, self.repository.repository, self.repository.basedir)
91        repo.update(node)
92
93        return self._changesetForRevision(repo, revision)
94
95    def _getUpstreamChangesets(self, sincerev):
96        """Fetch new changesets from the source"""
97        repo = self._getRepo()
98
99        self._hgCommand('pull', 'default')
100
101        from mercurial.node import bin
102        for rev in xrange(repo.changelog.rev(bin(sincerev)) + 1,
103                          repo.changelog.count()):
104            yield self._changesetForRevision(repo, str(rev))
105
106    def _applyChangeset(self, changeset):
107        repo = self._getRepo()
108        node = self._getNode(repo, changeset.revision)
109
110        self.log.info('Updating to %r', changeset.revision)
111        res = repo.update(node)
112        if res:
113            # Files in to-be-merged changesets not on the trunk will
114            # cause a merge error on update. If no files are modified,
115            # added, removed, or deleted, do update -C
116            modified = [x for x in repo.changes()[0:4] if x]
117            if modified:
118                return modified
119            return repo.update(node, force=True)
120
121    def _changesetForRevision(self, repo, revision):
122        from datetime import datetime
123        from vcpx.changes import Changeset, ChangesetEntry
124        from vcpx.tzinfo import FixedOffset
125
126        entries = []
127        node = self._getNode(repo, revision)
128        parents = repo.changelog.parents(node)
129        (manifest, user, date, files, message) = repo.changelog.read(node)
130
131        dt, tz = date
132        date = datetime.fromtimestamp(dt, FixedOffset(-tz/60)) # note the minus sign!
133
134        manifest = repo.manifest.read(manifest)
135
136        # To find adds, we get the manifests of any parents. If a file doesn't
137        # occur there, it's new.
138        pms = {}
139        for parent in repo.changelog.parents(node):
140            pms.update(repo.manifest.read(repo.changelog.read(parent)[0]))
141
142        # if files contains only '.hgtags', this is probably a tag cset.
143        # Tailor appears to only support tagging the current version, so only
144        # pass on tags that are for the immediate parents of the current node
145        tags = None
146        if files == ['.hgtags']:
147            tags = [tag for (tag, tagnode) in repo.tags().iteritems()
148                    if tagnode in parents]
149
150        # Don't include the file itself in the changeset. It's only useful
151        # to mercurial, and if we do end up making a tailor round trip
152        # the nodes will be wrong anyway.
153        if '.hgtags' in files:
154            files.remove('.hgtags')
155        if pms.has_key('.hgtags'):
156            del pms['.hgtags']
157
158        for f in files:
159            e = ChangesetEntry(f)
160            # find renames
161            fl = repo.file(f)
162            oldname = f in manifest and fl.renamed(manifest[f])
163            if oldname:
164                e.action_kind = ChangesetEntry.RENAMED
165                e.old_name = oldname[0]
166                # hg copy can copy the same file to multiple destinations
167                # Currently this is handled as multiple renames. It would
168                # probably be better to have ChangesetEntry.COPIED.
169                if pms.has_key(oldname[0]):
170                    pms.pop(oldname[0])
171            else:
172                if pms.has_key(f):
173                    e.action_kind = ChangesetEntry.UPDATED
174                else:
175                    e.action_kind = ChangesetEntry.ADDED
176
177            entries.append(e)
178
179        for df in [file for file in pms.iterkeys()
180                   if not manifest.has_key(file)]:
181            e = ChangesetEntry(df)
182            e.action_kind = ChangesetEntry.DELETED
183            entries.append(e)
184
185        from mercurial.node import hex
186        revision = hex(node)
187        return Changeset(revision, date, user, message, entries, tags=tags)
188
189    def _getUI(self):
190        try:
191            return self._ui
192        except AttributeError:
193            project = self.repository.projectref()
194            self._ui = ui.ui(project.verbose,
195                             project.config.get(self.repository.name,
196                                                'debug', False),
197                             not project.verbose, False)
198            return self._ui
199
200    def _getRepo(self):
201        try:
202            return self._hg
203        except AttributeError:
204            # dirstate walker uses simple string comparison between
205            # repo root and os.getcwd, so root should be canonified.
206            from os.path import realpath
207
208            ui = self._getUI()
209            self._hg = hg.repository(ui=ui, path=realpath(self.repository.basedir),
210                                     create=False)
211            # Pick up repository-specific UI settings.
212            self._ui = self._hg.ui
213            return self._hg
214
215    def _getNode(self, repo, revision):
216        """Convert a tailor revision ID into an hg node"""
217        if revision == "HEAD":
218            node = repo.changelog.tip()
219        else:
220            if revision == "INITIAL":
221                rev = "0"
222            else:
223                rev = revision
224            node = repo.changelog.lookup(rev)
225
226        return node
227
228    def _normalizeEntryPaths(self, entry):
229        """
230        Normalize the name and old_name of an entry.
231
232        This implementation uses ``mercurial.util.normpath()``, since
233        at this level hg is expecting UNIX style pathnames, with
234        forward slash"/" as separator, also under insane operating systems.
235        """
236
237        from mercurial.util import normpath
238
239        entry.name = normpath(self.repository.encode(entry.name))
240        if entry.old_name:
241            entry.old_name = normpath(self.repository.encode(entry.old_name))
242
243    def _addPathnames(self, names):
244        from os.path import join, isdir, normpath
245
246        notdirs = [n for n in names
247                   if not isdir(join(self.repository.basedir, normpath(n)))]
248        if notdirs:
249            self.log.info('Adding %s...', ', '.join(notdirs))
250            self._hg.add(notdirs)
251
252    def _commit(self, date, author, patchname, changelog=None, names=[]):
253        from calendar import timegm  # like mktime(), but returns UTC timestamp
254
255        encode = self.repository.encode
256
257        logmessage = []
258        if patchname:
259            logmessage.append(patchname)
260        if changelog:
261            logmessage.append(changelog)
262        if logmessage:
263            self.log.info('Committing %r...', logmessage[0])
264            logmessage = encode('\n'.join(logmessage))
265        else:
266            self.log.info('Committing...')
267            logmessage = "Empty changelog"
268
269        timestamp = timegm(date.utctimetuple())
270        timezone  = date.utcoffset().seconds + date.utcoffset().days * 24 * 3600
271
272        opts = {}
273        opts['message'] = logmessage
274        opts['user'] = encode(author)
275        opts['date'] =  '%d %d' % (timestamp, -timezone) # note the minus sign!
276        self._hgCommand('commit', *[encode(n) for n in names], **opts)
277
278    def _tag(self, tag):
279        """ Tag the tip with a given identifier """
280        # TODO: keep a handle on the changeset holding this tag? Then
281        # we can extract author, log, date from it.
282
283        # This seems gross. I don't get why I'm getting a unicode tag when
284        # it's just ascii underneath. Something weird is happening in CVS.
285        tag = self.repository.encode(tag)
286
287        # CVS can't tell when a tag was applied so it tends to pass around
288        # too many. We want to support retagging so we can't just ignore
289        # duplicates. But we can safely ignore a tag if it is contained
290        # in the commit history from tip back to the last non-tag commit.
291        repo = self._getRepo()
292        tagnodes = repo.tags().values()
293        try:
294            tagnode = repo.tags()[tag]
295            # tag commit can't be merge, right?
296            parent = repo.changelog.parents(repo.changelog.tip())[0]
297            while parent in tagnodes:
298                if tagnode == parent:
299                    return
300                parent = repo.changelog.parents(parent)[0]
301        except KeyError:
302            pass
303        self._hgCommand('tag', tag)
304
305    def _defaultOpts(self, cmd):
306        # Not sure this is public. commands.parse might be, but this
307        # is easier, and while dispatch is easiest, you lose ui.
308        if hasattr(commands, 'findcmd'):
309            findcmd = commands.findcmd
310        else:
311            findcmd = commands.find
312        return dict([(f[1].replace('-', '_'), f[2]) for f in findcmd(cmd)[1][1]])
313
314    def _hgCommand(self, cmd, *args, **opts):
315        import os
316
317        allopts = self._defaultOpts(cmd)
318        allopts.update(opts)
319        cmd = getattr(commands, cmd)
320        cwd = os.getcwd()
321        os.chdir(self.repository.basedir)
322        try:
323            cmd(self._ui, self._hg, *args, **allopts)
324        finally:
325            os.chdir(cwd)
326
327    def _removePathnames(self, names):
328        """Remove a sequence of entries"""
329
330        from os.path import join
331
332        repo = self._getRepo()
333
334        self.log.info('Removing %s...', ', '.join(names))
335        for name in names:
336            files = self._walk(name)
337            # We can't use isdir because the source has already
338            # removed the entry, so we do a dirstate lookup.
339            if files:
340                for f in self._walk(name):
341                    repo.remove([join(name, f)])
342            else:
343                repo.remove([name])
344
345    def _renamePathname(self, oldname, newname):
346        """Rename an entry"""
347
348        from os.path import join, isdir, normpath
349
350        repo = self._getRepo()
351
352        self.log.info('Renaming %r to %r...', oldname, newname)
353        if isdir(join(self.repository.basedir, normpath(newname))):
354            # Given lack of support for directories in current HG,
355            # loop over all files under the old directory and
356            # do a copy on them.
357            for f in self._walk(oldname):
358                oldpath = join(oldname, f)
359                repo.copy(oldpath, join(newname, f))
360                repo.remove([oldpath])
361        else:
362            repo.copy(oldname, newname)
363            repo.remove([oldname])
364
365    def _prepareTargetRepository(self):
366        """
367        Create the base directory if it doesn't exist, and the
368        repository as well in the new working directory.
369        """
370
371        from os.path import join, exists, realpath
372
373        self._getUI()
374
375        if exists(join(self.repository.basedir, self.repository.METADIR)):
376            create = 0
377        else:
378            create = 1
379            self.log.info('Initializing new repository in %r...', self.repository.basedir)
380        self._hg = hg.repository(ui=self._ui, path=realpath(self.repository.basedir),
381                                 create=create)
382
383    def _prepareWorkingDirectory(self, source_repo):
384        """
385        Create the .hgignore.
386        """
387
388        from os.path import join
389        from re import escape
390        from vcpx.dualwd import IGNORED_METADIRS
391
392        # Create the .hgignore file, that contains a regexp per line
393        # with all known VCs metadirs to be skipped.
394        ignore = open(join(self.repository.basedir, '.hgignore'), 'w')
395        ignore.write('\n'.join(['(^|/)%s($|/)' % escape(md)
396                                for md in IGNORED_METADIRS]))
397        ignore.write('\n')
398        if self.logfile.startswith(self.repository.basedir):
399            ignore.write('^')
400            ignore.write(self.logfile[len(self.repository.basedir)+1:])
401            ignore.write('$\n')
402        if self.state_file.filename.startswith(self.repository.basedir):
403            sfrelname = self.state_file.filename[len(self.repository.basedir)+1:]
404            ignore.write('^')
405            ignore.write(sfrelname)
406            ignore.write('$\n')
407            ignore.write('^')
408            ignore.write(sfrelname+'.old')
409            ignore.write('$\n')
410            ignore.write('^')
411            ignore.write(sfrelname+'.journal')
412            ignore.write('$\n')
413        ignore.close()
414
415    def _initializeWorkingDir(self):
416        self._hgCommand('add')
417
418    def _walk(self, subdir):
419        """
420        Returns the files mercurial knows about under subdir, relative
421        to subdir.
422        """
423        from os.path import join, split
424
425        files = []
426        for src, path in self._getRepo().dirstate.walk([subdir]):
427            # If subdir is a plain file, just return
428            if path == subdir:
429                return None
430            (hd, tl) = split(path)
431            while hd != subdir:
432                hd, nt = split(hd)
433                tl = join(nt, tl)
434            files.append(tl)
435        return files
Note: See TracBrowser for help on using the repository browser.