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