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