1#!/usr/local/bin/python3.8
2#
3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
4#
5# Author: Simon Hausmann <simon@lst.de>
6# Copyright: 2007 Simon Hausmann <simon@lst.de>
7#            2007 Trolltech ASA
8# License: MIT <http://www.opensource.org/licenses/mit-license.php>
9#
10# pylint: disable=invalid-name,missing-docstring,too-many-arguments,broad-except
11# pylint: disable=no-self-use,wrong-import-position,consider-iterating-dictionary
12# pylint: disable=wrong-import-order,unused-import,too-few-public-methods
13# pylint: disable=too-many-lines,ungrouped-imports,fixme,too-many-locals
14# pylint: disable=line-too-long,bad-whitespace,superfluous-parens
15# pylint: disable=too-many-statements,too-many-instance-attributes
16# pylint: disable=too-many-branches,too-many-nested-blocks
17#
18import sys
19if sys.version_info.major < 3 and sys.version_info.minor < 7:
20    sys.stderr.write("git-p4: requires Python 2.7 or later.\n")
21    sys.exit(1)
22import os
23import optparse
24import functools
25import marshal
26import subprocess
27import tempfile
28import time
29import platform
30import re
31import shutil
32import stat
33import zipfile
34import zlib
35import ctypes
36import errno
37import glob
38
39# On python2.7 where raw_input() and input() are both availble,
40# we want raw_input's semantics, but aliased to input for python3
41# compatibility
42# support basestring in python3
43try:
44    if raw_input and input:
45        input = raw_input
46except:
47    pass
48
49verbose = False
50
51# Only labels/tags matching this will be imported/exported
52defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
53
54# The block size is reduced automatically if required
55defaultBlockSize = 1<<20
56
57p4_access_checked = False
58
59def p4_build_cmd(cmd):
60    """Build a suitable p4 command line.
61
62    This consolidates building and returning a p4 command line into one
63    location. It means that hooking into the environment, or other configuration
64    can be done more easily.
65    """
66    real_cmd = ["p4"]
67
68    user = gitConfig("git-p4.user")
69    if len(user) > 0:
70        real_cmd += ["-u",user]
71
72    password = gitConfig("git-p4.password")
73    if len(password) > 0:
74        real_cmd += ["-P", password]
75
76    port = gitConfig("git-p4.port")
77    if len(port) > 0:
78        real_cmd += ["-p", port]
79
80    host = gitConfig("git-p4.host")
81    if len(host) > 0:
82        real_cmd += ["-H", host]
83
84    client = gitConfig("git-p4.client")
85    if len(client) > 0:
86        real_cmd += ["-c", client]
87
88    retries = gitConfigInt("git-p4.retries")
89    if retries is None:
90        # Perform 3 retries by default
91        retries = 3
92    if retries > 0:
93        # Provide a way to not pass this option by setting git-p4.retries to 0
94        real_cmd += ["-r", str(retries)]
95
96    if not isinstance(cmd, list):
97        real_cmd = ' '.join(real_cmd) + ' ' + cmd
98    else:
99        real_cmd += cmd
100
101    # now check that we can actually talk to the server
102    global p4_access_checked
103    if not p4_access_checked:
104        p4_access_checked = True    # suppress access checks in p4_check_access itself
105        p4_check_access()
106
107    return real_cmd
108
109def git_dir(path):
110    """ Return TRUE if the given path is a git directory (/path/to/dir/.git).
111        This won't automatically add ".git" to a directory.
112    """
113    d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
114    if not d or len(d) == 0:
115        return None
116    else:
117        return d
118
119def chdir(path, is_client_path=False):
120    """Do chdir to the given path, and set the PWD environment
121       variable for use by P4.  It does not look at getcwd() output.
122       Since we're not using the shell, it is necessary to set the
123       PWD environment variable explicitly.
124
125       Normally, expand the path to force it to be absolute.  This
126       addresses the use of relative path names inside P4 settings,
127       e.g. P4CONFIG=.p4config.  P4 does not simply open the filename
128       as given; it looks for .p4config using PWD.
129
130       If is_client_path, the path was handed to us directly by p4,
131       and may be a symbolic link.  Do not call os.getcwd() in this
132       case, because it will cause p4 to think that PWD is not inside
133       the client path.
134       """
135
136    os.chdir(path)
137    if not is_client_path:
138        path = os.getcwd()
139    os.environ['PWD'] = path
140
141def calcDiskFree():
142    """Return free space in bytes on the disk of the given dirname."""
143    if platform.system() == 'Windows':
144        free_bytes = ctypes.c_ulonglong(0)
145        ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
146        return free_bytes.value
147    else:
148        st = os.statvfs(os.getcwd())
149        return st.f_bavail * st.f_frsize
150
151def die(msg):
152    """ Terminate execution. Make sure that any running child processes have been wait()ed for before
153        calling this.
154    """
155    if verbose:
156        raise Exception(msg)
157    else:
158        sys.stderr.write(msg + "\n")
159        sys.exit(1)
160
161def prompt(prompt_text):
162    """ Prompt the user to choose one of the choices
163
164    Choices are identified in the prompt_text by square brackets around
165    a single letter option.
166    """
167    choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
168    while True:
169        sys.stderr.flush()
170        sys.stdout.write(prompt_text)
171        sys.stdout.flush()
172        response=sys.stdin.readline().strip().lower()
173        if not response:
174            continue
175        response = response[0]
176        if response in choices:
177            return response
178
179# We need different encoding/decoding strategies for text data being passed
180# around in pipes depending on python version
181if bytes is not str:
182    # For python3, always encode and decode as appropriate
183    def decode_text_stream(s):
184        return s.decode() if isinstance(s, bytes) else s
185    def encode_text_stream(s):
186        return s.encode() if isinstance(s, str) else s
187else:
188    # For python2.7, pass read strings as-is, but also allow writing unicode
189    def decode_text_stream(s):
190        return s
191    def encode_text_stream(s):
192        return s.encode('utf_8') if isinstance(s, unicode) else s
193
194def decode_path(path):
195    """Decode a given string (bytes or otherwise) using configured path encoding options
196    """
197    encoding = gitConfig('git-p4.pathEncoding') or 'utf_8'
198    if bytes is not str:
199        return path.decode(encoding, errors='replace') if isinstance(path, bytes) else path
200    else:
201        try:
202            path.decode('ascii')
203        except:
204            path = path.decode(encoding, errors='replace')
205            if verbose:
206                print('Path with non-ASCII characters detected. Used {} to decode: {}'.format(encoding, path))
207        return path
208
209def run_git_hook(cmd, param=[]):
210    """Execute a hook if the hook exists."""
211    if verbose:
212        sys.stderr.write("Looking for hook: %s\n" % cmd)
213        sys.stderr.flush()
214
215    hooks_path = gitConfig("core.hooksPath")
216    if len(hooks_path) <= 0:
217        hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
218
219    if not isinstance(param, list):
220        param=[param]
221
222    # resolve hook file name, OS depdenent
223    hook_file = os.path.join(hooks_path, cmd)
224    if platform.system() == 'Windows':
225        if not os.path.isfile(hook_file):
226            # look for the file with an extension
227            files = glob.glob(hook_file + ".*")
228            if not files:
229                return True
230            files.sort()
231            hook_file = files.pop()
232            while hook_file.upper().endswith(".SAMPLE"):
233                # The file is a sample hook. We don't want it
234                if len(files) > 0:
235                    hook_file = files.pop()
236                else:
237                    return True
238
239    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
240        return True
241
242    return run_hook_command(hook_file, param) == 0
243
244def run_hook_command(cmd, param):
245    """Executes a git hook command
246       cmd = the command line file to be executed. This can be
247       a file that is run by OS association.
248
249       param = a list of parameters to pass to the cmd command
250
251       On windows, the extension is checked to see if it should
252       be run with the Git for Windows Bash shell.  If there
253       is no file extension, the file is deemed a bash shell
254       and will be handed off to sh.exe. Otherwise, Windows
255       will be called with the shell to handle the file assocation.
256
257       For non Windows operating systems, the file is called
258       as an executable.
259    """
260    cli = [cmd] + param
261    use_shell = False
262    if platform.system() == 'Windows':
263        (root,ext) = os.path.splitext(cmd)
264        if ext == "":
265            exe_path = os.environ.get("EXEPATH")
266            if exe_path is None:
267                exe_path = ""
268            else:
269                exe_path = os.path.join(exe_path, "bin")
270            cli = [os.path.join(exe_path, "SH.EXE")] + cli
271        else:
272            use_shell = True
273    return subprocess.call(cli, shell=use_shell)
274
275
276def write_pipe(c, stdin):
277    if verbose:
278        sys.stderr.write('Writing pipe: %s\n' % str(c))
279
280    expand = not isinstance(c, list)
281    p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
282    pipe = p.stdin
283    val = pipe.write(stdin)
284    pipe.close()
285    if p.wait():
286        die('Command failed: %s' % str(c))
287
288    return val
289
290def p4_write_pipe(c, stdin):
291    real_cmd = p4_build_cmd(c)
292    if bytes is not str and isinstance(stdin, str):
293        stdin = encode_text_stream(stdin)
294    return write_pipe(real_cmd, stdin)
295
296def read_pipe_full(c):
297    """ Read output from  command. Returns a tuple
298        of the return status, stdout text and stderr
299        text.
300    """
301    if verbose:
302        sys.stderr.write('Reading pipe: %s\n' % str(c))
303
304    expand = not isinstance(c, list)
305    p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
306    (out, err) = p.communicate()
307    return (p.returncode, out, decode_text_stream(err))
308
309def read_pipe(c, ignore_error=False, raw=False):
310    """ Read output from  command. Returns the output text on
311        success. On failure, terminates execution, unless
312        ignore_error is True, when it returns an empty string.
313
314        If raw is True, do not attempt to decode output text.
315    """
316    (retcode, out, err) = read_pipe_full(c)
317    if retcode != 0:
318        if ignore_error:
319            out = ""
320        else:
321            die('Command failed: %s\nError: %s' % (str(c), err))
322    if not raw:
323        out = decode_text_stream(out)
324    return out
325
326def read_pipe_text(c):
327    """ Read output from a command with trailing whitespace stripped.
328        On error, returns None.
329    """
330    (retcode, out, err) = read_pipe_full(c)
331    if retcode != 0:
332        return None
333    else:
334        return decode_text_stream(out).rstrip()
335
336def p4_read_pipe(c, ignore_error=False, raw=False):
337    real_cmd = p4_build_cmd(c)
338    return read_pipe(real_cmd, ignore_error, raw=raw)
339
340def read_pipe_lines(c):
341    if verbose:
342        sys.stderr.write('Reading pipe: %s\n' % str(c))
343
344    expand = not isinstance(c, list)
345    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
346    pipe = p.stdout
347    val = [decode_text_stream(line) for line in pipe.readlines()]
348    if pipe.close() or p.wait():
349        die('Command failed: %s' % str(c))
350    return val
351
352def p4_read_pipe_lines(c):
353    """Specifically invoke p4 on the command supplied. """
354    real_cmd = p4_build_cmd(c)
355    return read_pipe_lines(real_cmd)
356
357def p4_has_command(cmd):
358    """Ask p4 for help on this command.  If it returns an error, the
359       command does not exist in this version of p4."""
360    real_cmd = p4_build_cmd(["help", cmd])
361    p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
362                                   stderr=subprocess.PIPE)
363    p.communicate()
364    return p.returncode == 0
365
366def p4_has_move_command():
367    """See if the move command exists, that it supports -k, and that
368       it has not been administratively disabled.  The arguments
369       must be correct, but the filenames do not have to exist.  Use
370       ones with wildcards so even if they exist, it will fail."""
371
372    if not p4_has_command("move"):
373        return False
374    cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
375    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
376    (out, err) = p.communicate()
377    err = decode_text_stream(err)
378    # return code will be 1 in either case
379    if err.find("Invalid option") >= 0:
380        return False
381    if err.find("disabled") >= 0:
382        return False
383    # assume it failed because @... was invalid changelist
384    return True
385
386def system(cmd, ignore_error=False):
387    expand = not isinstance(cmd, list)
388    if verbose:
389        sys.stderr.write("executing %s\n" % str(cmd))
390    retcode = subprocess.call(cmd, shell=expand)
391    if retcode and not ignore_error:
392        raise CalledProcessError(retcode, cmd)
393
394    return retcode
395
396def p4_system(cmd):
397    """Specifically invoke p4 as the system command. """
398    real_cmd = p4_build_cmd(cmd)
399    expand = not isinstance(real_cmd, list)
400    retcode = subprocess.call(real_cmd, shell=expand)
401    if retcode:
402        raise CalledProcessError(retcode, real_cmd)
403
404def die_bad_access(s):
405    die("failure accessing depot: {0}".format(s.rstrip()))
406
407def p4_check_access(min_expiration=1):
408    """ Check if we can access Perforce - account still logged in
409    """
410    results = p4CmdList(["login", "-s"])
411
412    if len(results) == 0:
413        # should never get here: always get either some results, or a p4ExitCode
414        assert("could not parse response from perforce")
415
416    result = results[0]
417
418    if 'p4ExitCode' in result:
419        # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
420        die_bad_access("could not run p4")
421
422    code = result.get("code")
423    if not code:
424        # we get here if we couldn't connect and there was nothing to unmarshal
425        die_bad_access("could not connect")
426
427    elif code == "stat":
428        expiry = result.get("TicketExpiration")
429        if expiry:
430            expiry = int(expiry)
431            if expiry > min_expiration:
432                # ok to carry on
433                return
434            else:
435                die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
436
437        else:
438            # account without a timeout - all ok
439            return
440
441    elif code == "error":
442        data = result.get("data")
443        if data:
444            die_bad_access("p4 error: {0}".format(data))
445        else:
446            die_bad_access("unknown error")
447    elif code == "info":
448        return
449    else:
450        die_bad_access("unknown error code {0}".format(code))
451
452_p4_version_string = None
453def p4_version_string():
454    """Read the version string, showing just the last line, which
455       hopefully is the interesting version bit.
456
457       $ p4 -V
458       Perforce - The Fast Software Configuration Management System.
459       Copyright 1995-2011 Perforce Software.  All rights reserved.
460       Rev. P4/NTX86/2011.1/393975 (2011/12/16).
461    """
462    global _p4_version_string
463    if not _p4_version_string:
464        a = p4_read_pipe_lines(["-V"])
465        _p4_version_string = a[-1].rstrip()
466    return _p4_version_string
467
468def p4_integrate(src, dest):
469    p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
470
471def p4_sync(f, *options):
472    p4_system(["sync"] + list(options) + [wildcard_encode(f)])
473
474def p4_add(f):
475    # forcibly add file names with wildcards
476    if wildcard_present(f):
477        p4_system(["add", "-f", f])
478    else:
479        p4_system(["add", f])
480
481def p4_delete(f):
482    p4_system(["delete", wildcard_encode(f)])
483
484def p4_edit(f, *options):
485    p4_system(["edit"] + list(options) + [wildcard_encode(f)])
486
487def p4_revert(f):
488    p4_system(["revert", wildcard_encode(f)])
489
490def p4_reopen(type, f):
491    p4_system(["reopen", "-t", type, wildcard_encode(f)])
492
493def p4_reopen_in_change(changelist, files):
494    cmd = ["reopen", "-c", str(changelist)] + files
495    p4_system(cmd)
496
497def p4_move(src, dest):
498    p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
499
500def p4_last_change():
501    results = p4CmdList(["changes", "-m", "1"], skip_info=True)
502    return int(results[0]['change'])
503
504def p4_describe(change, shelved=False):
505    """Make sure it returns a valid result by checking for
506       the presence of field "time".  Return a dict of the
507       results."""
508
509    cmd = ["describe", "-s"]
510    if shelved:
511        cmd += ["-S"]
512    cmd += [str(change)]
513
514    ds = p4CmdList(cmd, skip_info=True)
515    if len(ds) != 1:
516        die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
517
518    d = ds[0]
519
520    if "p4ExitCode" in d:
521        die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
522                                                      str(d)))
523    if "code" in d:
524        if d["code"] == "error":
525            die("p4 describe -s %d returned error code: %s" % (change, str(d)))
526
527    if "time" not in d:
528        die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
529
530    return d
531
532#
533# Canonicalize the p4 type and return a tuple of the
534# base type, plus any modifiers.  See "p4 help filetypes"
535# for a list and explanation.
536#
537def split_p4_type(p4type):
538
539    p4_filetypes_historical = {
540        "ctempobj": "binary+Sw",
541        "ctext": "text+C",
542        "cxtext": "text+Cx",
543        "ktext": "text+k",
544        "kxtext": "text+kx",
545        "ltext": "text+F",
546        "tempobj": "binary+FSw",
547        "ubinary": "binary+F",
548        "uresource": "resource+F",
549        "uxbinary": "binary+Fx",
550        "xbinary": "binary+x",
551        "xltext": "text+Fx",
552        "xtempobj": "binary+Swx",
553        "xtext": "text+x",
554        "xunicode": "unicode+x",
555        "xutf16": "utf16+x",
556    }
557    if p4type in p4_filetypes_historical:
558        p4type = p4_filetypes_historical[p4type]
559    mods = ""
560    s = p4type.split("+")
561    base = s[0]
562    mods = ""
563    if len(s) > 1:
564        mods = s[1]
565    return (base, mods)
566
567#
568# return the raw p4 type of a file (text, text+ko, etc)
569#
570def p4_type(f):
571    results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
572    return results[0]['headType']
573
574#
575# Given a type base and modifier, return a regexp matching
576# the keywords that can be expanded in the file
577#
578def p4_keywords_regexp_for_type(base, type_mods):
579    if base in ("text", "unicode", "binary"):
580        kwords = None
581        if "ko" in type_mods:
582            kwords = 'Id|Header'
583        elif "k" in type_mods:
584            kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
585        else:
586            return None
587        pattern = r"""
588            \$              # Starts with a dollar, followed by...
589            (%s)            # one of the keywords, followed by...
590            (:[^$\n]+)?     # possibly an old expansion, followed by...
591            \$              # another dollar
592            """ % kwords
593        return pattern
594    else:
595        return None
596
597#
598# Given a file, return a regexp matching the possible
599# RCS keywords that will be expanded, or None for files
600# with kw expansion turned off.
601#
602def p4_keywords_regexp_for_file(file):
603    if not os.path.exists(file):
604        return None
605    else:
606        (type_base, type_mods) = split_p4_type(p4_type(file))
607        return p4_keywords_regexp_for_type(type_base, type_mods)
608
609def setP4ExecBit(file, mode):
610    # Reopens an already open file and changes the execute bit to match
611    # the execute bit setting in the passed in mode.
612
613    p4Type = "+x"
614
615    if not isModeExec(mode):
616        p4Type = getP4OpenedType(file)
617        p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
618        p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
619        if p4Type[-1] == "+":
620            p4Type = p4Type[0:-1]
621
622    p4_reopen(p4Type, file)
623
624def getP4OpenedType(file):
625    # Returns the perforce file type for the given file.
626
627    result = p4_read_pipe(["opened", wildcard_encode(file)])
628    match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
629    if match:
630        return match.group(1)
631    else:
632        die("Could not determine file type for %s (result: '%s')" % (file, result))
633
634# Return the set of all p4 labels
635def getP4Labels(depotPaths):
636    labels = set()
637    if not isinstance(depotPaths, list):
638        depotPaths = [depotPaths]
639
640    for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
641        label = l['label']
642        labels.add(label)
643
644    return labels
645
646# Return the set of all git tags
647def getGitTags():
648    gitTags = set()
649    for line in read_pipe_lines(["git", "tag"]):
650        tag = line.strip()
651        gitTags.add(tag)
652    return gitTags
653
654_diff_tree_pattern = None
655
656def parseDiffTreeEntry(entry):
657    """Parses a single diff tree entry into its component elements.
658
659    See git-diff-tree(1) manpage for details about the format of the diff
660    output. This method returns a dictionary with the following elements:
661
662    src_mode - The mode of the source file
663    dst_mode - The mode of the destination file
664    src_sha1 - The sha1 for the source file
665    dst_sha1 - The sha1 fr the destination file
666    status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
667    status_score - The score for the status (applicable for 'C' and 'R'
668                   statuses). This is None if there is no score.
669    src - The path for the source file.
670    dst - The path for the destination file. This is only present for
671          copy or renames. If it is not present, this is None.
672
673    If the pattern is not matched, None is returned."""
674
675    global _diff_tree_pattern
676    if not _diff_tree_pattern:
677        _diff_tree_pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
678
679    match = _diff_tree_pattern.match(entry)
680    if match:
681        return {
682            'src_mode': match.group(1),
683            'dst_mode': match.group(2),
684            'src_sha1': match.group(3),
685            'dst_sha1': match.group(4),
686            'status': match.group(5),
687            'status_score': match.group(6),
688            'src': match.group(7),
689            'dst': match.group(10)
690        }
691    return None
692
693def isModeExec(mode):
694    # Returns True if the given git mode represents an executable file,
695    # otherwise False.
696    return mode[-3:] == "755"
697
698class P4Exception(Exception):
699    """ Base class for exceptions from the p4 client """
700    def __init__(self, exit_code):
701        self.p4ExitCode = exit_code
702
703class P4ServerException(P4Exception):
704    """ Base class for exceptions where we get some kind of marshalled up result from the server """
705    def __init__(self, exit_code, p4_result):
706        super(P4ServerException, self).__init__(exit_code)
707        self.p4_result = p4_result
708        self.code = p4_result[0]['code']
709        self.data = p4_result[0]['data']
710
711class P4RequestSizeException(P4ServerException):
712    """ One of the maxresults or maxscanrows errors """
713    def __init__(self, exit_code, p4_result, limit):
714        super(P4RequestSizeException, self).__init__(exit_code, p4_result)
715        self.limit = limit
716
717class P4CommandException(P4Exception):
718    """ Something went wrong calling p4 which means we have to give up """
719    def __init__(self, msg):
720        self.msg = msg
721
722    def __str__(self):
723        return self.msg
724
725def isModeExecChanged(src_mode, dst_mode):
726    return isModeExec(src_mode) != isModeExec(dst_mode)
727
728def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
729        errors_as_exceptions=False):
730
731    if not isinstance(cmd, list):
732        cmd = "-G " + cmd
733        expand = True
734    else:
735        cmd = ["-G"] + cmd
736        expand = False
737
738    cmd = p4_build_cmd(cmd)
739    if verbose:
740        sys.stderr.write("Opening pipe: %s\n" % str(cmd))
741
742    # Use a temporary file to avoid deadlocks without
743    # subprocess.communicate(), which would put another copy
744    # of stdout into memory.
745    stdin_file = None
746    if stdin is not None:
747        stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
748        if not isinstance(stdin, list):
749            stdin_file.write(stdin)
750        else:
751            for i in stdin:
752                stdin_file.write(encode_text_stream(i))
753                stdin_file.write(b'\n')
754        stdin_file.flush()
755        stdin_file.seek(0)
756
757    p4 = subprocess.Popen(cmd,
758                          shell=expand,
759                          stdin=stdin_file,
760                          stdout=subprocess.PIPE)
761
762    result = []
763    try:
764        while True:
765            entry = marshal.load(p4.stdout)
766            if bytes is not str:
767                # Decode unmarshalled dict to use str keys and values, except for:
768                #   - `data` which may contain arbitrary binary data
769                #   - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text
770                decoded_entry = {}
771                for key, value in entry.items():
772                    key = key.decode()
773                    if isinstance(value, bytes) and not (key in ('data', 'path', 'clientFile') or key.startswith('depotFile')):
774                        value = value.decode()
775                    decoded_entry[key] = value
776                # Parse out data if it's an error response
777                if decoded_entry.get('code') == 'error' and 'data' in decoded_entry:
778                    decoded_entry['data'] = decoded_entry['data'].decode()
779                entry = decoded_entry
780            if skip_info:
781                if 'code' in entry and entry['code'] == 'info':
782                    continue
783            if cb is not None:
784                cb(entry)
785            else:
786                result.append(entry)
787    except EOFError:
788        pass
789    exitCode = p4.wait()
790    if exitCode != 0:
791        if errors_as_exceptions:
792            if len(result) > 0:
793                data = result[0].get('data')
794                if data:
795                    m = re.search('Too many rows scanned \(over (\d+)\)', data)
796                    if not m:
797                        m = re.search('Request too large \(over (\d+)\)', data)
798
799                    if m:
800                        limit = int(m.group(1))
801                        raise P4RequestSizeException(exitCode, result, limit)
802
803                raise P4ServerException(exitCode, result)
804            else:
805                raise P4Exception(exitCode)
806        else:
807            entry = {}
808            entry["p4ExitCode"] = exitCode
809            result.append(entry)
810
811    return result
812
813def p4Cmd(cmd):
814    list = p4CmdList(cmd)
815    result = {}
816    for entry in list:
817        result.update(entry)
818    return result;
819
820def p4Where(depotPath):
821    if not depotPath.endswith("/"):
822        depotPath += "/"
823    depotPathLong = depotPath + "..."
824    outputList = p4CmdList(["where", depotPathLong])
825    output = None
826    for entry in outputList:
827        if "depotFile" in entry:
828            # Search for the base client side depot path, as long as it starts with the branch's P4 path.
829            # The base path always ends with "/...".
830            entry_path = decode_path(entry['depotFile'])
831            if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...":
832                output = entry
833                break
834        elif "data" in entry:
835            data = entry.get("data")
836            space = data.find(" ")
837            if data[:space] == depotPath:
838                output = entry
839                break
840    if output == None:
841        return ""
842    if output["code"] == "error":
843        return ""
844    clientPath = ""
845    if "path" in output:
846        clientPath = decode_path(output['path'])
847    elif "data" in output:
848        data = output.get("data")
849        lastSpace = data.rfind(b" ")
850        clientPath = decode_path(data[lastSpace + 1:])
851
852    if clientPath.endswith("..."):
853        clientPath = clientPath[:-3]
854    return clientPath
855
856def currentGitBranch():
857    return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
858
859def isValidGitDir(path):
860    return git_dir(path) != None
861
862def parseRevision(ref):
863    return read_pipe("git rev-parse %s" % ref).strip()
864
865def branchExists(ref):
866    rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
867                     ignore_error=True)
868    return len(rev) > 0
869
870def extractLogMessageFromGitCommit(commit):
871    logMessage = ""
872
873    ## fixme: title is first line of commit, not 1st paragraph.
874    foundTitle = False
875    for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
876       if not foundTitle:
877           if len(log) == 1:
878               foundTitle = True
879           continue
880
881       logMessage += log
882    return logMessage
883
884def extractSettingsGitLog(log):
885    values = {}
886    for line in log.split("\n"):
887        line = line.strip()
888        m = re.search (r"^ *\[git-p4: (.*)\]$", line)
889        if not m:
890            continue
891
892        assignments = m.group(1).split (':')
893        for a in assignments:
894            vals = a.split ('=')
895            key = vals[0].strip()
896            val = ('='.join (vals[1:])).strip()
897            if val.endswith ('\"') and val.startswith('"'):
898                val = val[1:-1]
899
900            values[key] = val
901
902    paths = values.get("depot-paths")
903    if not paths:
904        paths = values.get("depot-path")
905    if paths:
906        values['depot-paths'] = paths.split(',')
907    return values
908
909def gitBranchExists(branch):
910    proc = subprocess.Popen(["git", "rev-parse", branch],
911                            stderr=subprocess.PIPE, stdout=subprocess.PIPE);
912    return proc.wait() == 0;
913
914def gitUpdateRef(ref, newvalue):
915    subprocess.check_call(["git", "update-ref", ref, newvalue])
916
917def gitDeleteRef(ref):
918    subprocess.check_call(["git", "update-ref", "-d", ref])
919
920_gitConfig = {}
921
922def gitConfig(key, typeSpecifier=None):
923    if key not in _gitConfig:
924        cmd = [ "git", "config" ]
925        if typeSpecifier:
926            cmd += [ typeSpecifier ]
927        cmd += [ key ]
928        s = read_pipe(cmd, ignore_error=True)
929        _gitConfig[key] = s.strip()
930    return _gitConfig[key]
931
932def gitConfigBool(key):
933    """Return a bool, using git config --bool.  It is True only if the
934       variable is set to true, and False if set to false or not present
935       in the config."""
936
937    if key not in _gitConfig:
938        _gitConfig[key] = gitConfig(key, '--bool') == "true"
939    return _gitConfig[key]
940
941def gitConfigInt(key):
942    if key not in _gitConfig:
943        cmd = [ "git", "config", "--int", key ]
944        s = read_pipe(cmd, ignore_error=True)
945        v = s.strip()
946        try:
947            _gitConfig[key] = int(gitConfig(key, '--int'))
948        except ValueError:
949            _gitConfig[key] = None
950    return _gitConfig[key]
951
952def gitConfigList(key):
953    if key not in _gitConfig:
954        s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
955        _gitConfig[key] = s.strip().splitlines()
956        if _gitConfig[key] == ['']:
957            _gitConfig[key] = []
958    return _gitConfig[key]
959
960def p4BranchesInGit(branchesAreInRemotes=True):
961    """Find all the branches whose names start with "p4/", looking
962       in remotes or heads as specified by the argument.  Return
963       a dictionary of { branch: revision } for each one found.
964       The branch names are the short names, without any
965       "p4/" prefix."""
966
967    branches = {}
968
969    cmdline = "git rev-parse --symbolic "
970    if branchesAreInRemotes:
971        cmdline += "--remotes"
972    else:
973        cmdline += "--branches"
974
975    for line in read_pipe_lines(cmdline):
976        line = line.strip()
977
978        # only import to p4/
979        if not line.startswith('p4/'):
980            continue
981        # special symbolic ref to p4/master
982        if line == "p4/HEAD":
983            continue
984
985        # strip off p4/ prefix
986        branch = line[len("p4/"):]
987
988        branches[branch] = parseRevision(line)
989
990    return branches
991
992def branch_exists(branch):
993    """Make sure that the given ref name really exists."""
994
995    cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
996    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
997    out, _ = p.communicate()
998    out = decode_text_stream(out)
999    if p.returncode:
1000        return False
1001    # expect exactly one line of output: the branch name
1002    return out.rstrip() == branch
1003
1004def findUpstreamBranchPoint(head = "HEAD"):
1005    branches = p4BranchesInGit()
1006    # map from depot-path to branch name
1007    branchByDepotPath = {}
1008    for branch in branches.keys():
1009        tip = branches[branch]
1010        log = extractLogMessageFromGitCommit(tip)
1011        settings = extractSettingsGitLog(log)
1012        if "depot-paths" in settings:
1013            paths = ",".join(settings["depot-paths"])
1014            branchByDepotPath[paths] = "remotes/p4/" + branch
1015
1016    settings = None
1017    parent = 0
1018    while parent < 65535:
1019        commit = head + "~%s" % parent
1020        log = extractLogMessageFromGitCommit(commit)
1021        settings = extractSettingsGitLog(log)
1022        if "depot-paths" in settings:
1023            paths = ",".join(settings["depot-paths"])
1024            if paths in branchByDepotPath:
1025                return [branchByDepotPath[paths], settings]
1026
1027        parent = parent + 1
1028
1029    return ["", settings]
1030
1031def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
1032    if not silent:
1033        print("Creating/updating branch(es) in %s based on origin branch(es)"
1034               % localRefPrefix)
1035
1036    originPrefix = "origin/p4/"
1037
1038    for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
1039        line = line.strip()
1040        if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
1041            continue
1042
1043        headName = line[len(originPrefix):]
1044        remoteHead = localRefPrefix + headName
1045        originHead = line
1046
1047        original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
1048        if ('depot-paths' not in original
1049            or 'change' not in original):
1050            continue
1051
1052        update = False
1053        if not gitBranchExists(remoteHead):
1054            if verbose:
1055                print("creating %s" % remoteHead)
1056            update = True
1057        else:
1058            settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
1059            if 'change' in settings:
1060                if settings['depot-paths'] == original['depot-paths']:
1061                    originP4Change = int(original['change'])
1062                    p4Change = int(settings['change'])
1063                    if originP4Change > p4Change:
1064                        print("%s (%s) is newer than %s (%s). "
1065                               "Updating p4 branch from origin."
1066                               % (originHead, originP4Change,
1067                                  remoteHead, p4Change))
1068                        update = True
1069                else:
1070                    print("Ignoring: %s was imported from %s while "
1071                           "%s was imported from %s"
1072                           % (originHead, ','.join(original['depot-paths']),
1073                              remoteHead, ','.join(settings['depot-paths'])))
1074
1075        if update:
1076            system("git update-ref %s %s" % (remoteHead, originHead))
1077
1078def originP4BranchesExist():
1079        return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
1080
1081
1082def p4ParseNumericChangeRange(parts):
1083    changeStart = int(parts[0][1:])
1084    if parts[1] == '#head':
1085        changeEnd = p4_last_change()
1086    else:
1087        changeEnd = int(parts[1])
1088
1089    return (changeStart, changeEnd)
1090
1091def chooseBlockSize(blockSize):
1092    if blockSize:
1093        return blockSize
1094    else:
1095        return defaultBlockSize
1096
1097def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1098    assert depotPaths
1099
1100    # Parse the change range into start and end. Try to find integer
1101    # revision ranges as these can be broken up into blocks to avoid
1102    # hitting server-side limits (maxrows, maxscanresults). But if
1103    # that doesn't work, fall back to using the raw revision specifier
1104    # strings, without using block mode.
1105
1106    if changeRange is None or changeRange == '':
1107        changeStart = 1
1108        changeEnd = p4_last_change()
1109        block_size = chooseBlockSize(requestedBlockSize)
1110    else:
1111        parts = changeRange.split(',')
1112        assert len(parts) == 2
1113        try:
1114            (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
1115            block_size = chooseBlockSize(requestedBlockSize)
1116        except ValueError:
1117            changeStart = parts[0][1:]
1118            changeEnd = parts[1]
1119            if requestedBlockSize:
1120                die("cannot use --changes-block-size with non-numeric revisions")
1121            block_size = None
1122
1123    changes = set()
1124
1125    # Retrieve changes a block at a time, to prevent running
1126    # into a MaxResults/MaxScanRows error from the server. If
1127    # we _do_ hit one of those errors, turn down the block size
1128
1129    while True:
1130        cmd = ['changes']
1131
1132        if block_size:
1133            end = min(changeEnd, changeStart + block_size)
1134            revisionRange = "%d,%d" % (changeStart, end)
1135        else:
1136            revisionRange = "%s,%s" % (changeStart, changeEnd)
1137
1138        for p in depotPaths:
1139            cmd += ["%s...@%s" % (p, revisionRange)]
1140
1141        # fetch the changes
1142        try:
1143            result = p4CmdList(cmd, errors_as_exceptions=True)
1144        except P4RequestSizeException as e:
1145            if not block_size:
1146                block_size = e.limit
1147            elif block_size > e.limit:
1148                block_size = e.limit
1149            else:
1150                block_size = max(2, block_size // 2)
1151
1152            if verbose: print("block size error, retrying with block size {0}".format(block_size))
1153            continue
1154        except P4Exception as e:
1155            die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1156
1157        # Insert changes in chronological order
1158        for entry in reversed(result):
1159            if 'change' not in entry:
1160                continue
1161            changes.add(int(entry['change']))
1162
1163        if not block_size:
1164            break
1165
1166        if end >= changeEnd:
1167            break
1168
1169        changeStart = end + 1
1170
1171    changes = sorted(changes)
1172    return changes
1173
1174def p4PathStartsWith(path, prefix):
1175    # This method tries to remedy a potential mixed-case issue:
1176    #
1177    # If UserA adds  //depot/DirA/file1
1178    # and UserB adds //depot/dira/file2
1179    #
1180    # we may or may not have a problem. If you have core.ignorecase=true,
1181    # we treat DirA and dira as the same directory
1182    if gitConfigBool("core.ignorecase"):
1183        return path.lower().startswith(prefix.lower())
1184    return path.startswith(prefix)
1185
1186def getClientSpec():
1187    """Look at the p4 client spec, create a View() object that contains
1188       all the mappings, and return it."""
1189
1190    specList = p4CmdList("client -o")
1191    if len(specList) != 1:
1192        die('Output from "client -o" is %d lines, expecting 1' %
1193            len(specList))
1194
1195    # dictionary of all client parameters
1196    entry = specList[0]
1197
1198    # the //client/ name
1199    client_name = entry["Client"]
1200
1201    # just the keys that start with "View"
1202    view_keys = [ k for k in entry.keys() if k.startswith("View") ]
1203
1204    # hold this new View
1205    view = View(client_name)
1206
1207    # append the lines, in order, to the view
1208    for view_num in range(len(view_keys)):
1209        k = "View%d" % view_num
1210        if k not in view_keys:
1211            die("Expected view key %s missing" % k)
1212        view.append(entry[k])
1213
1214    return view
1215
1216def getClientRoot():
1217    """Grab the client directory."""
1218
1219    output = p4CmdList("client -o")
1220    if len(output) != 1:
1221        die('Output from "client -o" is %d lines, expecting 1' % len(output))
1222
1223    entry = output[0]
1224    if "Root" not in entry:
1225        die('Client has no "Root"')
1226
1227    return entry["Root"]
1228
1229#
1230# P4 wildcards are not allowed in filenames.  P4 complains
1231# if you simply add them, but you can force it with "-f", in
1232# which case it translates them into %xx encoding internally.
1233#
1234def wildcard_decode(path):
1235    # Search for and fix just these four characters.  Do % last so
1236    # that fixing it does not inadvertently create new %-escapes.
1237    # Cannot have * in a filename in windows; untested as to
1238    # what p4 would do in such a case.
1239    if not platform.system() == "Windows":
1240        path = path.replace("%2A", "*")
1241    path = path.replace("%23", "#") \
1242               .replace("%40", "@") \
1243               .replace("%25", "%")
1244    return path
1245
1246def wildcard_encode(path):
1247    # do % first to avoid double-encoding the %s introduced here
1248    path = path.replace("%", "%25") \
1249               .replace("*", "%2A") \
1250               .replace("#", "%23") \
1251               .replace("@", "%40")
1252    return path
1253
1254def wildcard_present(path):
1255    m = re.search("[*#@%]", path)
1256    return m is not None
1257
1258class LargeFileSystem(object):
1259    """Base class for large file system support."""
1260
1261    def __init__(self, writeToGitStream):
1262        self.largeFiles = set()
1263        self.writeToGitStream = writeToGitStream
1264
1265    def generatePointer(self, cloneDestination, contentFile):
1266        """Return the content of a pointer file that is stored in Git instead of
1267           the actual content."""
1268        assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1269
1270    def pushFile(self, localLargeFile):
1271        """Push the actual content which is not stored in the Git repository to
1272           a server."""
1273        assert False, "Method 'pushFile' required in " + self.__class__.__name__
1274
1275    def hasLargeFileExtension(self, relPath):
1276        return functools.reduce(
1277            lambda a, b: a or b,
1278            [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1279            False
1280        )
1281
1282    def generateTempFile(self, contents):
1283        contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1284        for d in contents:
1285            contentFile.write(d)
1286        contentFile.close()
1287        return contentFile.name
1288
1289    def exceedsLargeFileThreshold(self, relPath, contents):
1290        if gitConfigInt('git-p4.largeFileThreshold'):
1291            contentsSize = sum(len(d) for d in contents)
1292            if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1293                return True
1294        if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1295            contentsSize = sum(len(d) for d in contents)
1296            if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1297                return False
1298            contentTempFile = self.generateTempFile(contents)
1299            compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1300            with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1301                zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1302                compressedContentsSize = zf.infolist()[0].compress_size
1303            os.remove(contentTempFile)
1304            if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1305                return True
1306        return False
1307
1308    def addLargeFile(self, relPath):
1309        self.largeFiles.add(relPath)
1310
1311    def removeLargeFile(self, relPath):
1312        self.largeFiles.remove(relPath)
1313
1314    def isLargeFile(self, relPath):
1315        return relPath in self.largeFiles
1316
1317    def processContent(self, git_mode, relPath, contents):
1318        """Processes the content of git fast import. This method decides if a
1319           file is stored in the large file system and handles all necessary
1320           steps."""
1321        if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1322            contentTempFile = self.generateTempFile(contents)
1323            (pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1324            if pointer_git_mode:
1325                git_mode = pointer_git_mode
1326            if localLargeFile:
1327                # Move temp file to final location in large file system
1328                largeFileDir = os.path.dirname(localLargeFile)
1329                if not os.path.isdir(largeFileDir):
1330                    os.makedirs(largeFileDir)
1331                shutil.move(contentTempFile, localLargeFile)
1332                self.addLargeFile(relPath)
1333                if gitConfigBool('git-p4.largeFilePush'):
1334                    self.pushFile(localLargeFile)
1335                if verbose:
1336                    sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
1337        return (git_mode, contents)
1338
1339class MockLFS(LargeFileSystem):
1340    """Mock large file system for testing."""
1341
1342    def generatePointer(self, contentFile):
1343        """The pointer content is the original content prefixed with "pointer-".
1344           The local filename of the large file storage is derived from the file content.
1345           """
1346        with open(contentFile, 'r') as f:
1347            content = next(f)
1348            gitMode = '100644'
1349            pointerContents = 'pointer-' + content
1350            localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1351            return (gitMode, pointerContents, localLargeFile)
1352
1353    def pushFile(self, localLargeFile):
1354        """The remote filename of the large file storage is the same as the local
1355           one but in a different directory.
1356           """
1357        remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1358        if not os.path.exists(remotePath):
1359            os.makedirs(remotePath)
1360        shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1361
1362class GitLFS(LargeFileSystem):
1363    """Git LFS as backend for the git-p4 large file system.
1364       See https://git-lfs.github.com/ for details."""
1365
1366    def __init__(self, *args):
1367        LargeFileSystem.__init__(self, *args)
1368        self.baseGitAttributes = []
1369
1370    def generatePointer(self, contentFile):
1371        """Generate a Git LFS pointer for the content. Return LFS Pointer file
1372           mode and content which is stored in the Git repository instead of
1373           the actual content. Return also the new location of the actual
1374           content.
1375           """
1376        if os.path.getsize(contentFile) == 0:
1377            return (None, '', None)
1378
1379        pointerProcess = subprocess.Popen(
1380            ['git', 'lfs', 'pointer', '--file=' + contentFile],
1381            stdout=subprocess.PIPE
1382        )
1383        pointerFile = decode_text_stream(pointerProcess.stdout.read())
1384        if pointerProcess.wait():
1385            os.remove(contentFile)
1386            die('git-lfs pointer command failed. Did you install the extension?')
1387
1388        # Git LFS removed the preamble in the output of the 'pointer' command
1389        # starting from version 1.2.0. Check for the preamble here to support
1390        # earlier versions.
1391        # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1392        if pointerFile.startswith('Git LFS pointer for'):
1393            pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1394
1395        oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
1396        # if someone use external lfs.storage ( not in local repo git )
1397        lfs_path = gitConfig('lfs.storage')
1398        if not lfs_path:
1399            lfs_path = 'lfs'
1400        if not os.path.isabs(lfs_path):
1401            lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
1402        localLargeFile = os.path.join(
1403            lfs_path,
1404            'objects', oid[:2], oid[2:4],
1405            oid,
1406        )
1407        # LFS Spec states that pointer files should not have the executable bit set.
1408        gitMode = '100644'
1409        return (gitMode, pointerFile, localLargeFile)
1410
1411    def pushFile(self, localLargeFile):
1412        uploadProcess = subprocess.Popen(
1413            ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1414        )
1415        if uploadProcess.wait():
1416            die('git-lfs push command failed. Did you define a remote?')
1417
1418    def generateGitAttributes(self):
1419        return (
1420            self.baseGitAttributes +
1421            [
1422                '\n',
1423                '#\n',
1424                '# Git LFS (see https://git-lfs.github.com/)\n',
1425                '#\n',
1426            ] +
1427            ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1428                for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1429            ] +
1430            ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
1431                for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1432            ]
1433        )
1434
1435    def addLargeFile(self, relPath):
1436        LargeFileSystem.addLargeFile(self, relPath)
1437        self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1438
1439    def removeLargeFile(self, relPath):
1440        LargeFileSystem.removeLargeFile(self, relPath)
1441        self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1442
1443    def processContent(self, git_mode, relPath, contents):
1444        if relPath == '.gitattributes':
1445            self.baseGitAttributes = contents
1446            return (git_mode, self.generateGitAttributes())
1447        else:
1448            return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1449
1450class Command:
1451    delete_actions = ( "delete", "move/delete", "purge" )
1452    add_actions = ( "add", "branch", "move/add" )
1453
1454    def __init__(self):
1455        self.usage = "usage: %prog [options]"
1456        self.needsGit = True
1457        self.verbose = False
1458
1459    # This is required for the "append" update_shelve action
1460    def ensure_value(self, attr, value):
1461        if not hasattr(self, attr) or getattr(self, attr) is None:
1462            setattr(self, attr, value)
1463        return getattr(self, attr)
1464
1465class P4UserMap:
1466    def __init__(self):
1467        self.userMapFromPerforceServer = False
1468        self.myP4UserId = None
1469
1470    def p4UserId(self):
1471        if self.myP4UserId:
1472            return self.myP4UserId
1473
1474        results = p4CmdList("user -o")
1475        for r in results:
1476            if 'User' in r:
1477                self.myP4UserId = r['User']
1478                return r['User']
1479        die("Could not find your p4 user id")
1480
1481    def p4UserIsMe(self, p4User):
1482        # return True if the given p4 user is actually me
1483        me = self.p4UserId()
1484        if not p4User or p4User != me:
1485            return False
1486        else:
1487            return True
1488
1489    def getUserCacheFilename(self):
1490        home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1491        return home + "/.gitp4-usercache.txt"
1492
1493    def getUserMapFromPerforceServer(self):
1494        if self.userMapFromPerforceServer:
1495            return
1496        self.users = {}
1497        self.emails = {}
1498
1499        for output in p4CmdList("users"):
1500            if "User" not in output:
1501                continue
1502            self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1503            self.emails[output["Email"]] = output["User"]
1504
1505        mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1506        for mapUserConfig in gitConfigList("git-p4.mapUser"):
1507            mapUser = mapUserConfigRegex.findall(mapUserConfig)
1508            if mapUser and len(mapUser[0]) == 3:
1509                user = mapUser[0][0]
1510                fullname = mapUser[0][1]
1511                email = mapUser[0][2]
1512                self.users[user] = fullname + " <" + email + ">"
1513                self.emails[email] = user
1514
1515        s = ''
1516        for (key, val) in self.users.items():
1517            s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1518
1519        open(self.getUserCacheFilename(), 'w').write(s)
1520        self.userMapFromPerforceServer = True
1521
1522    def loadUserMapFromCache(self):
1523        self.users = {}
1524        self.userMapFromPerforceServer = False
1525        try:
1526            cache = open(self.getUserCacheFilename(), 'r')
1527            lines = cache.readlines()
1528            cache.close()
1529            for line in lines:
1530                entry = line.strip().split("\t")
1531                self.users[entry[0]] = entry[1]
1532        except IOError:
1533            self.getUserMapFromPerforceServer()
1534
1535class P4Debug(Command):
1536    def __init__(self):
1537        Command.__init__(self)
1538        self.options = []
1539        self.description = "A tool to debug the output of p4 -G."
1540        self.needsGit = False
1541
1542    def run(self, args):
1543        j = 0
1544        for output in p4CmdList(args):
1545            print('Element: %d' % j)
1546            j += 1
1547            print(output)
1548        return True
1549
1550class P4RollBack(Command):
1551    def __init__(self):
1552        Command.__init__(self)
1553        self.options = [
1554            optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
1555        ]
1556        self.description = "A tool to debug the multi-branch import. Don't use :)"
1557        self.rollbackLocalBranches = False
1558
1559    def run(self, args):
1560        if len(args) != 1:
1561            return False
1562        maxChange = int(args[0])
1563
1564        if "p4ExitCode" in p4Cmd("changes -m 1"):
1565            die("Problems executing p4");
1566
1567        if self.rollbackLocalBranches:
1568            refPrefix = "refs/heads/"
1569            lines = read_pipe_lines("git rev-parse --symbolic --branches")
1570        else:
1571            refPrefix = "refs/remotes/"
1572            lines = read_pipe_lines("git rev-parse --symbolic --remotes")
1573
1574        for line in lines:
1575            if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
1576                line = line.strip()
1577                ref = refPrefix + line
1578                log = extractLogMessageFromGitCommit(ref)
1579                settings = extractSettingsGitLog(log)
1580
1581                depotPaths = settings['depot-paths']
1582                change = settings['change']
1583
1584                changed = False
1585
1586                if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
1587                                                           for p in depotPaths]))) == 0:
1588                    print("Branch %s did not exist at change %s, deleting." % (ref, maxChange))
1589                    system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1590                    continue
1591
1592                while change and int(change) > maxChange:
1593                    changed = True
1594                    if self.verbose:
1595                        print("%s is at %s ; rewinding towards %s" % (ref, change, maxChange))
1596                    system("git update-ref %s \"%s^\"" % (ref, ref))
1597                    log = extractLogMessageFromGitCommit(ref)
1598                    settings =  extractSettingsGitLog(log)
1599
1600
1601                    depotPaths = settings['depot-paths']
1602                    change = settings['change']
1603
1604                if changed:
1605                    print("%s rewound to %s" % (ref, change))
1606
1607        return True
1608
1609class P4Submit(Command, P4UserMap):
1610
1611    conflict_behavior_choices = ("ask", "skip", "quit")
1612
1613    def __init__(self):
1614        Command.__init__(self)
1615        P4UserMap.__init__(self)
1616        self.options = [
1617                optparse.make_option("--origin", dest="origin"),
1618                optparse.make_option("-M", dest="detectRenames", action="store_true"),
1619                # preserve the user, requires relevant p4 permissions
1620                optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
1621                optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
1622                optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
1623                optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
1624                optparse.make_option("--conflict", dest="conflict_behavior",
1625                                     choices=self.conflict_behavior_choices),
1626                optparse.make_option("--branch", dest="branch"),
1627                optparse.make_option("--shelve", dest="shelve", action="store_true",
1628                                     help="Shelve instead of submit. Shelved files are reverted, "
1629                                     "restoring the workspace to the state before the shelve"),
1630                optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
1631                                     metavar="CHANGELIST",
1632                                     help="update an existing shelved changelist, implies --shelve, "
1633                                           "repeat in-order for multiple shelved changelists"),
1634                optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1635                                     help="submit only the specified commit(s), one commit or xxx..xxx"),
1636                optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1637                                     help="Disable rebase after submit is completed. Can be useful if you "
1638                                     "work from a local git branch that is not master"),
1639                optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1640                                     help="Skip Perforce sync of p4/master after submit or shelve"),
1641                optparse.make_option("--no-verify", dest="no_verify", action="store_true",
1642                                     help="Bypass p4-pre-submit and p4-changelist hooks"),
1643        ]
1644        self.description = """Submit changes from git to the perforce depot.\n
1645    The `p4-pre-submit` hook is executed if it exists and is executable. It
1646    can be bypassed with the `--no-verify` command line option. The hook takes
1647    no parameters and nothing from standard input. Exiting with a non-zero status
1648    from this script prevents `git-p4 submit` from launching.
1649
1650    One usage scenario is to run unit tests in the hook.
1651
1652    The `p4-prepare-changelist` hook is executed right after preparing the default
1653    changelist message and before the editor is started. It takes one parameter,
1654    the name of the file that contains the changelist text. Exiting with a non-zero
1655    status from the script will abort the process.
1656
1657    The purpose of the hook is to edit the message file in place, and it is not
1658    supressed by the `--no-verify` option. This hook is called even if
1659    `--prepare-p4-only` is set.
1660
1661    The `p4-changelist` hook is executed after the changelist message has been
1662    edited by the user. It can be bypassed with the `--no-verify` option. It
1663    takes a single parameter, the name of the file that holds the proposed
1664    changelist text. Exiting with a non-zero status causes the command to abort.
1665
1666    The hook is allowed to edit the changelist file and can be used to normalize
1667    the text into some project standard format. It can also be used to refuse the
1668    Submit after inspect the message file.
1669
1670    The `p4-post-changelist` hook is invoked after the submit has successfully
1671    occurred in P4. It takes no parameters and is meant primarily for notification
1672    and cannot affect the outcome of the git p4 submit action.
1673    """
1674
1675        self.usage += " [name of git branch to submit into perforce depot]"
1676        self.origin = ""
1677        self.detectRenames = False
1678        self.preserveUser = gitConfigBool("git-p4.preserveUser")
1679        self.dry_run = False
1680        self.shelve = False
1681        self.update_shelve = list()
1682        self.commit = ""
1683        self.disable_rebase = gitConfigBool("git-p4.disableRebase")
1684        self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
1685        self.prepare_p4_only = False
1686        self.conflict_behavior = None
1687        self.isWindows = (platform.system() == "Windows")
1688        self.exportLabels = False
1689        self.p4HasMoveCommand = p4_has_move_command()
1690        self.branch = None
1691        self.no_verify = False
1692
1693        if gitConfig('git-p4.largeFileSystem'):
1694            die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1695
1696    def check(self):
1697        if len(p4CmdList("opened ...")) > 0:
1698            die("You have files opened with perforce! Close them before starting the sync.")
1699
1700    def separate_jobs_from_description(self, message):
1701        """Extract and return a possible Jobs field in the commit
1702           message.  It goes into a separate section in the p4 change
1703           specification.
1704
1705           A jobs line starts with "Jobs:" and looks like a new field
1706           in a form.  Values are white-space separated on the same
1707           line or on following lines that start with a tab.
1708
1709           This does not parse and extract the full git commit message
1710           like a p4 form.  It just sees the Jobs: line as a marker
1711           to pass everything from then on directly into the p4 form,
1712           but outside the description section.
1713
1714           Return a tuple (stripped log message, jobs string)."""
1715
1716        m = re.search(r'^Jobs:', message, re.MULTILINE)
1717        if m is None:
1718            return (message, None)
1719
1720        jobtext = message[m.start():]
1721        stripped_message = message[:m.start()].rstrip()
1722        return (stripped_message, jobtext)
1723
1724    def prepareLogMessage(self, template, message, jobs):
1725        """Edits the template returned from "p4 change -o" to insert
1726           the message in the Description field, and the jobs text in
1727           the Jobs field."""
1728        result = ""
1729
1730        inDescriptionSection = False
1731
1732        for line in template.split("\n"):
1733            if line.startswith("#"):
1734                result += line + "\n"
1735                continue
1736
1737            if inDescriptionSection:
1738                if line.startswith("Files:") or line.startswith("Jobs:"):
1739                    inDescriptionSection = False
1740                    # insert Jobs section
1741                    if jobs:
1742                        result += jobs + "\n"
1743                else:
1744                    continue
1745            else:
1746                if line.startswith("Description:"):
1747                    inDescriptionSection = True
1748                    line += "\n"
1749                    for messageLine in message.split("\n"):
1750                        line += "\t" + messageLine + "\n"
1751
1752            result += line + "\n"
1753
1754        return result
1755
1756    def patchRCSKeywords(self, file, pattern):
1757        # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1758        (handle, outFileName) = tempfile.mkstemp(dir='.')
1759        try:
1760            outFile = os.fdopen(handle, "w+")
1761            inFile = open(file, "r")
1762            regexp = re.compile(pattern, re.VERBOSE)
1763            for line in inFile.readlines():
1764                line = regexp.sub(r'$\1$', line)
1765                outFile.write(line)
1766            inFile.close()
1767            outFile.close()
1768            # Forcibly overwrite the original file
1769            os.unlink(file)
1770            shutil.move(outFileName, file)
1771        except:
1772            # cleanup our temporary file
1773            os.unlink(outFileName)
1774            print("Failed to strip RCS keywords in %s" % file)
1775            raise
1776
1777        print("Patched up RCS keywords in %s" % file)
1778
1779    def p4UserForCommit(self,id):
1780        # Return the tuple (perforce user,git email) for a given git commit id
1781        self.getUserMapFromPerforceServer()
1782        gitEmail = read_pipe(["git", "log", "--max-count=1",
1783                              "--format=%ae", id])
1784        gitEmail = gitEmail.strip()
1785        if gitEmail not in self.emails:
1786            return (None,gitEmail)
1787        else:
1788            return (self.emails[gitEmail],gitEmail)
1789
1790    def checkValidP4Users(self,commits):
1791        # check if any git authors cannot be mapped to p4 users
1792        for id in commits:
1793            (user,email) = self.p4UserForCommit(id)
1794            if not user:
1795                msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
1796                if gitConfigBool("git-p4.allowMissingP4Users"):
1797                    print("%s" % msg)
1798                else:
1799                    die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1800
1801    def lastP4Changelist(self):
1802        # Get back the last changelist number submitted in this client spec. This
1803        # then gets used to patch up the username in the change. If the same
1804        # client spec is being used by multiple processes then this might go
1805        # wrong.
1806        results = p4CmdList("client -o")        # find the current client
1807        client = None
1808        for r in results:
1809            if 'Client' in r:
1810                client = r['Client']
1811                break
1812        if not client:
1813            die("could not get client spec")
1814        results = p4CmdList(["changes", "-c", client, "-m", "1"])
1815        for r in results:
1816            if 'change' in r:
1817                return r['change']
1818        die("Could not get changelist number for last submit - cannot patch up user details")
1819
1820    def modifyChangelistUser(self, changelist, newUser):
1821        # fixup the user field of a changelist after it has been submitted.
1822        changes = p4CmdList("change -o %s" % changelist)
1823        if len(changes) != 1:
1824            die("Bad output from p4 change modifying %s to user %s" %
1825                (changelist, newUser))
1826
1827        c = changes[0]
1828        if c['User'] == newUser: return   # nothing to do
1829        c['User'] = newUser
1830        # p4 does not understand format version 3 and above
1831        input = marshal.dumps(c, 2)
1832
1833        result = p4CmdList("change -f -i", stdin=input)
1834        for r in result:
1835            if 'code' in r:
1836                if r['code'] == 'error':
1837                    die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1838            if 'data' in r:
1839                print("Updated user field for changelist %s to %s" % (changelist, newUser))
1840                return
1841        die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1842
1843    def canChangeChangelists(self):
1844        # check to see if we have p4 admin or super-user permissions, either of
1845        # which are required to modify changelists.
1846        results = p4CmdList(["protects", self.depotPath])
1847        for r in results:
1848            if 'perm' in r:
1849                if r['perm'] == 'admin':
1850                    return 1
1851                if r['perm'] == 'super':
1852                    return 1
1853        return 0
1854
1855    def prepareSubmitTemplate(self, changelist=None):
1856        """Run "p4 change -o" to grab a change specification template.
1857           This does not use "p4 -G", as it is nice to keep the submission
1858           template in original order, since a human might edit it.
1859
1860           Remove lines in the Files section that show changes to files
1861           outside the depot path we're committing into."""
1862
1863        [upstream, settings] = findUpstreamBranchPoint()
1864
1865        template = """\
1866# A Perforce Change Specification.
1867#
1868#  Change:      The change number. 'new' on a new changelist.
1869#  Date:        The date this specification was last modified.
1870#  Client:      The client on which the changelist was created.  Read-only.
1871#  User:        The user who created the changelist.
1872#  Status:      Either 'pending' or 'submitted'. Read-only.
1873#  Type:        Either 'public' or 'restricted'. Default is 'public'.
1874#  Description: Comments about the changelist.  Required.
1875#  Jobs:        What opened jobs are to be closed by this changelist.
1876#               You may delete jobs from this list.  (New changelists only.)
1877#  Files:       What opened files from the default changelist are to be added
1878#               to this changelist.  You may delete files from this list.
1879#               (New changelists only.)
1880"""
1881        files_list = []
1882        inFilesSection = False
1883        change_entry = None
1884        args = ['change', '-o']
1885        if changelist:
1886            args.append(str(changelist))
1887        for entry in p4CmdList(args):
1888            if 'code' not in entry:
1889                continue
1890            if entry['code'] == 'stat':
1891                change_entry = entry
1892                break
1893        if not change_entry:
1894            die('Failed to decode output of p4 change -o')
1895        for key, value in change_entry.items():
1896            if key.startswith('File'):
1897                if 'depot-paths' in settings:
1898                    if not [p for p in settings['depot-paths']
1899                            if p4PathStartsWith(value, p)]:
1900                        continue
1901                else:
1902                    if not p4PathStartsWith(value, self.depotPath):
1903                        continue
1904                files_list.append(value)
1905                continue
1906        # Output in the order expected by prepareLogMessage
1907        for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1908            if key not in change_entry:
1909                continue
1910            template += '\n'
1911            template += key + ':'
1912            if key == 'Description':
1913                template += '\n'
1914            for field_line in change_entry[key].splitlines():
1915                template += '\t'+field_line+'\n'
1916        if len(files_list) > 0:
1917            template += '\n'
1918            template += 'Files:\n'
1919        for path in files_list:
1920            template += '\t'+path+'\n'
1921        return template
1922
1923    def edit_template(self, template_file):
1924        """Invoke the editor to let the user change the submission
1925           message.  Return true if okay to continue with the submit."""
1926
1927        # if configured to skip the editing part, just submit
1928        if gitConfigBool("git-p4.skipSubmitEdit"):
1929            return True
1930
1931        # look at the modification time, to check later if the user saved
1932        # the file
1933        mtime = os.stat(template_file).st_mtime
1934
1935        # invoke the editor
1936        if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
1937            editor = os.environ.get("P4EDITOR")
1938        else:
1939            editor = read_pipe("git var GIT_EDITOR").strip()
1940        system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
1941
1942        # If the file was not saved, prompt to see if this patch should
1943        # be skipped.  But skip this verification step if configured so.
1944        if gitConfigBool("git-p4.skipSubmitEditCheck"):
1945            return True
1946
1947        # modification time updated means user saved the file
1948        if os.stat(template_file).st_mtime > mtime:
1949            return True
1950
1951        response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1952        if response == 'y':
1953            return True
1954        if response == 'n':
1955            return False
1956
1957    def get_diff_description(self, editedFiles, filesToAdd, symlinks):
1958        # diff
1959        if "P4DIFF" in os.environ:
1960            del(os.environ["P4DIFF"])
1961        diff = ""
1962        for editedFile in editedFiles:
1963            diff += p4_read_pipe(['diff', '-du',
1964                                  wildcard_encode(editedFile)])
1965
1966        # new file diff
1967        newdiff = ""
1968        for newFile in filesToAdd:
1969            newdiff += "==== new file ====\n"
1970            newdiff += "--- /dev/null\n"
1971            newdiff += "+++ %s\n" % newFile
1972
1973            is_link = os.path.islink(newFile)
1974            expect_link = newFile in symlinks
1975
1976            if is_link and expect_link:
1977                newdiff += "+%s\n" % os.readlink(newFile)
1978            else:
1979                f = open(newFile, "r")
1980                try:
1981                    for line in f.readlines():
1982                        newdiff += "+" + line
1983                except UnicodeDecodeError:
1984                    pass # Found non-text data and skip, since diff description should only include text
1985                f.close()
1986
1987        return (diff + newdiff).replace('\r\n', '\n')
1988
1989    def applyCommit(self, id):
1990        """Apply one commit, return True if it succeeded."""
1991
1992        print("Applying", read_pipe(["git", "show", "-s",
1993                                     "--format=format:%h %s", id]))
1994
1995        (p4User, gitEmail) = self.p4UserForCommit(id)
1996
1997        diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
1998        filesToAdd = set()
1999        filesToChangeType = set()
2000        filesToDelete = set()
2001        editedFiles = set()
2002        pureRenameCopy = set()
2003        symlinks = set()
2004        filesToChangeExecBit = {}
2005        all_files = list()
2006
2007        for line in diff:
2008            diff = parseDiffTreeEntry(line)
2009            modifier = diff['status']
2010            path = diff['src']
2011            all_files.append(path)
2012
2013            if modifier == "M":
2014                p4_edit(path)
2015                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2016                    filesToChangeExecBit[path] = diff['dst_mode']
2017                editedFiles.add(path)
2018            elif modifier == "A":
2019                filesToAdd.add(path)
2020                filesToChangeExecBit[path] = diff['dst_mode']
2021                if path in filesToDelete:
2022                    filesToDelete.remove(path)
2023
2024                dst_mode = int(diff['dst_mode'], 8)
2025                if dst_mode == 0o120000:
2026                    symlinks.add(path)
2027
2028            elif modifier == "D":
2029                filesToDelete.add(path)
2030                if path in filesToAdd:
2031                    filesToAdd.remove(path)
2032            elif modifier == "C":
2033                src, dest = diff['src'], diff['dst']
2034                all_files.append(dest)
2035                p4_integrate(src, dest)
2036                pureRenameCopy.add(dest)
2037                if diff['src_sha1'] != diff['dst_sha1']:
2038                    p4_edit(dest)
2039                    pureRenameCopy.discard(dest)
2040                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2041                    p4_edit(dest)
2042                    pureRenameCopy.discard(dest)
2043                    filesToChangeExecBit[dest] = diff['dst_mode']
2044                if self.isWindows:
2045                    # turn off read-only attribute
2046                    os.chmod(dest, stat.S_IWRITE)
2047                os.unlink(dest)
2048                editedFiles.add(dest)
2049            elif modifier == "R":
2050                src, dest = diff['src'], diff['dst']
2051                all_files.append(dest)
2052                if self.p4HasMoveCommand:
2053                    p4_edit(src)        # src must be open before move
2054                    p4_move(src, dest)  # opens for (move/delete, move/add)
2055                else:
2056                    p4_integrate(src, dest)
2057                    if diff['src_sha1'] != diff['dst_sha1']:
2058                        p4_edit(dest)
2059                    else:
2060                        pureRenameCopy.add(dest)
2061                if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2062                    if not self.p4HasMoveCommand:
2063                        p4_edit(dest)   # with move: already open, writable
2064                    filesToChangeExecBit[dest] = diff['dst_mode']
2065                if not self.p4HasMoveCommand:
2066                    if self.isWindows:
2067                        os.chmod(dest, stat.S_IWRITE)
2068                    os.unlink(dest)
2069                    filesToDelete.add(src)
2070                editedFiles.add(dest)
2071            elif modifier == "T":
2072                filesToChangeType.add(path)
2073            else:
2074                die("unknown modifier %s for %s" % (modifier, path))
2075
2076        diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
2077        patchcmd = diffcmd + " | git apply "
2078        tryPatchCmd = patchcmd + "--check -"
2079        applyPatchCmd = patchcmd + "--check --apply -"
2080        patch_succeeded = True
2081
2082        if verbose:
2083            print("TryPatch: %s" % tryPatchCmd)
2084
2085        if os.system(tryPatchCmd) != 0:
2086            fixed_rcs_keywords = False
2087            patch_succeeded = False
2088            print("Unfortunately applying the change failed!")
2089
2090            # Patch failed, maybe it's just RCS keyword woes. Look through
2091            # the patch to see if that's possible.
2092            if gitConfigBool("git-p4.attemptRCSCleanup"):
2093                file = None
2094                pattern = None
2095                kwfiles = {}
2096                for file in editedFiles | filesToDelete:
2097                    # did this file's delta contain RCS keywords?
2098                    pattern = p4_keywords_regexp_for_file(file)
2099
2100                    if pattern:
2101                        # this file is a possibility...look for RCS keywords.
2102                        regexp = re.compile(pattern, re.VERBOSE)
2103                        for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
2104                            if regexp.search(line):
2105                                if verbose:
2106                                    print("got keyword match on %s in %s in %s" % (pattern, line, file))
2107                                kwfiles[file] = pattern
2108                                break
2109
2110                for file in kwfiles:
2111                    if verbose:
2112                        print("zapping %s with %s" % (line,pattern))
2113                    # File is being deleted, so not open in p4.  Must
2114                    # disable the read-only bit on windows.
2115                    if self.isWindows and file not in editedFiles:
2116                        os.chmod(file, stat.S_IWRITE)
2117                    self.patchRCSKeywords(file, kwfiles[file])
2118                    fixed_rcs_keywords = True
2119
2120            if fixed_rcs_keywords:
2121                print("Retrying the patch with RCS keywords cleaned up")
2122                if os.system(tryPatchCmd) == 0:
2123                    patch_succeeded = True
2124                    print("Patch succeesed this time with RCS keywords cleaned")
2125
2126        if not patch_succeeded:
2127            for f in editedFiles:
2128                p4_revert(f)
2129            return False
2130
2131        #
2132        # Apply the patch for real, and do add/delete/+x handling.
2133        #
2134        system(applyPatchCmd)
2135
2136        for f in filesToChangeType:
2137            p4_edit(f, "-t", "auto")
2138        for f in filesToAdd:
2139            p4_add(f)
2140        for f in filesToDelete:
2141            p4_revert(f)
2142            p4_delete(f)
2143
2144        # Set/clear executable bits
2145        for f in filesToChangeExecBit.keys():
2146            mode = filesToChangeExecBit[f]
2147            setP4ExecBit(f, mode)
2148
2149        update_shelve = 0
2150        if len(self.update_shelve) > 0:
2151            update_shelve = self.update_shelve.pop(0)
2152            p4_reopen_in_change(update_shelve, all_files)
2153
2154        #
2155        # Build p4 change description, starting with the contents
2156        # of the git commit message.
2157        #
2158        logMessage = extractLogMessageFromGitCommit(id)
2159        logMessage = logMessage.strip()
2160        (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
2161
2162        template = self.prepareSubmitTemplate(update_shelve)
2163        submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
2164
2165        if self.preserveUser:
2166           submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
2167
2168        if self.checkAuthorship and not self.p4UserIsMe(p4User):
2169            submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2170            submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2171            submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2172
2173        separatorLine = "######## everything below this line is just the diff #######\n"
2174        if not self.prepare_p4_only:
2175            submitTemplate += separatorLine
2176            submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
2177
2178        (handle, fileName) = tempfile.mkstemp()
2179        tmpFile = os.fdopen(handle, "w+b")
2180        if self.isWindows:
2181            submitTemplate = submitTemplate.replace("\n", "\r\n")
2182        tmpFile.write(encode_text_stream(submitTemplate))
2183        tmpFile.close()
2184
2185        submitted = False
2186
2187        try:
2188            # Allow the hook to edit the changelist text before presenting it
2189            # to the user.
2190            if not run_git_hook("p4-prepare-changelist", [fileName]):
2191                return False
2192
2193            if self.prepare_p4_only:
2194                #
2195                # Leave the p4 tree prepared, and the submit template around
2196                # and let the user decide what to do next
2197                #
2198                submitted = True
2199                print("")
2200                print("P4 workspace prepared for submission.")
2201                print("To submit or revert, go to client workspace")
2202                print("  " + self.clientPath)
2203                print("")
2204                print("To submit, use \"p4 submit\" to write a new description,")
2205                print("or \"p4 submit -i <%s\" to use the one prepared by" \
2206                      " \"git p4\"." % fileName)
2207                print("You can delete the file \"%s\" when finished." % fileName)
2208
2209                if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
2210                    print("To preserve change ownership by user %s, you must\n" \
2211                          "do \"p4 change -f <change>\" after submitting and\n" \
2212                          "edit the User field.")
2213                if pureRenameCopy:
2214                    print("After submitting, renamed files must be re-synced.")
2215                    print("Invoke \"p4 sync -f\" on each of these files:")
2216                    for f in pureRenameCopy:
2217                        print("  " + f)
2218
2219                print("")
2220                print("To revert the changes, use \"p4 revert ...\", and delete")
2221                print("the submit template file \"%s\"" % fileName)
2222                if filesToAdd:
2223                    print("Since the commit adds new files, they must be deleted:")
2224                    for f in filesToAdd:
2225                        print("  " + f)
2226                print("")
2227                sys.stdout.flush()
2228                return True
2229
2230            if self.edit_template(fileName):
2231                if not self.no_verify:
2232                    if not run_git_hook("p4-changelist", [fileName]):
2233                        print("The p4-changelist hook failed.")
2234                        sys.stdout.flush()
2235                        return False
2236
2237                # read the edited message and submit
2238                tmpFile = open(fileName, "rb")
2239                message = decode_text_stream(tmpFile.read())
2240                tmpFile.close()
2241                if self.isWindows:
2242                    message = message.replace("\r\n", "\n")
2243                if message.find(separatorLine) != -1:
2244                    submitTemplate = message[:message.index(separatorLine)]
2245                else:
2246                    submitTemplate = message
2247
2248                if len(submitTemplate.strip()) == 0:
2249                    print("Changelist is empty, aborting this changelist.")
2250                    sys.stdout.flush()
2251                    return False
2252
2253                if update_shelve:
2254                    p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2255                elif self.shelve:
2256                    p4_write_pipe(['shelve', '-i'], submitTemplate)
2257                else:
2258                    p4_write_pipe(['submit', '-i'], submitTemplate)
2259                    # The rename/copy happened by applying a patch that created a
2260                    # new file.  This leaves it writable, which confuses p4.
2261                    for f in pureRenameCopy:
2262                        p4_sync(f, "-f")
2263
2264                if self.preserveUser:
2265                    if p4User:
2266                        # Get last changelist number. Cannot easily get it from
2267                        # the submit command output as the output is
2268                        # unmarshalled.
2269                        changelist = self.lastP4Changelist()
2270                        self.modifyChangelistUser(changelist, p4User)
2271
2272                submitted = True
2273
2274                run_git_hook("p4-post-changelist")
2275        finally:
2276            # Revert changes if we skip this patch
2277            if not submitted or self.shelve:
2278                if self.shelve:
2279                    print ("Reverting shelved files.")
2280                else:
2281                    print ("Submission cancelled, undoing p4 changes.")
2282                sys.stdout.flush()
2283                for f in editedFiles | filesToDelete:
2284                    p4_revert(f)
2285                for f in filesToAdd:
2286                    p4_revert(f)
2287                    os.remove(f)
2288
2289            if not self.prepare_p4_only:
2290                os.remove(fileName)
2291        return submitted
2292
2293    # Export git tags as p4 labels. Create a p4 label and then tag
2294    # with that.
2295    def exportGitTags(self, gitTags):
2296        validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2297        if len(validLabelRegexp) == 0:
2298            validLabelRegexp = defaultLabelRegexp
2299        m = re.compile(validLabelRegexp)
2300
2301        for name in gitTags:
2302
2303            if not m.match(name):
2304                if verbose:
2305                    print("tag %s does not match regexp %s" % (name, validLabelRegexp))
2306                continue
2307
2308            # Get the p4 commit this corresponds to
2309            logMessage = extractLogMessageFromGitCommit(name)
2310            values = extractSettingsGitLog(logMessage)
2311
2312            if 'change' not in values:
2313                # a tag pointing to something not sent to p4; ignore
2314                if verbose:
2315                    print("git tag %s does not give a p4 commit" % name)
2316                continue
2317            else:
2318                changelist = values['change']
2319
2320            # Get the tag details.
2321            inHeader = True
2322            isAnnotated = False
2323            body = []
2324            for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2325                l = l.strip()
2326                if inHeader:
2327                    if re.match(r'tag\s+', l):
2328                        isAnnotated = True
2329                    elif re.match(r'\s*$', l):
2330                        inHeader = False
2331                        continue
2332                else:
2333                    body.append(l)
2334
2335            if not isAnnotated:
2336                body = ["lightweight tag imported by git p4\n"]
2337
2338            # Create the label - use the same view as the client spec we are using
2339            clientSpec = getClientSpec()
2340
2341            labelTemplate  = "Label: %s\n" % name
2342            labelTemplate += "Description:\n"
2343            for b in body:
2344                labelTemplate += "\t" + b + "\n"
2345            labelTemplate += "View:\n"
2346            for depot_side in clientSpec.mappings:
2347                labelTemplate += "\t%s\n" % depot_side
2348
2349            if self.dry_run:
2350                print("Would create p4 label %s for tag" % name)
2351            elif self.prepare_p4_only:
2352                print("Not creating p4 label %s for tag due to option" \
2353                      " --prepare-p4-only" % name)
2354            else:
2355                p4_write_pipe(["label", "-i"], labelTemplate)
2356
2357                # Use the label
2358                p4_system(["tag", "-l", name] +
2359                          ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
2360
2361                if verbose:
2362                    print("created p4 label for tag %s" % name)
2363
2364    def run(self, args):
2365        if len(args) == 0:
2366            self.master = currentGitBranch()
2367        elif len(args) == 1:
2368            self.master = args[0]
2369            if not branchExists(self.master):
2370                die("Branch %s does not exist" % self.master)
2371        else:
2372            return False
2373
2374        for i in self.update_shelve:
2375            if i <= 0:
2376                sys.exit("invalid changelist %d" % i)
2377
2378        if self.master:
2379            allowSubmit = gitConfig("git-p4.allowSubmit")
2380            if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2381                die("%s is not in git-p4.allowSubmit" % self.master)
2382
2383        [upstream, settings] = findUpstreamBranchPoint()
2384        self.depotPath = settings['depot-paths'][0]
2385        if len(self.origin) == 0:
2386            self.origin = upstream
2387
2388        if len(self.update_shelve) > 0:
2389            self.shelve = True
2390
2391        if self.preserveUser:
2392            if not self.canChangeChangelists():
2393                die("Cannot preserve user names without p4 super-user or admin permissions")
2394
2395        # if not set from the command line, try the config file
2396        if self.conflict_behavior is None:
2397            val = gitConfig("git-p4.conflict")
2398            if val:
2399                if val not in self.conflict_behavior_choices:
2400                    die("Invalid value '%s' for config git-p4.conflict" % val)
2401            else:
2402                val = "ask"
2403            self.conflict_behavior = val
2404
2405        if self.verbose:
2406            print("Origin branch is " + self.origin)
2407
2408        if len(self.depotPath) == 0:
2409            print("Internal error: cannot locate perforce depot path from existing branches")
2410            sys.exit(128)
2411
2412        self.useClientSpec = False
2413        if gitConfigBool("git-p4.useclientspec"):
2414            self.useClientSpec = True
2415        if self.useClientSpec:
2416            self.clientSpecDirs = getClientSpec()
2417
2418        # Check for the existence of P4 branches
2419        branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2420
2421        if self.useClientSpec and not branchesDetected:
2422            # all files are relative to the client spec
2423            self.clientPath = getClientRoot()
2424        else:
2425            self.clientPath = p4Where(self.depotPath)
2426
2427        if self.clientPath == "":
2428            die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
2429
2430        print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
2431        self.oldWorkingDirectory = os.getcwd()
2432
2433        # ensure the clientPath exists
2434        new_client_dir = False
2435        if not os.path.exists(self.clientPath):
2436            new_client_dir = True
2437            os.makedirs(self.clientPath)
2438
2439        chdir(self.clientPath, is_client_path=True)
2440        if self.dry_run:
2441            print("Would synchronize p4 checkout in %s" % self.clientPath)
2442        else:
2443            print("Synchronizing p4 checkout...")
2444            if new_client_dir:
2445                # old one was destroyed, and maybe nobody told p4
2446                p4_sync("...", "-f")
2447            else:
2448                p4_sync("...")
2449        self.check()
2450
2451        commits = []
2452        if self.master:
2453            committish = self.master
2454        else:
2455            committish = 'HEAD'
2456
2457        if self.commit != "":
2458            if self.commit.find("..") != -1:
2459                limits_ish = self.commit.split("..")
2460                for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2461                    commits.append(line.strip())
2462                commits.reverse()
2463            else:
2464                commits.append(self.commit)
2465        else:
2466            for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
2467                commits.append(line.strip())
2468            commits.reverse()
2469
2470        if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
2471            self.checkAuthorship = False
2472        else:
2473            self.checkAuthorship = True
2474
2475        if self.preserveUser:
2476            self.checkValidP4Users(commits)
2477
2478        #
2479        # Build up a set of options to be passed to diff when
2480        # submitting each commit to p4.
2481        #
2482        if self.detectRenames:
2483            # command-line -M arg
2484            self.diffOpts = "-M"
2485        else:
2486            # If not explicitly set check the config variable
2487            detectRenames = gitConfig("git-p4.detectRenames")
2488
2489            if detectRenames.lower() == "false" or detectRenames == "":
2490                self.diffOpts = ""
2491            elif detectRenames.lower() == "true":
2492                self.diffOpts = "-M"
2493            else:
2494                self.diffOpts = "-M%s" % detectRenames
2495
2496        # no command-line arg for -C or --find-copies-harder, just
2497        # config variables
2498        detectCopies = gitConfig("git-p4.detectCopies")
2499        if detectCopies.lower() == "false" or detectCopies == "":
2500            pass
2501        elif detectCopies.lower() == "true":
2502            self.diffOpts += " -C"
2503        else:
2504            self.diffOpts += " -C%s" % detectCopies
2505
2506        if gitConfigBool("git-p4.detectCopiesHarder"):
2507            self.diffOpts += " --find-copies-harder"
2508
2509        num_shelves = len(self.update_shelve)
2510        if num_shelves > 0 and num_shelves != len(commits):
2511            sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2512                     (len(commits), num_shelves))
2513
2514        if not self.no_verify:
2515            try:
2516                if not run_git_hook("p4-pre-submit"):
2517                    print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip " \
2518                        "this pre-submission check by adding\nthe command line option '--no-verify', " \
2519                        "however,\nthis will also skip the p4-changelist hook as well.")
2520                    sys.exit(1)
2521            except Exception as e:
2522                print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "\
2523                    "with the error '{0}'".format(e.message) )
2524                sys.exit(1)
2525
2526        #
2527        # Apply the commits, one at a time.  On failure, ask if should
2528        # continue to try the rest of the patches, or quit.
2529        #
2530        if self.dry_run:
2531            print("Would apply")
2532        applied = []
2533        last = len(commits) - 1
2534        for i, commit in enumerate(commits):
2535            if self.dry_run:
2536                print(" ", read_pipe(["git", "show", "-s",
2537                                      "--format=format:%h %s", commit]))
2538                ok = True
2539            else:
2540                ok = self.applyCommit(commit)
2541            if ok:
2542                applied.append(commit)
2543                if self.prepare_p4_only:
2544                    if i < last:
2545                        print("Processing only the first commit due to option" \
2546                                " --prepare-p4-only")
2547                    break
2548            else:
2549                if i < last:
2550                    # prompt for what to do, or use the option/variable
2551                    if self.conflict_behavior == "ask":
2552                        print("What do you want to do?")
2553                        response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2554                    elif self.conflict_behavior == "skip":
2555                        response = "s"
2556                    elif self.conflict_behavior == "quit":
2557                        response = "q"
2558                    else:
2559                        die("Unknown conflict_behavior '%s'" %
2560                            self.conflict_behavior)
2561
2562                    if response == "s":
2563                        print("Skipping this commit, but applying the rest")
2564                    if response == "q":
2565                        print("Quitting")
2566                        break
2567
2568        chdir(self.oldWorkingDirectory)
2569        shelved_applied = "shelved" if self.shelve else "applied"
2570        if self.dry_run:
2571            pass
2572        elif self.prepare_p4_only:
2573            pass
2574        elif len(commits) == len(applied):
2575            print("All commits {0}!".format(shelved_applied))
2576
2577            sync = P4Sync()
2578            if self.branch:
2579                sync.branch = self.branch
2580            if self.disable_p4sync:
2581                sync.sync_origin_only()
2582            else:
2583                sync.run([])
2584
2585                if not self.disable_rebase:
2586                    rebase = P4Rebase()
2587                    rebase.rebase()
2588
2589        else:
2590            if len(applied) == 0:
2591                print("No commits {0}.".format(shelved_applied))
2592            else:
2593                print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
2594                for c in commits:
2595                    if c in applied:
2596                        star = "*"
2597                    else:
2598                        star = " "
2599                    print(star, read_pipe(["git", "show", "-s",
2600                                           "--format=format:%h %s",  c]))
2601                print("You will have to do 'git p4 sync' and rebase.")
2602
2603        if gitConfigBool("git-p4.exportLabels"):
2604            self.exportLabels = True
2605
2606        if self.exportLabels:
2607            p4Labels = getP4Labels(self.depotPath)
2608            gitTags = getGitTags()
2609
2610            missingGitTags = gitTags - p4Labels
2611            self.exportGitTags(missingGitTags)
2612
2613        # exit with error unless everything applied perfectly
2614        if len(commits) != len(applied):
2615                sys.exit(1)
2616
2617        return True
2618
2619class View(object):
2620    """Represent a p4 view ("p4 help views"), and map files in a
2621       repo according to the view."""
2622
2623    def __init__(self, client_name):
2624        self.mappings = []
2625        self.client_prefix = "//%s/" % client_name
2626        # cache results of "p4 where" to lookup client file locations
2627        self.client_spec_path_cache = {}
2628
2629    def append(self, view_line):
2630        """Parse a view line, splitting it into depot and client
2631           sides.  Append to self.mappings, preserving order.  This
2632           is only needed for tag creation."""
2633
2634        # Split the view line into exactly two words.  P4 enforces
2635        # structure on these lines that simplifies this quite a bit.
2636        #
2637        # Either or both words may be double-quoted.
2638        # Single quotes do not matter.
2639        # Double-quote marks cannot occur inside the words.
2640        # A + or - prefix is also inside the quotes.
2641        # There are no quotes unless they contain a space.
2642        # The line is already white-space stripped.
2643        # The two words are separated by a single space.
2644        #
2645        if view_line[0] == '"':
2646            # First word is double quoted.  Find its end.
2647            close_quote_index = view_line.find('"', 1)
2648            if close_quote_index <= 0:
2649                die("No first-word closing quote found: %s" % view_line)
2650            depot_side = view_line[1:close_quote_index]
2651            # skip closing quote and space
2652            rhs_index = close_quote_index + 1 + 1
2653        else:
2654            space_index = view_line.find(" ")
2655            if space_index <= 0:
2656                die("No word-splitting space found: %s" % view_line)
2657            depot_side = view_line[0:space_index]
2658            rhs_index = space_index + 1
2659
2660        # prefix + means overlay on previous mapping
2661        if depot_side.startswith("+"):
2662            depot_side = depot_side[1:]
2663
2664        # prefix - means exclude this path, leave out of mappings
2665        exclude = False
2666        if depot_side.startswith("-"):
2667            exclude = True
2668            depot_side = depot_side[1:]
2669
2670        if not exclude:
2671            self.mappings.append(depot_side)
2672
2673    def convert_client_path(self, clientFile):
2674        # chop off //client/ part to make it relative
2675        if not decode_path(clientFile).startswith(self.client_prefix):
2676            die("No prefix '%s' on clientFile '%s'" %
2677                (self.client_prefix, clientFile))
2678        return clientFile[len(self.client_prefix):]
2679
2680    def update_client_spec_path_cache(self, files):
2681        """ Caching file paths by "p4 where" batch query """
2682
2683        # List depot file paths exclude that already cached
2684        fileArgs = [f['path'] for f in files if decode_path(f['path']) not in self.client_spec_path_cache]
2685
2686        if len(fileArgs) == 0:
2687            return  # All files in cache
2688
2689        where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2690        for res in where_result:
2691            if "code" in res and res["code"] == "error":
2692                # assume error is "... file(s) not in client view"
2693                continue
2694            if "clientFile" not in res:
2695                die("No clientFile in 'p4 where' output")
2696            if "unmap" in res:
2697                # it will list all of them, but only one not unmap-ped
2698                continue
2699            depot_path = decode_path(res['depotFile'])
2700            if gitConfigBool("core.ignorecase"):
2701                depot_path = depot_path.lower()
2702            self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
2703
2704        # not found files or unmap files set to ""
2705        for depotFile in fileArgs:
2706            depotFile = decode_path(depotFile)
2707            if gitConfigBool("core.ignorecase"):
2708                depotFile = depotFile.lower()
2709            if depotFile not in self.client_spec_path_cache:
2710                self.client_spec_path_cache[depotFile] = b''
2711
2712    def map_in_client(self, depot_path):
2713        """Return the relative location in the client where this
2714           depot file should live.  Returns "" if the file should
2715           not be mapped in the client."""
2716
2717        if gitConfigBool("core.ignorecase"):
2718            depot_path = depot_path.lower()
2719
2720        if depot_path in self.client_spec_path_cache:
2721            return self.client_spec_path_cache[depot_path]
2722
2723        die( "Error: %s is not found in client spec path" % depot_path )
2724        return ""
2725
2726def cloneExcludeCallback(option, opt_str, value, parser):
2727    # prepend "/" because the first "/" was consumed as part of the option itself.
2728    # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2729    parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2730
2731class P4Sync(Command, P4UserMap):
2732
2733    def __init__(self):
2734        Command.__init__(self)
2735        P4UserMap.__init__(self)
2736        self.options = [
2737                optparse.make_option("--branch", dest="branch"),
2738                optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2739                optparse.make_option("--changesfile", dest="changesFile"),
2740                optparse.make_option("--silent", dest="silent", action="store_true"),
2741                optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
2742                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
2743                optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2744                                     help="Import into refs/heads/ , not refs/remotes"),
2745                optparse.make_option("--max-changes", dest="maxChanges",
2746                                     help="Maximum number of changes to import"),
2747                optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2748                                     help="Internal block size to use when iteratively calling p4 changes"),
2749                optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
2750                                     help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2751                optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
2752                                     help="Only sync files that are included in the Perforce Client Spec"),
2753                optparse.make_option("-/", dest="cloneExclude",
2754                                     action="callback", callback=cloneExcludeCallback, type="string",
2755                                     help="exclude depot path"),
2756        ]
2757        self.description = """Imports from Perforce into a git repository.\n
2758    example:
2759    //depot/my/project/ -- to import the current head
2760    //depot/my/project/@all -- to import everything
2761    //depot/my/project/@1,6 -- to import only from revision 1 to 6
2762
2763    (a ... is not needed in the path p4 specification, it's added implicitly)"""
2764
2765        self.usage += " //depot/path[@revRange]"
2766        self.silent = False
2767        self.createdBranches = set()
2768        self.committedChanges = set()
2769        self.branch = ""
2770        self.detectBranches = False
2771        self.detectLabels = False
2772        self.importLabels = False
2773        self.changesFile = ""
2774        self.syncWithOrigin = True
2775        self.importIntoRemotes = True
2776        self.maxChanges = ""
2777        self.changes_block_size = None
2778        self.keepRepoPath = False
2779        self.depotPaths = None
2780        self.p4BranchesInGit = []
2781        self.cloneExclude = []
2782        self.useClientSpec = False
2783        self.useClientSpec_from_options = False
2784        self.clientSpecDirs = None
2785        self.tempBranches = []
2786        self.tempBranchLocation = "refs/git-p4-tmp"
2787        self.largeFileSystem = None
2788        self.suppress_meta_comment = False
2789
2790        if gitConfig('git-p4.largeFileSystem'):
2791            largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2792            self.largeFileSystem = largeFileSystemConstructor(
2793                lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2794            )
2795
2796        if gitConfig("git-p4.syncFromOrigin") == "false":
2797            self.syncWithOrigin = False
2798
2799        self.depotPaths = []
2800        self.changeRange = ""
2801        self.previousDepotPaths = []
2802        self.hasOrigin = False
2803
2804        # map from branch depot path to parent branch
2805        self.knownBranches = {}
2806        self.initialParents = {}
2807
2808        self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2809        self.labels = {}
2810
2811    # Force a checkpoint in fast-import and wait for it to finish
2812    def checkpoint(self):
2813        self.gitStream.write("checkpoint\n\n")
2814        self.gitStream.write("progress checkpoint\n\n")
2815        self.gitStream.flush()
2816        out = self.gitOutput.readline()
2817        if self.verbose:
2818            print("checkpoint finished: " + out)
2819
2820    def isPathWanted(self, path):
2821        for p in self.cloneExclude:
2822            if p.endswith("/"):
2823                if p4PathStartsWith(path, p):
2824                    return False
2825            # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2826            elif path.lower() == p.lower():
2827                return False
2828        for p in self.depotPaths:
2829            if p4PathStartsWith(path, decode_path(p)):
2830                return True
2831        return False
2832
2833    def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0):
2834        files = []
2835        fnum = 0
2836        while "depotFile%s" % fnum in commit:
2837            path =  commit["depotFile%s" % fnum]
2838            found = self.isPathWanted(decode_path(path))
2839            if not found:
2840                fnum = fnum + 1
2841                continue
2842
2843            file = {}
2844            file["path"] = path
2845            file["rev"] = commit["rev%s" % fnum]
2846            file["action"] = commit["action%s" % fnum]
2847            file["type"] = commit["type%s" % fnum]
2848            if shelved:
2849                file["shelved_cl"] = int(shelved_cl)
2850            files.append(file)
2851            fnum = fnum + 1
2852        return files
2853
2854    def extractJobsFromCommit(self, commit):
2855        jobs = []
2856        jnum = 0
2857        while "job%s" % jnum in commit:
2858            job = commit["job%s" % jnum]
2859            jobs.append(job)
2860            jnum = jnum + 1
2861        return jobs
2862
2863    def stripRepoPath(self, path, prefixes):
2864        """When streaming files, this is called to map a p4 depot path
2865           to where it should go in git.  The prefixes are either
2866           self.depotPaths, or self.branchPrefixes in the case of
2867           branch detection."""
2868
2869        if self.useClientSpec:
2870            # branch detection moves files up a level (the branch name)
2871            # from what client spec interpretation gives
2872            path = decode_path(self.clientSpecDirs.map_in_client(path))
2873            if self.detectBranches:
2874                for b in self.knownBranches:
2875                    if p4PathStartsWith(path, b + "/"):
2876                        path = path[len(b)+1:]
2877
2878        elif self.keepRepoPath:
2879            # Preserve everything in relative path name except leading
2880            # //depot/; just look at first prefix as they all should
2881            # be in the same depot.
2882            depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2883            if p4PathStartsWith(path, depot):
2884                path = path[len(depot):]
2885
2886        else:
2887            for p in prefixes:
2888                if p4PathStartsWith(path, p):
2889                    path = path[len(p):]
2890                    break
2891
2892        path = wildcard_decode(path)
2893        return path
2894
2895    def splitFilesIntoBranches(self, commit):
2896        """Look at each depotFile in the commit to figure out to what
2897           branch it belongs."""
2898
2899        if self.clientSpecDirs:
2900            files = self.extractFilesFromCommit(commit)
2901            self.clientSpecDirs.update_client_spec_path_cache(files)
2902
2903        branches = {}
2904        fnum = 0
2905        while "depotFile%s" % fnum in commit:
2906            raw_path = commit["depotFile%s" % fnum]
2907            path = decode_path(raw_path)
2908            found = self.isPathWanted(path)
2909            if not found:
2910                fnum = fnum + 1
2911                continue
2912
2913            file = {}
2914            file["path"] = raw_path
2915            file["rev"] = commit["rev%s" % fnum]
2916            file["action"] = commit["action%s" % fnum]
2917            file["type"] = commit["type%s" % fnum]
2918            fnum = fnum + 1
2919
2920            # start with the full relative path where this file would
2921            # go in a p4 client
2922            if self.useClientSpec:
2923                relPath = decode_path(self.clientSpecDirs.map_in_client(path))
2924            else:
2925                relPath = self.stripRepoPath(path, self.depotPaths)
2926
2927            for branch in self.knownBranches.keys():
2928                # add a trailing slash so that a commit into qt/4.2foo
2929                # doesn't end up in qt/4.2, e.g.
2930                if p4PathStartsWith(relPath, branch + "/"):
2931                    if branch not in branches:
2932                        branches[branch] = []
2933                    branches[branch].append(file)
2934                    break
2935
2936        return branches
2937
2938    def writeToGitStream(self, gitMode, relPath, contents):
2939        self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
2940        self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2941        for d in contents:
2942            self.gitStream.write(d)
2943        self.gitStream.write('\n')
2944
2945    def encodeWithUTF8(self, path):
2946        try:
2947            path.decode('ascii')
2948        except:
2949            encoding = 'utf8'
2950            if gitConfig('git-p4.pathEncoding'):
2951                encoding = gitConfig('git-p4.pathEncoding')
2952            path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2953            if self.verbose:
2954                print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
2955        return path
2956
2957    # output one file from the P4 stream
2958    # - helper for streamP4Files
2959
2960    def streamOneP4File(self, file, contents):
2961        file_path = file['depotFile']
2962        relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
2963
2964        if verbose:
2965            if 'fileSize' in self.stream_file:
2966                size = int(self.stream_file['fileSize'])
2967            else:
2968                size = 0 # deleted files don't get a fileSize apparently
2969            sys.stdout.write('\r%s --> %s (%i MB)\n' % (file_path, relPath, size/1024/1024))
2970            sys.stdout.flush()
2971
2972        (type_base, type_mods) = split_p4_type(file["type"])
2973
2974        git_mode = "100644"
2975        if "x" in type_mods:
2976            git_mode = "100755"
2977        if type_base == "symlink":
2978            git_mode = "120000"
2979            # p4 print on a symlink sometimes contains "target\n";
2980            # if it does, remove the newline
2981            data = ''.join(decode_text_stream(c) for c in contents)
2982            if not data:
2983                # Some version of p4 allowed creating a symlink that pointed
2984                # to nothing.  This causes p4 errors when checking out such
2985                # a change, and errors here too.  Work around it by ignoring
2986                # the bad symlink; hopefully a future change fixes it.
2987                print("\nIgnoring empty symlink in %s" % file_path)
2988                return
2989            elif data[-1] == '\n':
2990                contents = [data[:-1]]
2991            else:
2992                contents = [data]
2993
2994        if type_base == "utf16":
2995            # p4 delivers different text in the python output to -G
2996            # than it does when using "print -o", or normal p4 client
2997            # operations.  utf16 is converted to ascii or utf8, perhaps.
2998            # But ascii text saved as -t utf16 is completely mangled.
2999            # Invoke print -o to get the real contents.
3000            #
3001            # On windows, the newlines will always be mangled by print, so put
3002            # them back too.  This is not needed to the cygwin windows version,
3003            # just the native "NT" type.
3004            #
3005            try:
3006                text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
3007            except Exception as e:
3008                if 'Translation of file content failed' in str(e):
3009                    type_base = 'binary'
3010                else:
3011                    raise e
3012            else:
3013                if p4_version_string().find('/NT') >= 0:
3014                    text = text.replace(b'\r\n', b'\n')
3015                contents = [ text ]
3016
3017        if type_base == "apple":
3018            # Apple filetype files will be streamed as a concatenation of
3019            # its appledouble header and the contents.  This is useless
3020            # on both macs and non-macs.  If using "print -q -o xx", it
3021            # will create "xx" with the data, and "%xx" with the header.
3022            # This is also not very useful.
3023            #
3024            # Ideally, someday, this script can learn how to generate
3025            # appledouble files directly and import those to git, but
3026            # non-mac machines can never find a use for apple filetype.
3027            print("\nIgnoring apple filetype file %s" % file['depotFile'])
3028            return
3029
3030        # Note that we do not try to de-mangle keywords on utf16 files,
3031        # even though in theory somebody may want that.
3032        pattern = p4_keywords_regexp_for_type(type_base, type_mods)
3033        if pattern:
3034            regexp = re.compile(pattern, re.VERBOSE)
3035            text = ''.join(decode_text_stream(c) for c in contents)
3036            text = regexp.sub(r'$\1$', text)
3037            contents = [ encode_text_stream(text) ]
3038
3039        if self.largeFileSystem:
3040            (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
3041
3042        self.writeToGitStream(git_mode, relPath, contents)
3043
3044    def streamOneP4Deletion(self, file):
3045        relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
3046        if verbose:
3047            sys.stdout.write("delete %s\n" % relPath)
3048            sys.stdout.flush()
3049        self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
3050
3051        if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
3052            self.largeFileSystem.removeLargeFile(relPath)
3053
3054    # handle another chunk of streaming data
3055    def streamP4FilesCb(self, marshalled):
3056
3057        # catch p4 errors and complain
3058        err = None
3059        if "code" in marshalled:
3060            if marshalled["code"] == "error":
3061                if "data" in marshalled:
3062                    err = marshalled["data"].rstrip()
3063
3064        if not err and 'fileSize' in self.stream_file:
3065            required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3066            if required_bytes > 0:
3067                err = 'Not enough space left on %s! Free at least %i MB.' % (
3068                    os.getcwd(), required_bytes/1024/1024
3069                )
3070
3071        if err:
3072            f = None
3073            if self.stream_have_file_info:
3074                if "depotFile" in self.stream_file:
3075                    f = self.stream_file["depotFile"]
3076            # force a failure in fast-import, else an empty
3077            # commit will be made
3078            self.gitStream.write("\n")
3079            self.gitStream.write("die-now\n")
3080            self.gitStream.close()
3081            # ignore errors, but make sure it exits first
3082            self.importProcess.wait()
3083            if f:
3084                die("Error from p4 print for %s: %s" % (f, err))
3085            else:
3086                die("Error from p4 print: %s" % err)
3087
3088        if 'depotFile' in marshalled and self.stream_have_file_info:
3089            # start of a new file - output the old one first
3090            self.streamOneP4File(self.stream_file, self.stream_contents)
3091            self.stream_file = {}
3092            self.stream_contents = []
3093            self.stream_have_file_info = False
3094
3095        # pick up the new file information... for the
3096        # 'data' field we need to append to our array
3097        for k in marshalled.keys():
3098            if k == 'data':
3099                if 'streamContentSize' not in self.stream_file:
3100                    self.stream_file['streamContentSize'] = 0
3101                self.stream_file['streamContentSize'] += len(marshalled['data'])
3102                self.stream_contents.append(marshalled['data'])
3103            else:
3104                self.stream_file[k] = marshalled[k]
3105
3106        if (verbose and
3107            'streamContentSize' in self.stream_file and
3108            'fileSize' in self.stream_file and
3109            'depotFile' in self.stream_file):
3110            size = int(self.stream_file["fileSize"])
3111            if size > 0:
3112                progress = 100*self.stream_file['streamContentSize']/size
3113                sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
3114                sys.stdout.flush()
3115
3116        self.stream_have_file_info = True
3117
3118    # Stream directly from "p4 files" into "git fast-import"
3119    def streamP4Files(self, files):
3120        filesForCommit = []
3121        filesToRead = []
3122        filesToDelete = []
3123
3124        for f in files:
3125            filesForCommit.append(f)
3126            if f['action'] in self.delete_actions:
3127                filesToDelete.append(f)
3128            else:
3129                filesToRead.append(f)
3130
3131        # deleted files...
3132        for f in filesToDelete:
3133            self.streamOneP4Deletion(f)
3134
3135        if len(filesToRead) > 0:
3136            self.stream_file = {}
3137            self.stream_contents = []
3138            self.stream_have_file_info = False
3139
3140            # curry self argument
3141            def streamP4FilesCbSelf(entry):
3142                self.streamP4FilesCb(entry)
3143
3144            fileArgs = []
3145            for f in filesToRead:
3146                if 'shelved_cl' in f:
3147                    # Handle shelved CLs using the "p4 print file@=N" syntax to print
3148                    # the contents
3149                    fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
3150                else:
3151                    fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
3152
3153                fileArgs.append(fileArg)
3154
3155            p4CmdList(["-x", "-", "print"],
3156                      stdin=fileArgs,
3157                      cb=streamP4FilesCbSelf)
3158
3159            # do the last chunk
3160            if 'depotFile' in self.stream_file:
3161                self.streamOneP4File(self.stream_file, self.stream_contents)
3162
3163    def make_email(self, userid):
3164        if userid in self.users:
3165            return self.users[userid]
3166        else:
3167            return "%s <a@b>" % userid
3168
3169    def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
3170        """ Stream a p4 tag.
3171        commit is either a git commit, or a fast-import mark, ":<p4commit>"
3172        """
3173
3174        if verbose:
3175            print("writing tag %s for commit %s" % (labelName, commit))
3176        gitStream.write("tag %s\n" % labelName)
3177        gitStream.write("from %s\n" % commit)
3178
3179        if 'Owner' in labelDetails:
3180            owner = labelDetails["Owner"]
3181        else:
3182            owner = None
3183
3184        # Try to use the owner of the p4 label, or failing that,
3185        # the current p4 user id.
3186        if owner:
3187            email = self.make_email(owner)
3188        else:
3189            email = self.make_email(self.p4UserId())
3190        tagger = "%s %s %s" % (email, epoch, self.tz)
3191
3192        gitStream.write("tagger %s\n" % tagger)
3193
3194        print("labelDetails=",labelDetails)
3195        if 'Description' in labelDetails:
3196            description = labelDetails['Description']
3197        else:
3198            description = 'Label from git p4'
3199
3200        gitStream.write("data %d\n" % len(description))
3201        gitStream.write(description)
3202        gitStream.write("\n")
3203
3204    def inClientSpec(self, path):
3205        if not self.clientSpecDirs:
3206            return True
3207        inClientSpec = self.clientSpecDirs.map_in_client(path)
3208        if not inClientSpec and self.verbose:
3209            print('Ignoring file outside of client spec: {0}'.format(path))
3210        return inClientSpec
3211
3212    def hasBranchPrefix(self, path):
3213        if not self.branchPrefixes:
3214            return True
3215        hasPrefix = [p for p in self.branchPrefixes
3216                        if p4PathStartsWith(path, p)]
3217        if not hasPrefix and self.verbose:
3218            print('Ignoring file outside of prefix: {0}'.format(path))
3219        return hasPrefix
3220
3221    def findShadowedFiles(self, files, change):
3222        # Perforce allows you commit files and directories with the same name,
3223        # so you could have files //depot/foo and //depot/foo/bar both checked
3224        # in.  A p4 sync of a repository in this state fails.  Deleting one of
3225        # the files recovers the repository.
3226        #
3227        # Git will not allow the broken state to exist and only the most recent
3228        # of the conflicting names is left in the repository.  When one of the
3229        # conflicting files is deleted we need to re-add the other one to make
3230        # sure the git repository recovers in the same way as perforce.
3231        deleted = [f for f in files if f['action'] in self.delete_actions]
3232        to_check = set()
3233        for f in deleted:
3234            path = decode_path(f['path'])
3235            to_check.add(path + '/...')
3236            while True:
3237                path = path.rsplit("/", 1)[0]
3238                if path == "/" or path in to_check:
3239                    break
3240                to_check.add(path)
3241        to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
3242            if self.hasBranchPrefix(p)]
3243        if to_check:
3244            stat_result = p4CmdList(["-x", "-", "fstat", "-T",
3245                "depotFile,headAction,headRev,headType"], stdin=to_check)
3246            for record in stat_result:
3247                if record['code'] != 'stat':
3248                    continue
3249                if record['headAction'] in self.delete_actions:
3250                    continue
3251                files.append({
3252                    'action': 'add',
3253                    'path': record['depotFile'],
3254                    'rev': record['headRev'],
3255                    'type': record['headType']})
3256
3257    def commit(self, details, files, branch, parent = "", allow_empty=False):
3258        epoch = details["time"]
3259        author = details["user"]
3260        jobs = self.extractJobsFromCommit(details)
3261
3262        if self.verbose:
3263            print('commit into {0}'.format(branch))
3264
3265        files = [f for f in files
3266            if self.hasBranchPrefix(decode_path(f['path']))]
3267        self.findShadowedFiles(files, details['change'])
3268
3269        if self.clientSpecDirs:
3270            self.clientSpecDirs.update_client_spec_path_cache(files)
3271
3272        files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
3273
3274        if gitConfigBool('git-p4.keepEmptyCommits'):
3275            allow_empty = True
3276
3277        if not files and not allow_empty:
3278            print('Ignoring revision {0} as it would produce an empty commit.'
3279                .format(details['change']))
3280            return
3281
3282        self.gitStream.write("commit %s\n" % branch)
3283        self.gitStream.write("mark :%s\n" % details["change"])
3284        self.committedChanges.add(int(details["change"]))
3285        committer = ""
3286        if author not in self.users:
3287            self.getUserMapFromPerforceServer()
3288        committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
3289
3290        self.gitStream.write("committer %s\n" % committer)
3291
3292        self.gitStream.write("data <<EOT\n")
3293        self.gitStream.write(details["desc"])
3294        if len(jobs) > 0:
3295            self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
3296
3297        if not self.suppress_meta_comment:
3298            self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3299                                (','.join(self.branchPrefixes), details["change"]))
3300            if len(details['options']) > 0:
3301                self.gitStream.write(": options = %s" % details['options'])
3302            self.gitStream.write("]\n")
3303
3304        self.gitStream.write("EOT\n\n")
3305
3306        if len(parent) > 0:
3307            if self.verbose:
3308                print("parent %s" % parent)
3309            self.gitStream.write("from %s\n" % parent)
3310
3311        self.streamP4Files(files)
3312        self.gitStream.write("\n")
3313
3314        change = int(details["change"])
3315
3316        if change in self.labels:
3317            label = self.labels[change]
3318            labelDetails = label[0]
3319            labelRevisions = label[1]
3320            if self.verbose:
3321                print("Change %s is labelled %s" % (change, labelDetails))
3322
3323            files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
3324                                                for p in self.branchPrefixes])
3325
3326            if len(files) == len(labelRevisions):
3327
3328                cleanedFiles = {}
3329                for info in files:
3330                    if info["action"] in self.delete_actions:
3331                        continue
3332                    cleanedFiles[info["depotFile"]] = info["rev"]
3333
3334                if cleanedFiles == labelRevisions:
3335                    self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
3336
3337                else:
3338                    if not self.silent:
3339                        print("Tag %s does not match with change %s: files do not match."
3340                               % (labelDetails["label"], change))
3341
3342            else:
3343                if not self.silent:
3344                    print("Tag %s does not match with change %s: file count is different."
3345                           % (labelDetails["label"], change))
3346
3347    # Build a dictionary of changelists and labels, for "detect-labels" option.
3348    def getLabels(self):
3349        self.labels = {}
3350
3351        l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
3352        if len(l) > 0 and not self.silent:
3353            print("Finding files belonging to labels in %s" % self.depotPaths)
3354
3355        for output in l:
3356            label = output["label"]
3357            revisions = {}
3358            newestChange = 0
3359            if self.verbose:
3360                print("Querying files for label %s" % label)
3361            for file in p4CmdList(["files"] +
3362                                      ["%s...@%s" % (p, label)
3363                                          for p in self.depotPaths]):
3364                revisions[file["depotFile"]] = file["rev"]
3365                change = int(file["change"])
3366                if change > newestChange:
3367                    newestChange = change
3368
3369            self.labels[newestChange] = [output, revisions]
3370
3371        if self.verbose:
3372            print("Label changes: %s" % self.labels.keys())
3373
3374    # Import p4 labels as git tags. A direct mapping does not
3375    # exist, so assume that if all the files are at the same revision
3376    # then we can use that, or it's something more complicated we should
3377    # just ignore.
3378    def importP4Labels(self, stream, p4Labels):
3379        if verbose:
3380            print("import p4 labels: " + ' '.join(p4Labels))
3381
3382        ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
3383        validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
3384        if len(validLabelRegexp) == 0:
3385            validLabelRegexp = defaultLabelRegexp
3386        m = re.compile(validLabelRegexp)
3387
3388        for name in p4Labels:
3389            commitFound = False
3390
3391            if not m.match(name):
3392                if verbose:
3393                    print("label %s does not match regexp %s" % (name,validLabelRegexp))
3394                continue
3395
3396            if name in ignoredP4Labels:
3397                continue
3398
3399            labelDetails = p4CmdList(['label', "-o", name])[0]
3400
3401            # get the most recent changelist for each file in this label
3402            change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3403                                for p in self.depotPaths])
3404
3405            if 'change' in change:
3406                # find the corresponding git commit; take the oldest commit
3407                changelist = int(change['change'])
3408                if changelist in self.committedChanges:
3409                    gitCommit = ":%d" % changelist       # use a fast-import mark
3410                    commitFound = True
3411                else:
3412                    gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3413                        "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3414                    if len(gitCommit) == 0:
3415                        print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
3416                    else:
3417                        commitFound = True
3418                        gitCommit = gitCommit.strip()
3419
3420                if commitFound:
3421                    # Convert from p4 time format
3422                    try:
3423                        tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3424                    except ValueError:
3425                        print("Could not convert label time %s" % labelDetails['Update'])
3426                        tmwhen = 1
3427
3428                    when = int(time.mktime(tmwhen))
3429                    self.streamTag(stream, name, labelDetails, gitCommit, when)
3430                    if verbose:
3431                        print("p4 label %s mapped to git commit %s" % (name, gitCommit))
3432            else:
3433                if verbose:
3434                    print("Label %s has no changelists - possibly deleted?" % name)
3435
3436            if not commitFound:
3437                # We can't import this label; don't try again as it will get very
3438                # expensive repeatedly fetching all the files for labels that will
3439                # never be imported. If the label is moved in the future, the
3440                # ignore will need to be removed manually.
3441                system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3442
3443    def guessProjectName(self):
3444        for p in self.depotPaths:
3445            if p.endswith("/"):
3446                p = p[:-1]
3447            p = p[p.strip().rfind("/") + 1:]
3448            if not p.endswith("/"):
3449               p += "/"
3450            return p
3451
3452    def getBranchMapping(self):
3453        lostAndFoundBranches = set()
3454
3455        user = gitConfig("git-p4.branchUser")
3456        if len(user) > 0:
3457            command = "branches -u %s" % user
3458        else:
3459            command = "branches"
3460
3461        for info in p4CmdList(command):
3462            details = p4Cmd(["branch", "-o", info["branch"]])
3463            viewIdx = 0
3464            while "View%s" % viewIdx in details:
3465                paths = details["View%s" % viewIdx].split(" ")
3466                viewIdx = viewIdx + 1
3467                # require standard //depot/foo/... //depot/bar/... mapping
3468                if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3469                    continue
3470                source = paths[0]
3471                destination = paths[1]
3472                ## HACK
3473                if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
3474                    source = source[len(self.depotPaths[0]):-4]
3475                    destination = destination[len(self.depotPaths[0]):-4]
3476
3477                    if destination in self.knownBranches:
3478                        if not self.silent:
3479                            print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3480                            print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
3481                        continue
3482
3483                    self.knownBranches[destination] = source
3484
3485                    lostAndFoundBranches.discard(destination)
3486
3487                    if source not in self.knownBranches:
3488                        lostAndFoundBranches.add(source)
3489
3490        # Perforce does not strictly require branches to be defined, so we also
3491        # check git config for a branch list.
3492        #
3493        # Example of branch definition in git config file:
3494        # [git-p4]
3495        #   branchList=main:branchA
3496        #   branchList=main:branchB
3497        #   branchList=branchA:branchC
3498        configBranches = gitConfigList("git-p4.branchList")
3499        for branch in configBranches:
3500            if branch:
3501                (source, destination) = branch.split(":")
3502                self.knownBranches[destination] = source
3503
3504                lostAndFoundBranches.discard(destination)
3505
3506                if source not in self.knownBranches:
3507                    lostAndFoundBranches.add(source)
3508
3509
3510        for branch in lostAndFoundBranches:
3511            self.knownBranches[branch] = branch
3512
3513    def getBranchMappingFromGitBranches(self):
3514        branches = p4BranchesInGit(self.importIntoRemotes)
3515        for branch in branches.keys():
3516            if branch == "master":
3517                branch = "main"
3518            else:
3519                branch = branch[len(self.projectName):]
3520            self.knownBranches[branch] = branch
3521
3522    def updateOptionDict(self, d):
3523        option_keys = {}
3524        if self.keepRepoPath:
3525            option_keys['keepRepoPath'] = 1
3526
3527        d["options"] = ' '.join(sorted(option_keys.keys()))
3528
3529    def readOptions(self, d):
3530        self.keepRepoPath = ('options' in d
3531                             and ('keepRepoPath' in d['options']))
3532
3533    def gitRefForBranch(self, branch):
3534        if branch == "main":
3535            return self.refPrefix + "master"
3536
3537        if len(branch) <= 0:
3538            return branch
3539
3540        return self.refPrefix + self.projectName + branch
3541
3542    def gitCommitByP4Change(self, ref, change):
3543        if self.verbose:
3544            print("looking in ref " + ref + " for change %s using bisect..." % change)
3545
3546        earliestCommit = ""
3547        latestCommit = parseRevision(ref)
3548
3549        while True:
3550            if self.verbose:
3551                print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
3552            next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3553            if len(next) == 0:
3554                if self.verbose:
3555                    print("argh")
3556                return ""
3557            log = extractLogMessageFromGitCommit(next)
3558            settings = extractSettingsGitLog(log)
3559            currentChange = int(settings['change'])
3560            if self.verbose:
3561                print("current change %s" % currentChange)
3562
3563            if currentChange == change:
3564                if self.verbose:
3565                    print("found %s" % next)
3566                return next
3567
3568            if currentChange < change:
3569                earliestCommit = "^%s" % next
3570            else:
3571                if next == latestCommit:
3572                    die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3573                latestCommit = "%s^@" % next
3574
3575        return ""
3576
3577    def importNewBranch(self, branch, maxChange):
3578        # make fast-import flush all changes to disk and update the refs using the checkpoint
3579        # command so that we can try to find the branch parent in the git history
3580        self.gitStream.write("checkpoint\n\n");
3581        self.gitStream.flush();
3582        branchPrefix = self.depotPaths[0] + branch + "/"
3583        range = "@1,%s" % maxChange
3584        #print "prefix" + branchPrefix
3585        changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
3586        if len(changes) <= 0:
3587            return False
3588        firstChange = changes[0]
3589        #print "first change in branch: %s" % firstChange
3590        sourceBranch = self.knownBranches[branch]
3591        sourceDepotPath = self.depotPaths[0] + sourceBranch
3592        sourceRef = self.gitRefForBranch(sourceBranch)
3593        #print "source " + sourceBranch
3594
3595        branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
3596        #print "branch parent: %s" % branchParentChange
3597        gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3598        if len(gitParent) > 0:
3599            self.initialParents[self.gitRefForBranch(branch)] = gitParent
3600            #print "parent git commit: %s" % gitParent
3601
3602        self.importChanges(changes)
3603        return True
3604
3605    def searchParent(self, parent, branch, target):
3606        targetTree = read_pipe(["git", "rev-parse",
3607                                "{}^{{tree}}".format(target)]).strip()
3608        for line in read_pipe_lines(["git", "rev-list", "--format=%H %T",
3609                                     "--no-merges", parent]):
3610            if line.startswith("commit "):
3611                continue
3612            commit, tree = line.strip().split(" ")
3613            if tree == targetTree:
3614                if self.verbose:
3615                    print("Found parent of %s in commit %s" % (branch, commit))
3616                return commit
3617        return None
3618
3619    def importChanges(self, changes, origin_revision=0):
3620        cnt = 1
3621        for change in changes:
3622            description = p4_describe(change)
3623            self.updateOptionDict(description)
3624
3625            if not self.silent:
3626                sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3627                sys.stdout.flush()
3628            cnt = cnt + 1
3629
3630            try:
3631                if self.detectBranches:
3632                    branches = self.splitFilesIntoBranches(description)
3633                    for branch in branches.keys():
3634                        ## HACK  --hwn
3635                        branchPrefix = self.depotPaths[0] + branch + "/"
3636                        self.branchPrefixes = [ branchPrefix ]
3637
3638                        parent = ""
3639
3640                        filesForCommit = branches[branch]
3641
3642                        if self.verbose:
3643                            print("branch is %s" % branch)
3644
3645                        self.updatedBranches.add(branch)
3646
3647                        if branch not in self.createdBranches:
3648                            self.createdBranches.add(branch)
3649                            parent = self.knownBranches[branch]
3650                            if parent == branch:
3651                                parent = ""
3652                            else:
3653                                fullBranch = self.projectName + branch
3654                                if fullBranch not in self.p4BranchesInGit:
3655                                    if not self.silent:
3656                                        print("\n    Importing new branch %s" % fullBranch);
3657                                    if self.importNewBranch(branch, change - 1):
3658                                        parent = ""
3659                                        self.p4BranchesInGit.append(fullBranch)
3660                                    if not self.silent:
3661                                        print("\n    Resuming with change %s" % change);
3662
3663                                if self.verbose:
3664                                    print("parent determined through known branches: %s" % parent)
3665
3666                        branch = self.gitRefForBranch(branch)
3667                        parent = self.gitRefForBranch(parent)
3668
3669                        if self.verbose:
3670                            print("looking for initial parent for %s; current parent is %s" % (branch, parent))
3671
3672                        if len(parent) == 0 and branch in self.initialParents:
3673                            parent = self.initialParents[branch]
3674                            del self.initialParents[branch]
3675
3676                        blob = None
3677                        if len(parent) > 0:
3678                            tempBranch = "%s/%d" % (self.tempBranchLocation, change)
3679                            if self.verbose:
3680                                print("Creating temporary branch: " + tempBranch)
3681                            self.commit(description, filesForCommit, tempBranch)
3682                            self.tempBranches.append(tempBranch)
3683                            self.checkpoint()
3684                            blob = self.searchParent(parent, branch, tempBranch)
3685                        if blob:
3686                            self.commit(description, filesForCommit, branch, blob)
3687                        else:
3688                            if self.verbose:
3689                                print("Parent of %s not found. Committing into head of %s" % (branch, parent))
3690                            self.commit(description, filesForCommit, branch, parent)
3691                else:
3692                    files = self.extractFilesFromCommit(description)
3693                    self.commit(description, files, self.branch,
3694                                self.initialParent)
3695                    # only needed once, to connect to the previous commit
3696                    self.initialParent = ""
3697            except IOError:
3698                print(self.gitError.read())
3699                sys.exit(1)
3700
3701    def sync_origin_only(self):
3702        if self.syncWithOrigin:
3703            self.hasOrigin = originP4BranchesExist()
3704            if self.hasOrigin:
3705                if not self.silent:
3706                    print('Syncing with origin first, using "git fetch origin"')
3707                system("git fetch origin")
3708
3709    def importHeadRevision(self, revision):
3710        print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
3711
3712        details = {}
3713        details["user"] = "git perforce import user"
3714        details["desc"] = ("Initial import of %s from the state at revision %s\n"
3715                           % (' '.join(self.depotPaths), revision))
3716        details["change"] = revision
3717        newestRevision = 0
3718
3719        fileCnt = 0
3720        fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3721
3722        for info in p4CmdList(["files"] + fileArgs):
3723
3724            if 'code' in info and info['code'] == 'error':
3725                sys.stderr.write("p4 returned an error: %s\n"
3726                                 % info['data'])
3727                if info['data'].find("must refer to client") >= 0:
3728                    sys.stderr.write("This particular p4 error is misleading.\n")
3729                    sys.stderr.write("Perhaps the depot path was misspelled.\n");
3730                    sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
3731                sys.exit(1)
3732            if 'p4ExitCode' in info:
3733                sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
3734                sys.exit(1)
3735
3736
3737            change = int(info["change"])
3738            if change > newestRevision:
3739                newestRevision = change
3740
3741            if info["action"] in self.delete_actions:
3742                # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3743                #fileCnt = fileCnt + 1
3744                continue
3745
3746            for prop in ["depotFile", "rev", "action", "type" ]:
3747                details["%s%s" % (prop, fileCnt)] = info[prop]
3748
3749            fileCnt = fileCnt + 1
3750
3751        details["change"] = newestRevision
3752
3753        # Use time from top-most change so that all git p4 clones of
3754        # the same p4 repo have the same commit SHA1s.
3755        res = p4_describe(newestRevision)
3756        details["time"] = res["time"]
3757
3758        self.updateOptionDict(details)
3759        try:
3760            self.commit(details, self.extractFilesFromCommit(details), self.branch)
3761        except IOError as err:
3762            print("IO error with git fast-import. Is your git version recent enough?")
3763            print("IO error details: {}".format(err))
3764            print(self.gitError.read())
3765
3766
3767    def importRevisions(self, args, branch_arg_given):
3768        changes = []
3769
3770        if len(self.changesFile) > 0:
3771            with open(self.changesFile) as f:
3772                output = f.readlines()
3773            changeSet = set()
3774            for line in output:
3775                changeSet.add(int(line))
3776
3777            for change in changeSet:
3778                changes.append(change)
3779
3780            changes.sort()
3781        else:
3782            # catch "git p4 sync" with no new branches, in a repo that
3783            # does not have any existing p4 branches
3784            if len(args) == 0:
3785                if not self.p4BranchesInGit:
3786                    raise P4CommandException("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.")
3787
3788                # The default branch is master, unless --branch is used to
3789                # specify something else.  Make sure it exists, or complain
3790                # nicely about how to use --branch.
3791                if not self.detectBranches:
3792                    if not branch_exists(self.branch):
3793                        if branch_arg_given:
3794                            raise P4CommandException("Error: branch %s does not exist." % self.branch)
3795                        else:
3796                            raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
3797                                self.branch)
3798
3799            if self.verbose:
3800                print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3801                                                          self.changeRange))
3802            changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3803
3804            if len(self.maxChanges) > 0:
3805                changes = changes[:min(int(self.maxChanges), len(changes))]
3806
3807        if len(changes) == 0:
3808            if not self.silent:
3809                print("No changes to import!")
3810        else:
3811            if not self.silent and not self.detectBranches:
3812                print("Import destination: %s" % self.branch)
3813
3814            self.updatedBranches = set()
3815
3816            if not self.detectBranches:
3817                if args:
3818                    # start a new branch
3819                    self.initialParent = ""
3820                else:
3821                    # build on a previous revision
3822                    self.initialParent = parseRevision(self.branch)
3823
3824            self.importChanges(changes)
3825
3826            if not self.silent:
3827                print("")
3828                if len(self.updatedBranches) > 0:
3829                    sys.stdout.write("Updated branches: ")
3830                    for b in self.updatedBranches:
3831                        sys.stdout.write("%s " % b)
3832                    sys.stdout.write("\n")
3833
3834    def openStreams(self):
3835        self.importProcess = subprocess.Popen(["git", "fast-import"],
3836                                              stdin=subprocess.PIPE,
3837                                              stdout=subprocess.PIPE,
3838                                              stderr=subprocess.PIPE);
3839        self.gitOutput = self.importProcess.stdout
3840        self.gitStream = self.importProcess.stdin
3841        self.gitError = self.importProcess.stderr
3842
3843        if bytes is not str:
3844            # Wrap gitStream.write() so that it can be called using `str` arguments
3845            def make_encoded_write(write):
3846                def encoded_write(s):
3847                    return write(s.encode() if isinstance(s, str) else s)
3848                return encoded_write
3849
3850            self.gitStream.write = make_encoded_write(self.gitStream.write)
3851
3852    def closeStreams(self):
3853        if self.gitStream is None:
3854            return
3855        self.gitStream.close()
3856        if self.importProcess.wait() != 0:
3857            die("fast-import failed: %s" % self.gitError.read())
3858        self.gitOutput.close()
3859        self.gitError.close()
3860        self.gitStream = None
3861
3862    def run(self, args):
3863        if self.importIntoRemotes:
3864            self.refPrefix = "refs/remotes/p4/"
3865        else:
3866            self.refPrefix = "refs/heads/p4/"
3867
3868        self.sync_origin_only()
3869
3870        branch_arg_given = bool(self.branch)
3871        if len(self.branch) == 0:
3872            self.branch = self.refPrefix + "master"
3873            if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
3874                system("git update-ref %s refs/heads/p4" % self.branch)
3875                system("git branch -D p4")
3876
3877        # accept either the command-line option, or the configuration variable
3878        if self.useClientSpec:
3879            # will use this after clone to set the variable
3880            self.useClientSpec_from_options = True
3881        else:
3882            if gitConfigBool("git-p4.useclientspec"):
3883                self.useClientSpec = True
3884        if self.useClientSpec:
3885            self.clientSpecDirs = getClientSpec()
3886
3887        # TODO: should always look at previous commits,
3888        # merge with previous imports, if possible.
3889        if args == []:
3890            if self.hasOrigin:
3891                createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
3892
3893            # branches holds mapping from branch name to sha1
3894            branches = p4BranchesInGit(self.importIntoRemotes)
3895
3896            # restrict to just this one, disabling detect-branches
3897            if branch_arg_given:
3898                short = self.branch.split("/")[-1]
3899                if short in branches:
3900                    self.p4BranchesInGit = [ short ]
3901            else:
3902                self.p4BranchesInGit = branches.keys()
3903
3904            if len(self.p4BranchesInGit) > 1:
3905                if not self.silent:
3906                    print("Importing from/into multiple branches")
3907                self.detectBranches = True
3908                for branch in branches.keys():
3909                    self.initialParents[self.refPrefix + branch] = \
3910                        branches[branch]
3911
3912            if self.verbose:
3913                print("branches: %s" % self.p4BranchesInGit)
3914
3915            p4Change = 0
3916            for branch in self.p4BranchesInGit:
3917                logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
3918
3919                settings = extractSettingsGitLog(logMsg)
3920
3921                self.readOptions(settings)
3922                if ('depot-paths' in settings
3923                    and 'change' in settings):
3924                    change = int(settings['change']) + 1
3925                    p4Change = max(p4Change, change)
3926
3927                    depotPaths = sorted(settings['depot-paths'])
3928                    if self.previousDepotPaths == []:
3929                        self.previousDepotPaths = depotPaths
3930                    else:
3931                        paths = []
3932                        for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
3933                            prev_list = prev.split("/")
3934                            cur_list = cur.split("/")
3935                            for i in range(0, min(len(cur_list), len(prev_list))):
3936                                if cur_list[i] != prev_list[i]:
3937                                    i = i - 1
3938                                    break
3939
3940                            paths.append ("/".join(cur_list[:i + 1]))
3941
3942                        self.previousDepotPaths = paths
3943
3944            if p4Change > 0:
3945                self.depotPaths = sorted(self.previousDepotPaths)
3946                self.changeRange = "@%s,#head" % p4Change
3947                if not self.silent and not self.detectBranches:
3948                    print("Performing incremental import into %s git branch" % self.branch)
3949
3950        # accept multiple ref name abbreviations:
3951        #    refs/foo/bar/branch -> use it exactly
3952        #    p4/branch -> prepend refs/remotes/ or refs/heads/
3953        #    branch -> prepend refs/remotes/p4/ or refs/heads/p4/
3954        if not self.branch.startswith("refs/"):
3955            if self.importIntoRemotes:
3956                prepend = "refs/remotes/"
3957            else:
3958                prepend = "refs/heads/"
3959            if not self.branch.startswith("p4/"):
3960                prepend += "p4/"
3961            self.branch = prepend + self.branch
3962
3963        if len(args) == 0 and self.depotPaths:
3964            if not self.silent:
3965                print("Depot paths: %s" % ' '.join(self.depotPaths))
3966        else:
3967            if self.depotPaths and self.depotPaths != args:
3968                print("previous import used depot path %s and now %s was specified. "
3969                       "This doesn't work!" % (' '.join (self.depotPaths),
3970                                               ' '.join (args)))
3971                sys.exit(1)
3972
3973            self.depotPaths = sorted(args)
3974
3975        revision = ""
3976        self.users = {}
3977
3978        # Make sure no revision specifiers are used when --changesfile
3979        # is specified.
3980        bad_changesfile = False
3981        if len(self.changesFile) > 0:
3982            for p in self.depotPaths:
3983                if p.find("@") >= 0 or p.find("#") >= 0:
3984                    bad_changesfile = True
3985                    break
3986        if bad_changesfile:
3987            die("Option --changesfile is incompatible with revision specifiers")
3988
3989        newPaths = []
3990        for p in self.depotPaths:
3991            if p.find("@") != -1:
3992                atIdx = p.index("@")
3993                self.changeRange = p[atIdx:]
3994                if self.changeRange == "@all":
3995                    self.changeRange = ""
3996                elif ',' not in self.changeRange:
3997                    revision = self.changeRange
3998                    self.changeRange = ""
3999                p = p[:atIdx]
4000            elif p.find("#") != -1:
4001                hashIdx = p.index("#")
4002                revision = p[hashIdx:]
4003                p = p[:hashIdx]
4004            elif self.previousDepotPaths == []:
4005                # pay attention to changesfile, if given, else import
4006                # the entire p4 tree at the head revision
4007                if len(self.changesFile) == 0:
4008                    revision = "#head"
4009
4010            p = re.sub ("\.\.\.$", "", p)
4011            if not p.endswith("/"):
4012                p += "/"
4013
4014            newPaths.append(p)
4015
4016        self.depotPaths = newPaths
4017
4018        # --detect-branches may change this for each branch
4019        self.branchPrefixes = self.depotPaths
4020
4021        self.loadUserMapFromCache()
4022        self.labels = {}
4023        if self.detectLabels:
4024            self.getLabels();
4025
4026        if self.detectBranches:
4027            ## FIXME - what's a P4 projectName ?
4028            self.projectName = self.guessProjectName()
4029
4030            if self.hasOrigin:
4031                self.getBranchMappingFromGitBranches()
4032            else:
4033                self.getBranchMapping()
4034            if self.verbose:
4035                print("p4-git branches: %s" % self.p4BranchesInGit)
4036                print("initial parents: %s" % self.initialParents)
4037            for b in self.p4BranchesInGit:
4038                if b != "master":
4039
4040                    ## FIXME
4041                    b = b[len(self.projectName):]
4042                self.createdBranches.add(b)
4043
4044        p4_check_access()
4045
4046        self.openStreams()
4047
4048        err = None
4049
4050        try:
4051            if revision:
4052                self.importHeadRevision(revision)
4053            else:
4054                self.importRevisions(args, branch_arg_given)
4055
4056            if gitConfigBool("git-p4.importLabels"):
4057                self.importLabels = True
4058
4059            if self.importLabels:
4060                p4Labels = getP4Labels(self.depotPaths)
4061                gitTags = getGitTags()
4062
4063                missingP4Labels = p4Labels - gitTags
4064                self.importP4Labels(self.gitStream, missingP4Labels)
4065
4066        except P4CommandException as e:
4067            err = e
4068
4069        finally:
4070            self.closeStreams()
4071
4072        if err:
4073            die(str(err))
4074
4075        # Cleanup temporary branches created during import
4076        if self.tempBranches != []:
4077            for branch in self.tempBranches:
4078                read_pipe("git update-ref -d %s" % branch)
4079            os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
4080
4081        # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4082        # a convenient shortcut refname "p4".
4083        if self.importIntoRemotes:
4084            head_ref = self.refPrefix + "HEAD"
4085            if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
4086                system(["git", "symbolic-ref", head_ref, self.branch])
4087
4088        return True
4089
4090class P4Rebase(Command):
4091    def __init__(self):
4092        Command.__init__(self)
4093        self.options = [
4094                optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
4095        ]
4096        self.importLabels = False
4097        self.description = ("Fetches the latest revision from perforce and "
4098                            + "rebases the current work (branch) against it")
4099
4100    def run(self, args):
4101        sync = P4Sync()
4102        sync.importLabels = self.importLabels
4103        sync.run([])
4104
4105        return self.rebase()
4106
4107    def rebase(self):
4108        if os.system("git update-index --refresh") != 0:
4109            die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up to date or stash away all your changes with git stash.");
4110        if len(read_pipe("git diff-index HEAD --")) > 0:
4111            die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
4112
4113        [upstream, settings] = findUpstreamBranchPoint()
4114        if len(upstream) == 0:
4115            die("Cannot find upstream branchpoint for rebase")
4116
4117        # the branchpoint may be p4/foo~3, so strip off the parent
4118        upstream = re.sub("~[0-9]+$", "", upstream)
4119
4120        print("Rebasing the current branch onto %s" % upstream)
4121        oldHead = read_pipe("git rev-parse HEAD").strip()
4122        system("git rebase %s" % upstream)
4123        system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
4124        return True
4125
4126class P4Clone(P4Sync):
4127    def __init__(self):
4128        P4Sync.__init__(self)
4129        self.description = "Creates a new git repository and imports from Perforce into it"
4130        self.usage = "usage: %prog [options] //depot/path[@revRange]"
4131        self.options += [
4132            optparse.make_option("--destination", dest="cloneDestination",
4133                                 action='store', default=None,
4134                                 help="where to leave result of the clone"),
4135            optparse.make_option("--bare", dest="cloneBare",
4136                                 action="store_true", default=False),
4137        ]
4138        self.cloneDestination = None
4139        self.needsGit = False
4140        self.cloneBare = False
4141
4142    def defaultDestination(self, args):
4143        ## TODO: use common prefix of args?
4144        depotPath = args[0]
4145        depotDir = re.sub("(@[^@]*)$", "", depotPath)
4146        depotDir = re.sub("(#[^#]*)$", "", depotDir)
4147        depotDir = re.sub(r"\.\.\.$", "", depotDir)
4148        depotDir = re.sub(r"/$", "", depotDir)
4149        return os.path.split(depotDir)[1]
4150
4151    def run(self, args):
4152        if len(args) < 1:
4153            return False
4154
4155        if self.keepRepoPath and not self.cloneDestination:
4156            sys.stderr.write("Must specify destination for --keep-path\n")
4157            sys.exit(1)
4158
4159        depotPaths = args
4160
4161        if not self.cloneDestination and len(depotPaths) > 1:
4162            self.cloneDestination = depotPaths[-1]
4163            depotPaths = depotPaths[:-1]
4164
4165        for p in depotPaths:
4166            if not p.startswith("//"):
4167                sys.stderr.write('Depot paths must start with "//": %s\n' % p)
4168                return False
4169
4170        if not self.cloneDestination:
4171            self.cloneDestination = self.defaultDestination(args)
4172
4173        print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
4174
4175        if not os.path.exists(self.cloneDestination):
4176            os.makedirs(self.cloneDestination)
4177        chdir(self.cloneDestination)
4178
4179        init_cmd = [ "git", "init" ]
4180        if self.cloneBare:
4181            init_cmd.append("--bare")
4182        retcode = subprocess.call(init_cmd)
4183        if retcode:
4184            raise CalledProcessError(retcode, init_cmd)
4185
4186        if not P4Sync.run(self, depotPaths):
4187            return False
4188
4189        # create a master branch and check out a work tree
4190        if gitBranchExists(self.branch):
4191            system([ "git", "branch", currentGitBranch(), self.branch ])
4192            if not self.cloneBare:
4193                system([ "git", "checkout", "-f" ])
4194        else:
4195            print('Not checking out any branch, use ' \
4196                  '"git checkout -q -b master <branch>"')
4197
4198        # auto-set this variable if invoked with --use-client-spec
4199        if self.useClientSpec_from_options:
4200            system("git config --bool git-p4.useclientspec true")
4201
4202        return True
4203
4204class P4Unshelve(Command):
4205    def __init__(self):
4206        Command.__init__(self)
4207        self.options = []
4208        self.origin = "HEAD"
4209        self.description = "Unshelve a P4 changelist into a git commit"
4210        self.usage = "usage: %prog [options] changelist"
4211        self.options += [
4212                optparse.make_option("--origin", dest="origin",
4213                    help="Use this base revision instead of the default (%s)" % self.origin),
4214        ]
4215        self.verbose = False
4216        self.noCommit = False
4217        self.destbranch = "refs/remotes/p4-unshelved"
4218
4219    def renameBranch(self, branch_name):
4220        """ Rename the existing branch to branch_name.N
4221        """
4222
4223        found = True
4224        for i in range(0,1000):
4225            backup_branch_name = "{0}.{1}".format(branch_name, i)
4226            if not gitBranchExists(backup_branch_name):
4227                gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
4228                gitDeleteRef(branch_name)
4229                found = True
4230                print("renamed old unshelve branch to {0}".format(backup_branch_name))
4231                break
4232
4233        if not found:
4234            sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4235
4236    def findLastP4Revision(self, starting_point):
4237        """ Look back from starting_point for the first commit created by git-p4
4238            to find the P4 commit we are based on, and the depot-paths.
4239        """
4240
4241        for parent in (range(65535)):
4242            log = extractLogMessageFromGitCommit("{0}~{1}".format(starting_point, parent))
4243            settings = extractSettingsGitLog(log)
4244            if 'change' in settings:
4245                return settings
4246
4247        sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4248
4249    def createShelveParent(self, change, branch_name, sync, origin):
4250        """ Create a commit matching the parent of the shelved changelist 'change'
4251        """
4252        parent_description = p4_describe(change, shelved=True)
4253        parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4254        files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4255
4256        parent_files = []
4257        for f in files:
4258            # if it was added in the shelved changelist, it won't exist in the parent
4259            if f['action'] in self.add_actions:
4260                continue
4261
4262            # if it was deleted in the shelved changelist it must not be deleted
4263            # in the parent - we might even need to create it if the origin branch
4264            # does not have it
4265            if f['action'] in self.delete_actions:
4266                f['action'] = 'add'
4267
4268            parent_files.append(f)
4269
4270        sync.commit(parent_description, parent_files, branch_name,
4271                parent=origin, allow_empty=True)
4272        print("created parent commit for {0} based on {1} in {2}".format(
4273            change, self.origin, branch_name))
4274
4275    def run(self, args):
4276        if len(args) != 1:
4277            return False
4278
4279        if not gitBranchExists(self.origin):
4280            sys.exit("origin branch {0} does not exist".format(self.origin))
4281
4282        sync = P4Sync()
4283        changes = args
4284
4285        # only one change at a time
4286        change = changes[0]
4287
4288        # if the target branch already exists, rename it
4289        branch_name = "{0}/{1}".format(self.destbranch, change)
4290        if gitBranchExists(branch_name):
4291            self.renameBranch(branch_name)
4292        sync.branch = branch_name
4293
4294        sync.verbose = self.verbose
4295        sync.suppress_meta_comment = True
4296
4297        settings = self.findLastP4Revision(self.origin)
4298        sync.depotPaths = settings['depot-paths']
4299        sync.branchPrefixes = sync.depotPaths
4300
4301        sync.openStreams()
4302        sync.loadUserMapFromCache()
4303        sync.silent = True
4304
4305        # create a commit for the parent of the shelved changelist
4306        self.createShelveParent(change, branch_name, sync, self.origin)
4307
4308        # create the commit for the shelved changelist itself
4309        description = p4_describe(change, True)
4310        files = sync.extractFilesFromCommit(description, True, change)
4311
4312        sync.commit(description, files, branch_name, "")
4313        sync.closeStreams()
4314
4315        print("unshelved changelist {0} into {1}".format(change, branch_name))
4316
4317        return True
4318
4319class P4Branches(Command):
4320    def __init__(self):
4321        Command.__init__(self)
4322        self.options = [ ]
4323        self.description = ("Shows the git branches that hold imports and their "
4324                            + "corresponding perforce depot paths")
4325        self.verbose = False
4326
4327    def run(self, args):
4328        if originP4BranchesExist():
4329            createOrUpdateBranchesFromOrigin()
4330
4331        cmdline = "git rev-parse --symbolic "
4332        cmdline += " --remotes"
4333
4334        for line in read_pipe_lines(cmdline):
4335            line = line.strip()
4336
4337            if not line.startswith('p4/') or line == "p4/HEAD":
4338                continue
4339            branch = line
4340
4341            log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4342            settings = extractSettingsGitLog(log)
4343
4344            print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
4345        return True
4346
4347class HelpFormatter(optparse.IndentedHelpFormatter):
4348    def __init__(self):
4349        optparse.IndentedHelpFormatter.__init__(self)
4350
4351    def format_description(self, description):
4352        if description:
4353            return description + "\n"
4354        else:
4355            return ""
4356
4357def printUsage(commands):
4358    print("usage: %s <command> [options]" % sys.argv[0])
4359    print("")
4360    print("valid commands: %s" % ", ".join(commands))
4361    print("")
4362    print("Try %s <command> --help for command specific help." % sys.argv[0])
4363    print("")
4364
4365commands = {
4366    "debug" : P4Debug,
4367    "submit" : P4Submit,
4368    "commit" : P4Submit,
4369    "sync" : P4Sync,
4370    "rebase" : P4Rebase,
4371    "clone" : P4Clone,
4372    "rollback" : P4RollBack,
4373    "branches" : P4Branches,
4374    "unshelve" : P4Unshelve,
4375}
4376
4377def main():
4378    if len(sys.argv[1:]) == 0:
4379        printUsage(commands.keys())
4380        sys.exit(2)
4381
4382    cmdName = sys.argv[1]
4383    try:
4384        klass = commands[cmdName]
4385        cmd = klass()
4386    except KeyError:
4387        print("unknown command %s" % cmdName)
4388        print("")
4389        printUsage(commands.keys())
4390        sys.exit(2)
4391
4392    options = cmd.options
4393    cmd.gitdir = os.environ.get("GIT_DIR", None)
4394
4395    args = sys.argv[2:]
4396
4397    options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
4398    if cmd.needsGit:
4399        options.append(optparse.make_option("--git-dir", dest="gitdir"))
4400
4401    parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4402                                   options,
4403                                   description = cmd.description,
4404                                   formatter = HelpFormatter())
4405
4406    try:
4407        (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
4408    except:
4409        parser.print_help()
4410        raise
4411
4412    global verbose
4413    verbose = cmd.verbose
4414    if cmd.needsGit:
4415        if cmd.gitdir == None:
4416            cmd.gitdir = os.path.abspath(".git")
4417            if not isValidGitDir(cmd.gitdir):
4418                # "rev-parse --git-dir" without arguments will try $PWD/.git
4419                cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
4420                if os.path.exists(cmd.gitdir):
4421                    cdup = read_pipe("git rev-parse --show-cdup").strip()
4422                    if len(cdup) > 0:
4423                        chdir(cdup);
4424
4425        if not isValidGitDir(cmd.gitdir):
4426            if isValidGitDir(cmd.gitdir + "/.git"):
4427                cmd.gitdir += "/.git"
4428            else:
4429                die("fatal: cannot locate git repository at %s" % cmd.gitdir)
4430
4431        # so git commands invoked from the P4 workspace will succeed
4432        os.environ["GIT_DIR"] = cmd.gitdir
4433
4434    if not cmd.run(args):
4435        parser.print_help()
4436        sys.exit(2)
4437
4438
4439if __name__ == '__main__':
4440    main()
4441