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

Revision 1193, 15.0 KB checked in by Andrea Arcangeli <andrea@…>, 7 years ago (diff)

Replace dashes with underscores in the hg command names

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.basedir):
64            mkdir(self.basedir)
65
66        # clone it only if .hg does not exist
67        if not exists(join(self.basedir, ".hg")):
68            # Hg won't check out into an existing directory
69            checkoutdir = join(self.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.basedir,".hg"))
75            rmdir(checkoutdir)
76        else:
77            # Does hgrc exist? If not, we write one
78            hgrc = join(self.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.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
125        entries = []
126        node = self._getNode(repo, revision)
127        parents = repo.changelog.parents(node)
128        (manifest, user, date, files, message) = repo.changelog.read(node)
129
130        # Different targets seem to handle the TZ differently. It looks like
131        # darcs may be the most correct.
132        dt, tz = date
133        date = datetime.fromtimestamp(int(dt) + int(tz))
134
135        manifest = repo.manifest.read(manifest)
136
137        # To find adds, we get the manifests of any parents. If a file doesn't
138        # occur there, it's new.
139        pms = {}
140        for parent in repo.changelog.parents(node):
141            pms.update(repo.manifest.read(repo.changelog.read(parent)[0]))
142
143        # if files contains only '.hgtags', this is probably a tag cset.
144        # Tailor appears to only support tagging the current version, so only
145        # pass on tags that are for the immediate parents of the current node
146        tags = None
147        if files == ['.hgtags']:
148            tags = [tag for (tag, tagnode) in repo.tags().iteritems()
149                    if tagnode in parents]
150
151        # Don't include the file itself in the changeset. It's only useful
152        # to mercurial, and if we do end up making a tailor round trip
153        # the nodes will be wrong anyway.
154        if '.hgtags' in files:
155            files.remove('.hgtags')
156        if pms.has_key('.hgtags'):
157            del pms['.hgtags']
158
159        for f in files:
160            e = ChangesetEntry(f)
161            # find renames
162            fl = repo.file(f)
163            oldname = f in manifest and fl.renamed(manifest[f])
164            if oldname:
165                e.action_kind = ChangesetEntry.RENAMED
166                e.old_name = oldname[0]
167                # hg copy can copy the same file to multiple destinations
168                # Currently this is handled as multiple renames. It would
169                # probably be better to have ChangesetEntry.COPIED.
170                if pms.has_key(oldname[0]):
171                    pms.pop(oldname[0])
172            else:
173                if pms.has_key(f):
174                    e.action_kind = ChangesetEntry.UPDATED
175                else:
176                    e.action_kind = ChangesetEntry.ADDED
177
178            entries.append(e)
179
180        for df in [file for file in pms.iterkeys()
181                   if not manifest.has_key(file)]:
182            e = ChangesetEntry(df)
183            e.action_kind = ChangesetEntry.DELETED
184            entries.append(e)
185
186        from mercurial.node import hex
187        revision = hex(node)
188        return Changeset(revision, date, user, message, entries, tags=tags)
189
190    def _getUI(self):
191        try:
192            return self._ui
193        except AttributeError:
194            project = self.repository.projectref()
195            self._ui = ui.ui(project.verbose,
196                             project.config.get(self.repository.name,
197                                                'debug', False),
198                             not project.verbose, False)
199            return self._ui
200
201    def _getRepo(self):
202        try:
203            return self._hg
204        except AttributeError:
205            # dirstate walker uses simple string comparison between
206            # repo root and os.getcwd, so root should be canonified.
207            from os.path import realpath
208
209            ui = self._getUI()
210            self._hg = hg.repository(ui=ui, path=realpath(self.basedir),
211                                     create=False)
212            # Pick up repository-specific UI settings.
213            self._ui = self._hg.ui
214            return self._hg
215
216    def _getNode(self, repo, revision):
217        """Convert a tailor revision ID into an hg node"""
218        if revision == "HEAD":
219            node = repo.changelog.tip()
220        else:
221            if revision == "INITIAL":
222                rev = "0"
223            else:
224                rev = revision
225            node = repo.changelog.lookup(rev)
226
227        return node
228
229    def _normalizeEntryPaths(self, entry):
230        """
231        Normalize the name and old_name of an entry.
232
233        This implementation uses ``mercurial.util.normpath()``, since
234        at this level hg is expecting UNIX style pathnames, with
235        forward slash"/" as separator, also under insane operating systems.
236        """
237
238        from mercurial.util import normpath
239
240        entry.name = normpath(self.repository.encode(entry.name))
241        if entry.old_name:
242            entry.old_name = normpath(self.repository.encode(entry.old_name))
243
244    def _addPathnames(self, names):
245        from os.path import join, isdir, normpath
246
247        notdirs = [n for n in names
248                   if not isdir(join(self.basedir, normpath(n)))]
249        if notdirs:
250            self.log.info('Adding %s...', ', '.join(notdirs))
251            self._hg.add(notdirs)
252
253    def _commit(self, date, author, patchname, changelog=None, names=[]):
254        from time import mktime
255
256        encode = self.repository.encode
257
258        logmessage = []
259        if patchname:
260            logmessage.append(patchname)
261        if changelog:
262            logmessage.append(changelog)
263        if logmessage:
264            self.log.info('Committing %r...', logmessage[0])
265            logmessage = encode('\n'.join(logmessage))
266        else:
267            self.log.info('Committing...')
268            logmessage = "Empty changelog"
269        opts = {}
270        opts['message'] = logmessage
271        opts['user'] = encode(author)
272        opts['date'] =  '%d 0' % mktime(date.timetuple())
273        self._hgCommand('commit', *[encode(n) for n in names], **opts)
274
275    def _tag(self, tag):
276        """ Tag the tip with a given identifier """
277        # TODO: keep a handle on the changeset holding this tag? Then
278        # we can extract author, log, date from it.
279
280        # This seems gross. I don't get why I'm getting a unicode tag when
281        # it's just ascii underneath. Something weird is happening in CVS.
282        tag = self.repository.encode(tag)
283
284        # CVS can't tell when a tag was applied so it tends to pass around
285        # too many. We want to support retagging so we can't just ignore
286        # duplicates. But we can safely ignore a tag if it is contained
287        # in the commit history from tip back to the last non-tag commit.
288        repo = self._getRepo()
289        tagnodes = repo.tags().values()
290        try:
291            tagnode = repo.tags()[tag]
292            # tag commit can't be merge, right?
293            parent = repo.changelog.parents(repo.changelog.tip())[0]
294            while parent in tagnodes:
295                if tagnode == parent:
296                    return
297                parent = repo.changelog.parents(parent)[0]
298        except KeyError:
299            pass
300        self._hgCommand('tag', tag)
301
302    def _defaultOpts(self, cmd):
303        # Not sure this is public. commands.parse might be, but this
304        # is easier, and while dispatch is easiest, you lose ui.
305        return dict([(f[1].replace('-', '_'), f[2]) for f in commands.find(cmd)[1][1]])
306
307    def _hgCommand(self, cmd, *args, **opts):
308        import os
309
310        allopts = self._defaultOpts(cmd)
311        allopts.update(opts)
312        cmd = getattr(commands, cmd)
313        cwd = os.getcwd()
314        os.chdir(self.basedir)
315        try:
316            cmd(self._ui, self._hg, *args, **allopts)
317        finally:
318            os.chdir(cwd)
319
320    def _removePathnames(self, names):
321        """Remove a sequence of entries"""
322
323        from os.path import join
324
325        repo = self._getRepo()
326
327        self.log.info('Removing %s...', ', '.join(names))
328        for name in names:
329            files = self._walk(name)
330            # We can't use isdir because the source has already
331            # removed the entry, so we do a dirstate lookup.
332            if files:
333                for f in self._walk(name):
334                    repo.remove([join(name, f)])
335            else:
336                repo.remove([name])
337
338    def _renamePathname(self, oldname, newname):
339        """Rename an entry"""
340
341        from os.path import join, isdir, normpath
342
343        repo = self._getRepo()
344
345        self.log.info('Renaming %r to %r...', oldname, newname)
346        if isdir(join(self.basedir, normpath(newname))):
347            # Given lack of support for directories in current HG,
348            # loop over all files under the old directory and
349            # do a copy on them.
350            for f in self._walk(oldname):
351                oldpath = join(oldname, f)
352                repo.copy(oldpath, join(newname, f))
353                repo.remove([oldpath])
354        else:
355            repo.copy(oldname, newname)
356            repo.remove([oldname])
357
358    def _prepareTargetRepository(self):
359        """
360        Create the base directory if it doesn't exist, and the
361        repository as well in the new working directory.
362        """
363
364        from os.path import join, exists, realpath
365
366        self._getUI()
367
368        if exists(join(self.basedir, self.repository.METADIR)):
369            create = 0
370        else:
371            create = 1
372            self.log.info('Initializing new repository in %r...', self.basedir)
373        self._hg = hg.repository(ui=self._ui, path=realpath(self.basedir),
374                                 create=create)
375
376    def _prepareWorkingDirectory(self, source_repo):
377        """
378        Create the .hgignore.
379        """
380
381        from os.path import join
382        from re import escape
383        from vcpx.dualwd import IGNORED_METADIRS
384
385        # Create the .hgignore file, that contains a regexp per line
386        # with all known VCs metadirs to be skipped.
387        ignore = open(join(self.basedir, '.hgignore'), 'w')
388        ignore.write('\n'.join(['(^|/)%s($|/)' % escape(md)
389                                for md in IGNORED_METADIRS]))
390        ignore.write('\n')
391        if self.logfile.startswith(self.basedir):
392            ignore.write('^')
393            ignore.write(self.logfile[len(self.basedir)+1:])
394            ignore.write('$\n')
395        if self.state_file.filename.startswith(self.basedir):
396            sfrelname = self.state_file.filename[len(self.basedir)+1:]
397            ignore.write('^')
398            ignore.write(sfrelname)
399            ignore.write('$\n')
400            ignore.write('^')
401            ignore.write(sfrelname+'.old')
402            ignore.write('$\n')
403            ignore.write('^')
404            ignore.write(sfrelname+'.journal')
405            ignore.write('$\n')
406        ignore.close()
407
408    def _initializeWorkingDir(self):
409        self._hgCommand('add')
410
411    def _walk(self, subdir):
412        """
413        Returns the files mercurial knows about under subdir, relative
414        to subdir.
415        """
416        from os.path import join, split
417
418        files = []
419        for src, path in self._getRepo().dirstate.walk([subdir]):
420            # If subdir is a plain file, just return
421            if path == subdir:
422                return None
423            (hd, tl) = split(path)
424            while hd != subdir:
425                hd, nt = split(hd)
426                tl = join(nt, tl)
427            files.append(tl)
428        return files
Note: See TracBrowser for help on using the repository browser.