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