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