| 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 | """ |
|---|
| 10 | This module implements the backends for Mercurial, using its native API |
|---|
| 11 | instead of thru the command line. |
|---|
| 12 | """ |
|---|
| 13 | |
|---|
| 14 | __docformat__ = 'reStructuredText' |
|---|
| 15 | |
|---|
| 16 | from source import UpdatableSourceWorkingDir |
|---|
| 17 | from target import SyncronizableTargetWorkingDir, TargetInitializationFailure |
|---|
| 18 | from mercurial import ui, hg, commands, util |
|---|
| 19 | import os |
|---|
| 20 | |
|---|
| 21 | class 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) |
|---|