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