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