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