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