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

Revision 1298, 15.7 KB checked in by Brendan Cully <brendan@…>, 6 years ago (diff)

hg: make findcmd call compatible with 0.9.1 and 0.9.2.

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, added, removed, deleted = repo.changes()[0:4]
117            conflicting = modified + added + removed + deleted
118            if conflicting:
119                return conflicting
120            return repo.update(node, force=True)
121
122    def _changesetForRevision(self, repo, revision):
123        from datetime import datetime
124        from vcpx.changes import Changeset, ChangesetEntry
125        from vcpx.tzinfo import FixedOffset
126
127        entries = []
128        node = self._getNode(repo, revision)
129        parents = repo.changelog.parents(node)
130        (manifest, user, date, files, message) = repo.changelog.read(node)
131
132        dt, tz = date
133        date = datetime.fromtimestamp(dt, FixedOffset(-tz/60)) # note the minus sign!
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.repository.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.repository.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 calendar import timegm  # like mktime(), but returns UTC timestamp
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
270        timestamp = timegm(date.utctimetuple())
271        timezone  = date.utcoffset().seconds + date.utcoffset().days * 24 * 3600
272
273        opts = {}
274        opts['message'] = logmessage
275        opts['user'] = encode(author)
276        opts['date'] =  '%d %d' % (timestamp, -timezone) # note the minus sign!
277        self._hgCommand('commit', *[encode(n) for n in names], **opts)
278
279    def _tag(self, tag):
280        """ Tag the tip with a given identifier """
281        # TODO: keep a handle on the changeset holding this tag? Then
282        # we can extract author, log, date from it.
283
284        # This seems gross. I don't get why I'm getting a unicode tag when
285        # it's just ascii underneath. Something weird is happening in CVS.
286        tag = self.repository.encode(tag)
287
288        # CVS can't tell when a tag was applied so it tends to pass around
289        # too many. We want to support retagging so we can't just ignore
290        # duplicates. But we can safely ignore a tag if it is contained
291        # in the commit history from tip back to the last non-tag commit.
292        repo = self._getRepo()
293        tagnodes = repo.tags().values()
294        try:
295            tagnode = repo.tags()[tag]
296            # tag commit can't be merge, right?
297            parent = repo.changelog.parents(repo.changelog.tip())[0]
298            while parent in tagnodes:
299                if tagnode == parent:
300                    return
301                parent = repo.changelog.parents(parent)[0]
302        except KeyError:
303            pass
304        self._hgCommand('tag', tag)
305
306    def _defaultOpts(self, cmd):
307        # Not sure this is public. commands.parse might be, but this
308        # is easier, and while dispatch is easiest, you lose ui.
309        if hasattr(commands, 'findcmd'):
310            if commands.findcmd.func_code.co_argcount == 1:
311                findcmd = commands.findcmd
312            else:
313                def findcmd(cmd):
314                    return commands.findcmd(self._getUI(), cmd)
315        else:
316            findcmd = commands.find
317        return dict([(f[1].replace('-', '_'), f[2]) for f in findcmd(cmd)[1][1]])
318
319    def _hgCommand(self, cmd, *args, **opts):
320        import os
321
322        allopts = self._defaultOpts(cmd)
323        allopts.update(opts)
324        cmd = getattr(commands, cmd)
325        cwd = os.getcwd()
326        os.chdir(self.repository.basedir)
327        try:
328            cmd(self._ui, self._hg, *args, **allopts)
329        finally:
330            os.chdir(cwd)
331
332    def _removePathnames(self, names):
333        """Remove a sequence of entries"""
334
335        from os.path import join
336
337        repo = self._getRepo()
338
339        self.log.info('Removing %s...', ', '.join(names))
340        for name in names:
341            files = self._walk(name)
342            # We can't use isdir because the source has already
343            # removed the entry, so we do a dirstate lookup.
344            if files:
345                for f in self._walk(name):
346                    repo.remove([join(name, f)])
347            else:
348                repo.remove([name])
349
350    def _renamePathname(self, oldname, newname):
351        """Rename an entry"""
352
353        from os.path import join, isdir, normpath
354
355        repo = self._getRepo()
356
357        self.log.info('Renaming %r to %r...', oldname, newname)
358        if isdir(join(self.repository.basedir, normpath(newname))):
359            # Given lack of support for directories in current HG,
360            # loop over all files under the old directory and
361            # do a copy on them.
362            for f in self._walk(oldname):
363                oldpath = join(oldname, f)
364                repo.copy(oldpath, join(newname, f))
365                repo.remove([oldpath])
366        else:
367            repo.copy(oldname, newname)
368            repo.remove([oldname])
369
370    def _prepareTargetRepository(self):
371        """
372        Create the base directory if it doesn't exist, and the
373        repository as well in the new working directory.
374        """
375
376        from os.path import join, exists, realpath
377
378        self._getUI()
379
380        if exists(join(self.repository.basedir, self.repository.METADIR)):
381            create = 0
382        else:
383            create = 1
384            self.log.info('Initializing new repository in %r...', self.repository.basedir)
385        self._hg = hg.repository(ui=self._ui, path=realpath(self.repository.basedir),
386                                 create=create)
387
388    def _prepareWorkingDirectory(self, source_repo):
389        """
390        Create the .hgignore.
391        """
392
393        from os.path import join
394        from re import escape
395        from vcpx.dualwd import IGNORED_METADIRS
396
397        # Create the .hgignore file, that contains a regexp per line
398        # with all known VCs metadirs to be skipped.
399        ignore = open(join(self.repository.basedir, '.hgignore'), 'w')
400        ignore.write('\n'.join(['(^|/)%s($|/)' % escape(md)
401                                for md in IGNORED_METADIRS]))
402        ignore.write('\n')
403        if self.logfile.startswith(self.repository.basedir):
404            ignore.write('^')
405            ignore.write(self.logfile[len(self.repository.basedir)+1:])
406            ignore.write('$\n')
407        if self.state_file.filename.startswith(self.repository.basedir):
408            sfrelname = self.state_file.filename[len(self.repository.basedir)+1:]
409            ignore.write('^')
410            ignore.write(sfrelname)
411            ignore.write('$\n')
412            ignore.write('^')
413            ignore.write(sfrelname+'.old')
414            ignore.write('$\n')
415            ignore.write('^')
416            ignore.write(sfrelname+'.journal')
417            ignore.write('$\n')
418        ignore.close()
419
420    def _initializeWorkingDir(self):
421        self._hgCommand('add')
422
423    def _walk(self, subdir):
424        """
425        Returns the files mercurial knows about under subdir, relative
426        to subdir.
427        """
428        from os.path import join, split
429
430        files = []
431        for src, path in self._getRepo().dirstate.walk([subdir]):
432            # If subdir is a plain file, just return
433            if path == subdir:
434                return None
435            (hd, tl) = split(path)
436            while hd != subdir and hd != '':
437                hd, nt = split(hd)
438                tl = join(nt, tl)
439            files.append(tl)
440        return files
Note: See TracBrowser for help on using the repository browser.