1#!/usr/bin/env python 2# 3# arch-tag: ED474BFA-4169-11D8-904A-000393CFE6B8 4 5# Copyright (c) 2002 Trent Mick 6# 7# Permission is hereby granted, free of charge, to any person obtaining a 8# copy of this software and associated documentation files (the 9# "Software"), to deal in the Software without restriction, including 10# without limitation the rights to use, copy, modify, merge, publish, 11# distribute, sublicense, and/or sell copies of the Software, and to 12# permit persons to whom the Software is furnished to do so, subject to 13# the following conditions: 14# 15# The above copyright notice and this permission notice shall be included 16# in all copies or substantial portions of the Software. 17# 18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 26""" 27 An OO interface to 'p4' (the Perforce client command line app). 28 29 Usage: 30 import p4lib 31 p4 = p4lib.P4(<p4options>) 32 result = p4.<command>(<options>) 33 34 For more information see the doc string on each command. For example: 35 print p4lib.P4.opened.__doc__ 36 37 Implemented commands: 38 add (limited test suite), branch, branches, change, changes (no 39 test suite), client, clients, delete, describe (no test suite), 40 diff, edit (no test suite), files (no test suite), filelog (no 41 test suite), flush, have (no test suite), label, labels, opened, 42 print (as print_, no test suite), resolve, revert (no test 43 suite), submit, sync, where (no test suite) 44 Partially implemented commands: 45 diff2 46 Unimplemented commands: 47 admin, counter, counters, depot, depots, dirs, fix, fixes, 48 fstat, group, groups, help (no point), integrate, integrated, 49 job, jobs, jobspec, labelsync, lock, obliterate, passwd, 50 protect, rename (no point), reopen, resolved, review, reviews, 51 set, triggers, typemap, unlock, user, users, verify 52 53 XXX Describe usage of parseForm() and makeForm(). 54""" 55#TODO: 56# - There is much similarity in some commands, e.g. clients, changes, 57# branches in one group; client, change, branch, label in another. 58# Should share implementation between these all. 59 60from past.builtins import cmp 61from builtins import str 62from builtins import range 63from builtins import object 64import os 65import sys 66import pprint 67import cmd 68import re 69import types 70import marshal 71import getopt 72import tempfile 73 74 75#---- exceptions 76 77class P4LibError(Exception): 78 pass 79 80 81#---- global data 82 83_version_ = (0, 7, 2) 84 85 86#---- internal logging facility 87 88class _Logger(object): 89 DEBUG, INFO, WARN, ERROR, CRITICAL = list(range(5)) 90 def __init__(self, threshold=None, streamOrFileName=sys.stderr): 91 if threshold is None: 92 self.threshold = self.WARN 93 else: 94 self.threshold = threshold 95 if type(streamOrFileName) == bytes: 96 self.stream = open(streamOrFileName, 'w') 97 self._opennedStream = 1 98 else: 99 self.stream = streamOrFileName 100 self._opennedStream = 0 101 def __del__(self): 102 if self._opennedStream: 103 self.stream.close() 104 def _getLevelName(self, level): 105 levelNameMap = { 106 self.DEBUG: "DEBUG", 107 self.INFO: "INFO", 108 self.WARN: "WARN", 109 self.ERROR: "ERROR", 110 self.CRITICAL: "CRITICAL", 111 } 112 return levelNameMap[level] 113 def log(self, level, msg, *args): 114 if level < self.threshold: 115 return 116 message = "%s: " % self._getLevelName(level).lower() 117 message = message + (msg % args) + "\n" 118 self.stream.write(message) 119 self.stream.flush() 120 def debug(self, msg, *args): 121 self.log(self.DEBUG, msg, *args) 122 def info(self, msg, *args): 123 self.log(self.INFO, msg, *args) 124 def warn(self, msg, *args): 125 self.log(self.WARN, msg, *args) 126 def error(self, msg, *args): 127 self.log(self.ERROR, msg, *args) 128 def fatal(self, msg, *args): 129 self.log(self.CRITICAL, msg, *args) 130 131if 1: # normal 132 log = _Logger(_Logger.WARN) 133else: # debugging 134 log = _Logger(_Logger.DEBUG) 135 136 137#---- internal support stuff 138 139def _escapeArg(arg): 140 """Escape the given command line argument for the shell.""" 141 #XXX There is a *lot* more that we should escape here. 142 #XXX This is also not right on Linux, just try putting 'p4' is a dir 143 # with spaces. 144 return arg.replace('"', r'\"').replace("'", r"\'") 145 146 147def _joinArgv(argv): 148 r"""Join an arglist to a string appropriate for running. 149 >>> import os 150 >>> _joinArgv(['foo', 'bar "baz']) 151 'foo "bar \\"baz"' 152 """ 153 cmdstr = "" 154 for arg in argv: 155 # Quote args with '*' because don't want shell to expand the 156 # argument. (XXX Perhaps that should only be done for Windows.) 157 # if ' ' in arg or '*' in arg: 158 # cmdstr += '"%s"' % _escapeArg(arg) 159 # else: 160 # cmdstr += _escapeArg(arg) 161 # Why not always quote it? 162 cmdstr += "'%s'" % _escapeArg(arg) 163 cmdstr += ' ' 164 if cmdstr.endswith(' '): cmdstr = cmdstr[:-1] # strip trailing space 165 return cmdstr 166 167 168def _run(argv): 169 """Prepare and run the given arg vector, 'argv', and return the 170 results. Returns (<stdout lines>, <stderr lines>, <return value>). 171 Note: 'argv' may also just be the command string. 172 """ 173 if type(argv) in (list, tuple): 174 cmd = _joinArgv(argv) 175 else: 176 cmd = argv 177 log.debug("Running '%s'..." % cmd) 178 if sys.platform.startswith('win'): 179 i, o, e = os.popen3(cmd) 180 output = o.read() 181 error = e.read() 182 i.close() 183 e.close() 184 retval = o.close() 185 else: 186 import popen2 187 p = popen2.Popen3(cmd, 1) 188 i, o, e = p.tochild, p.fromchild, p.childerr 189 output = o.read() 190 error = e.read() 191 i.close() 192 o.close() 193 e.close() 194 rv = p.wait() 195 if os.WIFEXITED(rv): 196 retval = os.WEXITSTATUS(rv) 197 else: 198 raise P4LibError("Error running '%s', it did not exit "\ 199 "properly: rv=%d" % (cmd, rv)) 200 if retval: 201 raise P4LibError("Error running '%s': error='%s' retval='%s'"\ 202 % (cmd, error, retval)) 203 log.debug("output='%s'", output) 204 log.debug("error='%s'", error) 205 log.debug("retval='%s'", retval) 206 return output, error, retval 207 208 209def _specialsLast(a, b, specials): 210 """A cmp-like function, sorting in alphabetical order with 211 'special's last. 212 """ 213 if a in specials and b in specials: 214 return cmp(a, b) 215 elif a in specials: 216 return 1 217 elif b in specials: 218 return -1 219 else: 220 return cmp(a, b) 221 222 223#---- public stuff 224 225 226def makeForm(**kwargs): 227 """Return an appropriate P4 form filled out with the given data. 228 229 In general this just means tranforming each keyword and (string) 230 value to separate blocks in the form. The section name is the 231 capitalized keyword. Single line values go on the same line as 232 the section name. Multi-line value succeed the section name, 233 prefixed with a tab, except some special section names (e.g. 234 'differences'). Text for "special" sections are NOT indented, have a 235 blank line after the header, and are placed at the end of the form. 236 Sections are separated by a blank line. 237 238 The 'files' key is handled specially. It is expected to be a 239 list of dicts of the form: 240 {'action': 'add', # 'action' may or may not be there 241 'depotFile': '//depot/test_edit_pending_change.txt'} 242 As well, the 'change' value may be an int. 243 """ 244 # Do special preprocessing on the data. 245 for key, value in list(kwargs.items()): 246 if key == 'files': 247 strval = '' 248 for f in value: 249 if 'action' in f: 250 strval += '%(depotFile)s\t# %(action)s\n' % f 251 else: 252 strval += '%(depotFile)s\n' % f 253 kwargs[key] = strval 254 if key == 'change': 255 kwargs[key] = str(value) 256 257 # Create the form 258 form = '' 259 specials = ['differences'] 260 keys = list(kwargs.keys()) 261 keys.sort(lambda a,b,s=specials: _specialsLast(a,b,s)) 262 for key in keys: 263 value = kwargs[key] 264 if value is None: 265 pass 266 elif len(value.split('\n')) > 1: # a multi-line block 267 form += '%s:\n' % key.capitalize() 268 if key in specials: 269 form += '\n' 270 for line in value.split('\n'): 271 if key in specials: 272 form += line + '\n' 273 else: 274 form += '\t' + line + '\n' 275 else: 276 form += '%s:\t%s\n' % (key.capitalize(), value) 277 form += '\n' 278 return form 279 280def parseForm(content): 281 """Parse an arbitrary Perforce form and return a dict result. 282 283 The result is a dict with a key for each "section" in the 284 form (the key name will be the section name lowercased), 285 whose value will, in general, be a string with the following 286 exceptions: 287 - A "Files" section will translate into a list of dicts 288 each with 'depotFile' and 'action' keys. 289 - A "Change" value will be converted to an int if 290 appropriate. 291 """ 292 if type(content) in (str,): 293 lines = content.splitlines(1) 294 else: 295 lines = content 296 # Example form: 297 # # A Perforce Change Specification. 298 # # 299 # # Change: The change number. 'new' on a n... 300 # <snip> 301 # # to this changelist. You may de... 302 # 303 # Change: 1 304 # 305 # Date: 2002/05/08 23:24:54 306 # <snip> 307 # Description: 308 # create the initial change 309 # 310 # Files: 311 # //depot/test_edit_pending_change.txt # add 312 spec = {} 313 314 # Parse out all sections into strings. 315 currkey = None # If non-None, then we are in a multi-line block. 316 for line in lines: 317 if line.strip().startswith('#'): 318 continue # skip comment lines 319 if currkey: # i.e. accumulating a multi-line block 320 if line.startswith('\t'): 321 spec[currkey] += line[1:] 322 elif not line.strip(): 323 spec[currkey] += '\n' 324 else: 325 # This is the start of a new section. Trim all 326 # trailing newlines from block section, as 327 # Perforce does. 328 while spec[currkey].endswith('\n'): 329 spec[currkey] = spec[currkey][:-1] 330 currkey = None 331 if not currkey: # i.e. not accumulating a multi-line block 332 if not line.strip(): continue # skip empty lines 333 key, remainder = line.split(':', 1) 334 if not remainder.strip(): # this is a multi-line block 335 currkey = key.lower() 336 spec[currkey] = '' 337 else: 338 spec[key.lower()] = remainder.strip() 339 if currkey: 340 # Trim all trailing newlines from block section, as 341 # Perforce does. 342 while spec[currkey].endswith('\n'): 343 spec[currkey] = spec[currkey][:-1] 344 345 # Do any special processing on values. 346 for key, value in list(spec.items()): 347 if key == "change": 348 try: 349 spec[key] = int(value) 350 except ValueError: 351 pass 352 elif key == "files": 353 spec[key] = [] 354 fileRe = re.compile('^(?P<depotFile>//.+?)\t'\ 355 '# (?P<action>\w+)$') 356 for line in value.split('\n'): 357 if not line.strip(): continue 358 match = fileRe.match(line) 359 try: 360 spec[key].append(match.groupdict()) 361 except AttributeError: 362 pprint.pprint(value) 363 pprint.pprint(spec) 364 err = "Internal error: could not parse P4 form "\ 365 "'Files:' section line: '%s'" % line 366 raise P4LibError(err) 367 368 return spec 369 370 371def makeOptv(**options): 372 """Create a p4 option vector from the given p4 option dictionary. 373 374 "options" is an option dictionary. Valid keys and values are defined by 375 what class P4's constructor accepts via P4(**optd). 376 377 Example: 378 >>> makeOptv(client='swatter', dir='D:\\trentm') 379 ['-c', 'client', '-d', 'D:\\trentm'] 380 >>> makeOptv(client='swatter', dir=None) 381 ['-c', 'client'] 382 """ 383 optv = [] 384 for key, val in list(options.items()): 385 if val is None: 386 continue 387 if key == 'client': 388 optv.append('-c') 389 elif key == 'dir': 390 optv.append('-d') 391 elif key == 'host': 392 optv.append('-H') 393 elif key == 'port': 394 optv.append('-p') 395 elif key == 'password': 396 optv.append('-P') 397 elif key == 'user': 398 optv.append('-u') 399 else: 400 raise P4LibError("Unexpected keyword arg: '%s'" % key) 401 optv.append(val) 402 return optv 403 404def parseOptv(optv): 405 """Return an option dictionary representing the given p4 option vector. 406 407 "optv" is a list of p4 options. See 'p4 help usage' for a list. 408 409 The returned option dictionary is suitable passing to the P4 constructor. 410 411 Example: 412 >>> parseP4Optv(['-c', 'swatter', '-d', 'D:\\trentm']) 413 {'client': 'swatter', 414 'dir': 'D:\\trentm'} 415 """ 416 # Some of p4's options are not appropriate for later 417 # invocations. For example, '-h' and '-V' override output from 418 # running, say, 'p4 opened'; and '-G' and '-s' control the 419 # output format which this module is parsing (hence this module 420 # should control use of those options). 421 optlist, dummy = getopt.getopt(optv, 'hVc:d:H:p:P:u:x:Gs') 422 optd = {} 423 for opt, optarg in optlist: 424 if opt in ('-h', '-V', '-x'): 425 raise P4LibError("The '%s' p4 option is not appropriate "\ 426 "for p4lib.P4." % opt) 427 elif opt in ('-G', '-s'): 428 log.info("Ignoring '%s' option." % opt) 429 elif opt == '-c': 430 optd['client'] = optarg 431 elif opt == '-d': 432 optd['dir'] = optarg 433 elif opt == '-H': 434 optd['host'] = optarg 435 elif opt == '-p': 436 optd['port'] = optarg 437 elif opt == '-P': 438 optd['password'] = optarg 439 elif opt == '-u': 440 optd['user'] = optarg 441 return optd 442 443 444class P4(object): 445 """A proxy to the Perforce client app 'p4'.""" 446 def __init__(self, p4='p4', **options): 447 """Create a 'p4' proxy object. 448 449 "p4" is the Perforce client to execute commands with. Defaults 450 to 'p4'. 451 Optional keyword arguments: 452 "client" specifies the client name, overriding the value of 453 $P4CLIENT in the environment and the default (the hostname). 454 "dir" specifies the current directory, overriding the value of 455 $PWD in the environment and the default (the current 456 directory). 457 "host" specifies the host name, overriding the value of $P4HOST 458 in the environment and the default (the hostname). 459 "port" specifies the server's listen address, overriding the 460 value of $P4PORT in the environment and the default 461 (perforce:1666). 462 "password" specifies the password, overriding the value of 463 $P4PASSWD in the environment. 464 "user" specifies the user name, overriding the value of $P4USER, 465 $USER, and $USERNAME in the environment. 466 """ 467 self.p4 = p4 468 self.optd = options 469 self._optv = makeOptv(**self.optd) 470 471 def _p4run(self, argv, **p4options): 472 """Run the given p4 command. 473 474 The current instance's p4 and p4 options (optionally overriden by 475 **p4options) are used. The 3-tuple (<output>, <error>, <retval>) is 476 returned. 477 """ 478 if p4options: 479 d = self.optd 480 d.update(p4options) 481 p4optv = makeOptv(**d) 482 else: 483 p4optv = self._optv 484 argv = [self.p4] + p4optv + argv 485 return _run(argv) 486 487 def opened(self, files=[], allClients=0, change=None, _raw=0, 488 **p4options): 489 """Get a list of files opened in a pending changelist. 490 491 "files" is a list of files or file wildcards to check. Defaults 492 to the whole client view. 493 "allClients" (-a) specifies to list opened files in all clients. 494 "change" (-c) is a pending change with which to associate the 495 opened file(s). 496 497 Returns a list of dicts, each representing one opened file. The 498 dict contains the keys 'depotFile', 'rev', 'action', 'change', 499 'type', and, as well, 'user' and 'client' if the -a option 500 is used. 501 502 If '_raw' is true then the return value is simply a dictionary 503 with the unprocessed results of calling p4: 504 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 505 """ 506 # Output examples: 507 # - normal: 508 # //depot/apps/px/px.py#3 - edit default change (text) 509 # - with '-a': 510 # //depot/foo.txt#1 - edit change 12345 (text+w) by trentm@trentm-pliers 511 # - none opened: 512 # foo.txt - file(s) not opened on this client. 513 optv = [] 514 if allClients: optv += ['-a'] 515 if change: optv += ['-c', str(change)] 516 if type(files) in (str,): 517 files = [files] 518 519 argv = ['opened'] + optv 520 if files: 521 argv += files 522 output, error, retval = self._p4run(argv, **p4options) 523 if _raw: 524 return {'stdout': output, 'stderr': error, 'retval': retval} 525 526 lineRe = re.compile('''^ 527 (?P<depotFile>.*?)\#(?P<rev>\d+) # //depot/foo.txt#1 528 \s-\s(?P<action>\w+) # - edit 529 \s(default\schange|change\s(?P<change>\d+)) # change 12345 530 \s\((?P<type>[\w+]+)\) # (text+w) 531 (\sby\s)? # by 532 ((?P<user>[^\s@]+)@(?P<client>[^\s@]+))? # trentm@trentm-pliers 533 ''', re.VERBOSE) 534 files = [] 535 for line in output.splitlines(1): 536 match = lineRe.search(line) 537 if not match: 538 raise P4LibError("Internal error: 'p4 opened' regex did not "\ 539 "match '%s'. Please report this to the "\ 540 "author." % line) 541 file = match.groupdict() 542 file['rev'] = int(file['rev']) 543 if not file['change']: 544 file['change'] = 'default' 545 else: 546 file['change'] = int(file['change']) 547 for key in list(file.keys()): 548 if file[key] is None: 549 del file[key] 550 files.append(file) 551 return files 552 553 def where(self, files=[], _raw=0, **p4options): 554 """Show how filenames map through the client view. 555 556 "files" is a list of files or file wildcards to check. Defaults 557 to the whole client view. 558 559 Returns a list of dicts, each representing one element of the 560 mapping. Each mapping include a 'depotFile', 'clientFile', and 561 'localFile' and a 'minus' boolean (indicating if the entry is an 562 Exclusion. 563 564 If '_raw' is true then the return value is simply a dictionary 565 with the unprocessed results of calling p4: 566 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 567 """ 568 # Output examples: 569 # -//depot/foo/Py-2_1/... //trentm-ra/foo/Py-2_1/... c:\trentm\foo\Py-2_1\... 570 # //depot/foo/win/... //trentm-ra/foo/win/... c:\trentm\foo\win\... 571 # //depot/foo/Py Exts.dsw //trentm-ra/foo/Py Exts.dsw c:\trentm\foo\Py Exts.dsw 572 # //depot/foo/%1 //trentm-ra/foo/%1 c:\trentm\foo\%1 573 # The last one is surprising. It comes from using '*' in the 574 # client spec. 575 if type(files) in (str,): 576 files = [files] 577 578 argv = ['where'] 579 if files: 580 argv += files 581 output, error, retval = self._p4run(argv, **p4options) 582 if _raw: 583 return {'stdout': output, 'stderr': error, 'retval': retval} 584 585 results = [] 586 for line in output.splitlines(1): 587 file = {} 588 if line[-1] == '\n': line = line[:-1] 589 if line.startswith('-'): 590 file['minus'] = 1 591 line = line[1:] 592 else: 593 file['minus'] = 0 594 depotFileStart = line.find('//') 595 clientFileStart = line.find('//', depotFileStart+2) 596 file['depotFile'] = line[depotFileStart:clientFileStart-1] 597 if sys.platform.startswith('win'): 598 assert ':' not in file['depotFile'],\ 599 "Current parsing cannot handle this line '%s'." % line 600 localFileStart = line.find(':', clientFileStart+2) - 1 601 else: 602 assert file['depotFile'].find(' /') == -1,\ 603 "Current parsing cannot handle this line '%s'." % line 604 localFileStart = line.find(' /', clientFileStart+2) + 1 605 file['clientFile'] = line[clientFileStart:localFileStart-1] 606 file['localFile'] = line[localFileStart:] 607 results.append(file) 608 return results 609 610 def have(self, files=[], _raw=0, **p4options): 611 """Get list of file revisions last synced. 612 613 "files" is a list of files or file wildcards to check. Defaults 614 to the whole client view. 615 "options" can be any of p4 option specifiers allowed by .__init__() 616 (they override values given in the constructor for just this 617 command). 618 619 Returns a list of dicts, each representing one "hit". Each "hit" 620 includes 'depotFile', 'rev', and 'localFile' keys. 621 622 If '_raw' is true then the return value is simply a dictionary 623 with the unprocessed results of calling p4: 624 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 625 """ 626 if type(files) in (str,): 627 files = [files] 628 629 argv = ['have'] 630 if files: 631 argv += files 632 output, error, retval = self._p4run(argv, **p4options) 633 if _raw: 634 return {'stdout': output, 'stderr': error, 'retval': retval} 635 636 # Output format is 'depot-file#revision - client-file' 637 hits = [] 638 for line in output.splitlines(1): 639 if line[-1] == '\n': line = line[:-1] 640 hit = {} 641 hit['depotFile'], line = line.split('#') 642 hit['rev'], hit['localFile'] = line.split(' - ', 1) 643 hit['rev'] = int(hit['rev']) 644 hits.append(hit) 645 return hits 646 647 def describe(self, change, diffFormat='', shortForm=0, _raw=0, 648 **p4options): 649 """Get a description of the given changelist. 650 651 "change" is the changelist number to describe. 652 "diffFormat" (-d<flag>) is a flag to pass to the built-in diff 653 routine to control the output format. Valid values are '' 654 (plain, default), 'n' (RCS), 'c' (context), 's' (summary), 655 'u' (unified). 656 "shortForm" (-s) specifies to exclude the diff from the 657 description. 658 659 Returns a dict representing the change description. Keys are: 660 'change', 'date', 'client', 'user', 'description', 'files', 'diff' 661 (the latter is not included iff 'shortForm'). 662 663 If '_raw' is true then the return value is simply a dictionary 664 with the unprocessed results of calling p4: 665 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 666 """ 667 if diffFormat not in ('', 'n', 'c', 's', 'u'): 668 raise P4LibError("Incorrect diff format flag: '%s'" % diffFormat) 669 optv = [] 670 if diffFormat: 671 optv.append('-d%s' % diffFormat) 672 if shortForm: 673 optv.append('-s') 674 argv = ['describe'] + optv + [str(change)] 675 output, error, retval = self._p4run(argv, **p4options) 676 if _raw: 677 return {'stdout': output, 'stderr': error, 'retval': retval} 678 679 desc = {} 680 lines = output.splitlines(1) 681 lines = [line for line in lines if not line.strip().startswith("#")] 682 changeRe = re.compile('^Change (?P<change>\d+) by (?P<user>[^\s@]+)@'\ 683 '(?P<client>[^\s@]+) on (?P<date>[\d/ :]+)$') 684 desc = changeRe.match(lines[0]).groupdict() 685 desc['change'] = int(desc['change']) 686 filesIdx = lines.index("Affected files ...\n") 687 desc['description'] = "" 688 for line in lines[2:filesIdx-1]: 689 desc['description'] += line[1:] # drop the leading \t 690 if shortForm: 691 diffsIdx = len(lines) 692 else: 693 diffsIdx = lines.index("Differences ...\n") 694 desc['files'] = [] 695 fileRe = re.compile('^... (?P<depotFile>.+?)#(?P<rev>\d+) '\ 696 '(?P<action>\w+)$') 697 for line in lines[filesIdx+2:diffsIdx-1]: 698 file = fileRe.match(line).groupdict() 699 file['rev'] = int(file['rev']) 700 desc['files'].append(file) 701 if not shortForm: 702 desc['diff'] = self._parseDiffOutput(lines[diffsIdx+2:]) 703 return desc 704 705 def change(self, files=None, description=None, change=None, delete=0, 706 _raw=0, **p4options): 707 """Create, update, delete, or get a changelist description. 708 709 Creating a changelist: 710 p4.change([<list of opened files>], "change description") 711 OR 712 p4.change(description="change description for all opened files") 713 714 Updating a pending changelist: 715 p4.change(description="change description", 716 change=<a pending changelist#>) 717 OR 718 p4.change(files=[<new list of files>], 719 change=<a pending changelist#>) 720 721 Deleting a pending changelist: 722 p4.change(change=<a pending changelist#>, delete=1) 723 724 Getting a change description: 725 ch = p4.change(change=<a pending or submitted changelist#>) 726 727 Returns a dict. When getting a change desc the dict will include 728 'change', 'user', 'description', 'status', and possibly 'files' 729 keys. For all other actions the dict will include a 'change' 730 key, an 'action' key iff the intended action was successful, and 731 possibly a 'comment' key. 732 733 If '_raw' is true then the return value is simply a dictionary 734 with the unprocessed results of calling p4: 735 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 736 737 Limitations: The -s (jobs) and -f (force) flags are not 738 supported. 739 """ 740 #XXX .change() API should look more like .client() and .label(), 741 # i.e. passing around a dictionary. Should strings also be 742 # allowed: presumed to be forms? 743 formfile = None 744 try: 745 if type(files) in (str,): 746 files = [files] 747 748 action = None # note action to know how to parse output below 749 if change and files is None and not description: 750 if delete: 751 # Delete a pending change list. 752 action = 'delete' 753 argv = ['change', '-d', str(change)] 754 else: 755 # Get a change description. 756 action = 'get' 757 argv = ['change', '-o', str(change)] 758 else: 759 if delete: 760 raise P4LibError("Cannot specify 'delete' with either "\ 761 "'files' or 'description'.") 762 if change: 763 # Edit a current pending changelist. 764 action = 'update' 765 ch = self.change(change=change) 766 if files is None: # 'files' was not specified. 767 pass 768 elif files == []: # Explicitly specified no files. 769 # Explicitly specified no files. 770 ch['files'] = [] 771 else: 772 depotfiles = [{'depotFile': f['depotFile']}\ 773 for f in self.where(files)] 774 ch['files'] = depotfiles 775 if description: 776 ch['description'] = description 777 form = makeForm(**ch) 778 elif description: 779 # Creating a pending changelist. 780 action = 'create' 781 # Empty 'files' should default to all opened files in the 782 # 'default' changelist. 783 if files is None: 784 files = [{'depotFile': f['depotFile']}\ 785 for f in self.opened()] 786 elif files == []: # Explicitly specified no files. 787 pass 788 else: 789 #TODO: Add test to expect P4LibError if try to use 790 # p4 wildcards in files. Currently *do* get 791 # correct behaviour. 792 files = [{'depotFile': f['depotFile']}\ 793 for f in self.where(files)] 794 form = makeForm(files=files, description=description, 795 change='new') 796 else: 797 raise P4LibError("Incomplete/missing arguments.") 798 # Build submission form file. 799 formfile = tempfile.mktemp() 800 fout = open(formfile, 'w') 801 fout.write(form) 802 fout.close() 803 argv = ['change', '-i', '<', formfile] 804 805 output, error, retval = self._p4run(argv, **p4options) 806 if _raw: 807 return {'stdout': output, 'stderr': error, 'retval': retval} 808 809 if action == 'get': 810 change = parseForm(output) 811 elif action in ('create', 'update', 'delete'): 812 lines = output.splitlines(1) 813 resultRes = [ 814 re.compile("^Change (?P<change>\d+)"\ 815 " (?P<action>created|updated|deleted)\.$"), 816 re.compile("^Change (?P<change>\d+) (?P<action>created)"\ 817 " (?P<comment>.+?)\.$"), 818 re.compile("^Change (?P<change>\d+) (?P<action>updated)"\ 819 ", (?P<comment>.+?)\.$"), 820 # e.g., Change 1 has 1 open file(s) associated with it and can't be deleted. 821 re.compile("^Change (?P<change>\d+) (?P<comment>.+?)\.$"), 822 ] 823 for resultRe in resultRes: 824 match = resultRe.match(lines[0]) 825 if match: 826 change = match.groupdict() 827 change['change'] = int(change['change']) 828 break 829 else: 830 err = "Internal error: could not parse change '%s' "\ 831 "output: '%s'" % (action, output) 832 raise P4LibError(err) 833 else: 834 raise P4LibError("Internal error: unexpected action: '%s'"\ 835 % action) 836 837 return change 838 finally: 839 if formfile: 840 os.remove(formfile) 841 842 def changes(self, files=[], followIntegrations=0, longOutput=0, 843 max=None, status=None, _raw=0, **p4options): 844 """Return a list of pending and submitted changelists. 845 846 "files" is a list of files or file wildcards that will limit the 847 results to changes including these files. Defaults to the 848 whole client view. 849 "followIntegrations" (-i) specifies to include any changelists 850 integrated into the given files. 851 "longOutput" (-l) includes changelist descriptions. 852 "max" (-m) limits the results to the given number of most recent 853 relevant changes. 854 "status" (-s) limits the output to 'pending' or 'submitted' 855 changelists. 856 857 Returns a list of dicts, each representing one change spec. Keys 858 are: 'change', 'date', 'client', 'user', 'description'. 859 860 If '_raw' is true then the return value is simply a dictionary 861 with the unprocessed results of calling p4: 862 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 863 """ 864 if max is not None and type(max) != int: 865 raise P4LibError("Incorrect 'max' value. It must be an integer: "\ 866 "'%s' (type '%s')" % (max, type(max))) 867 if status is not None and status not in ("pending", "submitted"): 868 raise P4LibError("Incorrect 'status' value: '%s'" % status) 869 870 if type(files) in (str,): 871 files = [files] 872 873 optv = [] 874 if followIntegrations: 875 optv.append('-i') 876 if longOutput: 877 optv.append('-l') 878 if max is not None: 879 optv += ['-m', str(max)] 880 if status is not None: 881 optv += ['-s', status] 882 argv = ['changes'] + optv 883 if files: 884 argv += files 885 output, error, retval = self._p4run(argv, **p4options) 886 if _raw: 887 return {'stdout': output, 'stderr': error, 'retval': retval} 888 889 changes = [] 890 if longOutput: 891 changeRe = re.compile("^Change (?P<change>\d+) on "\ 892 "(?P<date>[\d/]+) by (?P<user>[^\s@]+)@"\ 893 "(?P<client>[^\s@]+)$") 894 for line in output.splitlines(1): 895 if not line.strip(): continue # skip blank lines 896 if line.startswith('\t'): 897 # Append this line (minus leading tab) to last 898 # change's description. 899 changes[-1]['description'] += line[1:] 900 else: 901 change = changeRe.match(line).groupdict() 902 change['change'] = int(change['change']) 903 change['description'] = '' 904 changes.append(change) 905 else: 906 changeRe = re.compile("^Change (?P<change>\d+) on "\ 907 "(?P<date>[\d/]+) by (?P<user>[^\s@]+)@"\ 908 "(?P<client>[^\s@]+) (\*pending\* )?"\ 909 "'(?P<description>.*?)'$") 910 for line in output.splitlines(1): 911 match = changeRe.match(line) 912 if match: 913 change = match.groupdict() 914 change['change'] = int(change['change']) 915 changes.append(change) 916 else: 917 raise P4LibError("Internal error: could not parse "\ 918 "'p4 changes' output line: '%s'" % line) 919 return changes 920 921 def sync(self, files=[], force=0, dryrun=0, _raw=0, **p4options): 922 """Synchronize the client with its view of the depot. 923 924 "files" is a list of files or file wildcards to sync. Defaults 925 to the whole client view. 926 "force" (-f) forces resynchronization even if the client already 927 has the file, and clobbers writable files. 928 "dryrun" (-n) causes sync to go through the motions and report 929 results but not actually make any changes. 930 931 Returns a list of dicts representing the sync'd files. Keys are: 932 'depotFile', 'rev', 'comment', and possibly 'notes'. 933 934 If '_raw' is true then the return value is simply a dictionary 935 with the unprocessed results of calling p4: 936 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 937 """ 938 if type(files) in (str,): 939 files = [files] 940 optv = [] 941 if force: 942 optv.append('-f') 943 if dryrun: 944 optv.append('-n') 945 946 argv = ['sync'] + optv 947 if files: 948 argv += files 949 output, error, retval = self._p4run(argv, **p4options) 950 if _raw: 951 return {'stdout': output, 'stderr': error, 'retval': retval} 952 953 # Forms of output: 954 # //depot/foo#1 - updating C:\foo 955 # //depot/foo#1 - is opened and not being changed 956 # //depot/foo#1 - is opened at a later revision - not changed 957 # //depot/foo#1 - deleted as C:\foo 958 # ... //depot/foo - must resolve #2 before submitting 959 # There are probably others forms. 960 hits = [] 961 lineRe = re.compile('^(?P<depotFile>.+?)#(?P<rev>\d+) - '\ 962 '(?P<comment>.+?)$') 963 for line in output.splitlines(1): 964 if line.startswith('... '): 965 note = line.split(' - ')[-1].strip() 966 hits[-1]['notes'].append(note) 967 continue 968 match = lineRe.match(line) 969 if match: 970 hit = match.groupdict() 971 hit['rev'] = int(hit['rev']) 972 hit['notes'] = [] 973 hits.append(hit) 974 continue 975 raise P4LibError("Internal error: could not parse 'p4 sync'"\ 976 "output line: '%s'" % line) 977 return hits 978 979 def edit(self, files, change=None, filetype=None, _raw=0, **p4options): 980 """Open an existing file for edit. 981 982 "files" is a list of files or file wildcards to open for edit. 983 "change" (-c) is a pending changelist number in which to put the 984 opened files. 985 "filetype" (-t) specifies to explicitly open the files with the 986 given filetype. 987 988 Returns a list of dicts representing commentary on each file 989 opened for edit. Keys are: 'depotFile', 'rev', 'comment', 'notes'. 990 991 If '_raw' is true then the return value is simply a dictionary 992 with the unprocessed results of calling p4: 993 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 994 """ 995 if type(files) in (str,): 996 files = [files] 997 optv = [] 998 if change: 999 optv += ['-c', str(change)] 1000 if filetype: 1001 optv += ['-t', filetype] 1002 1003 argv = ['edit'] + optv + files 1004 output, error, retval = self._p4run(argv, **p4options) 1005 if _raw: 1006 return {'stdout': output, 'stderr': error, 'retval': retval} 1007 1008 # Example output: 1009 # //depot/build.py#142 - opened for edit 1010 # ... //depot/build.py - must sync/resolve #143,#148 before submitting 1011 # ... //depot/build.py - also opened by davida@davida-bertha 1012 # ... //depot/build.py - also opened by davida@davida-loom 1013 # ... //depot/build.py - also opened by davida@davida-marteau 1014 # ... //depot/build.py - also opened by trentm@trentm-razor 1015 # //depot/BuildNum.txt#3 - currently opened for edit 1016 hits = [] 1017 lineRe = re.compile('^(?P<depotFile>.+?)#(?P<rev>\d+) - '\ 1018 '(?P<comment>.*)$') 1019 for line in output.splitlines(1): 1020 if line.startswith("..."): # this is a note for the latest hit 1021 note = line.split(' - ')[-1].strip() 1022 hits[-1]['notes'].append(note) 1023 else: 1024 hit = lineRe.match(line).groupdict() 1025 hit['rev'] = int(hit['rev']) 1026 hit['notes'] = [] 1027 hits.append(hit) 1028 return hits 1029 1030 def add(self, files, change=None, filetype=None, _raw=0, **p4options): 1031 """Open a new file to add it to the depot. 1032 1033 "files" is a list of files or file wildcards to open for add. 1034 "change" (-c) is a pending changelist number in which to put the 1035 opened files. 1036 "filetype" (-t) specifies to explicitly open the files with the 1037 given filetype. 1038 1039 Returns a list of dicts representing commentary on each file 1040 *attempted* to be opened for add. Keys are: 'depotFile', 'rev', 1041 'comment', 'notes'. If a given file is NOT added then the 'rev' 1042 will be None. 1043 1044 If '_raw' is true then the return value is simply a dictionary 1045 with the unprocessed results of calling p4: 1046 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1047 """ 1048 if type(files) in (str,): 1049 files = [files] 1050 optv = [] 1051 if change: 1052 optv += ['-c', str(change)] 1053 if filetype: 1054 optv += ['-t', filetype] 1055 1056 argv = ['add'] + optv + files 1057 output, error, retval = self._p4run(argv, **p4options) 1058 if _raw: 1059 return {'stdout': output, 'stderr': error, 'retval': retval} 1060 1061 # Example output: 1062 # //depot/apps/px/p4.py#1 - opened for add 1063 # c:\trentm\apps\px\p4.py - missing, assuming text. 1064 # 1065 # //depot/apps/px/px.py - can't add (already opened for edit) 1066 # ... //depot/apps/px/px.py - warning: add of existing file 1067 # 1068 # //depot/apps/px/px.cpp - can't add existing file 1069 # 1070 # //depot/apps/px/t#1 - opened for add 1071 # 1072 hits = [] 1073 hitRe = re.compile('^(?P<depotFile>//.+?)(#(?P<rev>\d+))? - '\ 1074 '(?P<comment>.*)$') 1075 for line in output.splitlines(1): 1076 match = hitRe.match(line) 1077 if match: 1078 hit = match.groupdict() 1079 if hit['rev'] is not None: 1080 hit['rev'] = int(hit['rev']) 1081 hit['notes'] = [] 1082 hits.append(hit) 1083 else: 1084 if line.startswith("..."): 1085 note = line.split(' - ')[-1].strip() 1086 else: 1087 note = line.strip() 1088 hits[-1]['notes'].append(note) 1089 return hits 1090 1091 def files(self, files, _raw=0, **p4options): 1092 """List files in the depot. 1093 1094 "files" is a list of files or file wildcards to list. Defaults 1095 to the whole client view. 1096 1097 Returns a list of dicts, each representing one matching file. Keys 1098 are: 'depotFile', 'rev', 'type', 'change', 'action'. 1099 1100 If '_raw' is true then the return value is simply a dictionary 1101 with the unprocessed results of calling p4: 1102 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1103 """ 1104 if type(files) in (str,): 1105 files = [files] 1106 if not files: 1107 raise P4LibError("Missing/wrong number of arguments.") 1108 1109 argv = ['files'] + files 1110 output, error, retval = self._p4run(argv, **p4options) 1111 if _raw: 1112 return {'stdout': output, 'stderr': error, 'retval': retval} 1113 1114 hits = [] 1115 fileRe = re.compile("^(?P<depotFile>//.*?)#(?P<rev>\d+) - "\ 1116 "(?P<action>\w+) change (?P<change>\d+) "\ 1117 "\((?P<type>[\w+]+)\)$") 1118 for line in output.splitlines(1): 1119 match = fileRe.match(line) 1120 hit = match.groupdict() 1121 hit['rev'] = int(hit['rev']) 1122 hit['change'] = int(hit['change']) 1123 hits.append(hit) 1124 return hits 1125 1126 def filelog(self, files, followIntegrations=0, longOutput=0, maxRevs=None, 1127 _raw=0, **p4options): 1128 """List revision histories of files. 1129 1130 "files" is a list of files or file wildcards to describe. 1131 "followIntegrations" (-i) specifies to follow branches. 1132 "longOutput" (-l) includes changelist descriptions. 1133 "maxRevs" (-m) limits the results to the given number of 1134 most recent revisions. 1135 1136 Returns a list of hits. Each hit is a dict with the following 1137 keys: 'depotFile', 'revs'. 'revs' is a list of dicts, each 1138 representing one submitted revision of 'depotFile' and 1139 containing the following keys: 'action', 'change', 'client', 1140 'date', 'type', 'notes', 'rev', 'user'. 1141 1142 If '_raw' is true then the return value is simply a dictionary 1143 with the unprocessed results of calling p4: 1144 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1145 """ 1146 if maxRevs is not None and type(maxRevs) != int: 1147 raise P4LibError("Incorrect 'maxRevs' value. It must be an "\ 1148 "integer: '%s' (type '%s')"\ 1149 % (maxRevs, type(maxRevs))) 1150 1151 if type(files) in (str,): 1152 files = [files] 1153 if not files: 1154 raise P4LibError("Missing/wrong number of arguments.") 1155 1156 optv = [] 1157 if followIntegrations: 1158 optv.append('-i') 1159 if longOutput: 1160 optv.append('-l') 1161 if maxRevs is not None: 1162 optv += ['-m', str(maxRevs)] 1163 argv = ['filelog'] + optv + files 1164 output, error, retval = self._p4run(argv, **p4options) 1165 if _raw: 1166 return {'stdout': output, 'stderr': error, 'retval': retval} 1167 1168 hits = [] 1169 revRe = re.compile("^... #(?P<rev>\d+) change (?P<change>\d+) "\ 1170 "(?P<action>\w+) on (?P<date>[\d/]+) by "\ 1171 "(?P<user>[^\s@]+)@(?P<client>[^\s@]+) "\ 1172 "\((?P<type>[\w+]+)\)( '(?P<description>.*?)')?$") 1173 for line in output.splitlines(1): 1174 if longOutput and not line.strip(): 1175 continue # skip blank lines 1176 elif line.startswith('//'): 1177 hit = {'depotFile': line.strip(), 'revs': []} 1178 hits.append(hit) 1179 elif line.startswith('... ... '): 1180 hits[-1]['revs'][-1]['notes'].append(line[8:].strip()) 1181 elif line.startswith('... '): 1182 match = revRe.match(line) 1183 if match: 1184 d = match.groupdict('') 1185 d['change'] = int(d['change']) 1186 d['rev'] = int(d['rev']) 1187 hits[-1]['revs'].append(d) 1188 hits[-1]['revs'][-1]['notes'] = [] 1189 else: 1190 raise P4LibError("Internal parsing error: '%s'" % line) 1191 elif longOutput and line.startswith('\t'): 1192 # Append this line (minus leading tab) to last hit's 1193 # last rev's description. 1194 hits[-1]['revs'][-1]['description'] += line[1:] 1195 else: 1196 raise P4LibError("Unexpected 'p4 filelog' output: '%s'"\ 1197 % line) 1198 return hits 1199 1200 def print_(self, files, localFile=None, quiet=0, **p4options): 1201 """Retrieve depot file contents. 1202 1203 "files" is a list of files or file wildcards to print. 1204 "localFile" (-o) is the name of a local file in which to put the 1205 output text. 1206 "quiet" (-q) suppresses some file meta-information. 1207 1208 Returns a list of dicts, each representing one matching file. 1209 Keys are: 'depotFile', 'rev', 'type', 'change', 'action', 1210 and 'text'. If 'quiet', the first five keys will not be present. 1211 The 'text' key will not be present if the file is binary. If 1212 both 'quiet' and 'localFile', there will be no hits at all. 1213 """ 1214 if type(files) in (str,): 1215 files = [files] 1216 if not files: 1217 raise P4LibError("Missing/wrong number of arguments.") 1218 1219 optv = [] 1220 if localFile: 1221 optv += ['-o', localFile] 1222 if quiet: 1223 optv.append('-q') 1224 # There is *no* way to properly and reliably parse out multiple file 1225 # output without using -s or -G. Use the latter. 1226 if p4options: 1227 d = self.optd 1228 d.update(p4options) 1229 p4optv = makeOptv(**d) 1230 else: 1231 p4optv = self._optv 1232 argv = [self.p4, '-G'] + p4optv + ['print'] + optv + files 1233 cmd = _joinArgv(argv) 1234 log.debug("popen3 '%s'..." % cmd) 1235 i, o, e = os.popen3(cmd) 1236 hits = [] 1237 fileRe = re.compile("^(?P<depotFile>//.*?)#(?P<rev>\d+) - "\ 1238 "(?P<action>\w+) change (?P<change>\d+) "\ 1239 "\((?P<type>[\w+]+)\)$") 1240 try: 1241 startHitWithNextNode = 1 1242 while 1: 1243 node = marshal.load(o) 1244 if node['code'] == 'info': 1245 # Always start a new hit with an 'info' node. 1246 match = fileRe.match(node['data']) 1247 hit = match.groupdict() 1248 hit['change'] = int(hit['change']) 1249 hit['rev'] = int(hit['rev']) 1250 hits.append(hit) 1251 startHitWithNextNode = 0 1252 elif node['code'] == 'text': 1253 if startHitWithNextNode: 1254 hit = {'text': node['data']} 1255 hits.append(hit) 1256 else: 1257 if 'text' not in hits[-1]\ 1258 or hits[-1]['text'] is None: 1259 hits[-1]['text'] = node['data'] 1260 else: 1261 hits[-1]['text'] += node['data'] 1262 startHitWithNextNode = not node['data'] 1263 except EOFError: 1264 pass 1265 return hits 1266 1267 def diff(self, files=[], diffFormat='', force=0, satisfying=None, 1268 text=0, _raw=0, **p4options): 1269 """Display diff of client files with depot files. 1270 1271 "files" is a list of files or file wildcards to diff. 1272 "diffFormat" (-d<flag>) is a flag to pass to the built-in diff 1273 routine to control the output format. Valid values are '' 1274 (plain, default), 'n' (RCS), 'c' (context), 's' (summary), 1275 'u' (unified). 1276 "force" (-f) forces a diff of every file. 1277 "satifying" (-s<flag>) limits the output to the names of files 1278 satisfying certain criteria: 1279 'a' Opened files that are different than the revision 1280 in the depot, or missing. 1281 'd' Unopened files that are missing on the client. 1282 'e' Unopened files that are different than the 1283 revision in the depot. 1284 'r' Opened files that are the same as the revision in 1285 the depot. 1286 "text" (-t) forces diffs of non-text files. 1287 1288 Returns a list of dicts representing each file diff'd. If 1289 "satifying" is specified each dict will simply include a 1290 'localFile' key. Otherwise, each dict will include 'localFile', 1291 'depotFile', 'rev', and 'binary' (boolean) keys and possibly a 1292 'text' or a 'notes' key iff there are any differences. Generally 1293 you will get a 'notes' key for differing binary files. 1294 1295 If '_raw' is true then the return value is simply a dictionary 1296 with the unprocessed results of calling p4: 1297 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1298 """ 1299 if type(files) in (str,): 1300 files = [files] 1301 if diffFormat not in ('', 'n', 'c', 's', 'u'): 1302 raise P4LibError("Incorrect diff format flag: '%s'" % diffFormat) 1303 if satisfying is not None\ 1304 and satisfying not in ('a', 'd', 'e', 'r'): 1305 raise P4LibError("Incorrect 'satisfying' flag: '%s'" % satisfying) 1306 optv = [] 1307 if diffFormat: 1308 optv.append('-d%s' % diffFormat) 1309 if satisfying: 1310 optv.append('-s%s' % satisfying) 1311 if force: 1312 optv.append('-f') 1313 if text: 1314 optv.append('-t') 1315 1316 # There is *no* to properly and reliably parse out multiple file 1317 # output without using -s or -G. Use the latter. (XXX Huh?) 1318 argv = ['diff'] + optv + files 1319 output, error, retval = self._p4run(argv, **p4options) 1320 if _raw: 1321 return {'stdout': output, 'stderr': error, 'retval': retval} 1322 1323 if satisfying is not None: 1324 hits = [{'localFile': line[:-1]} for line in output.splitlines(1)] 1325 else: 1326 hits = self._parseDiffOutput(output) 1327 return hits 1328 1329 def _parseDiffOutput(self, output): 1330 if type(output) in (str,): 1331 outputLines = output.splitlines(1) 1332 else: 1333 outputLines = output 1334 hits = [] 1335 # Example header lines: 1336 # - from 'p4 describe': 1337 # ==== //depot/apps/px/ReadMe.txt#5 (text) ==== 1338 # - from 'p4 diff': 1339 # ==== //depot/apps/px/p4lib.py#12 - c:\trentm\apps\px\p4lib.py ==== 1340 # ==== //depot/foo.doc#42 - c:\trentm\foo.doc ==== (binary) 1341 header1Re = re.compile("^==== (?P<depotFile>//.*?)#(?P<rev>\d+) "\ 1342 "\((?P<type>\w+)\) ====$") 1343 header2Re = re.compile("^==== (?P<depotFile>//.*?)#(?P<rev>\d+) - "\ 1344 "(?P<localFile>.+?) ===="\ 1345 "(?P<binary> \(binary\))?$") 1346 for line in outputLines: 1347 header1 = header1Re.match(line) 1348 header2 = header2Re.match(line) 1349 if header1: 1350 hit = header1.groupdict() 1351 hit['rev'] = int(hit['rev']) 1352 hits.append(hit) 1353 elif header2: 1354 hit = header2.groupdict() 1355 hit['rev'] = int(hit['rev']) 1356 hit['binary'] = not not hit['binary'] # get boolean value 1357 hits.append(hit) 1358 elif (len(hits) > 0) and ('text' not in hits[-1])\ 1359 and line == "(... files differ ...)\n": 1360 hits[-1]['notes'] = [line] 1361 elif len(hits) > 0: 1362 # This is a diff line. 1363 if 'text' not in hits[-1]: 1364 hits[-1]['text'] = '' 1365 # XXX 'p4 describe' diff text includes a single 1366 # blank line after each header line before the 1367 # actual diff. Should this be stripped? 1368 hits[-1]['text'] += line 1369 1370 return hits 1371 1372 def diff2(self, file1, file2, diffFormat='', quiet=0, text=0, 1373 **p4options): 1374 """Compare two depot files. 1375 1376 "file1" and "file2" are the two files to diff. 1377 "diffFormat" (-d<flag>) is a flag to pass to the built-in diff 1378 routine to control the output format. Valid values are '' 1379 (plain, default), 'n' (RCS), 'c' (context), 's' (summary), 1380 'u' (unified). 1381 "quiet" (-q) suppresses some meta information and all 1382 information if the files do not differ. 1383 1384 Returns a dict representing the diff. Keys are: 'depotFile1', 1385 'rev1', 'type1', 'depotFile2', 'rev2', 'type2', 1386 'summary', 'notes', 'text'. There may not be a 'text' key if the 1387 files are the same or are binary. The first eight keys will not 1388 be present if 'quiet'. 1389 1390 Note that the second 'p4 diff2' style is not supported: 1391 p4 diff2 [ -d<flag> -q -t ] -b branch [ [ file1 ] file2 ] 1392 """ 1393 if diffFormat not in ('', 'n', 'c', 's', 'u'): 1394 raise P4LibError("Incorrect diff format flag: '%s'" % diffFormat) 1395 optv = [] 1396 if diffFormat: 1397 optv.append('-d%s' % diffFormat) 1398 if quiet: 1399 optv.append('-q') 1400 if text: 1401 optv.append('-t') 1402 1403 # There is *no* way to properly and reliably parse out multiple 1404 # file output without using -s or -G. Use the latter. 1405 if p4options: 1406 d = self.optd 1407 d.update(p4options) 1408 p4optv = makeOptv(**d) 1409 else: 1410 p4optv = self._optv 1411 argv = [self.p4, '-G'] + p4optv + ['diff2'] + optv + [file1, file2] 1412 cmd = _joinArgv(argv) 1413 i, o, e = os.popen3(cmd) 1414 diff = {} 1415 infoRe = re.compile("^==== (?P<depotFile1>.+?)#(?P<rev1>\d+) "\ 1416 "\((?P<type1>[\w+]+)\) - "\ 1417 "(?P<depotFile2>.+?)#(?P<rev2>\d+) "\ 1418 "\((?P<type2>[\w+]+)\) "\ 1419 "==== (?P<summary>\w+)$") 1420 try: 1421 while 1: 1422 node = marshal.load(o) 1423 if node['code'] == 'info'\ 1424 and node['data'] == '(... files differ ...)': 1425 if 'notes' in diff: 1426 diff['notes'].append(node['data']) 1427 else: 1428 diff['notes'] = [ node['data'] ] 1429 elif node['code'] == 'info': 1430 match = infoRe.match(node['data']) 1431 d = match.groupdict() 1432 d['rev1'] = int(d['rev1']) 1433 d['rev2'] = int(d['rev2']) 1434 diff.update( match.groupdict() ) 1435 elif node['code'] == 'text': 1436 if 'text' not in diff or diff['text'] is None: 1437 diff['text'] = node['data'] 1438 else: 1439 diff['text'] += node['data'] 1440 except EOFError: 1441 pass 1442 return diff 1443 1444 def revert(self, files=[], change=None, unchangedOnly=0, _raw=0, 1445 **p4options): 1446 """Discard changes for the given opened files. 1447 1448 "files" is a list of files or file wildcards to revert. Default 1449 to the whole client view. 1450 "change" (-c) will limit to files opened in the given 1451 changelist. 1452 "unchangedOnly" (-a) will only revert opened files that are not 1453 different than the version in the depot. 1454 1455 Returns a list of dicts representing commentary on each file 1456 reverted. Keys are: 'depotFile', 'rev', 'comment'. 1457 1458 If '_raw' is true then the return value is simply a dictionary 1459 with the unprocessed results of calling p4: 1460 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1461 """ 1462 if type(files) in (str,): 1463 files = [files] 1464 optv = [] 1465 if change: 1466 optv += ['-c', str(change)] 1467 if unchangedOnly: 1468 optv += ['-a'] 1469 if not unchangedOnly and not files: 1470 raise P4LibError("Missing/wrong number of arguments.") 1471 1472 argv = ['revert'] + optv + files 1473 output, error, retval = self._p4run(argv, **p4options) 1474 if _raw: 1475 return {'stdout': output, 'stderr': error, 'retval': retval} 1476 1477 # Example output: 1478 # //depot/hello.txt#1 - was edit, reverted 1479 # //depot/test_g.txt#none - was add, abandoned 1480 hits = [] 1481 hitRe = re.compile('^(?P<depotFile>//.+?)(#(?P<rev>\w+))? - '\ 1482 '(?P<comment>.*)$') 1483 for line in output.splitlines(1): 1484 match = hitRe.match(line) 1485 if match: 1486 hit = match.groupdict() 1487 try: 1488 hit['rev'] = int(hit['rev']) 1489 except ValueError: 1490 pass 1491 hits.append(hit) 1492 else: 1493 raise P4LibError("Internal parsing error: '%s'" % line) 1494 return hits 1495 1496 def resolve(self, files=[], autoMode='', force=0, dryrun=0, 1497 text=0, verbose=0, _raw=0, **p4options): 1498 """Merge open files with other revisions or files. 1499 1500 This resolve, for obvious reasons, only supports the options to 1501 'p4 resolve' that will result is *no* command line interaction. 1502 1503 'files' is a list of files, of file wildcards, to resolve. 1504 'autoMode' (-a*) tells how to resolve merges. See below for 1505 valid values. 1506 'force' (-f) allows previously resolved files to be resolved again. 1507 'dryrun' (-n) lists the integrations that *would* be performed 1508 without performing them. 1509 'text' (-t) will force a textual merge, even for binary file types. 1510 'verbose' (-v) will cause markers to be placed in all changed 1511 files not just those that conflict. 1512 1513 Valid values of 'autoMode' are: 1514 '' '-a' I believe this is equivalent to '-am'. 1515 'f', 'force' '-af' Force acceptance of merged files with 1516 conflicts. 1517 'm', 'merge' '-am' Attempts to merge. 1518 's', 'safe' '-as' Does not attempt to merge. 1519 't', 'theirs' '-at' Accepts "their" changes, OVERWRITING yours. 1520 'y', 'yours' '-ay' Accepts your changes, OVERWRITING "theirs". 1521 Invalid values of 'autoMode': 1522 None As if no -a option had been specified. 1523 Invalid because this may result in command 1524 line interaction. 1525 1526 Returns a list of dicts representing commentary on each file for 1527 which a resolve was attempted. Keys are: 'localFile', 'clientFile' 1528 'comment', and 'action'; and possibly 'diff chunks' if there was 1529 anything to merge. 1530 1531 If '_raw' is true then the return value is simply a dictionary 1532 with the unprocessed results of calling p4: 1533 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1534 """ 1535 if type(files) in (str,): 1536 files = [files] 1537 optv = [] 1538 if autoMode is None: 1539 raise P4LibError("'autoMode' must be non-None, otherwise "\ 1540 "'p4 resolve' may initiate command line "\ 1541 "interaction, which will hang this method.") 1542 else: 1543 optv += ['-a%s' % autoMode] 1544 if force: 1545 optv += ['-f'] 1546 if dryrun: 1547 optv += ['-n'] 1548 if text: 1549 optv += ['-t'] 1550 if verbose: 1551 optv += ['-v'] 1552 argv = ['resolve'] + optv + files 1553 output, error, retval = self._p4run(argv, **p4options) 1554 if _raw: 1555 return {'stdout': output, 'stderr': error, 'retval': retval} 1556 1557 hits = [] 1558 # Example output: 1559 # C:\rootdir\foo.txt - merging //depot/foo.txt#2 1560 # Diff chunks: 0 yours + 0 theirs + 0 both + 1 conflicting 1561 # //client-name/foo.txt - resolve skipped. 1562 # Proposed result: 1563 # [{'localFile': 'C:\\rootdir\\foo.txt', 1564 # 'depotFile': '//depot/foo.txt', 1565 # 'rev': 2 1566 # 'clientFile': '//client-name/foo.txt', 1567 # 'diff chunks': {'yours': 0, 'theirs': 0, 'both': 0, 1568 # 'conflicting': 1} 1569 # 'action': 'resolve skipped'}] 1570 # 1571 # Example output: 1572 # C:\rootdir\foo.txt - vs //depot/foo.txt#2 1573 # //client-name/foo.txt - ignored //depot/foo.txt 1574 # Proposed result: 1575 # [{'localFile': 'C:\\rootdir\\foo.txt', 1576 # 'depotFile': '//depot/foo.txt', 1577 # 'rev': 2 1578 # 'clientFile': '//client-name/foo.txt', 1579 # 'diff chunks': {'yours': 0, 'theirs': 0, 'both': 0, 1580 # 'conflicting': 1} 1581 # 'action': 'ignored //depot/foo.txt'}] 1582 # 1583 introRe = re.compile('^(?P<localFile>.+?) - (merging|vs) '\ 1584 '(?P<depotFile>//.+?)#(?P<rev>\d+)$') 1585 diffRe = re.compile('^Diff chunks: (?P<yours>\d+) yours \+ '\ 1586 '(?P<theirs>\d+) theirs \+ (?P<both>\d+) both '\ 1587 '\+ (?P<conflicting>\d+) conflicting$') 1588 actionRe = re.compile('^(?P<clientFile>//.+?) - (?P<action>.+?)(\.)?$') 1589 for line in output.splitlines(1): 1590 match = introRe.match(line) 1591 if match: 1592 hit = match.groupdict() 1593 hit['rev'] = int(hit['rev']) 1594 hits.append(hit) 1595 log.info("parsed resolve 'intro' line: '%s'" % line.strip()) 1596 continue 1597 match = diffRe.match(line) 1598 if match: 1599 diff = match.groupdict() 1600 diff['yours'] = int(diff['yours']) 1601 diff['theirs'] = int(diff['theirs']) 1602 diff['both'] = int(diff['both']) 1603 diff['conflicting'] = int(diff['conflicting']) 1604 hits[-1]['diff chunks'] = diff 1605 log.info("parsed resolve 'diff' line: '%s'" % line.strip()) 1606 continue 1607 match = actionRe.match(line) 1608 if match: 1609 hits[-1].update(match.groupdict()) 1610 log.info("parsed resolve 'action' line: '%s'" % line.strip()) 1611 continue 1612 raise P4LibError("Internal error: could not parse 'p4 resolve' "\ 1613 "output line: line='%s' argv=%s" % (line, argv)) 1614 return hits 1615 1616 def submit(self, files=None, description=None, change=None, _raw=0, 1617 **p4options): 1618 """Submit open files to the depot. 1619 1620 There are two ways to call this method: 1621 - Submit specific files: 1622 p4.submit([...], "checkin message") 1623 - Submit a pending changelist: 1624 p4.submit(change=123) 1625 Note: 'change' should always be specified with a keyword 1626 argument. I reserve the right to extend this method by 1627 adding kwargs *before* the change arg. So p4.submit(None, 1628 None, 123) is not guaranteed to work. 1629 1630 Returns a dict with a 'files' key (which is a list of dicts with 1631 'depotFile', 'rev', and 'action' keys), and 'action' 1632 (=='submitted') and 'change' keys iff the submit is succesful. 1633 1634 Note: An equivalent for the '-s' option to 'p4 submit' is not 1635 supported, because I don't know how to use it and have never. 1636 Nor is the '-i' option supported, although it *is* used 1637 internally to drive 'p4 submit'. 1638 1639 If '_raw' is true then the return value is simply a dictionary 1640 with the unprocessed results of calling p4: 1641 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1642 """ 1643 #TODO: 1644 # - test when submission fails because files need to be 1645 # resolved 1646 # - Structure this code more like change, client, label, & branch. 1647 formfile = None 1648 try: 1649 if type(files) in (str,): 1650 files = [files] 1651 if change and not files and not description: 1652 argv = ['submit', '-c', str(change)] 1653 elif not change and files is not None and description: 1654 # Empty 'files' should default to all opened files in the 1655 # 'default' changelist. 1656 if not files: 1657 files = [{'depotFile': f['depotFile']}\ 1658 for f in self.opened()] 1659 else: 1660 #TODO: Add test to expect P4LibError if try to use 1661 # p4 wildcards in files. 1662 files = [{'depotFile': f['depotFile']}\ 1663 for f in self.where(files)] 1664 # Build submission form file. 1665 formfile = tempfile.mktemp() 1666 form = makeForm(files=files, description=description, 1667 change='new') 1668 fout = open(formfile, 'w') 1669 fout.write(form) 1670 fout.close() 1671 argv = ['submit', '-i', '<', formfile] 1672 else: 1673 raise P4LibError("Incorrect arguments. You must specify "\ 1674 "'change' OR you must specify 'files' and "\ 1675 "'description'.") 1676 1677 output, error, retval = self._p4run(argv, **p4options) 1678 if _raw: 1679 return {'stdout': output, 'stderr': error, 'retval': retval} 1680 1681 # Example output: 1682 # Change 1 created with 1 open file(s). 1683 # Submitting change 1. 1684 # Locking 1 files ... 1685 # add //depot/test_simple_submit.txt#1 1686 # Change 1 submitted. 1687 # This returns (similar to .change() output): 1688 # {'change': 1, 1689 # 'action': 'submitted', 1690 # 'files': [{'depotFile': '//depot/test_simple_submit.txt', 1691 # 'rev': 1, 1692 # 'action': 'add'}]} 1693 # i.e. only the file actions and the last "submitted" line are 1694 # looked for. 1695 skipRes = [ 1696 re.compile('^Change \d+ created with \d+ open file\(s\)\.$'), 1697 re.compile('^Submitting change \d+\.$'), 1698 re.compile('^Locking \d+ files \.\.\.$')] 1699 fileRe = re.compile('^(?P<action>\w+) (?P<depotFile>//.+?)'\ 1700 '#(?P<rev>\d+)$') 1701 resultRe = re.compile('^Change (?P<change>\d+) '\ 1702 '(?P<action>submitted)\.') 1703 result = {'files': []} 1704 for line in output.splitlines(1): 1705 match = fileRe.match(line) 1706 if match: 1707 file = match.groupdict() 1708 file['rev'] = int(file['rev']) 1709 result['files'].append(file) 1710 log.info("parsed submit 'file' line: '%s'", line.strip()) 1711 continue 1712 match = resultRe.match(line) 1713 if match: 1714 result.update(match.groupdict()) 1715 result['change'] = int(result['change']) 1716 log.info("parsed submit 'result' line: '%s'", 1717 line.strip()) 1718 continue 1719 # The following is technically just overhead but it is 1720 # considered more robust if we explicitly try to recognize 1721 # all output. Unrecognized output can be warned or raised. 1722 for skipRe in skipRes: 1723 match = skipRe.match(line) 1724 if match: 1725 log.info("parsed submit 'skip' line: '%s'", 1726 line.strip()) 1727 break 1728 else: 1729 log.warn("Unrecognized output line from running %s: "\ 1730 "'%s'. Please report this to the maintainer."\ 1731 % (argv, line)) 1732 return result 1733 finally: 1734 if formfile: 1735 os.remove(formfile) 1736 1737 def delete(self, files, change=None, _raw=0, **p4options): 1738 """Open an existing file to delete it from the depot. 1739 1740 "files" is a list of files or file wildcards to open for delete. 1741 "change" (-c) is a pending change with which to associate the 1742 opened file(s). 1743 1744 Returns a list of dicts each representing a file *attempted* to 1745 be open for delete. Keys are 'depotFile', 'rev', and 'comment'. 1746 If the file could *not* be openned for delete then 'rev' will be 1747 None. 1748 1749 If '_raw' is true then the return value is simply a dictionary 1750 with the unprocessed results of calling p4: 1751 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1752 """ 1753 if type(files) in (str,): 1754 files = [files] 1755 optv = [] 1756 if change: optv += ['-c', str(change)] 1757 1758 argv = ['delete'] + optv + files 1759 output, error, retval = self._p4run(argv, **p4options) 1760 if _raw: 1761 return {'stdout': output, 'stderr': error, 'retval': retval} 1762 1763 # Example output: 1764 # //depot/foo.txt#1 - opened for delete 1765 # //depot/foo.txt - can't delete (already opened for edit) 1766 hits = [] 1767 hitRe = re.compile('^(?P<depotFile>.+?)(#(?P<rev>\d+))? - '\ 1768 '(?P<comment>.*)$') 1769 for line in output.splitlines(1): 1770 match = hitRe.match(line) 1771 if match: 1772 hit = match.groupdict() 1773 if hit['rev'] is not None: 1774 hit['rev'] = int(hit['rev']) 1775 hits.append(hit) 1776 else: 1777 raise P4LibError("Internal error: could not parse "\ 1778 "'p4 delete' output line: '%s'. Please "\ 1779 "report this to the author." % line) 1780 return hits 1781 1782 def client(self, name=None, client=None, delete=0, _raw=0, **p4options): 1783 """Create, update, delete, or get a client specification. 1784 1785 Creating a new client spec or updating an existing one: 1786 p4.client(client=<client dictionary>) 1787 OR 1788 p4.client(name=<an existing client name>, 1789 client=<client dictionary>) 1790 Returns a dictionary of the following form: 1791 {'client': <clientname>, 'action': <action taken>} 1792 1793 Deleting a client spec: 1794 p4.client(name=<an existing client name>, delete=1) 1795 Returns a dictionary of the following form: 1796 {'client': <clientname>, 'action': 'deleted'} 1797 1798 Getting a client spec: 1799 ch = p4.client(name=<an existing client name>) 1800 Returns a dictionary describing the client. For example: 1801 {'access': '2002/07/16 00:05:31', 1802 'client': 'trentm-ra', 1803 'description': 'Created by trentm.', 1804 'host': 'ra', 1805 'lineend': 'local', 1806 'options': 'noallwrite noclobber nocompress unlocked nomodtime normdir', 1807 'owner': 'trentm', 1808 'root': 'c:\\trentm\\', 1809 'update': '2002/03/18 22:33:18', 1810 'view': '//depot/... //trentm-ra/...'} 1811 1812 If '_raw' is true then the return value is simply a dictionary 1813 with the unprocessed results of calling p4: 1814 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1815 1816 Limitations: The -f (force) and -t (template) flags are not 1817 supported. However, there is no strong need to support -t 1818 because the use of dictionaries in this API makes this trivial. 1819 """ 1820 formfile = None 1821 try: 1822 action = None # note action to know how to parse output below 1823 if delete: 1824 action = "delete" 1825 if name is None: 1826 raise P4LibError("Incomplete/missing arguments: must "\ 1827 "specify 'name' of client to delete.") 1828 argv = ['client', '-d', name] 1829 elif client is None: 1830 action = "get" 1831 if name is None: 1832 raise P4LibError("Incomplete/missing arguments: must "\ 1833 "specify 'name' of client to get.") 1834 argv = ['client', '-o', name] 1835 else: 1836 action = "create/update" 1837 if "client" in client: 1838 name = client["client"] 1839 if name is not None: 1840 cl = self.client(name=name) 1841 else: 1842 cl = {} 1843 cl.update(client) 1844 form = makeForm(**cl) 1845 1846 # Build submission form file. 1847 formfile = tempfile.mktemp() 1848 fout = open(formfile, 'w') 1849 fout.write(form) 1850 fout.close() 1851 argv = ['client', '-i', '<', formfile] 1852 1853 output, error, retval = self._p4run(argv, **p4options) 1854 if _raw: 1855 return {'stdout': output, 'stderr': error, 'retval': retval} 1856 1857 if action == 'get': 1858 rv = parseForm(output) 1859 elif action in ('create/update', 'delete'): 1860 lines = output.splitlines(1) 1861 # Example output: 1862 # Client trentm-ra not changed. 1863 # Client bertha-test deleted. 1864 # Client bertha-test saved. 1865 resultRe = re.compile("^Client (?P<client>[^\s@]+)"\ 1866 " (?P<action>not changed|deleted|saved)\.$") 1867 match = resultRe.match(lines[0]) 1868 if match: 1869 rv = match.groupdict() 1870 else: 1871 err = "Internal error: could not parse p4 client "\ 1872 "output: '%s'" % output 1873 raise P4LibError(err) 1874 else: 1875 raise P4LibError("Internal error: unexpected action: '%s'"\ 1876 % action) 1877 1878 return rv 1879 finally: 1880 if formfile: 1881 os.remove(formfile) 1882 1883 def clients(self, _raw=0, **p4options): 1884 """Return a list of clients. 1885 1886 Returns a list of dicts, each representing one client spec, e.g.: 1887 [{'client': 'trentm-ra', # client name 1888 'update': '2002/03/18', # client last modification date 1889 'root': 'c:\\trentm\\', # the client root directory 1890 'description': 'Created by trentm. '}, 1891 # *part* of the client description 1892 ... 1893 ] 1894 1895 If '_raw' is true then the return value is simply a dictionary 1896 with the unprocessed results of calling p4: 1897 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1898 """ 1899 argv = ['clients'] 1900 output, error, retval = self._p4run(argv, **p4options) 1901 if _raw: 1902 return {'stdout': output, 'stderr': error, 'retval': retval} 1903 1904 # Examples: 1905 # Client trentm-ra 2002/03/18 root c:\trentm\ 'Created by trentm. ' 1906 clientRe = re.compile("^Client (?P<client>[^\s@]+) "\ 1907 "(?P<update>[\d/]+) "\ 1908 "root (?P<root>.*?) '(?P<description>.*?)'$") 1909 clients = [] 1910 for line in output.splitlines(1): 1911 match = clientRe.match(line) 1912 if match: 1913 client = match.groupdict() 1914 clients.append(client) 1915 else: 1916 raise P4LibError("Internal error: could not parse "\ 1917 "'p4 clients' output line: '%s'" % line) 1918 return clients 1919 1920 def label(self, name=None, label=None, delete=0, _raw=0, **p4options): 1921 r"""Create, update, delete, or get a label specification. 1922 1923 Creating a new label spec or updating an existing one: 1924 p4.label(label=<label dictionary>) 1925 OR 1926 p4.label(name=<an existing label name>, 1927 label=<label dictionary>) 1928 Returns a dictionary of the following form: 1929 {'label': <labelname>, 'action': <action taken>} 1930 1931 Deleting a label spec: 1932 p4.label(name=<an existing label name>, delete=1) 1933 Returns a dictionary of the following form: 1934 {'label': <labelname>, 'action': 'deleted'} 1935 1936 Getting a label spec: 1937 ch = p4.label(name=<an existing label name>) 1938 Returns a dictionary describing the label. For example: 1939 {'access': '2001/07/13 10:42:32', 1940 'description': 'ActivePerl 623', 1941 'label': 'ActivePerl_623', 1942 'options': 'locked', 1943 'owner': 'daves', 1944 'update': '2000/12/15 20:15:48', 1945 'view': '//depot/main/Apps/ActivePerl/...\n//depot/main/support/...'} 1946 1947 If '_raw' is true then the return value is simply a dictionary 1948 with the unprocessed results of calling p4: 1949 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 1950 1951 Limitations: The -f (force) and -t (template) flags are not 1952 supported. However, there is no strong need to support -t 1953 because the use of dictionaries in this API makes this trivial. 1954 """ 1955 formfile = None 1956 try: 1957 action = None # note action to know how to parse output below 1958 if delete: 1959 action = "delete" 1960 if name is None: 1961 raise P4LibError("Incomplete/missing arguments: must "\ 1962 "specify 'name' of label to delete.") 1963 argv = ['label', '-d', name] 1964 elif label is None: 1965 action = "get" 1966 if name is None: 1967 raise P4LibError("Incomplete/missing arguments: must "\ 1968 "specify 'name' of label to get.") 1969 argv = ['label', '-o', name] 1970 else: 1971 action = "create/update" 1972 if "label" in label: 1973 name = label["label"] 1974 if name is not None: 1975 lbl = self.label(name=name) 1976 else: 1977 lbl = {} 1978 lbl.update(label) 1979 form = makeForm(**lbl) 1980 1981 # Build submission form file. 1982 formfile = tempfile.mktemp() 1983 fout = open(formfile, 'w') 1984 fout.write(form) 1985 fout.close() 1986 argv = ['label', '-i', '<', formfile] 1987 1988 output, error, retval = self._p4run(argv, **p4options) 1989 if _raw: 1990 return {'stdout': output, 'stderr': error, 'retval': retval} 1991 1992 if action == 'get': 1993 rv = parseForm(output) 1994 elif action in ('create/update', 'delete'): 1995 lines = output.splitlines(1) 1996 # Example output: 1997 # Client trentm-ra not changed. 1998 # Client bertha-test deleted. 1999 # Client bertha-test saved. 2000 resultRe = re.compile("^Label (?P<label>[^\s@]+)"\ 2001 " (?P<action>not changed|deleted|saved)\.$") 2002 match = resultRe.match(lines[0]) 2003 if match: 2004 rv = match.groupdict() 2005 else: 2006 err = "Internal error: could not parse p4 label "\ 2007 "output: '%s'" % output 2008 raise P4LibError(err) 2009 else: 2010 raise P4LibError("Internal error: unexpected action: '%s'"\ 2011 % action) 2012 2013 return rv 2014 finally: 2015 if formfile: 2016 os.remove(formfile) 2017 2018 def labels(self, _raw=0, **p4options): 2019 """Return a list of labels. 2020 2021 Returns a list of dicts, each representing one labels spec, e.g.: 2022 [{'label': 'ActivePerl_623', # label name 2023 'description': 'ActivePerl 623 ', 2024 # *part* of the label description 2025 'update': '2000/12/15'}, # label last modification date 2026 ... 2027 ] 2028 2029 If '_raw' is true then the return value is simply a dictionary 2030 with the unprocessed results of calling p4: 2031 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 2032 """ 2033 argv = ['labels'] 2034 output, error, retval = self._p4run(argv, **p4options) 2035 if _raw: 2036 return {'stdout': output, 'stderr': error, 'retval': retval} 2037 2038 labelRe = re.compile("^Label (?P<label>[^\s@]+) "\ 2039 "(?P<update>[\d/]+) "\ 2040 "'(?P<description>.*?)'$") 2041 labels = [] 2042 for line in output.splitlines(1): 2043 match = labelRe.match(line) 2044 if match: 2045 label = match.groupdict() 2046 labels.append(label) 2047 else: 2048 raise P4LibError("Internal error: could not parse "\ 2049 "'p4 labels' output line: '%s'" % line) 2050 return labels 2051 2052 def flush(self, files=[], force=0, dryrun=0, _raw=0, **p4options): 2053 """Fake a 'sync' by not moving files. 2054 2055 "files" is a list of files or file wildcards to flush. Defaults 2056 to the whole client view. 2057 "force" (-f) forces resynchronization even if the client already 2058 has the file, and clobbers writable files. 2059 "dryrun" (-n) causes sync to go through the motions and report 2060 results but not actually make any changes. 2061 2062 Returns a list of dicts representing the flush'd files. For 2063 example: 2064 [{'comment': 'added as C:\\...\\foo.txt', 2065 'depotFile': '//depot/.../foo.txt', 2066 'notes': [], 2067 'rev': 1}, 2068 {'comment': 'added as C:\\...\\bar.txt', 2069 'depotFile': '//depot/.../bar.txt', 2070 'notes': [], 2071 'rev': 1}, 2072 ] 2073 2074 If '_raw' is true then the return value is simply a dictionary 2075 with the unprocessed results of calling p4: 2076 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 2077 """ 2078 if type(files) in (str,): 2079 files = [files] 2080 optv = [] 2081 if force: 2082 optv.append('-f') 2083 if dryrun: 2084 optv.append('-n') 2085 2086 argv = ['flush'] + optv 2087 if files: 2088 argv += files 2089 output, error, retval = self._p4run(argv, **p4options) 2090 if _raw: 2091 return {'stdout': output, 'stderr': error, 'retval': retval} 2092 2093 # Forms of output: 2094 # //depot/foo#1 - updating C:\foo 2095 # //depot/foo#1 - is opened and not being changed 2096 # //depot/foo#1 - is opened at a later revision - not changed 2097 # //depot/foo#1 - deleted as C:\foo 2098 # ... //depot/foo - must resolve #2 before submitting 2099 # There are probably others forms. 2100 hits = [] 2101 lineRe = re.compile('^(?P<depotFile>.+?)#(?P<rev>\d+) - '\ 2102 '(?P<comment>.+?)$') 2103 for line in output.splitlines(1): 2104 if line.startswith('... '): 2105 note = line.split(' - ')[-1].strip() 2106 hits[-1]['notes'].append(note) 2107 continue 2108 match = lineRe.match(line) 2109 if match: 2110 hit = match.groupdict() 2111 hit['rev'] = int(hit['rev']) 2112 hit['notes'] = [] 2113 hits.append(hit) 2114 continue 2115 raise P4LibError("Internal error: could not parse 'p4 flush'"\ 2116 "output line: '%s'" % line) 2117 return hits 2118 2119 def branch(self, name=None, branch=None, delete=0, _raw=0, **p4options): 2120 r"""Create, update, delete, or get a branch specification. 2121 2122 Creating a new branch spec or updating an existing one: 2123 p4.branch(branch=<branch dictionary>) 2124 OR 2125 p4.branch(name=<an existing branch name>, 2126 branch=<branch dictionary>) 2127 Returns a dictionary of the following form: 2128 {'branch': <branchname>, 'action': <action taken>} 2129 2130 Deleting a branch spec: 2131 p4.branch(name=<an existing branch name>, delete=1) 2132 Returns a dictionary of the following form: 2133 {'branch': <branchname>, 'action': 'deleted'} 2134 2135 Getting a branch spec: 2136 ch = p4.branch(name=<an existing branch name>) 2137 Returns a dictionary describing the branch. For example: 2138 {'access': '2000/12/01 16:54:57', 2139 'branch': 'trentm-roundup', 2140 'description': 'Branch ...', 2141 'options': 'unlocked', 2142 'owner': 'trentm', 2143 'update': '2000/12/01 16:54:57', 2144 'view': '//depot/foo/... //depot/bar...'} 2145 2146 If '_raw' is true then the return value is simply a dictionary 2147 with the unprocessed results of calling p4: 2148 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 2149 2150 Limitations: The -f (force) and -t (template) flags are not 2151 supported. However, there is no strong need to support -t 2152 because the use of dictionaries in this API makes this trivial. 2153 """ 2154 formfile = None 2155 try: 2156 action = None # note action to know how to parse output below 2157 if delete: 2158 action = "delete" 2159 if name is None: 2160 raise P4LibError("Incomplete/missing arguments: must "\ 2161 "specify 'name' of branch to delete.") 2162 argv = ['branch', '-d', name] 2163 elif branch is None: 2164 action = "get" 2165 if name is None: 2166 raise P4LibError("Incomplete/missing arguments: must "\ 2167 "specify 'name' of branch to get.") 2168 argv = ['branch', '-o', name] 2169 else: 2170 action = "create/update" 2171 if "branch" in branch: 2172 name = branch["branch"] 2173 if name is not None: 2174 br = self.branch(name=name) 2175 else: 2176 br = {} 2177 br.update(branch) 2178 form = makeForm(**br) 2179 2180 # Build submission form file. 2181 formfile = tempfile.mktemp() 2182 fout = open(formfile, 'w') 2183 fout.write(form) 2184 fout.close() 2185 argv = ['branch', '-i', '<', formfile] 2186 2187 output, error, retval = self._p4run(argv, **p4options) 2188 if _raw: 2189 return {'stdout': output, 'stderr': error, 'retval': retval} 2190 2191 if action == 'get': 2192 rv = parseForm(output) 2193 elif action in ('create/update', 'delete'): 2194 lines = output.splitlines(1) 2195 # Example output: 2196 # Client trentm-ra not changed. 2197 # Client bertha-test deleted. 2198 # Client bertha-test saved. 2199 resultRe = re.compile("^Branch (?P<branch>[^\s@]+)"\ 2200 " (?P<action>not changed|deleted|saved)\.$") 2201 match = resultRe.match(lines[0]) 2202 if match: 2203 rv = match.groupdict() 2204 else: 2205 err = "Internal error: could not parse p4 branch "\ 2206 "output: '%s'" % output 2207 raise P4LibError(err) 2208 else: 2209 raise P4LibError("Internal error: unexpected action: '%s'"\ 2210 % action) 2211 2212 return rv 2213 finally: 2214 if formfile: 2215 os.remove(formfile) 2216 2217 def branches(self, _raw=0, **p4options): 2218 """Return a list of branches. 2219 2220 Returns a list of dicts, each representing one branches spec, 2221 e.g.: 2222 [{'branch': 'zope-aspn', 2223 'description': 'Contrib Zope into ASPN ', 2224 'update': '2001/10/15'}, 2225 ... 2226 ] 2227 2228 If '_raw' is true then the return value is simply a dictionary 2229 with the unprocessed results of calling p4: 2230 {'stdout': <stdout>, 'stderr': <stderr>, 'retval': <retval>} 2231 """ 2232 argv = ['branches'] 2233 output, error, retval = self._p4run(argv, **p4options) 2234 if _raw: 2235 return {'stdout': output, 'stderr': error, 'retval': retval} 2236 2237 branchRe = re.compile("^Branch (?P<branch>[^\s@]+) "\ 2238 "(?P<update>[\d/]+) "\ 2239 "'(?P<description>.*?)'$") 2240 branches = [] 2241 for line in output.splitlines(1): 2242 match = branchRe.match(line) 2243 if match: 2244 branch = match.groupdict() 2245 branches.append(branch) 2246 else: 2247 raise P4LibError("Internal error: could not parse "\ 2248 "'p4 branches' output line: '%s'" % line) 2249 return branches 2250 2251 2252