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