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