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