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