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