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