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