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