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