git-p4: small improvements to user-preservation
[git] / contrib / fast-import / git-p4
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
14
15 verbose = False
16
17
18 def p4_build_cmd(cmd):
19     """Build a suitable p4 command line.
20
21     This consolidates building and returning a p4 command line into one
22     location. It means that hooking into the environment, or other configuration
23     can be done more easily.
24     """
25     real_cmd = "%s " % "p4"
26
27     user = gitConfig("git-p4.user")
28     if len(user) > 0:
29         real_cmd += "-u %s " % user
30
31     password = gitConfig("git-p4.password")
32     if len(password) > 0:
33         real_cmd += "-P %s " % password
34
35     port = gitConfig("git-p4.port")
36     if len(port) > 0:
37         real_cmd += "-p %s " % port
38
39     host = gitConfig("git-p4.host")
40     if len(host) > 0:
41         real_cmd += "-h %s " % host
42
43     client = gitConfig("git-p4.client")
44     if len(client) > 0:
45         real_cmd += "-c %s " % client
46
47     real_cmd += "%s" % (cmd)
48     if verbose:
49         print real_cmd
50     return real_cmd
51
52 def chdir(dir):
53     if os.name == 'nt':
54         os.environ['PWD']=dir
55     os.chdir(dir)
56
57 def die(msg):
58     if verbose:
59         raise Exception(msg)
60     else:
61         sys.stderr.write(msg + "\n")
62         sys.exit(1)
63
64 def write_pipe(c, str):
65     if verbose:
66         sys.stderr.write('Writing pipe: %s\n' % c)
67
68     pipe = os.popen(c, 'w')
69     val = pipe.write(str)
70     if pipe.close():
71         die('Command failed: %s' % c)
72
73     return val
74
75 def p4_write_pipe(c, str):
76     real_cmd = p4_build_cmd(c)
77     return write_pipe(real_cmd, str)
78
79 def read_pipe(c, ignore_error=False):
80     if verbose:
81         sys.stderr.write('Reading pipe: %s\n' % c)
82
83     pipe = os.popen(c, 'rb')
84     val = pipe.read()
85     if pipe.close() and not ignore_error:
86         die('Command failed: %s' % c)
87
88     return val
89
90 def p4_read_pipe(c, ignore_error=False):
91     real_cmd = p4_build_cmd(c)
92     return read_pipe(real_cmd, ignore_error)
93
94 def read_pipe_lines(c):
95     if verbose:
96         sys.stderr.write('Reading pipe: %s\n' % c)
97     ## todo: check return status
98     pipe = os.popen(c, 'rb')
99     val = pipe.readlines()
100     if pipe.close():
101         die('Command failed: %s' % c)
102
103     return val
104
105 def p4_read_pipe_lines(c):
106     """Specifically invoke p4 on the command supplied. """
107     real_cmd = p4_build_cmd(c)
108     return read_pipe_lines(real_cmd)
109
110 def system(cmd):
111     if verbose:
112         sys.stderr.write("executing %s\n" % cmd)
113     if os.system(cmd) != 0:
114         die("command failed: %s" % cmd)
115
116 def p4_system(cmd):
117     """Specifically invoke p4 as the system command. """
118     real_cmd = p4_build_cmd(cmd)
119     return system(real_cmd)
120
121 def isP4Exec(kind):
122     """Determine if a Perforce 'kind' should have execute permission
123
124     'p4 help filetypes' gives a list of the types.  If it starts with 'x',
125     or x follows one of a few letters.  Otherwise, if there is an 'x' after
126     a plus sign, it is also executable"""
127     return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
128
129 def setP4ExecBit(file, mode):
130     # Reopens an already open file and changes the execute bit to match
131     # the execute bit setting in the passed in mode.
132
133     p4Type = "+x"
134
135     if not isModeExec(mode):
136         p4Type = getP4OpenedType(file)
137         p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
138         p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
139         if p4Type[-1] == "+":
140             p4Type = p4Type[0:-1]
141
142     p4_system("reopen -t %s %s" % (p4Type, file))
143
144 def getP4OpenedType(file):
145     # Returns the perforce file type for the given file.
146
147     result = p4_read_pipe("opened %s" % file)
148     match = re.match(".*\((.+)\)\r?$", result)
149     if match:
150         return match.group(1)
151     else:
152         die("Could not determine file type for %s (result: '%s')" % (file, result))
153
154 def diffTreePattern():
155     # This is a simple generator for the diff tree regex pattern. This could be
156     # a class variable if this and parseDiffTreeEntry were a part of a class.
157     pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
158     while True:
159         yield pattern
160
161 def parseDiffTreeEntry(entry):
162     """Parses a single diff tree entry into its component elements.
163
164     See git-diff-tree(1) manpage for details about the format of the diff
165     output. This method returns a dictionary with the following elements:
166
167     src_mode - The mode of the source file
168     dst_mode - The mode of the destination file
169     src_sha1 - The sha1 for the source file
170     dst_sha1 - The sha1 fr the destination file
171     status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
172     status_score - The score for the status (applicable for 'C' and 'R'
173                    statuses). This is None if there is no score.
174     src - The path for the source file.
175     dst - The path for the destination file. This is only present for
176           copy or renames. If it is not present, this is None.
177
178     If the pattern is not matched, None is returned."""
179
180     match = diffTreePattern().next().match(entry)
181     if match:
182         return {
183             'src_mode': match.group(1),
184             'dst_mode': match.group(2),
185             'src_sha1': match.group(3),
186             'dst_sha1': match.group(4),
187             'status': match.group(5),
188             'status_score': match.group(6),
189             'src': match.group(7),
190             'dst': match.group(10)
191         }
192     return None
193
194 def isModeExec(mode):
195     # Returns True if the given git mode represents an executable file,
196     # otherwise False.
197     return mode[-3:] == "755"
198
199 def isModeExecChanged(src_mode, dst_mode):
200     return isModeExec(src_mode) != isModeExec(dst_mode)
201
202 def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
203     cmd = p4_build_cmd("-G %s" % (cmd))
204     if verbose:
205         sys.stderr.write("Opening pipe: %s\n" % cmd)
206
207     # Use a temporary file to avoid deadlocks without
208     # subprocess.communicate(), which would put another copy
209     # of stdout into memory.
210     stdin_file = None
211     if stdin is not None:
212         stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
213         stdin_file.write(stdin)
214         stdin_file.flush()
215         stdin_file.seek(0)
216
217     p4 = subprocess.Popen(cmd, shell=True,
218                           stdin=stdin_file,
219                           stdout=subprocess.PIPE)
220
221     result = []
222     try:
223         while True:
224             entry = marshal.load(p4.stdout)
225             if cb is not None:
226                 cb(entry)
227             else:
228                 result.append(entry)
229     except EOFError:
230         pass
231     exitCode = p4.wait()
232     if exitCode != 0:
233         entry = {}
234         entry["p4ExitCode"] = exitCode
235         result.append(entry)
236
237     return result
238
239 def p4Cmd(cmd):
240     list = p4CmdList(cmd)
241     result = {}
242     for entry in list:
243         result.update(entry)
244     return result;
245
246 def p4Where(depotPath):
247     if not depotPath.endswith("/"):
248         depotPath += "/"
249     depotPath = depotPath + "..."
250     outputList = p4CmdList("where %s" % depotPath)
251     output = None
252     for entry in outputList:
253         if "depotFile" in entry:
254             if entry["depotFile"] == depotPath:
255                 output = entry
256                 break
257         elif "data" in entry:
258             data = entry.get("data")
259             space = data.find(" ")
260             if data[:space] == depotPath:
261                 output = entry
262                 break
263     if output == None:
264         return ""
265     if output["code"] == "error":
266         return ""
267     clientPath = ""
268     if "path" in output:
269         clientPath = output.get("path")
270     elif "data" in output:
271         data = output.get("data")
272         lastSpace = data.rfind(" ")
273         clientPath = data[lastSpace + 1:]
274
275     if clientPath.endswith("..."):
276         clientPath = clientPath[:-3]
277     return clientPath
278
279 def currentGitBranch():
280     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
281
282 def isValidGitDir(path):
283     if (os.path.exists(path + "/HEAD")
284         and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
285         return True;
286     return False
287
288 def parseRevision(ref):
289     return read_pipe("git rev-parse %s" % ref).strip()
290
291 def extractLogMessageFromGitCommit(commit):
292     logMessage = ""
293
294     ## fixme: title is first line of commit, not 1st paragraph.
295     foundTitle = False
296     for log in read_pipe_lines("git cat-file commit %s" % commit):
297        if not foundTitle:
298            if len(log) == 1:
299                foundTitle = True
300            continue
301
302        logMessage += log
303     return logMessage
304
305 def extractSettingsGitLog(log):
306     values = {}
307     for line in log.split("\n"):
308         line = line.strip()
309         m = re.search (r"^ *\[git-p4: (.*)\]$", line)
310         if not m:
311             continue
312
313         assignments = m.group(1).split (':')
314         for a in assignments:
315             vals = a.split ('=')
316             key = vals[0].strip()
317             val = ('='.join (vals[1:])).strip()
318             if val.endswith ('\"') and val.startswith('"'):
319                 val = val[1:-1]
320
321             values[key] = val
322
323     paths = values.get("depot-paths")
324     if not paths:
325         paths = values.get("depot-path")
326     if paths:
327         values['depot-paths'] = paths.split(',')
328     return values
329
330 def gitBranchExists(branch):
331     proc = subprocess.Popen(["git", "rev-parse", branch],
332                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
333     return proc.wait() == 0;
334
335 _gitConfig = {}
336 def gitConfig(key, args = None): # set args to "--bool", for instance
337     if not _gitConfig.has_key(key):
338         argsFilter = ""
339         if args != None:
340             argsFilter = "%s " % args
341         cmd = "git config %s%s" % (argsFilter, key)
342         _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
343     return _gitConfig[key]
344
345 def p4BranchesInGit(branchesAreInRemotes = True):
346     branches = {}
347
348     cmdline = "git rev-parse --symbolic "
349     if branchesAreInRemotes:
350         cmdline += " --remotes"
351     else:
352         cmdline += " --branches"
353
354     for line in read_pipe_lines(cmdline):
355         line = line.strip()
356
357         ## only import to p4/
358         if not line.startswith('p4/') or line == "p4/HEAD":
359             continue
360         branch = line
361
362         # strip off p4
363         branch = re.sub ("^p4/", "", line)
364
365         branches[branch] = parseRevision(line)
366     return branches
367
368 def findUpstreamBranchPoint(head = "HEAD"):
369     branches = p4BranchesInGit()
370     # map from depot-path to branch name
371     branchByDepotPath = {}
372     for branch in branches.keys():
373         tip = branches[branch]
374         log = extractLogMessageFromGitCommit(tip)
375         settings = extractSettingsGitLog(log)
376         if settings.has_key("depot-paths"):
377             paths = ",".join(settings["depot-paths"])
378             branchByDepotPath[paths] = "remotes/p4/" + branch
379
380     settings = None
381     parent = 0
382     while parent < 65535:
383         commit = head + "~%s" % parent
384         log = extractLogMessageFromGitCommit(commit)
385         settings = extractSettingsGitLog(log)
386         if settings.has_key("depot-paths"):
387             paths = ",".join(settings["depot-paths"])
388             if branchByDepotPath.has_key(paths):
389                 return [branchByDepotPath[paths], settings]
390
391         parent = parent + 1
392
393     return ["", settings]
394
395 def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
396     if not silent:
397         print ("Creating/updating branch(es) in %s based on origin branch(es)"
398                % localRefPrefix)
399
400     originPrefix = "origin/p4/"
401
402     for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
403         line = line.strip()
404         if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
405             continue
406
407         headName = line[len(originPrefix):]
408         remoteHead = localRefPrefix + headName
409         originHead = line
410
411         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
412         if (not original.has_key('depot-paths')
413             or not original.has_key('change')):
414             continue
415
416         update = False
417         if not gitBranchExists(remoteHead):
418             if verbose:
419                 print "creating %s" % remoteHead
420             update = True
421         else:
422             settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
423             if settings.has_key('change') > 0:
424                 if settings['depot-paths'] == original['depot-paths']:
425                     originP4Change = int(original['change'])
426                     p4Change = int(settings['change'])
427                     if originP4Change > p4Change:
428                         print ("%s (%s) is newer than %s (%s). "
429                                "Updating p4 branch from origin."
430                                % (originHead, originP4Change,
431                                   remoteHead, p4Change))
432                         update = True
433                 else:
434                     print ("Ignoring: %s was imported from %s while "
435                            "%s was imported from %s"
436                            % (originHead, ','.join(original['depot-paths']),
437                               remoteHead, ','.join(settings['depot-paths'])))
438
439         if update:
440             system("git update-ref %s %s" % (remoteHead, originHead))
441
442 def originP4BranchesExist():
443         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
444
445 def p4ChangesForPaths(depotPaths, changeRange):
446     assert depotPaths
447     output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
448                                                         for p in depotPaths]))
449
450     changes = {}
451     for line in output:
452         changeNum = int(line.split(" ")[1])
453         changes[changeNum] = True
454
455     changelist = changes.keys()
456     changelist.sort()
457     return changelist
458
459 def p4PathStartsWith(path, prefix):
460     # This method tries to remedy a potential mixed-case issue:
461     #
462     # If UserA adds  //depot/DirA/file1
463     # and UserB adds //depot/dira/file2
464     #
465     # we may or may not have a problem. If you have core.ignorecase=true,
466     # we treat DirA and dira as the same directory
467     ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
468     if ignorecase:
469         return path.lower().startswith(prefix.lower())
470     return path.startswith(prefix)
471
472 class Command:
473     def __init__(self):
474         self.usage = "usage: %prog [options]"
475         self.needsGit = True
476
477 class P4UserMap:
478     def __init__(self):
479         self.userMapFromPerforceServer = False
480
481     def getUserCacheFilename(self):
482         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
483         return home + "/.gitp4-usercache.txt"
484
485     def getUserMapFromPerforceServer(self):
486         if self.userMapFromPerforceServer:
487             return
488         self.users = {}
489         self.emails = {}
490
491         for output in p4CmdList("users"):
492             if not output.has_key("User"):
493                 continue
494             self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
495             self.emails[output["Email"]] = output["User"]
496
497
498         s = ''
499         for (key, val) in self.users.items():
500             s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
501
502         open(self.getUserCacheFilename(), "wb").write(s)
503         self.userMapFromPerforceServer = True
504
505     def loadUserMapFromCache(self):
506         self.users = {}
507         self.userMapFromPerforceServer = False
508         try:
509             cache = open(self.getUserCacheFilename(), "rb")
510             lines = cache.readlines()
511             cache.close()
512             for line in lines:
513                 entry = line.strip().split("\t")
514                 self.users[entry[0]] = entry[1]
515         except IOError:
516             self.getUserMapFromPerforceServer()
517
518 class P4Debug(Command):
519     def __init__(self):
520         Command.__init__(self)
521         self.options = [
522             optparse.make_option("--verbose", dest="verbose", action="store_true",
523                                  default=False),
524             ]
525         self.description = "A tool to debug the output of p4 -G."
526         self.needsGit = False
527         self.verbose = False
528
529     def run(self, args):
530         j = 0
531         for output in p4CmdList(" ".join(args)):
532             print 'Element: %d' % j
533             j += 1
534             print output
535         return True
536
537 class P4RollBack(Command):
538     def __init__(self):
539         Command.__init__(self)
540         self.options = [
541             optparse.make_option("--verbose", dest="verbose", action="store_true"),
542             optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
543         ]
544         self.description = "A tool to debug the multi-branch import. Don't use :)"
545         self.verbose = False
546         self.rollbackLocalBranches = False
547
548     def run(self, args):
549         if len(args) != 1:
550             return False
551         maxChange = int(args[0])
552
553         if "p4ExitCode" in p4Cmd("changes -m 1"):
554             die("Problems executing p4");
555
556         if self.rollbackLocalBranches:
557             refPrefix = "refs/heads/"
558             lines = read_pipe_lines("git rev-parse --symbolic --branches")
559         else:
560             refPrefix = "refs/remotes/"
561             lines = read_pipe_lines("git rev-parse --symbolic --remotes")
562
563         for line in lines:
564             if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
565                 line = line.strip()
566                 ref = refPrefix + line
567                 log = extractLogMessageFromGitCommit(ref)
568                 settings = extractSettingsGitLog(log)
569
570                 depotPaths = settings['depot-paths']
571                 change = settings['change']
572
573                 changed = False
574
575                 if len(p4Cmd("changes -m 1 "  + ' '.join (['%s...@%s' % (p, maxChange)
576                                                            for p in depotPaths]))) == 0:
577                     print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
578                     system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
579                     continue
580
581                 while change and int(change) > maxChange:
582                     changed = True
583                     if self.verbose:
584                         print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
585                     system("git update-ref %s \"%s^\"" % (ref, ref))
586                     log = extractLogMessageFromGitCommit(ref)
587                     settings =  extractSettingsGitLog(log)
588
589
590                     depotPaths = settings['depot-paths']
591                     change = settings['change']
592
593                 if changed:
594                     print "%s rewound to %s" % (ref, change)
595
596         return True
597
598 class P4Submit(Command, P4UserMap):
599     def __init__(self):
600         Command.__init__(self)
601         P4UserMap.__init__(self)
602         self.options = [
603                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
604                 optparse.make_option("--origin", dest="origin"),
605                 optparse.make_option("-M", dest="detectRenames", action="store_true"),
606                 # preserve the user, requires relevant p4 permissions
607                 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
608         ]
609         self.description = "Submit changes from git to the perforce depot."
610         self.usage += " [name of git branch to submit into perforce depot]"
611         self.interactive = True
612         self.origin = ""
613         self.detectRenames = False
614         self.verbose = False
615         self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
616         self.isWindows = (platform.system() == "Windows")
617
618     def check(self):
619         if len(p4CmdList("opened ...")) > 0:
620             die("You have files opened with perforce! Close them before starting the sync.")
621
622     # replaces everything between 'Description:' and the next P4 submit template field with the
623     # commit message
624     def prepareLogMessage(self, template, message):
625         result = ""
626
627         inDescriptionSection = False
628
629         for line in template.split("\n"):
630             if line.startswith("#"):
631                 result += line + "\n"
632                 continue
633
634             if inDescriptionSection:
635                 if line.startswith("Files:") or line.startswith("Jobs:"):
636                     inDescriptionSection = False
637                 else:
638                     continue
639             else:
640                 if line.startswith("Description:"):
641                     inDescriptionSection = True
642                     line += "\n"
643                     for messageLine in message.split("\n"):
644                         line += "\t" + messageLine + "\n"
645
646             result += line + "\n"
647
648         return result
649
650     def p4UserForCommit(self,id):
651         # Return the tuple (perforce user,git email) for a given git commit id
652         self.getUserMapFromPerforceServer()
653         gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
654         gitEmail = gitEmail.strip()
655         if not self.emails.has_key(gitEmail):
656             return (None,gitEmail)
657         else:
658             return (self.emails[gitEmail],gitEmail)
659
660     def checkValidP4Users(self,commits):
661         # check if any git authors cannot be mapped to p4 users
662         for id in commits:
663             (user,email) = self.p4UserForCommit(id)
664             if not user:
665                 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
666                 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
667                     print "%s" % msg
668                 else:
669                     die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
670
671     def lastP4Changelist(self):
672         # Get back the last changelist number submitted in this client spec. This
673         # then gets used to patch up the username in the change. If the same
674         # client spec is being used by multiple processes then this might go
675         # wrong.
676         results = p4CmdList("client -o")        # find the current client
677         client = None
678         for r in results:
679             if r.has_key('Client'):
680                 client = r['Client']
681                 break
682         if not client:
683             die("could not get client spec")
684         results = p4CmdList("changes -c %s -m 1" % client)
685         for r in results:
686             if r.has_key('change'):
687                 return r['change']
688         die("Could not get changelist number for last submit - cannot patch up user details")
689
690     def modifyChangelistUser(self, changelist, newUser):
691         # fixup the user field of a changelist after it has been submitted.
692         changes = p4CmdList("change -o %s" % changelist)
693         if len(changes) != 1:
694             die("Bad output from p4 change modifying %s to user %s" %
695                 (changelist, newUser))
696
697         c = changes[0]
698         if c['User'] == newUser: return   # nothing to do
699         c['User'] = newUser
700         input = marshal.dumps(c)
701
702         result = p4CmdList("change -f -i", stdin=input)
703         for r in result:
704             if r.has_key('code'):
705                 if r['code'] == 'error':
706                     die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
707             if r.has_key('data'):
708                 print("Updated user field for changelist %s to %s" % (changelist, newUser))
709                 return
710         die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
711
712     def canChangeChangelists(self):
713         # check to see if we have p4 admin or super-user permissions, either of
714         # which are required to modify changelists.
715         results = p4CmdList("protects %s" % self.depotPath)
716         for r in results:
717             if r.has_key('perm'):
718                 if r['perm'] == 'admin':
719                     return 1
720                 if r['perm'] == 'super':
721                     return 1
722         return 0
723
724     def prepareSubmitTemplate(self):
725         # remove lines in the Files section that show changes to files outside the depot path we're committing into
726         template = ""
727         inFilesSection = False
728         for line in p4_read_pipe_lines("change -o"):
729             if line.endswith("\r\n"):
730                 line = line[:-2] + "\n"
731             if inFilesSection:
732                 if line.startswith("\t"):
733                     # path starts and ends with a tab
734                     path = line[1:]
735                     lastTab = path.rfind("\t")
736                     if lastTab != -1:
737                         path = path[:lastTab]
738                         if not p4PathStartsWith(path, self.depotPath):
739                             continue
740                 else:
741                     inFilesSection = False
742             else:
743                 if line.startswith("Files:"):
744                     inFilesSection = True
745
746             template += line
747
748         return template
749
750     def applyCommit(self, id):
751         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
752
753         if self.preserveUser:
754             (p4User, gitEmail) = self.p4UserForCommit(id)
755
756         if not self.detectRenames:
757             # If not explicitly set check the config variable
758             self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
759
760         if self.detectRenames:
761             diffOpts = "-M"
762         else:
763             diffOpts = ""
764
765         if gitConfig("git-p4.detectCopies").lower() == "true":
766             diffOpts += " -C"
767
768         if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
769             diffOpts += " --find-copies-harder"
770
771         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
772         filesToAdd = set()
773         filesToDelete = set()
774         editedFiles = set()
775         filesToChangeExecBit = {}
776         for line in diff:
777             diff = parseDiffTreeEntry(line)
778             modifier = diff['status']
779             path = diff['src']
780             if modifier == "M":
781                 p4_system("edit \"%s\"" % path)
782                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
783                     filesToChangeExecBit[path] = diff['dst_mode']
784                 editedFiles.add(path)
785             elif modifier == "A":
786                 filesToAdd.add(path)
787                 filesToChangeExecBit[path] = diff['dst_mode']
788                 if path in filesToDelete:
789                     filesToDelete.remove(path)
790             elif modifier == "D":
791                 filesToDelete.add(path)
792                 if path in filesToAdd:
793                     filesToAdd.remove(path)
794             elif modifier == "C":
795                 src, dest = diff['src'], diff['dst']
796                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
797                 if diff['src_sha1'] != diff['dst_sha1']:
798                     p4_system("edit \"%s\"" % (dest))
799                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
800                     p4_system("edit \"%s\"" % (dest))
801                     filesToChangeExecBit[dest] = diff['dst_mode']
802                 os.unlink(dest)
803                 editedFiles.add(dest)
804             elif modifier == "R":
805                 src, dest = diff['src'], diff['dst']
806                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
807                 if diff['src_sha1'] != diff['dst_sha1']:
808                     p4_system("edit \"%s\"" % (dest))
809                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
810                     p4_system("edit \"%s\"" % (dest))
811                     filesToChangeExecBit[dest] = diff['dst_mode']
812                 os.unlink(dest)
813                 editedFiles.add(dest)
814                 filesToDelete.add(src)
815             else:
816                 die("unknown modifier %s for %s" % (modifier, path))
817
818         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
819         patchcmd = diffcmd + " | git apply "
820         tryPatchCmd = patchcmd + "--check -"
821         applyPatchCmd = patchcmd + "--check --apply -"
822
823         if os.system(tryPatchCmd) != 0:
824             print "Unfortunately applying the change failed!"
825             print "What do you want to do?"
826             response = "x"
827             while response != "s" and response != "a" and response != "w":
828                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
829                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
830             if response == "s":
831                 print "Skipping! Good luck with the next patches..."
832                 for f in editedFiles:
833                     p4_system("revert \"%s\"" % f);
834                 for f in filesToAdd:
835                     system("rm %s" %f)
836                 return
837             elif response == "a":
838                 os.system(applyPatchCmd)
839                 if len(filesToAdd) > 0:
840                     print "You may also want to call p4 add on the following files:"
841                     print " ".join(filesToAdd)
842                 if len(filesToDelete):
843                     print "The following files should be scheduled for deletion with p4 delete:"
844                     print " ".join(filesToDelete)
845                 die("Please resolve and submit the conflict manually and "
846                     + "continue afterwards with git-p4 submit --continue")
847             elif response == "w":
848                 system(diffcmd + " > patch.txt")
849                 print "Patch saved to patch.txt in %s !" % self.clientPath
850                 die("Please resolve and submit the conflict manually and "
851                     "continue afterwards with git-p4 submit --continue")
852
853         system(applyPatchCmd)
854
855         for f in filesToAdd:
856             p4_system("add \"%s\"" % f)
857         for f in filesToDelete:
858             p4_system("revert \"%s\"" % f)
859             p4_system("delete \"%s\"" % f)
860
861         # Set/clear executable bits
862         for f in filesToChangeExecBit.keys():
863             mode = filesToChangeExecBit[f]
864             setP4ExecBit(f, mode)
865
866         logMessage = extractLogMessageFromGitCommit(id)
867         logMessage = logMessage.strip()
868
869         template = self.prepareSubmitTemplate()
870
871         if self.interactive:
872             submitTemplate = self.prepareLogMessage(template, logMessage)
873
874             if self.preserveUser:
875                submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
876
877             if os.environ.has_key("P4DIFF"):
878                 del(os.environ["P4DIFF"])
879             diff = ""
880             for editedFile in editedFiles:
881                 diff += p4_read_pipe("diff -du %r" % editedFile)
882
883             newdiff = ""
884             for newFile in filesToAdd:
885                 newdiff += "==== new file ====\n"
886                 newdiff += "--- /dev/null\n"
887                 newdiff += "+++ %s\n" % newFile
888                 f = open(newFile, "r")
889                 for line in f.readlines():
890                     newdiff += "+" + line
891                 f.close()
892
893             separatorLine = "######## everything below this line is just the diff #######\n"
894
895             [handle, fileName] = tempfile.mkstemp()
896             tmpFile = os.fdopen(handle, "w+")
897             if self.isWindows:
898                 submitTemplate = submitTemplate.replace("\n", "\r\n")
899                 separatorLine = separatorLine.replace("\n", "\r\n")
900                 newdiff = newdiff.replace("\n", "\r\n")
901             tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
902             tmpFile.close()
903             mtime = os.stat(fileName).st_mtime
904             if os.environ.has_key("P4EDITOR"):
905                 editor = os.environ.get("P4EDITOR")
906             else:
907                 editor = read_pipe("git var GIT_EDITOR").strip()
908             system(editor + " " + fileName)
909
910             if gitConfig("git-p4.skipSubmitEditCheck") == "true":
911                 checkModTime = False
912             else:
913                 checkModTime = True
914
915             response = "y"
916             if checkModTime and (os.stat(fileName).st_mtime <= mtime):
917                 response = "x"
918                 while response != "y" and response != "n":
919                     response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
920
921             if response == "y":
922                 tmpFile = open(fileName, "rb")
923                 message = tmpFile.read()
924                 tmpFile.close()
925                 submitTemplate = message[:message.index(separatorLine)]
926                 if self.isWindows:
927                     submitTemplate = submitTemplate.replace("\r\n", "\n")
928                 p4_write_pipe("submit -i", submitTemplate)
929
930                 if self.preserveUser:
931                     if p4User:
932                         # Get last changelist number. Cannot easily get it from
933                         # the submit command output as the output is unmarshalled.
934                         changelist = self.lastP4Changelist()
935                         self.modifyChangelistUser(changelist, p4User)
936
937             else:
938                 for f in editedFiles:
939                     p4_system("revert \"%s\"" % f);
940                 for f in filesToAdd:
941                     p4_system("revert \"%s\"" % f);
942                     system("rm %s" %f)
943
944             os.remove(fileName)
945         else:
946             fileName = "submit.txt"
947             file = open(fileName, "w+")
948             file.write(self.prepareLogMessage(template, logMessage))
949             file.close()
950             print ("Perforce submit template written as %s. "
951                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
952                    % (fileName, fileName))
953
954     def run(self, args):
955         if len(args) == 0:
956             self.master = currentGitBranch()
957             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
958                 die("Detecting current git branch failed!")
959         elif len(args) == 1:
960             self.master = args[0]
961         else:
962             return False
963
964         allowSubmit = gitConfig("git-p4.allowSubmit")
965         if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
966             die("%s is not in git-p4.allowSubmit" % self.master)
967
968         [upstream, settings] = findUpstreamBranchPoint()
969         self.depotPath = settings['depot-paths'][0]
970         if len(self.origin) == 0:
971             self.origin = upstream
972
973         if self.preserveUser:
974             if not self.canChangeChangelists():
975                 die("Cannot preserve user names without p4 super-user or admin permissions")
976
977         if self.verbose:
978             print "Origin branch is " + self.origin
979
980         if len(self.depotPath) == 0:
981             print "Internal error: cannot locate perforce depot path from existing branches"
982             sys.exit(128)
983
984         self.clientPath = p4Where(self.depotPath)
985
986         if len(self.clientPath) == 0:
987             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
988             sys.exit(128)
989
990         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
991         self.oldWorkingDirectory = os.getcwd()
992
993         chdir(self.clientPath)
994         print "Synchronizing p4 checkout..."
995         p4_system("sync ...")
996
997         self.check()
998
999         commits = []
1000         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1001             commits.append(line.strip())
1002         commits.reverse()
1003
1004         if self.preserveUser:
1005             self.checkValidP4Users(commits)
1006
1007         while len(commits) > 0:
1008             commit = commits[0]
1009             commits = commits[1:]
1010             self.applyCommit(commit)
1011             if not self.interactive:
1012                 break
1013
1014         if len(commits) == 0:
1015             print "All changes applied!"
1016             chdir(self.oldWorkingDirectory)
1017
1018             sync = P4Sync()
1019             sync.run([])
1020
1021             rebase = P4Rebase()
1022             rebase.rebase()
1023
1024         return True
1025
1026 class P4Sync(Command, P4UserMap):
1027     delete_actions = ( "delete", "move/delete", "purge" )
1028
1029     def __init__(self):
1030         Command.__init__(self)
1031         P4UserMap.__init__(self)
1032         self.options = [
1033                 optparse.make_option("--branch", dest="branch"),
1034                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1035                 optparse.make_option("--changesfile", dest="changesFile"),
1036                 optparse.make_option("--silent", dest="silent", action="store_true"),
1037                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1038                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
1039                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1040                                      help="Import into refs/heads/ , not refs/remotes"),
1041                 optparse.make_option("--max-changes", dest="maxChanges"),
1042                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1043                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1044                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1045                                      help="Only sync files that are included in the Perforce Client Spec")
1046         ]
1047         self.description = """Imports from Perforce into a git repository.\n
1048     example:
1049     //depot/my/project/ -- to import the current head
1050     //depot/my/project/@all -- to import everything
1051     //depot/my/project/@1,6 -- to import only from revision 1 to 6
1052
1053     (a ... is not needed in the path p4 specification, it's added implicitly)"""
1054
1055         self.usage += " //depot/path[@revRange]"
1056         self.silent = False
1057         self.createdBranches = set()
1058         self.committedChanges = set()
1059         self.branch = ""
1060         self.detectBranches = False
1061         self.detectLabels = False
1062         self.changesFile = ""
1063         self.syncWithOrigin = True
1064         self.verbose = False
1065         self.importIntoRemotes = True
1066         self.maxChanges = ""
1067         self.isWindows = (platform.system() == "Windows")
1068         self.keepRepoPath = False
1069         self.depotPaths = None
1070         self.p4BranchesInGit = []
1071         self.cloneExclude = []
1072         self.useClientSpec = False
1073         self.clientSpecDirs = []
1074
1075         if gitConfig("git-p4.syncFromOrigin") == "false":
1076             self.syncWithOrigin = False
1077
1078     #
1079     # P4 wildcards are not allowed in filenames.  P4 complains
1080     # if you simply add them, but you can force it with "-f", in
1081     # which case it translates them into %xx encoding internally.
1082     # Search for and fix just these four characters.  Do % last so
1083     # that fixing it does not inadvertently create new %-escapes.
1084     #
1085     def wildcard_decode(self, path):
1086         # Cannot have * in a filename in windows; untested as to
1087         # what p4 would do in such a case.
1088         if not self.isWindows:
1089             path = path.replace("%2A", "*")
1090         path = path.replace("%23", "#") \
1091                    .replace("%40", "@") \
1092                    .replace("%25", "%")
1093         return path
1094
1095     def extractFilesFromCommit(self, commit):
1096         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1097                              for path in self.cloneExclude]
1098         files = []
1099         fnum = 0
1100         while commit.has_key("depotFile%s" % fnum):
1101             path =  commit["depotFile%s" % fnum]
1102
1103             if [p for p in self.cloneExclude
1104                 if p4PathStartsWith(path, p)]:
1105                 found = False
1106             else:
1107                 found = [p for p in self.depotPaths
1108                          if p4PathStartsWith(path, p)]
1109             if not found:
1110                 fnum = fnum + 1
1111                 continue
1112
1113             file = {}
1114             file["path"] = path
1115             file["rev"] = commit["rev%s" % fnum]
1116             file["action"] = commit["action%s" % fnum]
1117             file["type"] = commit["type%s" % fnum]
1118             files.append(file)
1119             fnum = fnum + 1
1120         return files
1121
1122     def stripRepoPath(self, path, prefixes):
1123         if self.useClientSpec:
1124
1125             # if using the client spec, we use the output directory
1126             # specified in the client.  For example, a view
1127             #   //depot/foo/branch/... //client/branch/foo/...
1128             # will end up putting all foo/branch files into
1129             #  branch/foo/
1130             for val in self.clientSpecDirs:
1131                 if path.startswith(val[0]):
1132                     # replace the depot path with the client path
1133                     path = path.replace(val[0], val[1][1])
1134                     # now strip out the client (//client/...)
1135                     path = re.sub("^(//[^/]+/)", '', path)
1136                     # the rest is all path
1137                     return path
1138
1139         if self.keepRepoPath:
1140             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1141
1142         for p in prefixes:
1143             if p4PathStartsWith(path, p):
1144                 path = path[len(p):]
1145
1146         return path
1147
1148     def splitFilesIntoBranches(self, commit):
1149         branches = {}
1150         fnum = 0
1151         while commit.has_key("depotFile%s" % fnum):
1152             path =  commit["depotFile%s" % fnum]
1153             found = [p for p in self.depotPaths
1154                      if p4PathStartsWith(path, p)]
1155             if not found:
1156                 fnum = fnum + 1
1157                 continue
1158
1159             file = {}
1160             file["path"] = path
1161             file["rev"] = commit["rev%s" % fnum]
1162             file["action"] = commit["action%s" % fnum]
1163             file["type"] = commit["type%s" % fnum]
1164             fnum = fnum + 1
1165
1166             relPath = self.stripRepoPath(path, self.depotPaths)
1167
1168             for branch in self.knownBranches.keys():
1169
1170                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1171                 if relPath.startswith(branch + "/"):
1172                     if branch not in branches:
1173                         branches[branch] = []
1174                     branches[branch].append(file)
1175                     break
1176
1177         return branches
1178
1179     # output one file from the P4 stream
1180     # - helper for streamP4Files
1181
1182     def streamOneP4File(self, file, contents):
1183         if file["type"] == "apple":
1184             print "\nfile %s is a strange apple file that forks. Ignoring" % \
1185                 file['depotFile']
1186             return
1187
1188         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1189         relPath = self.wildcard_decode(relPath)
1190         if verbose:
1191             sys.stderr.write("%s\n" % relPath)
1192
1193         mode = "644"
1194         if isP4Exec(file["type"]):
1195             mode = "755"
1196         elif file["type"] == "symlink":
1197             mode = "120000"
1198             # p4 print on a symlink contains "target\n", so strip it off
1199             data = ''.join(contents)
1200             contents = [data[:-1]]
1201
1202         if self.isWindows and file["type"].endswith("text"):
1203             mangled = []
1204             for data in contents:
1205                 data = data.replace("\r\n", "\n")
1206                 mangled.append(data)
1207             contents = mangled
1208
1209         if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1210             contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1211         elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1212             contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1213
1214         self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1215
1216         # total length...
1217         length = 0
1218         for d in contents:
1219             length = length + len(d)
1220
1221         self.gitStream.write("data %d\n" % length)
1222         for d in contents:
1223             self.gitStream.write(d)
1224         self.gitStream.write("\n")
1225
1226     def streamOneP4Deletion(self, file):
1227         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1228         if verbose:
1229             sys.stderr.write("delete %s\n" % relPath)
1230         self.gitStream.write("D %s\n" % relPath)
1231
1232     # handle another chunk of streaming data
1233     def streamP4FilesCb(self, marshalled):
1234
1235         if marshalled.has_key('depotFile') and self.stream_have_file_info:
1236             # start of a new file - output the old one first
1237             self.streamOneP4File(self.stream_file, self.stream_contents)
1238             self.stream_file = {}
1239             self.stream_contents = []
1240             self.stream_have_file_info = False
1241
1242         # pick up the new file information... for the
1243         # 'data' field we need to append to our array
1244         for k in marshalled.keys():
1245             if k == 'data':
1246                 self.stream_contents.append(marshalled['data'])
1247             else:
1248                 self.stream_file[k] = marshalled[k]
1249
1250         self.stream_have_file_info = True
1251
1252     # Stream directly from "p4 files" into "git fast-import"
1253     def streamP4Files(self, files):
1254         filesForCommit = []
1255         filesToRead = []
1256         filesToDelete = []
1257
1258         for f in files:
1259             includeFile = True
1260             for val in self.clientSpecDirs:
1261                 if f['path'].startswith(val[0]):
1262                     if val[1][0] <= 0:
1263                         includeFile = False
1264                     break
1265
1266             if includeFile:
1267                 filesForCommit.append(f)
1268                 if f['action'] in self.delete_actions:
1269                     filesToDelete.append(f)
1270                 else:
1271                     filesToRead.append(f)
1272
1273         # deleted files...
1274         for f in filesToDelete:
1275             self.streamOneP4Deletion(f)
1276
1277         if len(filesToRead) > 0:
1278             self.stream_file = {}
1279             self.stream_contents = []
1280             self.stream_have_file_info = False
1281
1282             # curry self argument
1283             def streamP4FilesCbSelf(entry):
1284                 self.streamP4FilesCb(entry)
1285
1286             p4CmdList("-x - print",
1287                 '\n'.join(['%s#%s' % (f['path'], f['rev'])
1288                                                   for f in filesToRead]),
1289                 cb=streamP4FilesCbSelf)
1290
1291             # do the last chunk
1292             if self.stream_file.has_key('depotFile'):
1293                 self.streamOneP4File(self.stream_file, self.stream_contents)
1294
1295     def commit(self, details, files, branch, branchPrefixes, parent = ""):
1296         epoch = details["time"]
1297         author = details["user"]
1298         self.branchPrefixes = branchPrefixes
1299
1300         if self.verbose:
1301             print "commit into %s" % branch
1302
1303         # start with reading files; if that fails, we should not
1304         # create a commit.
1305         new_files = []
1306         for f in files:
1307             if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1308                 new_files.append (f)
1309             else:
1310                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1311
1312         self.gitStream.write("commit %s\n" % branch)
1313 #        gitStream.write("mark :%s\n" % details["change"])
1314         self.committedChanges.add(int(details["change"]))
1315         committer = ""
1316         if author not in self.users:
1317             self.getUserMapFromPerforceServer()
1318         if author in self.users:
1319             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1320         else:
1321             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1322
1323         self.gitStream.write("committer %s\n" % committer)
1324
1325         self.gitStream.write("data <<EOT\n")
1326         self.gitStream.write(details["desc"])
1327         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1328                              % (','.join (branchPrefixes), details["change"]))
1329         if len(details['options']) > 0:
1330             self.gitStream.write(": options = %s" % details['options'])
1331         self.gitStream.write("]\nEOT\n\n")
1332
1333         if len(parent) > 0:
1334             if self.verbose:
1335                 print "parent %s" % parent
1336             self.gitStream.write("from %s\n" % parent)
1337
1338         self.streamP4Files(new_files)
1339         self.gitStream.write("\n")
1340
1341         change = int(details["change"])
1342
1343         if self.labels.has_key(change):
1344             label = self.labels[change]
1345             labelDetails = label[0]
1346             labelRevisions = label[1]
1347             if self.verbose:
1348                 print "Change %s is labelled %s" % (change, labelDetails)
1349
1350             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1351                                                     for p in branchPrefixes]))
1352
1353             if len(files) == len(labelRevisions):
1354
1355                 cleanedFiles = {}
1356                 for info in files:
1357                     if info["action"] in self.delete_actions:
1358                         continue
1359                     cleanedFiles[info["depotFile"]] = info["rev"]
1360
1361                 if cleanedFiles == labelRevisions:
1362                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1363                     self.gitStream.write("from %s\n" % branch)
1364
1365                     owner = labelDetails["Owner"]
1366                     tagger = ""
1367                     if author in self.users:
1368                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1369                     else:
1370                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1371                     self.gitStream.write("tagger %s\n" % tagger)
1372                     self.gitStream.write("data <<EOT\n")
1373                     self.gitStream.write(labelDetails["Description"])
1374                     self.gitStream.write("EOT\n\n")
1375
1376                 else:
1377                     if not self.silent:
1378                         print ("Tag %s does not match with change %s: files do not match."
1379                                % (labelDetails["label"], change))
1380
1381             else:
1382                 if not self.silent:
1383                     print ("Tag %s does not match with change %s: file count is different."
1384                            % (labelDetails["label"], change))
1385
1386     def getLabels(self):
1387         self.labels = {}
1388
1389         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1390         if len(l) > 0 and not self.silent:
1391             print "Finding files belonging to labels in %s" % `self.depotPaths`
1392
1393         for output in l:
1394             label = output["label"]
1395             revisions = {}
1396             newestChange = 0
1397             if self.verbose:
1398                 print "Querying files for label %s" % label
1399             for file in p4CmdList("files "
1400                                   +  ' '.join (["%s...@%s" % (p, label)
1401                                                 for p in self.depotPaths])):
1402                 revisions[file["depotFile"]] = file["rev"]
1403                 change = int(file["change"])
1404                 if change > newestChange:
1405                     newestChange = change
1406
1407             self.labels[newestChange] = [output, revisions]
1408
1409         if self.verbose:
1410             print "Label changes: %s" % self.labels.keys()
1411
1412     def guessProjectName(self):
1413         for p in self.depotPaths:
1414             if p.endswith("/"):
1415                 p = p[:-1]
1416             p = p[p.strip().rfind("/") + 1:]
1417             if not p.endswith("/"):
1418                p += "/"
1419             return p
1420
1421     def getBranchMapping(self):
1422         lostAndFoundBranches = set()
1423
1424         for info in p4CmdList("branches"):
1425             details = p4Cmd("branch -o %s" % info["branch"])
1426             viewIdx = 0
1427             while details.has_key("View%s" % viewIdx):
1428                 paths = details["View%s" % viewIdx].split(" ")
1429                 viewIdx = viewIdx + 1
1430                 # require standard //depot/foo/... //depot/bar/... mapping
1431                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1432                     continue
1433                 source = paths[0]
1434                 destination = paths[1]
1435                 ## HACK
1436                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1437                     source = source[len(self.depotPaths[0]):-4]
1438                     destination = destination[len(self.depotPaths[0]):-4]
1439
1440                     if destination in self.knownBranches:
1441                         if not self.silent:
1442                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1443                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1444                         continue
1445
1446                     self.knownBranches[destination] = source
1447
1448                     lostAndFoundBranches.discard(destination)
1449
1450                     if source not in self.knownBranches:
1451                         lostAndFoundBranches.add(source)
1452
1453
1454         for branch in lostAndFoundBranches:
1455             self.knownBranches[branch] = branch
1456
1457     def getBranchMappingFromGitBranches(self):
1458         branches = p4BranchesInGit(self.importIntoRemotes)
1459         for branch in branches.keys():
1460             if branch == "master":
1461                 branch = "main"
1462             else:
1463                 branch = branch[len(self.projectName):]
1464             self.knownBranches[branch] = branch
1465
1466     def listExistingP4GitBranches(self):
1467         # branches holds mapping from name to commit
1468         branches = p4BranchesInGit(self.importIntoRemotes)
1469         self.p4BranchesInGit = branches.keys()
1470         for branch in branches.keys():
1471             self.initialParents[self.refPrefix + branch] = branches[branch]
1472
1473     def updateOptionDict(self, d):
1474         option_keys = {}
1475         if self.keepRepoPath:
1476             option_keys['keepRepoPath'] = 1
1477
1478         d["options"] = ' '.join(sorted(option_keys.keys()))
1479
1480     def readOptions(self, d):
1481         self.keepRepoPath = (d.has_key('options')
1482                              and ('keepRepoPath' in d['options']))
1483
1484     def gitRefForBranch(self, branch):
1485         if branch == "main":
1486             return self.refPrefix + "master"
1487
1488         if len(branch) <= 0:
1489             return branch
1490
1491         return self.refPrefix + self.projectName + branch
1492
1493     def gitCommitByP4Change(self, ref, change):
1494         if self.verbose:
1495             print "looking in ref " + ref + " for change %s using bisect..." % change
1496
1497         earliestCommit = ""
1498         latestCommit = parseRevision(ref)
1499
1500         while True:
1501             if self.verbose:
1502                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1503             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1504             if len(next) == 0:
1505                 if self.verbose:
1506                     print "argh"
1507                 return ""
1508             log = extractLogMessageFromGitCommit(next)
1509             settings = extractSettingsGitLog(log)
1510             currentChange = int(settings['change'])
1511             if self.verbose:
1512                 print "current change %s" % currentChange
1513
1514             if currentChange == change:
1515                 if self.verbose:
1516                     print "found %s" % next
1517                 return next
1518
1519             if currentChange < change:
1520                 earliestCommit = "^%s" % next
1521             else:
1522                 latestCommit = "%s" % next
1523
1524         return ""
1525
1526     def importNewBranch(self, branch, maxChange):
1527         # make fast-import flush all changes to disk and update the refs using the checkpoint
1528         # command so that we can try to find the branch parent in the git history
1529         self.gitStream.write("checkpoint\n\n");
1530         self.gitStream.flush();
1531         branchPrefix = self.depotPaths[0] + branch + "/"
1532         range = "@1,%s" % maxChange
1533         #print "prefix" + branchPrefix
1534         changes = p4ChangesForPaths([branchPrefix], range)
1535         if len(changes) <= 0:
1536             return False
1537         firstChange = changes[0]
1538         #print "first change in branch: %s" % firstChange
1539         sourceBranch = self.knownBranches[branch]
1540         sourceDepotPath = self.depotPaths[0] + sourceBranch
1541         sourceRef = self.gitRefForBranch(sourceBranch)
1542         #print "source " + sourceBranch
1543
1544         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1545         #print "branch parent: %s" % branchParentChange
1546         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1547         if len(gitParent) > 0:
1548             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1549             #print "parent git commit: %s" % gitParent
1550
1551         self.importChanges(changes)
1552         return True
1553
1554     def importChanges(self, changes):
1555         cnt = 1
1556         for change in changes:
1557             description = p4Cmd("describe %s" % change)
1558             self.updateOptionDict(description)
1559
1560             if not self.silent:
1561                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1562                 sys.stdout.flush()
1563             cnt = cnt + 1
1564
1565             try:
1566                 if self.detectBranches:
1567                     branches = self.splitFilesIntoBranches(description)
1568                     for branch in branches.keys():
1569                         ## HACK  --hwn
1570                         branchPrefix = self.depotPaths[0] + branch + "/"
1571
1572                         parent = ""
1573
1574                         filesForCommit = branches[branch]
1575
1576                         if self.verbose:
1577                             print "branch is %s" % branch
1578
1579                         self.updatedBranches.add(branch)
1580
1581                         if branch not in self.createdBranches:
1582                             self.createdBranches.add(branch)
1583                             parent = self.knownBranches[branch]
1584                             if parent == branch:
1585                                 parent = ""
1586                             else:
1587                                 fullBranch = self.projectName + branch
1588                                 if fullBranch not in self.p4BranchesInGit:
1589                                     if not self.silent:
1590                                         print("\n    Importing new branch %s" % fullBranch);
1591                                     if self.importNewBranch(branch, change - 1):
1592                                         parent = ""
1593                                         self.p4BranchesInGit.append(fullBranch)
1594                                     if not self.silent:
1595                                         print("\n    Resuming with change %s" % change);
1596
1597                                 if self.verbose:
1598                                     print "parent determined through known branches: %s" % parent
1599
1600                         branch = self.gitRefForBranch(branch)
1601                         parent = self.gitRefForBranch(parent)
1602
1603                         if self.verbose:
1604                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1605
1606                         if len(parent) == 0 and branch in self.initialParents:
1607                             parent = self.initialParents[branch]
1608                             del self.initialParents[branch]
1609
1610                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1611                 else:
1612                     files = self.extractFilesFromCommit(description)
1613                     self.commit(description, files, self.branch, self.depotPaths,
1614                                 self.initialParent)
1615                     self.initialParent = ""
1616             except IOError:
1617                 print self.gitError.read()
1618                 sys.exit(1)
1619
1620     def importHeadRevision(self, revision):
1621         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1622
1623         details = { "user" : "git perforce import user", "time" : int(time.time()) }
1624         details["desc"] = ("Initial import of %s from the state at revision %s\n"
1625                            % (' '.join(self.depotPaths), revision))
1626         details["change"] = revision
1627         newestRevision = 0
1628
1629         fileCnt = 0
1630         for info in p4CmdList("files "
1631                               +  ' '.join(["%s...%s"
1632                                            % (p, revision)
1633                                            for p in self.depotPaths])):
1634
1635             if 'code' in info and info['code'] == 'error':
1636                 sys.stderr.write("p4 returned an error: %s\n"
1637                                  % info['data'])
1638                 if info['data'].find("must refer to client") >= 0:
1639                     sys.stderr.write("This particular p4 error is misleading.\n")
1640                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
1641                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
1642                 sys.exit(1)
1643             if 'p4ExitCode' in info:
1644                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1645                 sys.exit(1)
1646
1647
1648             change = int(info["change"])
1649             if change > newestRevision:
1650                 newestRevision = change
1651
1652             if info["action"] in self.delete_actions:
1653                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1654                 #fileCnt = fileCnt + 1
1655                 continue
1656
1657             for prop in ["depotFile", "rev", "action", "type" ]:
1658                 details["%s%s" % (prop, fileCnt)] = info[prop]
1659
1660             fileCnt = fileCnt + 1
1661
1662         details["change"] = newestRevision
1663         self.updateOptionDict(details)
1664         try:
1665             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1666         except IOError:
1667             print "IO error with git fast-import. Is your git version recent enough?"
1668             print self.gitError.read()
1669
1670
1671     def getClientSpec(self):
1672         specList = p4CmdList( "client -o" )
1673         temp = {}
1674         for entry in specList:
1675             for k,v in entry.iteritems():
1676                 if k.startswith("View"):
1677
1678                     # p4 has these %%1 to %%9 arguments in specs to
1679                     # reorder paths; which we can't handle (yet :)
1680                     if re.match('%%\d', v) != None:
1681                         print "Sorry, can't handle %%n arguments in client specs"
1682                         sys.exit(1)
1683
1684                     if v.startswith('"'):
1685                         start = 1
1686                     else:
1687                         start = 0
1688                     index = v.find("...")
1689
1690                     # save the "client view"; i.e the RHS of the view
1691                     # line that tells the client where to put the
1692                     # files for this view.
1693                     cv = v[index+3:].strip() # +3 to remove previous '...'
1694
1695                     # if the client view doesn't end with a
1696                     # ... wildcard, then we're going to mess up the
1697                     # output directory, so fail gracefully.
1698                     if not cv.endswith('...'):
1699                         print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1700                         sys.exit(1)
1701                     cv=cv[:-3]
1702
1703                     # now save the view; +index means included, -index
1704                     # means it should be filtered out.
1705                     v = v[start:index]
1706                     if v.startswith("-"):
1707                         v = v[1:]
1708                         include = -len(v)
1709                     else:
1710                         include = len(v)
1711
1712                     temp[v] = (include, cv)
1713
1714         self.clientSpecDirs = temp.items()
1715         self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1716
1717     def run(self, args):
1718         self.depotPaths = []
1719         self.changeRange = ""
1720         self.initialParent = ""
1721         self.previousDepotPaths = []
1722
1723         # map from branch depot path to parent branch
1724         self.knownBranches = {}
1725         self.initialParents = {}
1726         self.hasOrigin = originP4BranchesExist()
1727         if not self.syncWithOrigin:
1728             self.hasOrigin = False
1729
1730         if self.importIntoRemotes:
1731             self.refPrefix = "refs/remotes/p4/"
1732         else:
1733             self.refPrefix = "refs/heads/p4/"
1734
1735         if self.syncWithOrigin and self.hasOrigin:
1736             if not self.silent:
1737                 print "Syncing with origin first by calling git fetch origin"
1738             system("git fetch origin")
1739
1740         if len(self.branch) == 0:
1741             self.branch = self.refPrefix + "master"
1742             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1743                 system("git update-ref %s refs/heads/p4" % self.branch)
1744                 system("git branch -D p4");
1745             # create it /after/ importing, when master exists
1746             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1747                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1748
1749         if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1750             self.getClientSpec()
1751
1752         # TODO: should always look at previous commits,
1753         # merge with previous imports, if possible.
1754         if args == []:
1755             if self.hasOrigin:
1756                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1757             self.listExistingP4GitBranches()
1758
1759             if len(self.p4BranchesInGit) > 1:
1760                 if not self.silent:
1761                     print "Importing from/into multiple branches"
1762                 self.detectBranches = True
1763
1764             if self.verbose:
1765                 print "branches: %s" % self.p4BranchesInGit
1766
1767             p4Change = 0
1768             for branch in self.p4BranchesInGit:
1769                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1770
1771                 settings = extractSettingsGitLog(logMsg)
1772
1773                 self.readOptions(settings)
1774                 if (settings.has_key('depot-paths')
1775                     and settings.has_key ('change')):
1776                     change = int(settings['change']) + 1
1777                     p4Change = max(p4Change, change)
1778
1779                     depotPaths = sorted(settings['depot-paths'])
1780                     if self.previousDepotPaths == []:
1781                         self.previousDepotPaths = depotPaths
1782                     else:
1783                         paths = []
1784                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1785                             for i in range(0, min(len(cur), len(prev))):
1786                                 if cur[i] <> prev[i]:
1787                                     i = i - 1
1788                                     break
1789
1790                             paths.append (cur[:i + 1])
1791
1792                         self.previousDepotPaths = paths
1793
1794             if p4Change > 0:
1795                 self.depotPaths = sorted(self.previousDepotPaths)
1796                 self.changeRange = "@%s,#head" % p4Change
1797                 if not self.detectBranches:
1798                     self.initialParent = parseRevision(self.branch)
1799                 if not self.silent and not self.detectBranches:
1800                     print "Performing incremental import into %s git branch" % self.branch
1801
1802         if not self.branch.startswith("refs/"):
1803             self.branch = "refs/heads/" + self.branch
1804
1805         if len(args) == 0 and self.depotPaths:
1806             if not self.silent:
1807                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1808         else:
1809             if self.depotPaths and self.depotPaths != args:
1810                 print ("previous import used depot path %s and now %s was specified. "
1811                        "This doesn't work!" % (' '.join (self.depotPaths),
1812                                                ' '.join (args)))
1813                 sys.exit(1)
1814
1815             self.depotPaths = sorted(args)
1816
1817         revision = ""
1818         self.users = {}
1819
1820         newPaths = []
1821         for p in self.depotPaths:
1822             if p.find("@") != -1:
1823                 atIdx = p.index("@")
1824                 self.changeRange = p[atIdx:]
1825                 if self.changeRange == "@all":
1826                     self.changeRange = ""
1827                 elif ',' not in self.changeRange:
1828                     revision = self.changeRange
1829                     self.changeRange = ""
1830                 p = p[:atIdx]
1831             elif p.find("#") != -1:
1832                 hashIdx = p.index("#")
1833                 revision = p[hashIdx:]
1834                 p = p[:hashIdx]
1835             elif self.previousDepotPaths == []:
1836                 revision = "#head"
1837
1838             p = re.sub ("\.\.\.$", "", p)
1839             if not p.endswith("/"):
1840                 p += "/"
1841
1842             newPaths.append(p)
1843
1844         self.depotPaths = newPaths
1845
1846
1847         self.loadUserMapFromCache()
1848         self.labels = {}
1849         if self.detectLabels:
1850             self.getLabels();
1851
1852         if self.detectBranches:
1853             ## FIXME - what's a P4 projectName ?
1854             self.projectName = self.guessProjectName()
1855
1856             if self.hasOrigin:
1857                 self.getBranchMappingFromGitBranches()
1858             else:
1859                 self.getBranchMapping()
1860             if self.verbose:
1861                 print "p4-git branches: %s" % self.p4BranchesInGit
1862                 print "initial parents: %s" % self.initialParents
1863             for b in self.p4BranchesInGit:
1864                 if b != "master":
1865
1866                     ## FIXME
1867                     b = b[len(self.projectName):]
1868                 self.createdBranches.add(b)
1869
1870         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1871
1872         importProcess = subprocess.Popen(["git", "fast-import"],
1873                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1874                                          stderr=subprocess.PIPE);
1875         self.gitOutput = importProcess.stdout
1876         self.gitStream = importProcess.stdin
1877         self.gitError = importProcess.stderr
1878
1879         if revision:
1880             self.importHeadRevision(revision)
1881         else:
1882             changes = []
1883
1884             if len(self.changesFile) > 0:
1885                 output = open(self.changesFile).readlines()
1886                 changeSet = set()
1887                 for line in output:
1888                     changeSet.add(int(line))
1889
1890                 for change in changeSet:
1891                     changes.append(change)
1892
1893                 changes.sort()
1894             else:
1895                 # catch "git-p4 sync" with no new branches, in a repo that
1896                 # does not have any existing git-p4 branches
1897                 if len(args) == 0 and not self.p4BranchesInGit:
1898                     die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
1899                 if self.verbose:
1900                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1901                                                               self.changeRange)
1902                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1903
1904                 if len(self.maxChanges) > 0:
1905                     changes = changes[:min(int(self.maxChanges), len(changes))]
1906
1907             if len(changes) == 0:
1908                 if not self.silent:
1909                     print "No changes to import!"
1910                 return True
1911
1912             if not self.silent and not self.detectBranches:
1913                 print "Import destination: %s" % self.branch
1914
1915             self.updatedBranches = set()
1916
1917             self.importChanges(changes)
1918
1919             if not self.silent:
1920                 print ""
1921                 if len(self.updatedBranches) > 0:
1922                     sys.stdout.write("Updated branches: ")
1923                     for b in self.updatedBranches:
1924                         sys.stdout.write("%s " % b)
1925                     sys.stdout.write("\n")
1926
1927         self.gitStream.close()
1928         if importProcess.wait() != 0:
1929             die("fast-import failed: %s" % self.gitError.read())
1930         self.gitOutput.close()
1931         self.gitError.close()
1932
1933         return True
1934
1935 class P4Rebase(Command):
1936     def __init__(self):
1937         Command.__init__(self)
1938         self.options = [ ]
1939         self.description = ("Fetches the latest revision from perforce and "
1940                             + "rebases the current work (branch) against it")
1941         self.verbose = False
1942
1943     def run(self, args):
1944         sync = P4Sync()
1945         sync.run([])
1946
1947         return self.rebase()
1948
1949     def rebase(self):
1950         if os.system("git update-index --refresh") != 0:
1951             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.");
1952         if len(read_pipe("git diff-index HEAD --")) > 0:
1953             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1954
1955         [upstream, settings] = findUpstreamBranchPoint()
1956         if len(upstream) == 0:
1957             die("Cannot find upstream branchpoint for rebase")
1958
1959         # the branchpoint may be p4/foo~3, so strip off the parent
1960         upstream = re.sub("~[0-9]+$", "", upstream)
1961
1962         print "Rebasing the current branch onto %s" % upstream
1963         oldHead = read_pipe("git rev-parse HEAD").strip()
1964         system("git rebase %s" % upstream)
1965         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
1966         return True
1967
1968 class P4Clone(P4Sync):
1969     def __init__(self):
1970         P4Sync.__init__(self)
1971         self.description = "Creates a new git repository and imports from Perforce into it"
1972         self.usage = "usage: %prog [options] //depot/path[@revRange]"
1973         self.options += [
1974             optparse.make_option("--destination", dest="cloneDestination",
1975                                  action='store', default=None,
1976                                  help="where to leave result of the clone"),
1977             optparse.make_option("-/", dest="cloneExclude",
1978                                  action="append", type="string",
1979                                  help="exclude depot path"),
1980             optparse.make_option("--bare", dest="cloneBare",
1981                                  action="store_true", default=False),
1982         ]
1983         self.cloneDestination = None
1984         self.needsGit = False
1985         self.cloneBare = False
1986
1987     # This is required for the "append" cloneExclude action
1988     def ensure_value(self, attr, value):
1989         if not hasattr(self, attr) or getattr(self, attr) is None:
1990             setattr(self, attr, value)
1991         return getattr(self, attr)
1992
1993     def defaultDestination(self, args):
1994         ## TODO: use common prefix of args?
1995         depotPath = args[0]
1996         depotDir = re.sub("(@[^@]*)$", "", depotPath)
1997         depotDir = re.sub("(#[^#]*)$", "", depotDir)
1998         depotDir = re.sub(r"\.\.\.$", "", depotDir)
1999         depotDir = re.sub(r"/$", "", depotDir)
2000         return os.path.split(depotDir)[1]
2001
2002     def run(self, args):
2003         if len(args) < 1:
2004             return False
2005
2006         if self.keepRepoPath and not self.cloneDestination:
2007             sys.stderr.write("Must specify destination for --keep-path\n")
2008             sys.exit(1)
2009
2010         depotPaths = args
2011
2012         if not self.cloneDestination and len(depotPaths) > 1:
2013             self.cloneDestination = depotPaths[-1]
2014             depotPaths = depotPaths[:-1]
2015
2016         self.cloneExclude = ["/"+p for p in self.cloneExclude]
2017         for p in depotPaths:
2018             if not p.startswith("//"):
2019                 return False
2020
2021         if not self.cloneDestination:
2022             self.cloneDestination = self.defaultDestination(args)
2023
2024         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2025
2026         if not os.path.exists(self.cloneDestination):
2027             os.makedirs(self.cloneDestination)
2028         chdir(self.cloneDestination)
2029
2030         init_cmd = [ "git", "init" ]
2031         if self.cloneBare:
2032             init_cmd.append("--bare")
2033         subprocess.check_call(init_cmd)
2034
2035         if not P4Sync.run(self, depotPaths):
2036             return False
2037         if self.branch != "master":
2038             if self.importIntoRemotes:
2039                 masterbranch = "refs/remotes/p4/master"
2040             else:
2041                 masterbranch = "refs/heads/p4/master"
2042             if gitBranchExists(masterbranch):
2043                 system("git branch master %s" % masterbranch)
2044                 if not self.cloneBare:
2045                     system("git checkout -f")
2046             else:
2047                 print "Could not detect main branch. No checkout/master branch created."
2048
2049         return True
2050
2051 class P4Branches(Command):
2052     def __init__(self):
2053         Command.__init__(self)
2054         self.options = [ ]
2055         self.description = ("Shows the git branches that hold imports and their "
2056                             + "corresponding perforce depot paths")
2057         self.verbose = False
2058
2059     def run(self, args):
2060         if originP4BranchesExist():
2061             createOrUpdateBranchesFromOrigin()
2062
2063         cmdline = "git rev-parse --symbolic "
2064         cmdline += " --remotes"
2065
2066         for line in read_pipe_lines(cmdline):
2067             line = line.strip()
2068
2069             if not line.startswith('p4/') or line == "p4/HEAD":
2070                 continue
2071             branch = line
2072
2073             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2074             settings = extractSettingsGitLog(log)
2075
2076             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2077         return True
2078
2079 class HelpFormatter(optparse.IndentedHelpFormatter):
2080     def __init__(self):
2081         optparse.IndentedHelpFormatter.__init__(self)
2082
2083     def format_description(self, description):
2084         if description:
2085             return description + "\n"
2086         else:
2087             return ""
2088
2089 def printUsage(commands):
2090     print "usage: %s <command> [options]" % sys.argv[0]
2091     print ""
2092     print "valid commands: %s" % ", ".join(commands)
2093     print ""
2094     print "Try %s <command> --help for command specific help." % sys.argv[0]
2095     print ""
2096
2097 commands = {
2098     "debug" : P4Debug,
2099     "submit" : P4Submit,
2100     "commit" : P4Submit,
2101     "sync" : P4Sync,
2102     "rebase" : P4Rebase,
2103     "clone" : P4Clone,
2104     "rollback" : P4RollBack,
2105     "branches" : P4Branches
2106 }
2107
2108
2109 def main():
2110     if len(sys.argv[1:]) == 0:
2111         printUsage(commands.keys())
2112         sys.exit(2)
2113
2114     cmd = ""
2115     cmdName = sys.argv[1]
2116     try:
2117         klass = commands[cmdName]
2118         cmd = klass()
2119     except KeyError:
2120         print "unknown command %s" % cmdName
2121         print ""
2122         printUsage(commands.keys())
2123         sys.exit(2)
2124
2125     options = cmd.options
2126     cmd.gitdir = os.environ.get("GIT_DIR", None)
2127
2128     args = sys.argv[2:]
2129
2130     if len(options) > 0:
2131         options.append(optparse.make_option("--git-dir", dest="gitdir"))
2132
2133         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2134                                        options,
2135                                        description = cmd.description,
2136                                        formatter = HelpFormatter())
2137
2138         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2139     global verbose
2140     verbose = cmd.verbose
2141     if cmd.needsGit:
2142         if cmd.gitdir == None:
2143             cmd.gitdir = os.path.abspath(".git")
2144             if not isValidGitDir(cmd.gitdir):
2145                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2146                 if os.path.exists(cmd.gitdir):
2147                     cdup = read_pipe("git rev-parse --show-cdup").strip()
2148                     if len(cdup) > 0:
2149                         chdir(cdup);
2150
2151         if not isValidGitDir(cmd.gitdir):
2152             if isValidGitDir(cmd.gitdir + "/.git"):
2153                 cmd.gitdir += "/.git"
2154             else:
2155                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2156
2157         os.environ["GIT_DIR"] = cmd.gitdir
2158
2159     if not cmd.run(args):
2160         parser.print_help()
2161
2162
2163 if __name__ == '__main__':
2164     main()