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