Changeset 799 in tailor for vcpx/monotone.py
- Timestamp:
- 09/11/05 12:18:10 (8 years ago)
- Hash name:
- 20050911101810-97f81-4c44e1c3b57f65b4af73f77b01f6ee8e73df8f69
- File:
-
- 1 edited
-
vcpx/monotone.py (modified) (41 diffs)
Legend:
- Unmodified
- Added
- Removed
-
vcpx/monotone.py
r796 r799 31 31 class ExternalCommandChain: 32 32 """ 33 This class implements command piping, i.e. a chain of ExternalCommand, each feeding 34 its stdout to the stdin of next command in the chain 35 If a command fails, the chain breaks and returns error. 33 This class implements command piping, i.e. a chain of 34 ExternalCommand, each feeding its stdout to the stdin of next 35 command in the chain If a command fails, the chain breaks and 36 returns error. 37 36 38 Note: 37 39 This class implements only a subset of ExternalCommand functionality … … 41 43 self.cwd = cwd 42 44 self.exit_status = 0 43 45 44 46 def execute(self): 45 47 outstr = None … … 63 65 def __init__(self, linearized_ancestor, revision): 64 66 """ 65 Initializes a new MonotoneChangeset. The linearized_ancestor parameters is the fake ancestor 66 used for linearization. The very first revision tailorized has lin_ancestor==None 67 """ 67 Initializes a new MonotoneChangeset. The linearized_ancestor 68 parameters is the fake ancestor used for linearization. The 69 very first revision tailorized has lin_ancestor==None 70 """ 71 68 72 Changeset.__init__(self, revision=revision, date=None, author=None, log="") 69 73 self.lin_ancestor = linearized_ancestor … … 80 84 s.append('real ancestor(s): %s' % ','.join(self.real_ancestors)) 81 85 return '\n'.join(s) 82 86 83 87 def update(self, real_dates, authors, log, real_ancestors): 84 88 """ 85 Updates the monotone changeset secondary data 89 Updates the monotone changeset secondary data 86 90 """ 87 91 self.author=".".join(authors) … … 90 94 self.real_dates = real_dates 91 95 self.real_ancestors = real_ancestors 92 96 93 97 class MonotoneLogParser: 94 98 """ 95 Obtains and parses a *single* "monotone log" output, reconstructing the revision information 96 """ 97 99 Obtain and parse a *single* "monotone log" output, reconstructing 100 the revision information 101 """ 102 98 103 class PrefixRemover: 99 104 """ … … 103 108 self.str = str 104 109 self.value="" 105 110 106 111 def __call__(self, prefix): 107 112 if self.str.startswith(prefix): … … 112 117 113 118 # logfile states 114 SINGLE = 0 # single line state 119 SINGLE = 0 # single line state 115 120 ADD = 1 # in add file/dir listing 116 121 MOD = 2 # in mod file/dir listing … … 119 124 LOG = 5 # in changelog listing 120 125 CMT = 6 # in comment listing 121 126 122 127 def __init__(self, repository, working_dir): 123 128 self.working_dir = working_dir … … 126 131 def parse(self, revision): 127 132 from datetime import datetime 128 133 129 134 self.revision="" 130 135 self.ancestors=[] … … 139 144 if mtl.exit_status: 140 145 raise GetUpstreamChangesetsFailure("monotone log returned status %d" % mtl.exit_status) 141 146 142 147 logs = "" 143 148 comments = "" … … 145 150 loglines = outstr[0].getvalue().splitlines() 146 151 for curline in loglines: 147 152 148 153 pr = self.PrefixRemover(curline) 149 154 if pr("Revision:"): … … 204 209 raise GetUpstreamChangesetsFailure("Error parsing log of revision %s. Missing data" % revision) 205 210 self.changelog = logs + comments 206 211 207 212 def convertLog(self, chset): 208 213 self.parse(chset.revision) 209 210 chset.update(real_dates=self.dates, 211 authors=self.authors, 214 215 chset.update(real_dates=self.dates, 216 authors=self.authors, 212 217 log=self.changelog, 213 218 real_ancestors=self.ancestors) 214 219 215 220 return chset 216 221 217 222 class MonotoneDiffParser: 218 223 """ 219 This class obtains a diff beetween two arbitrary revisions, parsing it to get changeset entries. 220 Note: since monotone tracks directories implicitly, a fake "add dir" cset entry is generated 221 when a file is added to a subdir 222 """ 223 224 This class obtains a diff beetween two arbitrary revisions, parsing 225 it to get changeset entries. 226 227 Note: since monotone tracks directories implicitly, a fake "add dir" 228 cset entry is generated when a file is added to a subdir 229 """ 230 224 231 class BasicIOTokenizer: 225 # To write its control files, monotone uses a format called internally "basic IO", a stanza file 226 # format with items separated by blank lines. Lines are terminated by newlines. 227 # The format supports strings, sequence of chars contained by ". String could contain newlines and 228 # to insert a " in the middle you escape it with \ (and \\ is used to obtain the \ char itself) 229 # basic IO files are always UTF-8 232 # To write its control files, monotone uses a format called 233 # internally "basic IO", a stanza file format with items 234 # separated by blank lines. Lines are terminated by newlines. 235 # The format supports strings, sequence of chars contained by 236 # ". String could contain newlines and to insert a " in the 237 # middle you escape it with \ (and \\ is used to obtain the \ 238 # char itself) basic IO files are always UTF-8 230 239 # This class implements a small tokenizer for basic IO 231 240 232 241 def __init__(self, stream): 233 242 self.stream = stream 234 243 235 244 def _string_token(self): 236 245 # called at start of string, returns the complete string … … 246 255 elif ch=='\\': 247 256 escape=True 248 continue 257 continue 249 258 else: 250 259 str.append(ch) 251 260 if ch=='"': 252 261 break # end of filename string 253 return "".join(str) 262 return "".join(str) 254 263 255 264 def _normal_token(self, startch): … … 263 272 tok.append(ch) 264 273 265 return "".join(tok) 274 return "".join(tok) 266 275 267 276 def __iter__(self): … … 269 278 self.it = iter(self.stream) 270 279 return self 271 280 272 281 def next(self): 273 282 token ="" … … 287 296 break 288 297 return token 289 298 290 299 def __init__(self, repository, working_dir): 291 300 self.working_dir = working_dir 292 301 self.repository = repository 293 302 294 303 def convertDiff(self, chset): 295 304 """ 296 Fills a chset with the details data coming by a diff beetween chset lin_ancestor and revision 297 (i.e. the linearized history) 298 """ 299 if not chset.lin_ancestor or not chset.revision or chset.lin_ancestor == chset.revision: 300 raise GetUpstreamChangesetsFailure("internal error: MonotoneDiffParser.convertDiff called with invalid parameters: lin_ancestor %s, revision %s" % (chset.lin_ancestor, chset.revision)) 301 302 # the order of revisions is very important. Monotone gives a diff from the first to the second 305 Fills a chset with the details data coming by a diff beetween 306 chset lin_ancestor and revision (i.e. the linearized history) 307 """ 308 if (not chset.lin_ancestor or 309 not chset.revision or 310 chset.lin_ancestor == chset.revision): 311 raise GetUpstreamChangesetsFailure( 312 "Internal error: MonotoneDiffParser.convertDiff called " 313 "with invalid parameters: lin_ancestor %s, revision %s" % 314 (chset.lin_ancestor, chset.revision)) 315 316 # the order of revisions is very important. Monotone gives a 317 # diff from the first to the second 303 318 cmd = self.repository.command("diff", 304 319 "--db", self.repository.repository, … … 309 324 outstr = mtl.execute(stdout=PIPE) 310 325 if mtl.exit_status: 311 raise GetUpstreamChangesetsFailure("monotone diff returned status %d" % mtl.exit_status) 312 313 # monotone diffs are prefixed by a section containing metainformations about files 314 # The section terminates with the first file diff, and each line is prepended by the 315 # patch comment char (#). 326 raise GetUpstreamChangesetsFailure( 327 "monotone diff returned status %d" % mtl.exit_status) 328 329 # monotone diffs are prefixed by a section containing 330 # metainformations about files 331 # The section terminates with the first file diff, and each 332 # line is prepended by the patch comment char (#). 316 333 tk = self.BasicIOTokenizer(outstr[0].getvalue()) 317 334 tkiter = iter(tk) … … 326 343 else: 327 344 in_item = False 328 # now, next token should be a filename 345 # now, next token should be a filename 329 346 fname = tkiter.next() 330 347 if fname[0] != '"': 331 raise GetUpstreamChangesetsFailure("Unexpected token sequence: '%s' followed by '%s'" %(token, fname)) 332 348 raise GetUpstreamChangesetsFailure( 349 "Unexpected token sequence: '%s' " 350 "followed by '%s'" %(token, fname)) 351 333 352 # ok, is a file, control changesets data 334 353 if token == "add_file" or token=="add_directory": … … 339 358 chentry.action_kind = chentry.DELETED 340 359 elif token == "rename_file" or token=="rename_directory": 341 # renames are in the form: oldname to newname 360 # renames are in the form: oldname to newname 342 361 tow = tkiter.next() 343 362 newname = tkiter.next() 344 363 if tow != "to" or fname[0]!='"': 345 raise GetUpstreamChangesetsFailure("Unexpected rename token sequence: '%s' followed by '%s'" %(tow, newname)) 364 raise GetUpstreamChangesetsFailure( 365 "Unexpected rename token sequence: '%s' " 366 "followed by '%s'" %(tow, newname)) 346 367 chentry = chset.addEntry(newname[1:-1], chset.revision) 347 368 chentry.action_kind = chentry.RENAMED … … 354 375 newr = tkiter.next() 355 376 if fromw != "from" or tow != "to": 356 raise GetUpstreamChangesetsFailure("Unexpected patch token sequence: '%s' followed by '%s','%s','%s'" %(fromw,oldr,tow, newr)) 357 358 # patch entries are generated also for files added, so we must ignore the entry if already 359 # present 377 raise GetUpstreamChangesetsFailure( 378 "Unexpected patch token sequence: '%s' " 379 "followed by '%s','%s','%s'" % (fromw, oldr, 380 tow, newr)) 381 382 # patch entries are generated also for files 383 # added, so we must ignore the entry if 384 # already present 360 385 if len( [e for e in chset.entries if e.name==fname[1:-1]])==0: 361 386 # is a real update 362 387 chentry = chset.addEntry(fname[1:-1], chset.revision) 363 388 chentry.action_kind = chentry.UPDATED 364 365 except StopIteration: 389 390 except StopIteration: 366 391 if in_item: 367 392 raise GetUpstreamChangesetsFailure("Unexpected end of 'diff' parsing changeset info") 368 369 393 394 370 395 class MonotoneRevToCset: 371 396 """ 372 397 This class is used to create changesets from revision ids. 373 Since most backends (and tailor itself) doesn't support monotone multihead feature, sometimes we need to 374 linearize the revision graph, creating syntethized (i.e. fake) edges beetween revisions. 375 The revision itself is real, only its ancestors (and all changes beetween) are faked. 376 To properly do this, changeset are created by a mixture of 'log' and 'diff' output. Log gives the revision 377 data, diff the differences beetween revisions. 378 Monotone also supports multiple authors/tags/comments for each revision, while tailor allows only single values. 379 We collapse those multiple data (when present) to single entries in the following manner: 380 398 399 Since most backends (and tailor itself) doesn't support monotone 400 multihead feature, sometimes we need to linearize the revision 401 graph, creating syntethized (i.e. fake) edges beetween revisions. 402 403 The revision itself is real, only its ancestors (and all changes 404 beetween) are faked. 405 406 To properly do this, changeset are created by a mixture of 'log' 407 and 'diff' output. Log gives the revision data, diff the 408 differences beetween revisions. 409 410 Monotone also supports multiple authors/tags/comments for each 411 revision, while tailor allows only single values. 412 413 We collapse those multiple data (when present) to single entries 414 in the following manner: 415 381 416 author 382 417 all entries separated by a comma … … 389 424 390 425 comment 391 all comments are appended to the changelog string, prefixed by a "Note:" line 426 all comments are appended to the changelog string, prefixed by a 427 "Note:" line 428 392 429 tag 393 430 not used by tailor. Ignored … … 402 439 ignored 403 440 404 Changesets created by monotone will have additional fields with the original data: 441 Changesets created by monotone will have additional fields with 442 the original data: 405 443 406 444 real_ancestors … … 417 455 self.working_dir = working_dir 418 456 self.repository = repository 419 self.logparser = MonotoneLogParser(repository=repository, working_dir=working_dir) 420 self.diffparser = MonotoneDiffParser(repository=repository, working_dir=working_dir) 421 457 self.logparser = MonotoneLogParser(repository=repository, 458 working_dir=working_dir) 459 self.diffparser = MonotoneDiffParser(repository=repository, 460 working_dir=working_dir) 461 422 462 def _cset_from_rev(self, lin_ancestor, revision): 423 463 # prepare a new changeset and fill it with rev data … … 425 465 self.updateCset(chset) 426 466 return chset 427 467 428 468 def updateCset(self, chset): 429 469 # Parsing the log fills the changeset from revision data 430 470 self.logparser.convertLog(chset) 431 471 432 472 # if an ancestor is available, fills the cset with file/dir entries 433 473 if chset.lin_ancestor: … … 435 475 436 476 def getCset(self, revlist): 437 # receives a revlist, already toposorted (i.e. ordered by ancestry) and outputs a list of438 # changesets477 # receives a revlist, already toposorted (i.e. ordered by 478 # ancestry) and outputs a list of changesets 439 479 cslist=[] 440 480 anc=revlist[0] … … 443 483 anc=r 444 484 return cslist 445 446 485 486 447 487 class MonotoneWorkingDir(UpdatableSourceWorkingDir, SyncronizableTargetWorkingDir): 448 488 449 489 def convert_head_initial(self, repository, module, revision, working_dir): 450 490 """ 451 This method handles HEAD and INITIAL pseudo-revisions, converting them to monotone revids 491 This method handles HEAD and INITIAL pseudo-revisions, converting 492 them to monotone revids 452 493 """ 453 494 effective_rev = revision … … 464 505 if revision == 'HEAD': 465 506 if len(revision)>1: 466 raise InvocationError("Branch '%s' has multiple heads. Please choose only one." % module) 507 raise InvocationError("Branch '%s' has multiple heads. " 508 "Please choose only one." % module) 467 509 effective_rev=revision[0] 468 510 else: 469 # INITIAL requested. We must get the ancestors of current head(s), topologically sort them 470 # and pick the first (i.e. the "older" revision). Unfortunately if the branch has multiple 471 # heads then we could end up with only part of the ancestry graph. 511 # INITIAL requested. We must get the ancestors of 512 # current head(s), topologically sort them and pick 513 # the first (i.e. the "older" revision). Unfortunately 514 # if the branch has multiple heads then we could end 515 # up with only part of the ancestry graph. 472 516 if len(revision)>1: 473 stderr.write("Branch '%s' has multiple heads. There is no guarantee to reconstruct the full history." % module) 517 stderr.write("Branch '%s' has multiple heads. There " 518 "is no guarantee to reconstruct the " 519 "full history." % module) 474 520 cmd = [ self.repository.command("automate","ancestors", 475 521 "--db",repository), … … 481 527 outstr = cld.execute() 482 528 if cld.exit_status: 483 raise InvocationError("Ancestor reading returned status %d" % cld.exit_status) 529 raise InvocationError("Ancestor reading returned " 530 "status %d" % cld.exit_status) 484 531 revision = outstr[0].getvalue().split() 485 532 effective_rev=revision[0] 486 533 return effective_rev 487 534 488 535 ## UpdatableSourceWorkingDir 489 536 490 537 def _getUpstreamChangesets(self, sincerev=None): 491 538 # monotone descendents returns results sorted in alpha order 492 # here we want ancestry order, so descendents output is feed back to 539 # here we want ancestry order, so descendents output is feed back to 493 540 # mtn for a toposort ... 494 541 cmd = [ self.repository.command("automate","descendents", … … 502 549 outstr = cld.execute() 503 550 if cld.exit_status: 504 raise InvocationError("monotone descendents returned status %d" % cld.exit_status) 505 506 # now childs is a list of revids, we must transform it in a list of monotone changesets 507 # at this time we fill only the linearized ancestor and revision ids, because at this time we need 508 # only to know WICH changesets must be applied to the target repo, not WHAT are the changesets 551 raise InvocationError("monotone descendents returned " 552 "status %d" % cld.exit_status) 553 554 # now childs is a list of revids, we must transform it in a 555 # list of monotone changesets at this time we fill only the 556 # linearized ancestor and revision ids, because at this time 557 # we need only to know WICH changesets must be applied to the 558 # target repo, not WHAT are the changesets 509 559 childs = outstr[0].getvalue().split() 510 560 chlist = [] … … 520 570 mtl.execute() 521 571 if mtl.exit_status: 522 raise ChangesetApplicationFailure("'mtn update' returned status %s" % mtl.exit_status) 523 mtr = MonotoneRevToCset(repository=self.repository, working_dir=self.basedir) 572 raise ChangesetApplicationFailure("'mtn update' returned " 573 "status %s" % mtl.exit_status) 574 mtr = MonotoneRevToCset(repository=self.repository, 575 working_dir=self.basedir) 524 576 mtr.updateCset( changeset ) 525 577 526 578 return False # no conflicts 527 579 528 580 def _checkoutUpstreamRevision(self, revision): 529 581 """ 530 582 Concretely do the checkout of the FIRST upstream revision. 531 583 """ 532 effrev = self.convert_head_initial(self.repository.repository, self.repository.module, revision, self.basedir) 584 effrev = self.convert_head_initial(self.repository.repository, 585 self.repository.module, revision, 586 self.basedir) 533 587 if not exists(join(self.basedir, 'MT')): 534 588 self.log_info("checking out a working copy") 535 589 cmd = self.repository.command("co", 536 590 "--db", self.repository.repository, 537 "--revision", effrev, 591 "--revision", effrev, 538 592 "--branch", self.repository.module, 539 593 self.repository.subdir) … … 544 598 "'monotone co' returned status %s" % mtl.exit_status) 545 599 else: 546 self.log_info("%s already exists, assuming it's a monotone working dir" % self.basedir) 547 548 # ok, now the workdir contains the checked out revision. We need to return a changeset 549 # describing it. 550 # Since this is the first revision checked out, we don't have a (linearized) ancestor, so wne 551 # must use None as the lin_ancestor parameter 600 self.log_info("%s already exists, assuming it's a monotone " 601 "working dir" % self.basedir) 602 603 # Ok, now the workdir contains the checked out revision. We 604 # need to return a changeset describing it. Since this is the 605 # first revision checked out, we don't have a (linearized) 606 # ancestor, so we must use None as the lin_ancestor parameter 552 607 chset = MonotoneChangeset(None, effrev) 553 608 554 # now we update the new chset with basic data - without the linearized ancestor, changeset 555 # entries will NOT be filled 556 mtr = MonotoneRevToCset(repository=self.repository, working_dir=self.basedir) 609 # now we update the new chset with basic data - without the 610 # linearized ancestor, changeset entries will NOT be filled 611 mtr = MonotoneRevToCset(repository=self.repository, 612 working_dir=self.basedir) 557 613 mtr.updateCset(chset) 558 614 return chset 559 615 560 616 ## SyncronizableTargetWorkingDir 561 617 562 618 def _addPathnames(self, names): 563 619 """ 564 Add some new filesystem objects, skipping directories (directory addition is implicit in monotone) 620 Add some new filesystem objects, skipping directories (directory 621 addition is implicit in monotone) 565 622 """ 566 623 fnames=[] 567 624 for fn in names: 568 625 if isdir(join(self.basedir, fn)): 569 self.log_info("ignoring addition of directory '%s' (%s)" % (fn, join(self.basedir, fn)) ); 626 self.log_info("ignoring addition of directory '%s' (%s)" % 627 (fn, join(self.basedir, fn)) ) 570 628 else: 571 629 fnames.append(fn) … … 576 634 add.execute(fnames) 577 635 if add.exit_status: 578 raise ChangesetApplicationFailure("%s returned status %s" % (str(add),add.exit_status)) 579 636 raise ChangesetApplicationFailure("%s returned status %s" % 637 (str(add),add.exit_status)) 638 580 639 581 640 def _addSubtree(self, subdir): … … 587 646 add.execute(subdir) 588 647 if add.exit_status: 589 raise ChangesetApplicationFailure("%s returned status %s" % (str(add),add.exit_status)) 648 raise ChangesetApplicationFailure("%s returned status %s" % 649 (str(add),add.exit_status)) 590 650 591 651 def _commit(self, date, author, patchname, changelog=None, entries=None): … … 632 692 def _removePathnames(self, names): 633 693 """ 634 Remove some filesystem object. 635 """ 636 637 # Monotone currently doesn't allow removing a directory, 638 # so we must remove every item separately and intercept monotone directory errore messages. 639 # We can't just filter the directories, because the wc doesn't contain them anymore ... 694 Remove some filesystem object. 695 """ 696 697 # Monotone currently doesn't allow removing a directory, so we 698 # must remove every item separately and intercept monotone 699 # directory errore messages. We can't just filter the 700 # directories, because the wc doesn't contain them anymore ... 640 701 cmd = self.repository.command("drop") 641 702 drop = ExternalCommand(cwd=self.basedir, command=cmd) … … 645 706 if not error.read().find("drop <directory>"): 646 707 log_error(error.read()) 647 raise ChangesetApplicationFailure("%s returned status %s" % (str(drop),drop.exit_status)) 708 raise ChangesetApplicationFailure("%s returned status %s" % 709 (str(drop), 710 drop.exit_status)) 648 711 649 712 def _renamePathname(self, oldname, newname): … … 656 719 if access(join(self.basedir, newname), F_OK): 657 720 if access(join(self.basedir, oldname), F_OK): 658 raise ChangesetApplicationFailure("Can't rename %s to %s. Both names already exist" % (oldname, newname) ) 721 raise ChangesetApplicationFailure("Can't rename %s to %s. " 722 "Both names already exist" % 723 (oldname, newname)) 659 724 renames(join(self.basedir, newname), join(self.basedir, oldname)) 660 725 self.log_info("preparing to rename %s->%s" % (oldname, newname)) 661 726 662 727 cmd = self.repository.command("rename") 663 728 rename = ExternalCommand(cwd=self.basedir, command=cmd) 664 729 rename.execute(oldname, newname) 665 730 666 731 # redo the rename ... 667 732 renames(join(self.basedir, oldname), join(self.basedir, newname)) 668 733 if rename.exit_status: 669 raise ChangesetApplicationFailure("%s returned status %s" % (str(rename),rename.exit_status)) 734 raise ChangesetApplicationFailure("%s returned status %s" % 735 (str(rename),rename.exit_status)) 670 736 671 737 def __createRepository(self, target_repository): … … 692 758 regkey.execute(input=keyfile) 693 759 else: 694 # no keyfile specified, generate a new key - if a passphrase is defined, automatically695 # p rovide it696 # Thekeyid must be available760 # no keyfile specified, generate a new key - if a 761 # passphrase is defined, automatically provide it The 762 # keyid must be available 697 763 if not target_repository.keyid: 698 raise TargetInitializationFailure("Can't setup the monotone repository %r\n" 699 "A keyfile or keyid must be provided." % 764 raise TargetInitializationFailure("Can't setup the monotone " 765 "repository %r. " 766 "A keyfile or keyid must " 767 "be provided." % 700 768 target_repository) 701 769 cmd = self.repository.command("genkey", "--db", … … 703 771 regkey = ExternalCommand(command=cmd) 704 772 if target_repository.passphrase: 705 passp="%s\n%s\n" % (target_repository.passphrase,target_repository.passphrase) 773 passp="%s\n%s\n" % (target_repository.passphrase, 774 target_repository.passphrase) 706 775 regkey.execute(target_repository.keyid, input=passp) 707 776 … … 738 807 "--db", self.repository.repository, 739 808 "--branch", self.repository.module) 740 809 741 810 if not self.repository.module: 742 raise TargetInitializationFailure("Monotone needs a module defined (to be used as commit branch)") 811 raise TargetInitializationFailure("Monotone needs a module " 812 "defined (to be used as " 813 "commit branch)") 743 814 744 815 setup = ExternalCommand(command=cmd) … … 749 820 monotonerc.write(MONOTONERC % self.repository.passphrase) 750 821 monotonerc.close() 751 822 752 823 def _initializeWorkingDir(self): 753 824 """ … … 762 833 763 834 if not exists(join(self.basedir, 'MT')): 764 raise TargetInitializationFailure("Please setup '%s' as a monotone working directory" % self.basedir) 835 raise TargetInitializationFailure("Please setup '%s' as a " 836 "monotone working directory" % 837 self.basedir) 765 838 766 839 SyncronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracChangeset
for help on using the changeset viewer.
