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

Revision 1487, 18.2 KB checked in by lele@…, 5 years ago (diff)

Check both the oldname and the newname to see if its a directory rename

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, cmdutil, 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
113        # The following code is for backward compatibility: hg 0.9.5
114        # raises an Abort exception instead of just returning a status;
115        # but under 0.9.5 we reimplanted hg.clean() into repo.update():
116        # hg.clean() performs a clobbering clean merge and thus does
117        # not stop on that situation.
118        if res:
119            # Files in to-be-merged changesets not on the trunk will
120            # cause a merge error on update. If no files are modified,
121            # added, removed, or deleted, do update -C
122            modified, added, removed, deleted = repo.changes()[0:4]
123            conflicting = modified + added + removed + deleted
124            if conflicting:
125                return conflicting
126            return repo.update(node, force=True)
127
128    def _changesetForRevision(self, repo, revision):
129        from datetime import datetime
130        from vcpx.changes import Changeset, ChangesetEntry
131        from vcpx.tzinfo import FixedOffset
132
133        entries = []
134        node = self._getNode(repo, revision)
135        parents = repo.changelog.parents(node)
136        nodecontent = repo.changelog.read(node)
137        # hg 0.9.5+ returns a tuple of six elements, last seems useless for us
138        (manifest, user, date, files, message) = nodecontent[:5]
139
140        dt, tz = date
141        date = datetime.fromtimestamp(dt, FixedOffset(-tz/60)) # note the minus sign!
142
143        manifest = repo.manifest.read(manifest)
144
145        # To find adds, we get the manifests of any parents. If a file doesn't
146        # occur there, it's new.
147        pms = {}
148        for parent in repo.changelog.parents(node):
149            pms.update(repo.manifest.read(repo.changelog.read(parent)[0]))
150
151        # if files contains only '.hgtags', this is probably a tag cset.
152        # Tailor appears to only support tagging the current version, so only
153        # pass on tags that are for the immediate parents of the current node
154        tags = None
155        if files == ['.hgtags']:
156            tags = [tag for (tag, tagnode) in repo.tags().iteritems()
157                    if tagnode in parents]
158
159        # Don't include the file itself in the changeset. It's only useful
160        # to mercurial, and if we do end up making a tailor round trip
161        # the nodes will be wrong anyway.
162        if '.hgtags' in files:
163            files.remove('.hgtags')
164        if pms.has_key('.hgtags'):
165            del pms['.hgtags']
166
167        for f in files:
168            e = ChangesetEntry(f)
169            # find renames
170            fl = repo.file(f)
171            oldname = f in manifest and fl.renamed(manifest[f])
172            if oldname:
173                e.action_kind = ChangesetEntry.RENAMED
174                e.old_name = oldname[0]
175                # hg copy can copy the same file to multiple destinations
176                # Currently this is handled as multiple renames. It would
177                # probably be better to have ChangesetEntry.COPIED.
178                if pms.has_key(oldname[0]):
179                    pms.pop(oldname[0])
180            else:
181                if pms.has_key(f):
182                    e.action_kind = ChangesetEntry.UPDATED
183                else:
184                    e.action_kind = ChangesetEntry.ADDED
185
186            entries.append(e)
187
188        for df in [file for file in pms.iterkeys()
189                   if not manifest.has_key(file)]:
190            e = ChangesetEntry(df)
191            e.action_kind = ChangesetEntry.DELETED
192            entries.append(e)
193
194        from mercurial.node import hex
195        revision = hex(node)
196        return Changeset(revision, date, user, message, entries, tags=tags)
197
198    def _getUI(self):
199        try:
200            return self._ui
201        except AttributeError:
202            project = self.repository.projectref()
203            self._ui = ui.ui(project.verbose,
204                             project.config.get(self.repository.name,
205                                                'debug', False),
206                             not project.verbose, False)
207            return self._ui
208
209    def _getRepo(self):
210        try:
211            return self._hg
212        except AttributeError:
213            # dirstate walker uses simple string comparison between
214            # repo root and os.getcwd, so root should be canonified.
215            from os.path import realpath
216
217            ui = self._getUI()
218            self._hg = hg.repository(ui=ui, path=realpath(self.repository.basedir),
219                                     create=False)
220            # Pick up repository-specific UI settings.
221            self._ui = self._hg.ui
222
223            # 0.9.5 repos does not have update()...
224            if not hasattr(self._hg, 'update'):
225                # Use clean(), to force a clean merge clobbering local changes
226                self._hg.update = lambda n: hg.clean(self._hg, n)
227
228            return self._hg
229
230    def _getNode(self, repo, revision):
231        """Convert a tailor revision ID into an hg node"""
232        if revision == "HEAD":
233            node = repo.changelog.tip()
234        else:
235            if revision == "INITIAL":
236                rev = "0"
237            else:
238                rev = revision
239            node = repo.changelog.lookup(rev)
240
241        return node
242
243    def _normalizeEntryPaths(self, entry):
244        """
245        Normalize the name and old_name of an entry.
246
247        This implementation uses ``mercurial.util.normpath()``, since
248        at this level hg is expecting UNIX style pathnames, with
249        forward slash"/" as separator, also under insane operating systems.
250        """
251
252        from mercurial.util import normpath
253
254        entry.name = normpath(self.repository.encode(entry.name))
255        if entry.old_name:
256            entry.old_name = normpath(self.repository.encode(entry.old_name))
257
258    def _removeDirs(self, names):
259        from os.path import isdir, join, normpath
260        """Remove the names that reference a directory."""
261        return [n for n in names
262                if not isdir(join(self.repository.basedir, normpath(n)))]
263        return notdirs
264
265    def _addPathnames(self, names):
266        from os.path import join
267
268        notdirs = self._removeDirs(names)
269        if notdirs:
270            self.log.info('Adding %s...', ', '.join(notdirs))
271            self._hg.add(notdirs)
272
273    def _commit(self, date, author, patchname, changelog=None, names=[],
274                tags = [], isinitialcommit = False):
275        from calendar import timegm  # like mktime(), but returns UTC timestamp
276        from os.path import exists, join, normpath
277
278        encode = self.repository.encode
279
280        logmessage = []
281        if patchname:
282            logmessage.append(patchname)
283        if changelog:
284            logmessage.append(changelog)
285        if logmessage:
286            self.log.info('Committing %r...', logmessage[0])
287            logmessage = encode('\n'.join(logmessage))
288        else:
289            self.log.info('Committing...')
290            logmessage = "Empty changelog"
291
292        timestamp = timegm(date.utctimetuple())
293        timezone  = date.utcoffset().seconds + date.utcoffset().days * 24 * 3600
294
295        opts = {}
296        opts['message'] = logmessage
297        opts['user'] = encode(author)
298        opts['date'] =  '%d %d' % (timestamp, -timezone) # note the minus sign!
299        notdirs = self._removeDirs(names)
300        if (not isinitialcommit) and len(notdirs) == 0 and \
301               (tags is None or len(tags) == 0):
302            # Empty changeset; make sure we still see it
303            empty = open(join(self.repository.basedir, '.hgempty'), 'a')
304            empty.write("\nEmpty original changeset by %s:\n" % author)
305            empty.write(logmessage + "\n")
306            empty.close()
307            self._hg.add(['.hgempty'])
308        self._hgCommand('commit', **opts)
309
310    def _tag(self, tag, date, author):
311        """ Tag the tip with a given identifier """
312        # TODO: keep a handle on the changeset holding this tag? Then
313        # we can extract author, log, date from it.
314
315        # This seems gross. I don't get why I'm getting a unicode tag when
316        # it's just ascii underneath. Something weird is happening in CVS.
317        tag = self.repository.encode(tag)
318
319        # CVS can't tell when a tag was applied so it tends to pass around
320        # too many. We want to support retagging so we can't just ignore
321        # duplicates. But we can safely ignore a tag if it is contained
322        # in the commit history from tip back to the last non-tag commit.
323        repo = self._getRepo()
324        tagnodes = repo.tags().values()
325        try:
326            tagnode = repo.tags()[tag]
327            # tag commit can't be merge, right?
328            parent = repo.changelog.parents(repo.changelog.tip())[0]
329            while parent in tagnodes:
330                if tagnode == parent:
331                    return
332                parent = repo.changelog.parents(parent)[0]
333        except KeyError:
334            pass
335        self._hgCommand('tag', tag)
336
337    def _defaultOpts(self, cmd):
338        # Not sure this is public. commands.parse might be, but this
339        # is easier, and while dispatch is easiest, you lose ui.
340        # findxxx() is not public, and to make that clear, hg folks
341        # keep moving the function around...
342        if hasattr(cmdutil, 'findcmd'):            # >= 0.9.4
343            if cmdutil.findcmd.func_code.co_argcount == 2:     # 0.9.4
344                def findcmd(cmd):
345                    return cmdutil.findcmd(self._getUI(), cmd)
346            elif cmdutil.findcmd.func_code.co_argcount == 3:   # 0.9.5
347                def findcmd(cmd):
348                    return cmdutil.findcmd(self._getUI(), cmd, commands.table)
349        elif hasattr(commands, 'findcmd'):         # < 0.9.4
350            if commands.findcmd.func_code.co_argcount == 1:
351                findcmd = commands.findcmd
352            else:
353                def findcmd(cmd):
354                    return commands.findcmd(self._getUI(), cmd)
355        elif hasattr(commands, 'find'):            # ancient hg
356            findcmd = commands.find
357        else:
358            raise RuntimeError("unable to locate mercurial's 'findcmd()'")
359        return dict([(f[1].replace('-', '_'), f[2]) for f in findcmd(cmd)[1][1]])
360
361    def _hgCommand(self, cmd, *args, **opts):
362        import os
363
364        allopts = self._defaultOpts(cmd)
365        allopts.update(opts)
366        cmd = getattr(commands, cmd)
367        cwd = os.getcwd()
368        os.chdir(self.repository.basedir)
369        try:
370            cmd(self._ui, self._hg, *args, **allopts)
371        finally:
372            os.chdir(cwd)
373
374    def _removePathnames(self, names):
375        """Remove a sequence of entries"""
376
377        from os.path import join
378
379        repo = self._getRepo()
380
381        self.log.info('Removing %s...', ', '.join(names))
382        for name in names:
383            files = self._walk(name)
384            # We can't use isdir because the source has already
385            # removed the entry, so we do a dirstate lookup.
386            if files:
387                for f in self._walk(name):
388                    repo.remove([join(name, f)], unlink=True)
389            else:
390                repo.remove([name], unlink=True)
391
392    def _renamePathname(self, oldname, newname):
393        """Rename an entry"""
394
395        from os.path import join, isdir, normpath
396
397        repo = self._getRepo()
398
399        self.log.info('Renaming %r to %r...', oldname, newname)
400        # Check both names, because maybe we are operating in
401        # disjunct dirs, and the target may be renamed to a
402        # temporary name
403        if (isdir(join(self.repository.basedir, normpath(oldname)))
404            or isdir(join(self.repository.basedir, normpath(newname)))):
405            # Given lack of support for directories in current HG,
406            # loop over all files under the old directory and
407            # do a copy on them.
408            for f in self._walk(oldname):
409                oldpath = join(oldname, f)
410                repo.copy(oldpath, join(newname, f))
411                repo.remove([oldpath], unlink=True)
412        else:
413            repo.copy(oldname, newname)
414            repo.remove([oldname], unlink=True)
415
416    def _prepareTargetRepository(self):
417        """
418        Create the base directory if it doesn't exist, and the
419        repository as well in the new working directory.
420        """
421
422        from os.path import join, exists, realpath
423
424        self._getUI()
425
426        if exists(join(self.repository.basedir, self.repository.METADIR)):
427            create = 0
428        else:
429            create = 1
430            self.log.info('Initializing new repository in %r...', self.repository.basedir)
431        self._hg = hg.repository(ui=self._ui, path=realpath(self.repository.basedir),
432                                 create=create)
433
434    def _prepareWorkingDirectory(self, source_repo):
435        """
436        Create the .hgignore.
437        """
438
439        from os.path import join
440        from re import escape
441        from vcpx.dualwd import IGNORED_METADIRS
442
443        # Create the .hgignore file, that contains a regexp per line
444        # with all known VCs metadirs to be skipped.
445        ignore = open(join(self.repository.basedir, '.hgignore'), 'w')
446        ignore.write('\n'.join(['(^|/)%s($|/)' % escape(md)
447                                for md in IGNORED_METADIRS]))
448        ignore.write('\n')
449        if self.logfile.startswith(self.repository.basedir):
450            ignore.write('^')
451            ignore.write(self.logfile[len(self.repository.basedir)+1:])
452            ignore.write('$\n')
453        if self.state_file.filename.startswith(self.repository.basedir):
454            sfrelname = self.state_file.filename[len(self.repository.basedir)+1:]
455            ignore.write('^')
456            ignore.write(sfrelname)
457            ignore.write('$\n')
458            ignore.write('^')
459            ignore.write(sfrelname+'.old')
460            ignore.write('$\n')
461            ignore.write('^')
462            ignore.write(sfrelname+'.journal')
463            ignore.write('$\n')
464        ignore.close()
465        self._hg.add(['.hgignore'])
466        self._hgCommand('commit', '.hgignore',
467                        message = 'Tailor preparing to convert repo by adding .hgignore')
468
469    def _initializeWorkingDir(self):
470        self._hgCommand('add')
471
472    def _walk(self, subdir):
473        """
474        Returns the files mercurial knows about under subdir, relative
475        to subdir.
476        """
477        from os.path import join, split
478
479        files = []
480        for src, path in self._getRepo().dirstate.walk([subdir]):
481            # If subdir is a plain file, just return
482            if path == subdir:
483                return None
484            (hd, tl) = split(path)
485            while hd != subdir and hd != '':
486                hd, nt = split(hd)
487                tl = join(nt, tl)
488            files.append(tl)
489        return files
Note: See TracBrowser for help on using the repository browser.