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