| 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 | if not project_files: |
|---|
| 92 | return SynchronizableTargetWorkingDir._adaptChangeset(self, changeset) |
|---|
| 93 | |
|---|
| 94 | from copy import deepcopy |
|---|
| 95 | adapted = deepcopy(changeset) |
|---|
| 96 | |
|---|
| 97 | # |
|---|
| 98 | # adapt the entries: |
|---|
| 99 | # * delete entries with action_kind REMOVE not in the repository (DEL => ) |
|---|
| 100 | # * change to ADD a rename of a file non in the repository (REN => ADD) |
|---|
| 101 | # * remove from the changeset entries whith 2 operation (REN + UPD => REN); |
|---|
| 102 | # * change the ADD action_kind for files already in the repository (ADD => UPD); |
|---|
| 103 | # * change the UPD action_kind for files *non* in the repository (UPD => ADD); |
|---|
| 104 | # |
|---|
| 105 | for e in adapted.entries: |
|---|
| 106 | if e.action_kind == 'DEL' and project_files.count(e.name) == 0: |
|---|
| 107 | self.log.info("remove delete entry %s", e.name) |
|---|
| 108 | adapted.entries.remove(e) |
|---|
| 109 | |
|---|
| 110 | renamed_file = [] |
|---|
| 111 | for e in adapted.entries: |
|---|
| 112 | if e.action_kind == ChangesetEntry.RENAMED: |
|---|
| 113 | renamed_file.append(e.name) |
|---|
| 114 | |
|---|
| 115 | for e in adapted.entries: |
|---|
| 116 | if renamed_file.count(e.name) > 0 and e.action_kind != ChangesetEntry.RENAMED: |
|---|
| 117 | adapted.entries.remove(e) |
|---|
| 118 | if e.action_kind == ChangesetEntry.RENAMED and project_files.count(e.old_name) == 0: |
|---|
| 119 | e.action_kind = ChangesetEntry.ADDED |
|---|
| 120 | e.old_name = None |
|---|
| 121 | if e.action_kind == ChangesetEntry.ADDED and project_files.count(e.name) > 0: |
|---|
| 122 | e.action_kind = ChangesetEntry.UPDATED |
|---|
| 123 | elif e.action_kind == ChangesetEntry.UPDATED and project_files.count(e.name) == 0: |
|---|
| 124 | e.action_kind = ChangesetEntry.ADDED |
|---|
| 125 | |
|---|
| 126 | # |
|---|
| 127 | # Returns even if the changeset does not contain entries to |
|---|
| 128 | # give the opportunity to still register tags. |
|---|
| 129 | # |
|---|
| 130 | return SynchronizableTargetWorkingDir._adaptChangeset(self, adapted) |
|---|
| 131 | |
|---|
| 132 | def _initializeWorkingDir(self): |
|---|
| 133 | # |
|---|
| 134 | # This method is called only by importFirstRevision |
|---|
| 135 | # |
|---|
| 136 | self.__new_file('.') |
|---|
| 137 | |
|---|
| 138 | def _prepareWorkingDirectory(self, source_repo): |
|---|
| 139 | # |
|---|
| 140 | # Receive the first changeset from the source repository. |
|---|
| 141 | # |
|---|
| 142 | self.change_number = self.__new_change() |
|---|
| 143 | self.__develop_begin() |
|---|
| 144 | return True |
|---|
| 145 | |
|---|
| 146 | def _tag(self, tag, author, date): |
|---|
| 147 | self.__delta_name(tag) |
|---|
| 148 | |
|---|
| 149 | def _addSubtree(self, subdir): |
|---|
| 150 | # |
|---|
| 151 | # Aegis new_file command is recursive, there is no need to |
|---|
| 152 | # walk the directory tree. |
|---|
| 153 | # |
|---|
| 154 | pass |
|---|
| 155 | |
|---|
| 156 | def _addEntries(self, entries): |
|---|
| 157 | for e in entries: |
|---|
| 158 | if e.is_directory: |
|---|
| 159 | continue |
|---|
| 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 | if e.is_directory: |
|---|
| 173 | continue |
|---|
| 174 | self.__remove_file(e.name) |
|---|
| 175 | |
|---|
| 176 | def _removePathnames(self, names): |
|---|
| 177 | for name in names: |
|---|
| 178 | self.__remove_file(name) |
|---|
| 179 | |
|---|
| 180 | def _renameEntries(self, entries): |
|---|
| 181 | for e in entries: |
|---|
| 182 | if e.is_directory: |
|---|
| 183 | continue |
|---|
| 184 | self.__move_file(e.old_name, e.name) |
|---|
| 185 | |
|---|
| 186 | def _renamePathname(self, oldname, newname): |
|---|
| 187 | self.__move_file(oldname, newname) |
|---|
| 188 | |
|---|
| 189 | # |
|---|
| 190 | # The following methods wraps change's related aegis commands. |
|---|
| 191 | # |
|---|
| 192 | def __new_change(self, title = "none", description = "none"): |
|---|
| 193 | change_attr_file = \ |
|---|
| 194 | self.__change_attribute_file(brief_description = title, |
|---|
| 195 | description = description) |
|---|
| 196 | change_number_file = ReopenableNamedTemporaryFile('aegis', 'tailor') |
|---|
| 197 | cmd = self.repository.command("-new_change", |
|---|
| 198 | "-project", self.repository.module, |
|---|
| 199 | "-file", change_attr_file.name, |
|---|
| 200 | "-output", change_number_file.name) |
|---|
| 201 | new_change = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 202 | output = new_change.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 203 | if new_change.exit_status > 0: |
|---|
| 204 | raise ChangesetApplicationFailure( |
|---|
| 205 | "%s returned status %d, saying: %s" % (str(new_change), |
|---|
| 206 | new_change.exit_status, |
|---|
| 207 | output.read())) |
|---|
| 208 | fd = open(change_number_file.name, "rb") |
|---|
| 209 | change_number = fd.read() |
|---|
| 210 | fd.close() |
|---|
| 211 | return change_number.strip() |
|---|
| 212 | |
|---|
| 213 | def __develop_begin(self): |
|---|
| 214 | cmd = self.repository.command("-develop_begin", |
|---|
| 215 | "-project", self.repository.module, |
|---|
| 216 | "-change", self.change_number, |
|---|
| 217 | "-directory", self.repository.basedir) |
|---|
| 218 | develop_begin = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 219 | output = develop_begin.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 220 | if develop_begin.exit_status > 0: |
|---|
| 221 | raise ChangesetApplicationFailure( |
|---|
| 222 | "%s returned status %d, saying: %s" % |
|---|
| 223 | (str(develop_begin), develop_begin.exit_status, output.read())) |
|---|
| 224 | self.log.info(output.read()) |
|---|
| 225 | |
|---|
| 226 | def __develop_end(self): |
|---|
| 227 | self.__finish() |
|---|
| 228 | |
|---|
| 229 | def __integrate_begin(self): |
|---|
| 230 | cmd = self.repository.command("-integrate_begin", |
|---|
| 231 | "-project", self.repository.module, |
|---|
| 232 | "-change", self.change_number) |
|---|
| 233 | integrate_begin = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 234 | output = integrate_begin.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 235 | if integrate_begin.exit_status > 0: |
|---|
| 236 | raise ChangesetApplicationFailure( |
|---|
| 237 | "%s returned status %d, saying: %s" % |
|---|
| 238 | (str(integrate_begin), integrate_begin.exit_status, |
|---|
| 239 | output.read())) |
|---|
| 240 | |
|---|
| 241 | def __integrate_pass(self): |
|---|
| 242 | self.__finish() |
|---|
| 243 | |
|---|
| 244 | def __finish(self): |
|---|
| 245 | cmd = self.repository.command("-finish", |
|---|
| 246 | "-project", self.repository.module, |
|---|
| 247 | "-change", self.change_number) |
|---|
| 248 | finish = ExternalCommand(cwd="/tmp", command=cmd) |
|---|
| 249 | output = finish.execute(stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 250 | if finish.exit_status > 0: |
|---|
| 251 | raise ChangesetApplicationFailure( |
|---|
| 252 | "%s returned status %d, saying: %s" % |
|---|
| 253 | (str(finish), finish.exit_status, output.read())) |
|---|
| 254 | |
|---|
| 255 | def __change_attribute(self, file): |
|---|
| 256 | cmd = self.repository.command ("-change_attr", |
|---|
| 257 | "-project", self.repository.module, |
|---|
| 258 | "-change", self.change_number) |
|---|
| 259 | change_attr = ExternalCommand (cwd="/tmp", command=cmd) |
|---|
| 260 | output = change_attr.execute ("-file", file, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 261 | if change_attr.exit_status > 0: |
|---|
| 262 | raise ChangesetApplicationFailure( |
|---|
| 263 | "%s returned status %d, saying: %s" % |
|---|
| 264 | (str(change_attr), change_attr.exit_status, output.read())) |
|---|
| 265 | |
|---|
| 266 | def __delta_name (self, delta): |
|---|
| 267 | cmd = self.repository.command ("-delta_name", |
|---|
| 268 | "-project", self.repository.module) |
|---|
| 269 | delta_name = ExternalCommand (cwd="/tmp", command=cmd) |
|---|
| 270 | output = delta_name.execute (delta, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 271 | if delta_name.exit_status > 0: |
|---|
| 272 | raise ChangesetApplicationFailure( |
|---|
| 273 | "%s returned status %d, saying: %s" % |
|---|
| 274 | (str(delta_name), delta_name.exit_status, output.read())) |
|---|
| 275 | |
|---|
| 276 | # |
|---|
| 277 | # File's related methods. |
|---|
| 278 | # |
|---|
| 279 | def __new_file(self, file_names, usage = None): |
|---|
| 280 | # |
|---|
| 281 | # Tailor try to add also the aegis own log file and it's forbidden. |
|---|
| 282 | # |
|---|
| 283 | if file_names == "./aegis.log": |
|---|
| 284 | return |
|---|
| 285 | if usage == "config": |
|---|
| 286 | cmd = self.repository.command("-new_file", "-keep", "-config", |
|---|
| 287 | "-not-logging", |
|---|
| 288 | "-project", self.repository.module, |
|---|
| 289 | "-change", self.change_number) |
|---|
| 290 | else: |
|---|
| 291 | cmd = self.repository.command("-new_file", "-keep", "-not-logging", |
|---|
| 292 | "-project", self.repository.module, |
|---|
| 293 | "-change", self.change_number) |
|---|
| 294 | new_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 295 | command=cmd) |
|---|
| 296 | output = new_file.execute(file_names, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 297 | if new_file.exit_status > 0: |
|---|
| 298 | raise ChangesetApplicationFailure( |
|---|
| 299 | "%s returned status %d, saying: %s" % |
|---|
| 300 | (str(new_file), new_file.exit_status, output.read())) |
|---|
| 301 | |
|---|
| 302 | |
|---|
| 303 | def __copy_file(self, file_names): |
|---|
| 304 | cmd = self.repository.command("-copy", "-keep", |
|---|
| 305 | "-not-logging", |
|---|
| 306 | "-project", self.repository.module, |
|---|
| 307 | "-change", self.change_number) |
|---|
| 308 | copy_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 309 | command=cmd) |
|---|
| 310 | output = copy_file.execute(file_names, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 311 | if copy_file.exit_status > 0: |
|---|
| 312 | raise ChangesetApplicationFailure( |
|---|
| 313 | "%s returned status %d, saying: %s" % |
|---|
| 314 | (str(copy_file), copy_file.exit_status, output.read())) |
|---|
| 315 | |
|---|
| 316 | def __move_file(self, old_name, new_name): |
|---|
| 317 | # |
|---|
| 318 | # The aegis command to rename files does not have the -keep |
|---|
| 319 | # option to preserve the content of the file, do it manually. |
|---|
| 320 | # |
|---|
| 321 | fp = open(os.path.join(self.repository.basedir, new_name), 'rb') |
|---|
| 322 | content = fp.read() |
|---|
| 323 | fp.close() |
|---|
| 324 | cmd = self.repository.command("-move", |
|---|
| 325 | "-not-logging", |
|---|
| 326 | "-project", self.repository.module, |
|---|
| 327 | "-change", self.change_number) |
|---|
| 328 | move_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 329 | command=cmd) |
|---|
| 330 | output = move_file.execute(old_name, new_name, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 331 | if move_file.exit_status > 0: |
|---|
| 332 | raise ChangesetApplicationFailure( |
|---|
| 333 | "%s returned status %d, saying: %s" % |
|---|
| 334 | (str(move_file), move_file.exit_status, output.read())) |
|---|
| 335 | |
|---|
| 336 | # |
|---|
| 337 | # Restore the previously saved content of the renamed file. |
|---|
| 338 | # |
|---|
| 339 | fp = open(os.path.join(self.repository.basedir, new_name), 'wb') |
|---|
| 340 | fp.write(content) |
|---|
| 341 | fp.close() |
|---|
| 342 | |
|---|
| 343 | def __remove_file(self, file_name): |
|---|
| 344 | cmd = self.repository.command("-remove", |
|---|
| 345 | "-not-logging", |
|---|
| 346 | "-project", self.repository.module, |
|---|
| 347 | "-change", self.change_number) |
|---|
| 348 | remove_file = ExternalCommand(cwd=self.repository.basedir, |
|---|
| 349 | command=cmd) |
|---|
| 350 | output = remove_file.execute(file_name, stdout = PIPE, stderr = STDOUT)[0] |
|---|
| 351 | if remove_file.exit_status > 0: |
|---|
| 352 | raise ChangesetApplicationFailure( |
|---|
| 353 | "%s returned status %d, saying: %s" % |
|---|
| 354 | (str(remove_file), remove_file.exit_status, output.read())) |
|---|
| 355 | |
|---|
| 356 | def __change_attribute_file(self, *args, **kwargs): |
|---|
| 357 | """ |
|---|
| 358 | Create a temporary file to modify change's attributes. |
|---|
| 359 | """ |
|---|
| 360 | if kwargs['brief_description']: |
|---|
| 361 | brief_description = \ |
|---|
| 362 | self.repository.normalize(kwargs['brief_description']) |
|---|
| 363 | else: |
|---|
| 364 | brief_description = 'none' |
|---|
| 365 | if kwargs['description']: |
|---|
| 366 | description = \ |
|---|
| 367 | self.repository.normalize(kwargs['description']) |
|---|
| 368 | else: |
|---|
| 369 | description = "none" |
|---|
| 370 | attribute_file = ReopenableNamedTemporaryFile('aegis', 'tailor') |
|---|
| 371 | fd = open(attribute_file.name, 'wb') |
|---|
| 372 | fd.write(""" |
|---|
| 373 | brief_description = "%s"; |
|---|
| 374 | description = "%s"; |
|---|
| 375 | cause = external_improvement; |
|---|
| 376 | test_exempt = true; |
|---|
| 377 | test_baseline_exempt = true; |
|---|
| 378 | regression_test_exempt = true; |
|---|
| 379 | """ |
|---|
| 380 | % (brief_description, description)) |
|---|
| 381 | fd.close() |
|---|
| 382 | return attribute_file |
|---|
| 383 | |
|---|
| 384 | def __config_file(self, dir, name): |
|---|
| 385 | """ |
|---|
| 386 | Prepare the basic configuration to make aegis work: |
|---|
| 387 | * define a successfull build command (exit 0) |
|---|
| 388 | * define the history command |
|---|
| 389 | * define the merge commands |
|---|
| 390 | """ |
|---|
| 391 | c = open(os.path.join(dir, name), "wb", 0644) |
|---|
| 392 | c.write(""" |
|---|
| 393 | build_command = "exit 0"; |
|---|
| 394 | link_integration_directory = true; |
|---|
| 395 | |
|---|
| 396 | history_get_command = "aesvt -check-out -edit ${quote $edit} " |
|---|
| 397 | "-history ${quote $history} -f ${quote $output}"; |
|---|
| 398 | history_put_command = "aesvt -check-in -history ${quote $history} " |
|---|
| 399 | "-f ${quote $input}"; |
|---|
| 400 | history_query_command = "aesvt -query -history ${quote $history}"; |
|---|
| 401 | history_content_limitation = binary_capable; |
|---|
| 402 | |
|---|
| 403 | diff_command = "set +e; $diff $orig $i > $out; test $$? -le 1"; |
|---|
| 404 | merge_command = |
|---|
| 405 | "(diff3 -e $i $orig $mr | sed -e '/^w$$/d' -e '/^q$$/d'; echo '1,$$p') " |
|---|
| 406 | "| ed - $i > $out"; |
|---|
| 407 | patch_diff_command = |
|---|
| 408 | "set +e; $diff -C0 -L $index -L $index $orig $i > $out; test $$? -le 1"; |
|---|
| 409 | |
|---|
| 410 | shell_safe_filenames = false; |
|---|
| 411 | """) |
|---|
| 412 | c.close() |
|---|