| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: vcpx -- Aegis details |
|---|
| 3 | # :Creato: sab 24 mag 2008 15:44:00 CEST |
|---|
| 4 | # :Autore: Walter Franzini <walter.franzini@gmail.com> |
|---|
| 5 | # :Licenza: GNU General Public License |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | """ |
|---|
| 9 | This module contains the target specific bits of the Aegis backend. |
|---|
| 10 | """ |
|---|
| 11 | |
|---|
| 12 | __docformat__ = 'reStructuredText' |
|---|
| 13 | |
|---|
| 14 | import os |
|---|
| 15 | import os.path |
|---|
| 16 | import re |
|---|
| 17 | import shutil |
|---|
| 18 | |
|---|
| 19 | from vcpx.changes import ChangesetEntry |
|---|
| 20 | from vcpx.shwrap import ExternalCommand, ReopenableNamedTemporaryFile, PIPE, STDOUT |
|---|
| 21 | from vcpx.source import ChangesetApplicationFailure |
|---|
| 22 | from vcpx.target import SynchronizableTargetWorkingDir |
|---|
| 23 | |
|---|
| 24 | |
|---|
| 25 | class AegisTargetWorkingDir(SynchronizableTargetWorkingDir): |
|---|
| 26 | """ |
|---|
| 27 | A target working directory under ``Aegis``. |
|---|
| 28 | """ |
|---|
| 29 | |
|---|
| 30 | change_number = "not-a-number" |
|---|
| 31 | |
|---|
| 32 | def _commit(self, date, author, patchname, changelog=None, entries=None, |
|---|
| 33 | tags = [], isinitialcommit = False): |
|---|
| 34 | """ |
|---|
| 35 | Commit the changeset. |
|---|
| 36 | """ |
|---|
| 37 | |
|---|
| 38 | # |
|---|
| 39 | # The invocation for the initialcommit does not receive entries. |
|---|
| 40 | # |
|---|
| 41 | if isinitialcommit: |
|---|
| 42 | self.__new_file("aegis.conf", "config") |
|---|
| 43 | self.__config_file(self.repository.basedir, "aegis.conf") |
|---|
| 44 | elif not entries: |
|---|
| 45 | # |
|---|
| 46 | # Return successfully even if the changeset does not |
|---|
| 47 | # contain entries just in case it's a tag changeset. |
|---|
| 48 | # |
|---|
| 49 | return True |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | change_attribute_file = \ |
|---|
| 53 | self.__change_attribute_file(brief_description=patchname, |
|---|
| 54 | description=changelog) |
|---|
| 55 | self.__change_attribute(change_attribute_file.name) |
|---|
| 56 | self.__develop_end() |
|---|
| 57 | # |
|---|
| 58 | # If the change cannot be closed, e.g. because a file is |
|---|
| 59 | # modified in a change not yet integrated, then we should stop here. |
|---|
| 60 | # |
|---|
| 61 | # return False |
|---|
| 62 | # |
|---|
| 63 | # Next step only if develop_end => awaiting_integration |
|---|
| 64 | # |
|---|
| 65 | self.__integrate_begin() |
|---|
| 66 | self.__finish() |
|---|
| 67 | |
|---|
| 68 | def _prepareTargetRepository(self): |
|---|
| 69 | # |
|---|
| 70 | # Aegis refuse to use an already existing directory as the |
|---|
| 71 | # development directory of a change. |
|---|
| 72 | # |
|---|
| 73 | if os.path.exists(self.repository.basedir): |
|---|
| 74 | shutil.rmtree(self.repository.basedir) |
|---|
| 75 | |
|---|
| 76 | def _prepareToReplayChangeset(self, changeset): |
|---|
| 77 | """ |
|---|
| 78 | Runs aegis -New_Change -dir target.basedir |
|---|
| 79 | """ |
|---|
| 80 | |
|---|
| 81 | self._prepareTargetRepository() |
|---|
| 82 | self.change_number = self.__new_change(changeset.revision) |
|---|
| 83 | self.__develop_begin() |
|---|
| 84 | # |
|---|
| 85 | # This function MUST return |
|---|
| 86 | # |
|---|
| 87 | return True |
|---|
| 88 | |
|---|
| 89 | def _adaptChangeset(self, changeset): |
|---|
| 90 | project_files = self.repository.project_file_list_get() |
|---|
| 91 | |
|---|
| 92 | from copy import deepcopy |
|---|
| 93 | adapted = deepcopy(changeset) |
|---|
| 94 | |
|---|
| 95 | # |
|---|
| 96 | # adapt the entries: |
|---|
| 97 | # * delete directory entries |
|---|
| 98 | # * delete entries with action_kind REMOVE not in the repository (DEL => ) |
|---|
| 99 | # * change to ADD a rename of a file non in the repository (REN => ADD) |
|---|
| 100 | # * remove from the changeset entries whith 2 operation (REN + UPD => REN); |
|---|
| 101 | # * change the ADD action_kind for files already in the repository (ADD => UPD); |
|---|
| 102 | # * change the UPD action_kind for files *non* in the repository (UPD => ADD); |
|---|
| 103 | # |
|---|
| 104 | for e in adapted.entries[:]: |
|---|
| 105 | if e.is_directory: |
|---|
| 106 | adapted.entries.remove(e) |
|---|
| 107 | continue |
|---|
| 108 | if e.action_kind == e.DELETED and not project_files.count(e.name): |
|---|
| 109 | self.log.info("remove delete entry %s", e.name) |
|---|
| 110 | adapted.entries.remove(e) |
|---|
| 111 | |
|---|
| 112 | renamed_file = [] |
|---|
| 113 | for e in adapted.entries: |
|---|
| 114 | if e.action_kind == e.RENAMED: |
|---|
| 115 | renamed_file.append(e.name) |
|---|
| 116 | |
|---|
| 117 | for e in adapted.entries[:]: |
|---|
| 118 | if renamed_file.count(e.name) and e.action_kind != e.RENAMED: |
|---|
| 119 | adapted.entries.remove(e) |
|---|
| 120 | if e.action_kind == e.RENAMED and not project_files.count(e.old_name): |
|---|
| 121 | e.action_kind = e.ADDED |
|---|
| 122 | e.old_name = None |
|---|
| 123 | if e.action_kind == e.ADDED and project_files.count(e.name): |
|---|
| 124 | e.action_kind = ChangesetEntry.UPDATED |
|---|
| 125 | elif e.action_kind == e.UPDATED and not project_files.count(e.name): |
|---|
| 126 | e.action_kind = e.ADDED |
|---|
| 127 | |
|---|
| 128 | # |
|---|
| 129 | # Returns even if the changeset does not contain entries to |
|---|
| 130 | # give the opportunity to still register tags. |
|---|
| 131 | # |
|---|
| 132 | return SynchronizableTargetWorkingDir._adaptChangeset(self, adapted) |
|---|
| 133 | |
|---|
| 134 | def _initializeWorkingDir(self): |
|---|
| 135 | # |
|---|
| 136 | # This method is called only by importFirstRevision |
|---|
| 137 | # |
|---|
| 138 | self.__new_file('.') |
|---|
| 139 | |
|---|
| 140 | def _prepareWorkingDirectory(self, source_repo): |
|---|
| 141 | # |
|---|
| 142 | # Receive the first changeset from the source repository. |
|---|
| 143 | # |
|---|
| 144 | self.change_number = self.__new_change() |
|---|
| 145 | self.__develop_begin() |
|---|
| 146 | return True |
|---|
| 147 | |
|---|
| 148 | def _tag(self, tag, author, date): |
|---|
| 149 | self.__delta_name(tag) |
|---|
| 150 | |
|---|
| 151 | def _addSubtree(self, subdir): |
|---|
| 152 | # |
|---|
| 153 | # Aegis new_file command is recursive, there is no need to |
|---|
| 154 | # walk the directory tree. |
|---|
| 155 | # |
|---|
| 156 | pass |
|---|
| 157 | |
|---|
| 158 | def _addEntries(self, entries): |
|---|
| 159 | for e in entries: |
|---|
| 160 | self.__new_file(e.name) |
|---|
| 161 | |
|---|
| 162 | def _addPathnames(self, names): |
|---|
| 163 | for name in names: |
|---|
| 164 | self.__new_file(name) |
|---|
| 165 | |
|---|
| 166 | def _editPathnames(self, names): |
|---|
| 167 | for name in names: |
|---|
| 168 | self.__copy_file(name) |
|---|
| 169 | |
|---|
| 170 | def _removeEntries(self, entries): |
|---|
| 171 | for e in entries: |
|---|
| 172 | self.__remove_file(e.name) |
|---|
| 173 | |
|---|
| 174 | def _removePathnames(self, names): |
|---|
| 175 | for name in names: |
|---|
| 176 | self.__remove_file(name) |
|---|
| 177 | |
|---|
| 178 | def _renameEntries(self, entries): |
|---|
| 179 | for e in entries: |
|---|
| 180 | self.__move_file(e.old_name, e.name) |
|---|
| 181 | |
|---|
| 182 | def _renamePathname(self, oldname, newname): |
|---|
| 183 | self.__move_file(oldname, newname) |
|---|
| 184 | |
|---|
| 185 | # |
|---|
| 186 | # The following methods wraps change's related aegis commands. |
|---|
| 187 | # |
|---|
| 188 | def __new_change(self, title = "none", description = "none"): |
|---|
| 189 | change_attr_file = \ |
|---|
| 190 | self.__change_attribute_file(brief_description = title, |
|---|
| 191 | description = description) |
|---|
| 192 | change_number_file = ReopenableNamedTemporaryFile('aegis', 'tailor') |
|---|
| 193 | cmd = self.repository.command("-new_change", |
|---|
| 194 | "-project", self.repository.module, |
|---|
| 195 | "-file", change_attr_file.name, |
|---|
| 196 | "-output", change_number_file.name) |
|---|
| 197 | new_change = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 198 | output = new_change.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 199 | if new_change.exit_status > 0: |
|---|
| 200 | raise ChangesetApplicationFailure( |
|---|
| 201 | "%s returned status %d, saying: %s" % (str(new_change), |
|---|
| 202 | new_change.exit_status, |
|---|
| 203 | output.read())) |
|---|
| 204 | fd = open(change_number_file.name, "rb") |
|---|
| 205 | change_number = fd.read() |
|---|
| 206 | fd.close() |
|---|
| 207 | return change_number.strip() |
|---|
| 208 | |
|---|
| 209 | def __develop_begin(self): |
|---|
| 210 | cmd = self.repository.command("-develop_begin", |
|---|
| 211 | "-project", self.repository.module, |
|---|
| 212 | "-change", self.change_number, |
|---|
| 213 | "-directory", self.repository.basedir) |
|---|
| 214 | develop_begin = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 215 | output = develop_begin.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 216 | if develop_begin.exit_status > 0: |
|---|
| 217 | raise ChangesetApplicationFailure( |
|---|
| 218 | "%s returned status %d, saying: %s" % |
|---|
| 219 | (str(develop_begin), develop_begin.exit_status, output.read())) |
|---|
| 220 | self.log.info(output.read()) |
|---|
| 221 | |
|---|
| 222 | def __develop_end(self): |
|---|
| 223 | self.__finish() |
|---|
| 224 | |
|---|
| 225 | def __integrate_begin(self): |
|---|
| 226 | cmd = self.repository.command("-integrate_begin", |
|---|
| 227 | "-project", self.repository.module, |
|---|
| 228 | "-change", self.change_number) |
|---|
| 229 | integrate_begin = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 230 | output = integrate_begin.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 231 | if integrate_begin.exit_status > 0: |
|---|
| 232 | raise ChangesetApplicationFailure( |
|---|
| 233 | "%s returned status %d, saying: %s" % |
|---|
| 234 | (str(integrate_begin), integrate_begin.exit_status, |
|---|
| 235 | output.read())) |
|---|
| 236 | |
|---|
| 237 | def __integrate_pass(self): |
|---|
| 238 | self.__finish() |
|---|
| 239 | |
|---|
| 240 | def __finish(self): |
|---|
| 241 | cmd = self.repository.command("-finish", |
|---|
| 242 | "-project", self.repository.module, |
|---|
| 243 | "-change", self.change_number) |
|---|
| 244 | finish = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 245 | output = finish.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 246 | if finish.exit_status > 0: |
|---|
| 247 | raise ChangesetApplicationFailure( |
|---|
| 248 | "%s returned status %d, saying: %s" % |
|---|
| 249 | (str(finish), finish.exit_status, output.read())) |
|---|
| 250 | |
|---|
| 251 | def __change_attribute(self, file): |
|---|
| 252 | cmd = self.repository.command ("-change_attr", |
|---|
| 253 | "-project", self.repository.module, |
|---|
| 254 | "-change", self.change_number) |
|---|
| 255 | change_attr = ExternalCommand (cwd="/tmp", command=cmd) |
|---|
| 256 | output = change_attr.execute ("-file", file, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 257 | if change_attr.exit_status > 0: |
|---|
| 258 | raise ChangesetApplicationFailure( |
|---|
| 259 | "%s returned status %d, saying: %s" % |
|---|
| 260 | (str(change_attr), change_attr.exit_status, output.read())) |
|---|
| 261 | |
|---|
| 262 | def __delta_name (self, delta): |
|---|
| 263 | cmd = self.repository.command ("-delta_name", |
|---|
| 264 | "-project", self.repository.module) |
|---|
| 265 | delta_name = ExternalCommand (cwd="/tmp", command=cmd) |
|---|
| 266 | output = delta_name.execute (delta, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 267 | if delta_name.exit_status > 0: |
|---|
| 268 | raise ChangesetApplicationFailure( |
|---|
| 269 | "%s returned status %d, saying: %s" % |
|---|
| 270 | (str(delta_name), delta_name.exit_status, output.read())) |
|---|
| 271 | |
|---|
| 272 | # |
|---|
| 273 | # File's related methods. |
|---|
| 274 | # |
|---|
| 275 | def __new_file(self, file_names, usage = None): |
|---|
| 276 | # |
|---|
| 277 | # Tailor try to add also the aegis own log file and it's forbidden. |
|---|
| 278 | # |
|---|
| 279 | if file_names == "./aegis.log": |
|---|
| 280 | return |
|---|
| 281 | if usage == "config": |
|---|
| 282 | cmd = self.repository.command("-new_file", "-keep", "-config", |
|---|
| 283 | "-not-logging", |
|---|
| 284 | "-project", self.repository.module, |
|---|
| 285 | "-change", self.change_number) |
|---|
| 286 | else: |
|---|
| 287 | cmd = self.repository.command("-new_file", "-keep", "-not-logging", |
|---|
| 288 | "-project", self.repository.module, |
|---|
| 289 | "-change", self.change_number) |
|---|
| 290 | new_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 291 | command=cmd) |
|---|
| 292 | output = new_file.execute(file_names, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 293 | if new_file.exit_status > 0: |
|---|
| 294 | raise ChangesetApplicationFailure( |
|---|
| 295 | "%s returned status %d, saying: %s" % |
|---|
| 296 | (str(new_file), new_file.exit_status, output.read())) |
|---|
| 297 | |
|---|
| 298 | |
|---|
| 299 | def __copy_file(self, file_names): |
|---|
| 300 | cmd = self.repository.command("-copy", "-keep", |
|---|
| 301 | "-not-logging", |
|---|
| 302 | "-project", self.repository.module, |
|---|
| 303 | "-change", self.change_number) |
|---|
| 304 | copy_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 305 | command=cmd) |
|---|
| 306 | output = copy_file.execute(file_names, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 307 | if copy_file.exit_status > 0: |
|---|
| 308 | raise ChangesetApplicationFailure( |
|---|
| 309 | "%s returned status %d, saying: %s" % |
|---|
| 310 | (str(copy_file), copy_file.exit_status, output.read())) |
|---|
| 311 | |
|---|
| 312 | def __move_file(self, old_name, new_name): |
|---|
| 313 | # |
|---|
| 314 | # The aegis command to rename files does not have the -keep |
|---|
| 315 | # option to preserve the content of the file, do it manually. |
|---|
| 316 | # |
|---|
| 317 | fp = open(os.path.join(self.repository.basedir, new_name), 'rb') |
|---|
| 318 | content = fp.read() |
|---|
| 319 | fp.close() |
|---|
| 320 | cmd = self.repository.command("-move", |
|---|
| 321 | "-not-logging", |
|---|
| 322 | "-project", self.repository.module, |
|---|
| 323 | "-change", self.change_number) |
|---|
| 324 | move_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 325 | command=cmd) |
|---|
| 326 | output = move_file.execute(old_name, new_name, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 327 | if move_file.exit_status > 0: |
|---|
| 328 | raise ChangesetApplicationFailure( |
|---|
| 329 | "%s returned status %d, saying: %s" % |
|---|
| 330 | (str(move_file), move_file.exit_status, output.read())) |
|---|
| 331 | |
|---|
| 332 | # |
|---|
| 333 | # Restore the previously saved content of the renamed file. |
|---|
| 334 | # |
|---|
| 335 | fp = open(os.path.join(self.repository.basedir, new_name), 'wb') |
|---|
| 336 | fp.write(content) |
|---|
| 337 | fp.close() |
|---|
| 338 | |
|---|
| 339 | def __remove_file(self, file_name): |
|---|
| 340 | cmd = self.repository.command("-remove", |
|---|
| 341 | "-not-logging", |
|---|
| 342 | "-project", self.repository.module, |
|---|
| 343 | "-change", self.change_number) |
|---|
| 344 | remove_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 345 | command=cmd) |
|---|
| 346 | output = remove_file.execute(file_name, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 347 | if remove_file.exit_status > 0: |
|---|
| 348 | raise ChangesetApplicationFailure( |
|---|
| 349 | "%s returned status %d, saying: %s" % |
|---|
| 350 | (str(remove_file), remove_file.exit_status, output.read())) |
|---|
| 351 | |
|---|
| 352 | def __change_attribute_file(self, *args, **kwargs): |
|---|
| 353 | """ |
|---|
| 354 | Create a temporary file to modify change's attributes. |
|---|
| 355 | """ |
|---|
| 356 | if kwargs['brief_description']: |
|---|
| 357 | brief_description = \ |
|---|
| 358 | self.repository.normalize(kwargs['brief_description']) |
|---|
| 359 | else: |
|---|
| 360 | brief_description = 'none' |
|---|
| 361 | if kwargs['description']: |
|---|
| 362 | description = \ |
|---|
| 363 | self.repository.normalize(kwargs['description']) |
|---|
| 364 | else: |
|---|
| 365 | description = "none" |
|---|
| 366 | attribute_file = ReopenableNamedTemporaryFile('aegis', 'tailor') |
|---|
| 367 | fd = open(attribute_file.name, 'wb') |
|---|
| 368 | fd.write(""" |
|---|
| 369 | brief_description = "%s"; |
|---|
| 370 | description = "%s"; |
|---|
| 371 | cause = external_improvement; |
|---|
| 372 | test_exempt = true; |
|---|
| 373 | test_baseline_exempt = true; |
|---|
| 374 | regression_test_exempt = true; |
|---|
| 375 | """ |
|---|
| 376 | % (brief_description, description)) |
|---|
| 377 | fd.close() |
|---|
| 378 | return attribute_file |
|---|
| 379 | |
|---|
| 380 | def __config_file(self, dir, name): |
|---|
| 381 | """ |
|---|
| 382 | Prepare the basic configuration to make aegis work: |
|---|
| 383 | * define a successfull build command (exit 0) |
|---|
| 384 | * define the history command |
|---|
| 385 | * define the merge commands |
|---|
| 386 | """ |
|---|
| 387 | c = open(os.path.join(dir, name), "wb", 0644) |
|---|
| 388 | c.write(""" |
|---|
| 389 | build_command = "exit 0"; |
|---|
| 390 | link_integration_directory = true; |
|---|
| 391 | |
|---|
| 392 | history_get_command = "aesvt -check-out -edit ${quote $edit} " |
|---|
| 393 | "-history ${quote $history} -f ${quote $output}"; |
|---|
| 394 | history_put_command = "aesvt -check-in -history ${quote $history} " |
|---|
| 395 | "-f ${quote $input}"; |
|---|
| 396 | history_query_command = "aesvt -query -history ${quote $history}"; |
|---|
| 397 | history_content_limitation = binary_capable; |
|---|
| 398 | |
|---|
| 399 | diff_command = "set +e; $diff $orig $i > $out; test $$? -le 1"; |
|---|
| 400 | merge_command = |
|---|
| 401 | "(diff3 -e $i $orig $mr | sed -e '/^w$$/d' -e '/^q$$/d'; echo '1,$$p') " |
|---|
| 402 | "| ed - $i > $out"; |
|---|
| 403 | patch_diff_command = |
|---|
| 404 | "set +e; $diff -C0 -L $index -L $index $orig $i > $out; test $$? -le 1"; |
|---|
| 405 | |
|---|
| 406 | shell_safe_filenames = false; |
|---|
| 407 | """) |
|---|
| 408 | c.close() |
|---|