| [1221] | 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: Tailor -- Darcs peculiarities when used as a target |
|---|
| 3 | # :Creato: lun 10 lug 2006 00:12:15 CEST |
|---|
| 4 | # :Autore: Lele Gaifax <lele@nautilus.homeip.net> |
|---|
| 5 | # :Licenza: GNU General Public License |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | """ |
|---|
| 9 | This module contains the target specific bits of the darcs backend. |
|---|
| 10 | """ |
|---|
| 11 | |
|---|
| 12 | __docformat__ = 'reStructuredText' |
|---|
| 13 | |
|---|
| 14 | import re |
|---|
| 15 | |
|---|
| 16 | from vcpx.shwrap import ExternalCommand, PIPE, STDOUT |
|---|
| 17 | from vcpx.source import ChangesetApplicationFailure |
|---|
| 18 | from vcpx.target import SynchronizableTargetWorkingDir |
|---|
| 19 | from vcpx.tzinfo import UTC |
|---|
| 20 | |
|---|
| 21 | |
|---|
| 22 | MOTD = """\ |
|---|
| 23 | Tailorized equivalent of |
|---|
| 24 | %s |
|---|
| 25 | """ |
|---|
| 26 | |
|---|
| 27 | |
|---|
| 28 | class DarcsTargetWorkingDir(SynchronizableTargetWorkingDir): |
|---|
| 29 | """ |
|---|
| 30 | A target working directory under ``darcs``. |
|---|
| 31 | """ |
|---|
| 32 | |
|---|
| 33 | def _addPathnames(self, names): |
|---|
| 34 | """ |
|---|
| 35 | Add some new filesystem objects. |
|---|
| 36 | """ |
|---|
| 37 | |
|---|
| 38 | cmd = self.repository.command("add", "--case-ok", "--not-recursive", |
|---|
| 39 | "--quiet") |
|---|
| 40 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(names) |
|---|
| 41 | |
|---|
| 42 | def _addSubtree(self, subdir): |
|---|
| 43 | """ |
|---|
| 44 | Use the --recursive variant of ``darcs add`` to add a subtree. |
|---|
| 45 | """ |
|---|
| 46 | |
|---|
| 47 | cmd = self.repository.command("add", "--case-ok", "--recursive", |
|---|
| 48 | "--quiet") |
|---|
| 49 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(subdir) |
|---|
| 50 | |
|---|
| [1326] | 51 | def _commit(self, date, author, patchname, changelog=None, entries=None, |
|---|
| 52 | tags = []): |
|---|
| [1221] | 53 | """ |
|---|
| 54 | Commit the changeset. |
|---|
| 55 | """ |
|---|
| 56 | |
|---|
| 57 | logmessage = [] |
|---|
| 58 | |
|---|
| 59 | logmessage.append(date.astimezone(UTC).strftime('%Y/%m/%d %H:%M:%S UTC')) |
|---|
| 60 | logmessage.append(author) |
|---|
| 61 | if patchname: |
|---|
| 62 | logmessage.append(patchname) |
|---|
| 63 | if changelog: |
|---|
| 64 | logmessage.append(changelog) |
|---|
| 65 | if not patchname and not changelog: |
|---|
| 66 | logmessage.append('Unnamed patch') |
|---|
| 67 | |
|---|
| 68 | cmd = self.repository.command("record", "--all", "--pipe") |
|---|
| 69 | if not entries: |
|---|
| 70 | entries = ['.'] |
|---|
| 71 | |
|---|
| 72 | record = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 73 | record.execute(input=self.repository.encode('\n'.join(logmessage))) |
|---|
| 74 | |
|---|
| 75 | if record.exit_status: |
|---|
| 76 | raise ChangesetApplicationFailure( |
|---|
| 77 | "%s returned status %d" % (str(record), record.exit_status)) |
|---|
| 78 | |
|---|
| 79 | def _removePathnames(self, names): |
|---|
| 80 | """ |
|---|
| 81 | Remove some filesystem object. |
|---|
| 82 | """ |
|---|
| 83 | |
|---|
| 84 | from os.path import join, exists |
|---|
| 85 | |
|---|
| 86 | # darcs raises status 512 when it does not find the entry, |
|---|
| 87 | # removed by source. Since sometime a directory is left there |
|---|
| 88 | # because it's not empty, darcs fails. So, do an explicit |
|---|
| 89 | # remove on items that are still there. |
|---|
| 90 | |
|---|
| 91 | c = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 92 | command=self.repository.command("remove")) |
|---|
| 93 | existing = [n for n in names if exists(join(self.repository.basedir, n))] |
|---|
| 94 | if existing: |
|---|
| 95 | c.execute(existing) |
|---|
| 96 | |
|---|
| 97 | def _renamePathname(self, oldname, newname): |
|---|
| 98 | """ |
|---|
| 99 | Rename a filesystem object. |
|---|
| 100 | """ |
|---|
| 101 | |
|---|
| 102 | cmd = self.repository.command("mv") |
|---|
| 103 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(oldname, newname) |
|---|
| 104 | |
|---|
| 105 | def _prepareTargetRepository(self): |
|---|
| 106 | """ |
|---|
| 107 | Create the base directory if it doesn't exist, and execute |
|---|
| 108 | ``darcs initialize`` if needed. |
|---|
| 109 | """ |
|---|
| 110 | |
|---|
| 111 | from os.path import join, exists |
|---|
| 112 | |
|---|
| 113 | metadir = join(self.repository.basedir, '_darcs') |
|---|
| 114 | |
|---|
| 115 | if not exists(metadir): |
|---|
| 116 | self.repository.create() |
|---|
| 117 | |
|---|
| 118 | prefsdir = join(metadir, 'prefs') |
|---|
| 119 | prefsname = join(prefsdir, 'prefs') |
|---|
| 120 | boringname = join(prefsdir, 'boring') |
|---|
| 121 | if exists(prefsname): |
|---|
| 122 | for pref in open(prefsname, 'rU'): |
|---|
| 123 | if pref: |
|---|
| 124 | pname, pvalue = pref.split(' ', 1) |
|---|
| 125 | if pname == 'boringfile': |
|---|
| 126 | boringname = join(self.repository.basedir, pvalue[:-1]) |
|---|
| 127 | |
|---|
| 128 | boring = open(boringname, 'rU') |
|---|
| 129 | ignored = boring.read().rstrip().split('\n') |
|---|
| 130 | boring.close() |
|---|
| 131 | |
|---|
| 132 | # Build a list of compiled regular expressions, that will be |
|---|
| 133 | # used later to filter the entries. |
|---|
| 134 | self.__unwanted_entries = [re.compile(rx) for rx in ignored |
|---|
| 135 | if rx and not rx.startswith('#')] |
|---|
| 136 | |
|---|
| 137 | def _prepareWorkingDirectory(self, source_repo): |
|---|
| 138 | """ |
|---|
| 139 | Tweak the default settings of the repository. |
|---|
| 140 | """ |
|---|
| 141 | |
|---|
| 142 | from os.path import join |
|---|
| 143 | |
|---|
| 144 | motd = open(join(self.repository.basedir, '_darcs/prefs/motd'), 'w') |
|---|
| 145 | motd.write(MOTD % str(source_repo)) |
|---|
| 146 | motd.close() |
|---|
| 147 | |
|---|
| 148 | def _adaptEntries(self, changeset): |
|---|
| 149 | """ |
|---|
| 150 | Filter out boring files. |
|---|
| 151 | """ |
|---|
| 152 | |
|---|
| 153 | from copy import copy |
|---|
| 154 | |
|---|
| 155 | adapted = SynchronizableTargetWorkingDir._adaptEntries(self, changeset) |
|---|
| 156 | |
|---|
| 157 | # If there are no entries or no rules, there's nothing to do |
|---|
| 158 | if not adapted or not adapted.entries or not self.__unwanted_entries: |
|---|
| 159 | return adapted |
|---|
| 160 | |
|---|
| 161 | entries = [] |
|---|
| 162 | skipped = False |
|---|
| 163 | for e in adapted.entries: |
|---|
| 164 | skip = False |
|---|
| 165 | for rx in self.__unwanted_entries: |
|---|
| 166 | if rx.search(e.name): |
|---|
| 167 | skip = True |
|---|
| 168 | break |
|---|
| 169 | if skip: |
|---|
| 170 | self.log.info('Entry "%s" skipped per boring rules', e.name) |
|---|
| 171 | skipped = True |
|---|
| 172 | else: |
|---|
| 173 | entries.append(e) |
|---|
| 174 | |
|---|
| 175 | # All entries are gone, don't commit this changeset |
|---|
| 176 | if not entries: |
|---|
| 177 | self.log.info('All entries ignored, skipping whole ' |
|---|
| 178 | 'changeset "%s"', changeset.revision) |
|---|
| 179 | return None |
|---|
| 180 | |
|---|
| 181 | if skipped: |
|---|
| 182 | adapted = copy(adapted) |
|---|
| 183 | adapted.entries = entries |
|---|
| 184 | |
|---|
| 185 | return adapted |
|---|
| 186 | |
|---|
| 187 | def _tag(self, tag): |
|---|
| 188 | """ |
|---|
| 189 | Apply the given tag to the repository, unless it has already |
|---|
| 190 | been applied to the current state. (If it has been applied to |
|---|
| 191 | an earlier state, do apply it; the later tag overrides the |
|---|
| 192 | earlier one. |
|---|
| 193 | """ |
|---|
| 194 | if tag not in self._currentTags(): |
|---|
| 195 | cmd = self.repository.command("tag", "--author", "Unknown tagger") |
|---|
| 196 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(tag) |
|---|
| 197 | |
|---|
| 198 | def _currentTags(self): |
|---|
| 199 | """ |
|---|
| 200 | Return a list of tags that refer to the repository's current |
|---|
| 201 | state. Does not consider tags themselves to be part of the |
|---|
| 202 | state, so if the repo was tagged with T1 and then T2, then |
|---|
| 203 | both T1 and T2 are considered to refer to the current state, |
|---|
| 204 | even though 'darcs get --tag=T1' and 'darcs get --tag=T2' |
|---|
| 205 | would have different results (the latter creates a repo that |
|---|
| 206 | contains tag T2, but the former does not). |
|---|
| 207 | |
|---|
| 208 | This function assumes that a tag depends on all patches that |
|---|
| 209 | precede it in the "darcs changes" list. This assumption is |
|---|
| 210 | valid if tags only come into the repository via tailor; if the |
|---|
| 211 | user applies a tag by hand in the hybrid repository, or pulls |
|---|
| 212 | in a tag from another darcs repository, then the assumption |
|---|
| 213 | could be violated and mistagging could result. |
|---|
| 214 | """ |
|---|
| [1244] | 215 | |
|---|
| 216 | from vcpx.repository.darcs.source import changesets_from_darcschanges_unsafe |
|---|
| 217 | |
|---|
| [1221] | 218 | cmd = self.repository.command("changes", |
|---|
| 219 | "--from-match", "not name ^TAG", |
|---|
| 220 | "--xml-output", "--reverse") |
|---|
| 221 | changes = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 222 | output = changes.execute(stdout=PIPE, stderr=STDOUT)[0] |
|---|
| 223 | if changes.exit_status: |
|---|
| 224 | raise ChangesetApplicationFailure( |
|---|
| 225 | "%s returned status %d saying\n%s" % |
|---|
| 226 | (str(changes), changes.exit_status, output.read())) |
|---|
| 227 | |
|---|
| 228 | tags = [] |
|---|
| 229 | for cs in changesets_from_darcschanges_unsafe(output): |
|---|
| 230 | for tag in cs.tags: |
|---|
| 231 | if tag not in tags: |
|---|
| 232 | tags.append(tag) |
|---|
| 233 | return tags |
|---|