Code

git-p4: Allow filtering Perforce branches by user
[git.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 #
11 import optparse, sys, os, marshal, subprocess, shelve
12 import tempfile, getopt, os.path, time, platform
13 import re
15 verbose = False
18 def p4_build_cmd(cmd):
19     """Build a suitable p4 command line.
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"
27     user = gitConfig("git-p4.user")
28     if len(user) > 0:
29         real_cmd += "-u %s " % user
31     password = gitConfig("git-p4.password")
32     if len(password) > 0:
33         real_cmd += "-P %s " % password
35     port = gitConfig("git-p4.port")
36     if len(port) > 0:
37         real_cmd += "-p %s " % port
39     host = gitConfig("git-p4.host")
40     if len(host) > 0:
41         real_cmd += "-h %s " % host
43     client = gitConfig("git-p4.client")
44     if len(client) > 0:
45         real_cmd += "-c %s " % client
47     real_cmd += "%s" % (cmd)
48     if verbose:
49         print real_cmd
50     return real_cmd
52 def chdir(dir):
53     if os.name == 'nt':
54         os.environ['PWD']=dir
55     os.chdir(dir)
57 def die(msg):
58     if verbose:
59         raise Exception(msg)
60     else:
61         sys.stderr.write(msg + "\n")
62         sys.exit(1)
64 def write_pipe(c, str):
65     if verbose:
66         sys.stderr.write('Writing pipe: %s\n' % c)
68     pipe = os.popen(c, 'w')
69     val = pipe.write(str)
70     if pipe.close():
71         die('Command failed: %s' % c)
73     return val
75 def p4_write_pipe(c, str):
76     real_cmd = p4_build_cmd(c)
77     return write_pipe(real_cmd, str)
79 def read_pipe(c, ignore_error=False):
80     if verbose:
81         sys.stderr.write('Reading pipe: %s\n' % c)
83     pipe = os.popen(c, 'rb')
84     val = pipe.read()
85     if pipe.close() and not ignore_error:
86         die('Command failed: %s' % c)
88     return val
90 def p4_read_pipe(c, ignore_error=False):
91     real_cmd = p4_build_cmd(c)
92     return read_pipe(real_cmd, ignore_error)
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)
103     return val
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)
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)
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)
121 def isP4Exec(kind):
122     """Determine if a Perforce 'kind' should have execute permission
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)
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.
133     p4Type = "+x"
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]
142     p4_system("reopen -t %s %s" % (p4Type, file))
144 def getP4OpenedType(file):
145     # Returns the perforce file type for the given file.
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))
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
161 def parseDiffTreeEntry(entry):
162     """Parses a single diff tree entry into its component elements.
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:
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.
178     If the pattern is not matched, None is returned."""
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
194 def isModeExec(mode):
195     # Returns True if the given git mode represents an executable file,
196     # otherwise False.
197     return mode[-3:] == "755"
199 def isModeExecChanged(src_mode, dst_mode):
200     return isModeExec(src_mode) != isModeExec(dst_mode)
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)
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)
217     p4 = subprocess.Popen(cmd, shell=True,
218                           stdin=stdin_file,
219                           stdout=subprocess.PIPE)
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)
237     return result
239 def p4Cmd(cmd):
240     list = p4CmdList(cmd)
241     result = {}
242     for entry in list:
243         result.update(entry)
244     return result;
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:]
275     if clientPath.endswith("..."):
276         clientPath = clientPath[:-3]
277     return clientPath
279 def currentGitBranch():
280     return read_pipe("git name-rev HEAD").split(" ")[1].strip()
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
288 def parseRevision(ref):
289     return read_pipe("git rev-parse %s" % ref).strip()
291 def extractLogMessageFromGitCommit(commit):
292     logMessage = ""
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
302        logMessage += log
303     return logMessage
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
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]
321             values[key] = val
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
330 def gitBranchExists(branch):
331     proc = subprocess.Popen(["git", "rev-parse", branch],
332                             stderr=subprocess.PIPE, stdout=subprocess.PIPE);
333     return proc.wait() == 0;
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]
345 def p4BranchesInGit(branchesAreInRemotes = True):
346     branches = {}
348     cmdline = "git rev-parse --symbolic "
349     if branchesAreInRemotes:
350         cmdline += " --remotes"
351     else:
352         cmdline += " --branches"
354     for line in read_pipe_lines(cmdline):
355         line = line.strip()
357         ## only import to p4/
358         if not line.startswith('p4/') or line == "p4/HEAD":
359             continue
360         branch = line
362         # strip off p4
363         branch = re.sub ("^p4/", "", line)
365         branches[branch] = parseRevision(line)
366     return branches
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
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]
391         parent = parent + 1
393     return ["", settings]
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)
400     originPrefix = "origin/p4/"
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
407         headName = line[len(originPrefix):]
408         remoteHead = localRefPrefix + headName
409         originHead = line
411         original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
412         if (not original.has_key('depot-paths')
413             or not original.has_key('change')):
414             continue
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'])))
439         if update:
440             system("git update-ref %s %s" % (remoteHead, originHead))
442 def originP4BranchesExist():
443         return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
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]))
450     changes = {}
451     for line in output:
452         changeNum = int(line.split(" ")[1])
453         changes[changeNum] = True
455     changelist = changes.keys()
456     changelist.sort()
457     return changelist
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)
472 class Command:
473     def __init__(self):
474         self.usage = "usage: %prog [options]"
475         self.needsGit = True
477 class P4UserMap:
478     def __init__(self):
479         self.userMapFromPerforceServer = False
481     def getUserCacheFilename(self):
482         home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
483         return home + "/.gitp4-usercache.txt"
485     def getUserMapFromPerforceServer(self):
486         if self.userMapFromPerforceServer:
487             return
488         self.users = {}
489         self.emails = {}
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"]
498         s = ''
499         for (key, val) in self.users.items():
500             s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
502         open(self.getUserCacheFilename(), "wb").write(s)
503         self.userMapFromPerforceServer = True
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()
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
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
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
548     def run(self, args):
549         if len(args) != 1:
550             return False
551         maxChange = int(args[0])
553         if "p4ExitCode" in p4Cmd("changes -m 1"):
554             die("Problems executing p4");
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")
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)
570                 depotPaths = settings['depot-paths']
571                 change = settings['change']
573                 changed = False
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
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)
590                     depotPaths = settings['depot-paths']
591                     change = settings['change']
593                 if changed:
594                     print "%s rewound to %s" % (ref, change)
596         return True
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         self.myP4UserId = None
619     def check(self):
620         if len(p4CmdList("opened ...")) > 0:
621             die("You have files opened with perforce! Close them before starting the sync.")
623     # replaces everything between 'Description:' and the next P4 submit template field with the
624     # commit message
625     def prepareLogMessage(self, template, message):
626         result = ""
628         inDescriptionSection = False
630         for line in template.split("\n"):
631             if line.startswith("#"):
632                 result += line + "\n"
633                 continue
635             if inDescriptionSection:
636                 if line.startswith("Files:") or line.startswith("Jobs:"):
637                     inDescriptionSection = False
638                 else:
639                     continue
640             else:
641                 if line.startswith("Description:"):
642                     inDescriptionSection = True
643                     line += "\n"
644                     for messageLine in message.split("\n"):
645                         line += "\t" + messageLine + "\n"
647             result += line + "\n"
649         return result
651     def p4UserForCommit(self,id):
652         # Return the tuple (perforce user,git email) for a given git commit id
653         self.getUserMapFromPerforceServer()
654         gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
655         gitEmail = gitEmail.strip()
656         if not self.emails.has_key(gitEmail):
657             return (None,gitEmail)
658         else:
659             return (self.emails[gitEmail],gitEmail)
661     def checkValidP4Users(self,commits):
662         # check if any git authors cannot be mapped to p4 users
663         for id in commits:
664             (user,email) = self.p4UserForCommit(id)
665             if not user:
666                 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
667                 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
668                     print "%s" % msg
669                 else:
670                     die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
672     def lastP4Changelist(self):
673         # Get back the last changelist number submitted in this client spec. This
674         # then gets used to patch up the username in the change. If the same
675         # client spec is being used by multiple processes then this might go
676         # wrong.
677         results = p4CmdList("client -o")        # find the current client
678         client = None
679         for r in results:
680             if r.has_key('Client'):
681                 client = r['Client']
682                 break
683         if not client:
684             die("could not get client spec")
685         results = p4CmdList("changes -c %s -m 1" % client)
686         for r in results:
687             if r.has_key('change'):
688                 return r['change']
689         die("Could not get changelist number for last submit - cannot patch up user details")
691     def modifyChangelistUser(self, changelist, newUser):
692         # fixup the user field of a changelist after it has been submitted.
693         changes = p4CmdList("change -o %s" % changelist)
694         if len(changes) != 1:
695             die("Bad output from p4 change modifying %s to user %s" %
696                 (changelist, newUser))
698         c = changes[0]
699         if c['User'] == newUser: return   # nothing to do
700         c['User'] = newUser
701         input = marshal.dumps(c)
703         result = p4CmdList("change -f -i", stdin=input)
704         for r in result:
705             if r.has_key('code'):
706                 if r['code'] == 'error':
707                     die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
708             if r.has_key('data'):
709                 print("Updated user field for changelist %s to %s" % (changelist, newUser))
710                 return
711         die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
713     def canChangeChangelists(self):
714         # check to see if we have p4 admin or super-user permissions, either of
715         # which are required to modify changelists.
716         results = p4CmdList("protects %s" % self.depotPath)
717         for r in results:
718             if r.has_key('perm'):
719                 if r['perm'] == 'admin':
720                     return 1
721                 if r['perm'] == 'super':
722                     return 1
723         return 0
725     def p4UserId(self):
726         if self.myP4UserId:
727             return self.myP4UserId
729         results = p4CmdList("user -o")
730         for r in results:
731             if r.has_key('User'):
732                 self.myP4UserId = r['User']
733                 return r['User']
734         die("Could not find your p4 user id")
736     def p4UserIsMe(self, p4User):
737         # return True if the given p4 user is actually me
738         me = self.p4UserId()
739         if not p4User or p4User != me:
740             return False
741         else:
742             return True
744     def prepareSubmitTemplate(self):
745         # remove lines in the Files section that show changes to files outside the depot path we're committing into
746         template = ""
747         inFilesSection = False
748         for line in p4_read_pipe_lines("change -o"):
749             if line.endswith("\r\n"):
750                 line = line[:-2] + "\n"
751             if inFilesSection:
752                 if line.startswith("\t"):
753                     # path starts and ends with a tab
754                     path = line[1:]
755                     lastTab = path.rfind("\t")
756                     if lastTab != -1:
757                         path = path[:lastTab]
758                         if not p4PathStartsWith(path, self.depotPath):
759                             continue
760                 else:
761                     inFilesSection = False
762             else:
763                 if line.startswith("Files:"):
764                     inFilesSection = True
766             template += line
768         return template
770     def applyCommit(self, id):
771         print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
773         (p4User, gitEmail) = self.p4UserForCommit(id)
775         if not self.detectRenames:
776             # If not explicitly set check the config variable
777             self.detectRenames = gitConfig("git-p4.detectRenames")
779         if self.detectRenames.lower() == "false" or self.detectRenames == "":
780             diffOpts = ""
781         elif self.detectRenames.lower() == "true":
782             diffOpts = "-M"
783         else:
784             diffOpts = "-M%s" % self.detectRenames
786         detectCopies = gitConfig("git-p4.detectCopies")
787         if detectCopies.lower() == "true":
788             diffOpts += " -C"
789         elif detectCopies != "" and detectCopies.lower() != "false":
790             diffOpts += " -C%s" % detectCopies
792         if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
793             diffOpts += " --find-copies-harder"
795         diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
796         filesToAdd = set()
797         filesToDelete = set()
798         editedFiles = set()
799         filesToChangeExecBit = {}
800         for line in diff:
801             diff = parseDiffTreeEntry(line)
802             modifier = diff['status']
803             path = diff['src']
804             if modifier == "M":
805                 p4_system("edit \"%s\"" % path)
806                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
807                     filesToChangeExecBit[path] = diff['dst_mode']
808                 editedFiles.add(path)
809             elif modifier == "A":
810                 filesToAdd.add(path)
811                 filesToChangeExecBit[path] = diff['dst_mode']
812                 if path in filesToDelete:
813                     filesToDelete.remove(path)
814             elif modifier == "D":
815                 filesToDelete.add(path)
816                 if path in filesToAdd:
817                     filesToAdd.remove(path)
818             elif modifier == "C":
819                 src, dest = diff['src'], diff['dst']
820                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
821                 if diff['src_sha1'] != diff['dst_sha1']:
822                     p4_system("edit \"%s\"" % (dest))
823                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
824                     p4_system("edit \"%s\"" % (dest))
825                     filesToChangeExecBit[dest] = diff['dst_mode']
826                 os.unlink(dest)
827                 editedFiles.add(dest)
828             elif modifier == "R":
829                 src, dest = diff['src'], diff['dst']
830                 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
831                 if diff['src_sha1'] != diff['dst_sha1']:
832                     p4_system("edit \"%s\"" % (dest))
833                 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
834                     p4_system("edit \"%s\"" % (dest))
835                     filesToChangeExecBit[dest] = diff['dst_mode']
836                 os.unlink(dest)
837                 editedFiles.add(dest)
838                 filesToDelete.add(src)
839             else:
840                 die("unknown modifier %s for %s" % (modifier, path))
842         diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
843         patchcmd = diffcmd + " | git apply "
844         tryPatchCmd = patchcmd + "--check -"
845         applyPatchCmd = patchcmd + "--check --apply -"
847         if os.system(tryPatchCmd) != 0:
848             print "Unfortunately applying the change failed!"
849             print "What do you want to do?"
850             response = "x"
851             while response != "s" and response != "a" and response != "w":
852                 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
853                                      "and with .rej files / [w]rite the patch to a file (patch.txt) ")
854             if response == "s":
855                 print "Skipping! Good luck with the next patches..."
856                 for f in editedFiles:
857                     p4_system("revert \"%s\"" % f);
858                 for f in filesToAdd:
859                     system("rm %s" %f)
860                 return
861             elif response == "a":
862                 os.system(applyPatchCmd)
863                 if len(filesToAdd) > 0:
864                     print "You may also want to call p4 add on the following files:"
865                     print " ".join(filesToAdd)
866                 if len(filesToDelete):
867                     print "The following files should be scheduled for deletion with p4 delete:"
868                     print " ".join(filesToDelete)
869                 die("Please resolve and submit the conflict manually and "
870                     + "continue afterwards with git-p4 submit --continue")
871             elif response == "w":
872                 system(diffcmd + " > patch.txt")
873                 print "Patch saved to patch.txt in %s !" % self.clientPath
874                 die("Please resolve and submit the conflict manually and "
875                     "continue afterwards with git-p4 submit --continue")
877         system(applyPatchCmd)
879         for f in filesToAdd:
880             p4_system("add \"%s\"" % f)
881         for f in filesToDelete:
882             p4_system("revert \"%s\"" % f)
883             p4_system("delete \"%s\"" % f)
885         # Set/clear executable bits
886         for f in filesToChangeExecBit.keys():
887             mode = filesToChangeExecBit[f]
888             setP4ExecBit(f, mode)
890         logMessage = extractLogMessageFromGitCommit(id)
891         logMessage = logMessage.strip()
893         template = self.prepareSubmitTemplate()
895         if self.interactive:
896             submitTemplate = self.prepareLogMessage(template, logMessage)
898             if self.preserveUser:
899                submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
901             if os.environ.has_key("P4DIFF"):
902                 del(os.environ["P4DIFF"])
903             diff = ""
904             for editedFile in editedFiles:
905                 diff += p4_read_pipe("diff -du %r" % editedFile)
907             newdiff = ""
908             for newFile in filesToAdd:
909                 newdiff += "==== new file ====\n"
910                 newdiff += "--- /dev/null\n"
911                 newdiff += "+++ %s\n" % newFile
912                 f = open(newFile, "r")
913                 for line in f.readlines():
914                     newdiff += "+" + line
915                 f.close()
917             if self.checkAuthorship and not self.p4UserIsMe(p4User):
918                 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
919                 submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n"
920                 submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n"
922             separatorLine = "######## everything below this line is just the diff #######\n"
924             [handle, fileName] = tempfile.mkstemp()
925             tmpFile = os.fdopen(handle, "w+")
926             if self.isWindows:
927                 submitTemplate = submitTemplate.replace("\n", "\r\n")
928                 separatorLine = separatorLine.replace("\n", "\r\n")
929                 newdiff = newdiff.replace("\n", "\r\n")
930             tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
931             tmpFile.close()
932             mtime = os.stat(fileName).st_mtime
933             if os.environ.has_key("P4EDITOR"):
934                 editor = os.environ.get("P4EDITOR")
935             else:
936                 editor = read_pipe("git var GIT_EDITOR").strip()
937             system(editor + " " + fileName)
939             if gitConfig("git-p4.skipSubmitEditCheck") == "true":
940                 checkModTime = False
941             else:
942                 checkModTime = True
944             response = "y"
945             if checkModTime and (os.stat(fileName).st_mtime <= mtime):
946                 response = "x"
947                 while response != "y" and response != "n":
948                     response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
950             if response == "y":
951                 tmpFile = open(fileName, "rb")
952                 message = tmpFile.read()
953                 tmpFile.close()
954                 submitTemplate = message[:message.index(separatorLine)]
955                 if self.isWindows:
956                     submitTemplate = submitTemplate.replace("\r\n", "\n")
957                 p4_write_pipe("submit -i", submitTemplate)
959                 if self.preserveUser:
960                     if p4User:
961                         # Get last changelist number. Cannot easily get it from
962                         # the submit command output as the output is unmarshalled.
963                         changelist = self.lastP4Changelist()
964                         self.modifyChangelistUser(changelist, p4User)
966             else:
967                 for f in editedFiles:
968                     p4_system("revert \"%s\"" % f);
969                 for f in filesToAdd:
970                     p4_system("revert \"%s\"" % f);
971                     system("rm %s" %f)
973             os.remove(fileName)
974         else:
975             fileName = "submit.txt"
976             file = open(fileName, "w+")
977             file.write(self.prepareLogMessage(template, logMessage))
978             file.close()
979             print ("Perforce submit template written as %s. "
980                    + "Please review/edit and then use p4 submit -i < %s to submit directly!"
981                    % (fileName, fileName))
983     def run(self, args):
984         if len(args) == 0:
985             self.master = currentGitBranch()
986             if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
987                 die("Detecting current git branch failed!")
988         elif len(args) == 1:
989             self.master = args[0]
990         else:
991             return False
993         allowSubmit = gitConfig("git-p4.allowSubmit")
994         if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
995             die("%s is not in git-p4.allowSubmit" % self.master)
997         [upstream, settings] = findUpstreamBranchPoint()
998         self.depotPath = settings['depot-paths'][0]
999         if len(self.origin) == 0:
1000             self.origin = upstream
1002         if self.preserveUser:
1003             if not self.canChangeChangelists():
1004                 die("Cannot preserve user names without p4 super-user or admin permissions")
1006         if self.verbose:
1007             print "Origin branch is " + self.origin
1009         if len(self.depotPath) == 0:
1010             print "Internal error: cannot locate perforce depot path from existing branches"
1011             sys.exit(128)
1013         self.clientPath = p4Where(self.depotPath)
1015         if len(self.clientPath) == 0:
1016             print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
1017             sys.exit(128)
1019         print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
1020         self.oldWorkingDirectory = os.getcwd()
1022         chdir(self.clientPath)
1023         print "Synchronizing p4 checkout..."
1024         p4_system("sync ...")
1026         self.check()
1028         commits = []
1029         for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1030             commits.append(line.strip())
1031         commits.reverse()
1033         if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1034             self.checkAuthorship = False
1035         else:
1036             self.checkAuthorship = True
1038         if self.preserveUser:
1039             self.checkValidP4Users(commits)
1041         while len(commits) > 0:
1042             commit = commits[0]
1043             commits = commits[1:]
1044             self.applyCommit(commit)
1045             if not self.interactive:
1046                 break
1048         if len(commits) == 0:
1049             print "All changes applied!"
1050             chdir(self.oldWorkingDirectory)
1052             sync = P4Sync()
1053             sync.run([])
1055             rebase = P4Rebase()
1056             rebase.rebase()
1058         return True
1060 class P4Sync(Command, P4UserMap):
1061     delete_actions = ( "delete", "move/delete", "purge" )
1063     def __init__(self):
1064         Command.__init__(self)
1065         P4UserMap.__init__(self)
1066         self.options = [
1067                 optparse.make_option("--branch", dest="branch"),
1068                 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1069                 optparse.make_option("--changesfile", dest="changesFile"),
1070                 optparse.make_option("--silent", dest="silent", action="store_true"),
1071                 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
1072                 optparse.make_option("--verbose", dest="verbose", action="store_true"),
1073                 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1074                                      help="Import into refs/heads/ , not refs/remotes"),
1075                 optparse.make_option("--max-changes", dest="maxChanges"),
1076                 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
1077                                      help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1078                 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1079                                      help="Only sync files that are included in the Perforce Client Spec")
1080         ]
1081         self.description = """Imports from Perforce into a git repository.\n
1082     example:
1083     //depot/my/project/ -- to import the current head
1084     //depot/my/project/@all -- to import everything
1085     //depot/my/project/@1,6 -- to import only from revision 1 to 6
1087     (a ... is not needed in the path p4 specification, it's added implicitly)"""
1089         self.usage += " //depot/path[@revRange]"
1090         self.silent = False
1091         self.createdBranches = set()
1092         self.committedChanges = set()
1093         self.branch = ""
1094         self.detectBranches = False
1095         self.detectLabels = False
1096         self.changesFile = ""
1097         self.syncWithOrigin = True
1098         self.verbose = False
1099         self.importIntoRemotes = True
1100         self.maxChanges = ""
1101         self.isWindows = (platform.system() == "Windows")
1102         self.keepRepoPath = False
1103         self.depotPaths = None
1104         self.p4BranchesInGit = []
1105         self.cloneExclude = []
1106         self.useClientSpec = False
1107         self.clientSpecDirs = []
1109         if gitConfig("git-p4.syncFromOrigin") == "false":
1110             self.syncWithOrigin = False
1112     #
1113     # P4 wildcards are not allowed in filenames.  P4 complains
1114     # if you simply add them, but you can force it with "-f", in
1115     # which case it translates them into %xx encoding internally.
1116     # Search for and fix just these four characters.  Do % last so
1117     # that fixing it does not inadvertently create new %-escapes.
1118     #
1119     def wildcard_decode(self, path):
1120         # Cannot have * in a filename in windows; untested as to
1121         # what p4 would do in such a case.
1122         if not self.isWindows:
1123             path = path.replace("%2A", "*")
1124         path = path.replace("%23", "#") \
1125                    .replace("%40", "@") \
1126                    .replace("%25", "%")
1127         return path
1129     def extractFilesFromCommit(self, commit):
1130         self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1131                              for path in self.cloneExclude]
1132         files = []
1133         fnum = 0
1134         while commit.has_key("depotFile%s" % fnum):
1135             path =  commit["depotFile%s" % fnum]
1137             if [p for p in self.cloneExclude
1138                 if p4PathStartsWith(path, p)]:
1139                 found = False
1140             else:
1141                 found = [p for p in self.depotPaths
1142                          if p4PathStartsWith(path, p)]
1143             if not found:
1144                 fnum = fnum + 1
1145                 continue
1147             file = {}
1148             file["path"] = path
1149             file["rev"] = commit["rev%s" % fnum]
1150             file["action"] = commit["action%s" % fnum]
1151             file["type"] = commit["type%s" % fnum]
1152             files.append(file)
1153             fnum = fnum + 1
1154         return files
1156     def stripRepoPath(self, path, prefixes):
1157         if self.useClientSpec:
1159             # if using the client spec, we use the output directory
1160             # specified in the client.  For example, a view
1161             #   //depot/foo/branch/... //client/branch/foo/...
1162             # will end up putting all foo/branch files into
1163             #  branch/foo/
1164             for val in self.clientSpecDirs:
1165                 if path.startswith(val[0]):
1166                     # replace the depot path with the client path
1167                     path = path.replace(val[0], val[1][1])
1168                     # now strip out the client (//client/...)
1169                     path = re.sub("^(//[^/]+/)", '', path)
1170                     # the rest is all path
1171                     return path
1173         if self.keepRepoPath:
1174             prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1176         for p in prefixes:
1177             if p4PathStartsWith(path, p):
1178                 path = path[len(p):]
1180         return path
1182     def splitFilesIntoBranches(self, commit):
1183         branches = {}
1184         fnum = 0
1185         while commit.has_key("depotFile%s" % fnum):
1186             path =  commit["depotFile%s" % fnum]
1187             found = [p for p in self.depotPaths
1188                      if p4PathStartsWith(path, p)]
1189             if not found:
1190                 fnum = fnum + 1
1191                 continue
1193             file = {}
1194             file["path"] = path
1195             file["rev"] = commit["rev%s" % fnum]
1196             file["action"] = commit["action%s" % fnum]
1197             file["type"] = commit["type%s" % fnum]
1198             fnum = fnum + 1
1200             relPath = self.stripRepoPath(path, self.depotPaths)
1202             for branch in self.knownBranches.keys():
1204                 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1205                 if relPath.startswith(branch + "/"):
1206                     if branch not in branches:
1207                         branches[branch] = []
1208                     branches[branch].append(file)
1209                     break
1211         return branches
1213     # output one file from the P4 stream
1214     # - helper for streamP4Files
1216     def streamOneP4File(self, file, contents):
1217         if file["type"] == "apple":
1218             print "\nfile %s is a strange apple file that forks. Ignoring" % \
1219                 file['depotFile']
1220             return
1222         relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
1223         relPath = self.wildcard_decode(relPath)
1224         if verbose:
1225             sys.stderr.write("%s\n" % relPath)
1227         mode = "644"
1228         if isP4Exec(file["type"]):
1229             mode = "755"
1230         elif file["type"] == "symlink":
1231             mode = "120000"
1232             # p4 print on a symlink contains "target\n", so strip it off
1233             data = ''.join(contents)
1234             contents = [data[:-1]]
1236         if self.isWindows and file["type"].endswith("text"):
1237             mangled = []
1238             for data in contents:
1239                 data = data.replace("\r\n", "\n")
1240                 mangled.append(data)
1241             contents = mangled
1243         if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1244             contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1245         elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1246             contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1248         self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1250         # total length...
1251         length = 0
1252         for d in contents:
1253             length = length + len(d)
1255         self.gitStream.write("data %d\n" % length)
1256         for d in contents:
1257             self.gitStream.write(d)
1258         self.gitStream.write("\n")
1260     def streamOneP4Deletion(self, file):
1261         relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1262         if verbose:
1263             sys.stderr.write("delete %s\n" % relPath)
1264         self.gitStream.write("D %s\n" % relPath)
1266     # handle another chunk of streaming data
1267     def streamP4FilesCb(self, marshalled):
1269         if marshalled.has_key('depotFile') and self.stream_have_file_info:
1270             # start of a new file - output the old one first
1271             self.streamOneP4File(self.stream_file, self.stream_contents)
1272             self.stream_file = {}
1273             self.stream_contents = []
1274             self.stream_have_file_info = False
1276         # pick up the new file information... for the
1277         # 'data' field we need to append to our array
1278         for k in marshalled.keys():
1279             if k == 'data':
1280                 self.stream_contents.append(marshalled['data'])
1281             else:
1282                 self.stream_file[k] = marshalled[k]
1284         self.stream_have_file_info = True
1286     # Stream directly from "p4 files" into "git fast-import"
1287     def streamP4Files(self, files):
1288         filesForCommit = []
1289         filesToRead = []
1290         filesToDelete = []
1292         for f in files:
1293             includeFile = True
1294             for val in self.clientSpecDirs:
1295                 if f['path'].startswith(val[0]):
1296                     if val[1][0] <= 0:
1297                         includeFile = False
1298                     break
1300             if includeFile:
1301                 filesForCommit.append(f)
1302                 if f['action'] in self.delete_actions:
1303                     filesToDelete.append(f)
1304                 else:
1305                     filesToRead.append(f)
1307         # deleted files...
1308         for f in filesToDelete:
1309             self.streamOneP4Deletion(f)
1311         if len(filesToRead) > 0:
1312             self.stream_file = {}
1313             self.stream_contents = []
1314             self.stream_have_file_info = False
1316             # curry self argument
1317             def streamP4FilesCbSelf(entry):
1318                 self.streamP4FilesCb(entry)
1320             p4CmdList("-x - print",
1321                 '\n'.join(['%s#%s' % (f['path'], f['rev'])
1322                                                   for f in filesToRead]),
1323                 cb=streamP4FilesCbSelf)
1325             # do the last chunk
1326             if self.stream_file.has_key('depotFile'):
1327                 self.streamOneP4File(self.stream_file, self.stream_contents)
1329     def commit(self, details, files, branch, branchPrefixes, parent = ""):
1330         epoch = details["time"]
1331         author = details["user"]
1332         self.branchPrefixes = branchPrefixes
1334         if self.verbose:
1335             print "commit into %s" % branch
1337         # start with reading files; if that fails, we should not
1338         # create a commit.
1339         new_files = []
1340         for f in files:
1341             if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
1342                 new_files.append (f)
1343             else:
1344                 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
1346         self.gitStream.write("commit %s\n" % branch)
1347 #        gitStream.write("mark :%s\n" % details["change"])
1348         self.committedChanges.add(int(details["change"]))
1349         committer = ""
1350         if author not in self.users:
1351             self.getUserMapFromPerforceServer()
1352         if author in self.users:
1353             committer = "%s %s %s" % (self.users[author], epoch, self.tz)
1354         else:
1355             committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
1357         self.gitStream.write("committer %s\n" % committer)
1359         self.gitStream.write("data <<EOT\n")
1360         self.gitStream.write(details["desc"])
1361         self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1362                              % (','.join (branchPrefixes), details["change"]))
1363         if len(details['options']) > 0:
1364             self.gitStream.write(": options = %s" % details['options'])
1365         self.gitStream.write("]\nEOT\n\n")
1367         if len(parent) > 0:
1368             if self.verbose:
1369                 print "parent %s" % parent
1370             self.gitStream.write("from %s\n" % parent)
1372         self.streamP4Files(new_files)
1373         self.gitStream.write("\n")
1375         change = int(details["change"])
1377         if self.labels.has_key(change):
1378             label = self.labels[change]
1379             labelDetails = label[0]
1380             labelRevisions = label[1]
1381             if self.verbose:
1382                 print "Change %s is labelled %s" % (change, labelDetails)
1384             files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1385                                                     for p in branchPrefixes]))
1387             if len(files) == len(labelRevisions):
1389                 cleanedFiles = {}
1390                 for info in files:
1391                     if info["action"] in self.delete_actions:
1392                         continue
1393                     cleanedFiles[info["depotFile"]] = info["rev"]
1395                 if cleanedFiles == labelRevisions:
1396                     self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1397                     self.gitStream.write("from %s\n" % branch)
1399                     owner = labelDetails["Owner"]
1400                     tagger = ""
1401                     if author in self.users:
1402                         tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1403                     else:
1404                         tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1405                     self.gitStream.write("tagger %s\n" % tagger)
1406                     self.gitStream.write("data <<EOT\n")
1407                     self.gitStream.write(labelDetails["Description"])
1408                     self.gitStream.write("EOT\n\n")
1410                 else:
1411                     if not self.silent:
1412                         print ("Tag %s does not match with change %s: files do not match."
1413                                % (labelDetails["label"], change))
1415             else:
1416                 if not self.silent:
1417                     print ("Tag %s does not match with change %s: file count is different."
1418                            % (labelDetails["label"], change))
1420     def getLabels(self):
1421         self.labels = {}
1423         l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
1424         if len(l) > 0 and not self.silent:
1425             print "Finding files belonging to labels in %s" % `self.depotPaths`
1427         for output in l:
1428             label = output["label"]
1429             revisions = {}
1430             newestChange = 0
1431             if self.verbose:
1432                 print "Querying files for label %s" % label
1433             for file in p4CmdList("files "
1434                                   +  ' '.join (["%s...@%s" % (p, label)
1435                                                 for p in self.depotPaths])):
1436                 revisions[file["depotFile"]] = file["rev"]
1437                 change = int(file["change"])
1438                 if change > newestChange:
1439                     newestChange = change
1441             self.labels[newestChange] = [output, revisions]
1443         if self.verbose:
1444             print "Label changes: %s" % self.labels.keys()
1446     def guessProjectName(self):
1447         for p in self.depotPaths:
1448             if p.endswith("/"):
1449                 p = p[:-1]
1450             p = p[p.strip().rfind("/") + 1:]
1451             if not p.endswith("/"):
1452                p += "/"
1453             return p
1455     def getBranchMapping(self):
1456         lostAndFoundBranches = set()
1458         user = gitConfig("git-p4.branchUser")
1459         if len(user) > 0:
1460             command = "branches -u %s" % user
1461         else:
1462             command = "branches"
1464         for info in p4CmdList(command):
1465             details = p4Cmd("branch -o %s" % info["branch"])
1466             viewIdx = 0
1467             while details.has_key("View%s" % viewIdx):
1468                 paths = details["View%s" % viewIdx].split(" ")
1469                 viewIdx = viewIdx + 1
1470                 # require standard //depot/foo/... //depot/bar/... mapping
1471                 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1472                     continue
1473                 source = paths[0]
1474                 destination = paths[1]
1475                 ## HACK
1476                 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
1477                     source = source[len(self.depotPaths[0]):-4]
1478                     destination = destination[len(self.depotPaths[0]):-4]
1480                     if destination in self.knownBranches:
1481                         if not self.silent:
1482                             print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1483                             print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1484                         continue
1486                     self.knownBranches[destination] = source
1488                     lostAndFoundBranches.discard(destination)
1490                     if source not in self.knownBranches:
1491                         lostAndFoundBranches.add(source)
1494         for branch in lostAndFoundBranches:
1495             self.knownBranches[branch] = branch
1497     def getBranchMappingFromGitBranches(self):
1498         branches = p4BranchesInGit(self.importIntoRemotes)
1499         for branch in branches.keys():
1500             if branch == "master":
1501                 branch = "main"
1502             else:
1503                 branch = branch[len(self.projectName):]
1504             self.knownBranches[branch] = branch
1506     def listExistingP4GitBranches(self):
1507         # branches holds mapping from name to commit
1508         branches = p4BranchesInGit(self.importIntoRemotes)
1509         self.p4BranchesInGit = branches.keys()
1510         for branch in branches.keys():
1511             self.initialParents[self.refPrefix + branch] = branches[branch]
1513     def updateOptionDict(self, d):
1514         option_keys = {}
1515         if self.keepRepoPath:
1516             option_keys['keepRepoPath'] = 1
1518         d["options"] = ' '.join(sorted(option_keys.keys()))
1520     def readOptions(self, d):
1521         self.keepRepoPath = (d.has_key('options')
1522                              and ('keepRepoPath' in d['options']))
1524     def gitRefForBranch(self, branch):
1525         if branch == "main":
1526             return self.refPrefix + "master"
1528         if len(branch) <= 0:
1529             return branch
1531         return self.refPrefix + self.projectName + branch
1533     def gitCommitByP4Change(self, ref, change):
1534         if self.verbose:
1535             print "looking in ref " + ref + " for change %s using bisect..." % change
1537         earliestCommit = ""
1538         latestCommit = parseRevision(ref)
1540         while True:
1541             if self.verbose:
1542                 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1543             next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1544             if len(next) == 0:
1545                 if self.verbose:
1546                     print "argh"
1547                 return ""
1548             log = extractLogMessageFromGitCommit(next)
1549             settings = extractSettingsGitLog(log)
1550             currentChange = int(settings['change'])
1551             if self.verbose:
1552                 print "current change %s" % currentChange
1554             if currentChange == change:
1555                 if self.verbose:
1556                     print "found %s" % next
1557                 return next
1559             if currentChange < change:
1560                 earliestCommit = "^%s" % next
1561             else:
1562                 latestCommit = "%s" % next
1564         return ""
1566     def importNewBranch(self, branch, maxChange):
1567         # make fast-import flush all changes to disk and update the refs using the checkpoint
1568         # command so that we can try to find the branch parent in the git history
1569         self.gitStream.write("checkpoint\n\n");
1570         self.gitStream.flush();
1571         branchPrefix = self.depotPaths[0] + branch + "/"
1572         range = "@1,%s" % maxChange
1573         #print "prefix" + branchPrefix
1574         changes = p4ChangesForPaths([branchPrefix], range)
1575         if len(changes) <= 0:
1576             return False
1577         firstChange = changes[0]
1578         #print "first change in branch: %s" % firstChange
1579         sourceBranch = self.knownBranches[branch]
1580         sourceDepotPath = self.depotPaths[0] + sourceBranch
1581         sourceRef = self.gitRefForBranch(sourceBranch)
1582         #print "source " + sourceBranch
1584         branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1585         #print "branch parent: %s" % branchParentChange
1586         gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1587         if len(gitParent) > 0:
1588             self.initialParents[self.gitRefForBranch(branch)] = gitParent
1589             #print "parent git commit: %s" % gitParent
1591         self.importChanges(changes)
1592         return True
1594     def importChanges(self, changes):
1595         cnt = 1
1596         for change in changes:
1597             description = p4Cmd("describe %s" % change)
1598             self.updateOptionDict(description)
1600             if not self.silent:
1601                 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1602                 sys.stdout.flush()
1603             cnt = cnt + 1
1605             try:
1606                 if self.detectBranches:
1607                     branches = self.splitFilesIntoBranches(description)
1608                     for branch in branches.keys():
1609                         ## HACK  --hwn
1610                         branchPrefix = self.depotPaths[0] + branch + "/"
1612                         parent = ""
1614                         filesForCommit = branches[branch]
1616                         if self.verbose:
1617                             print "branch is %s" % branch
1619                         self.updatedBranches.add(branch)
1621                         if branch not in self.createdBranches:
1622                             self.createdBranches.add(branch)
1623                             parent = self.knownBranches[branch]
1624                             if parent == branch:
1625                                 parent = ""
1626                             else:
1627                                 fullBranch = self.projectName + branch
1628                                 if fullBranch not in self.p4BranchesInGit:
1629                                     if not self.silent:
1630                                         print("\n    Importing new branch %s" % fullBranch);
1631                                     if self.importNewBranch(branch, change - 1):
1632                                         parent = ""
1633                                         self.p4BranchesInGit.append(fullBranch)
1634                                     if not self.silent:
1635                                         print("\n    Resuming with change %s" % change);
1637                                 if self.verbose:
1638                                     print "parent determined through known branches: %s" % parent
1640                         branch = self.gitRefForBranch(branch)
1641                         parent = self.gitRefForBranch(parent)
1643                         if self.verbose:
1644                             print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1646                         if len(parent) == 0 and branch in self.initialParents:
1647                             parent = self.initialParents[branch]
1648                             del self.initialParents[branch]
1650                         self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1651                 else:
1652                     files = self.extractFilesFromCommit(description)
1653                     self.commit(description, files, self.branch, self.depotPaths,
1654                                 self.initialParent)
1655                     self.initialParent = ""
1656             except IOError:
1657                 print self.gitError.read()
1658                 sys.exit(1)
1660     def importHeadRevision(self, revision):
1661         print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1663         details = {}
1664         details["user"] = "git perforce import user"
1665         details["desc"] = ("Initial import of %s from the state at revision %s\n"
1666                            % (' '.join(self.depotPaths), revision))
1667         details["change"] = revision
1668         newestRevision = 0
1670         fileCnt = 0
1671         for info in p4CmdList("files "
1672                               +  ' '.join(["%s...%s"
1673                                            % (p, revision)
1674                                            for p in self.depotPaths])):
1676             if 'code' in info and info['code'] == 'error':
1677                 sys.stderr.write("p4 returned an error: %s\n"
1678                                  % info['data'])
1679                 if info['data'].find("must refer to client") >= 0:
1680                     sys.stderr.write("This particular p4 error is misleading.\n")
1681                     sys.stderr.write("Perhaps the depot path was misspelled.\n");
1682                     sys.stderr.write("Depot path:  %s\n" % " ".join(self.depotPaths))
1683                 sys.exit(1)
1684             if 'p4ExitCode' in info:
1685                 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
1686                 sys.exit(1)
1689             change = int(info["change"])
1690             if change > newestRevision:
1691                 newestRevision = change
1693             if info["action"] in self.delete_actions:
1694                 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1695                 #fileCnt = fileCnt + 1
1696                 continue
1698             for prop in ["depotFile", "rev", "action", "type" ]:
1699                 details["%s%s" % (prop, fileCnt)] = info[prop]
1701             fileCnt = fileCnt + 1
1703         details["change"] = newestRevision
1705         # Use time from top-most change so that all git-p4 clones of
1706         # the same p4 repo have the same commit SHA1s.
1707         res = p4CmdList("describe -s %d" % newestRevision)
1708         newestTime = None
1709         for r in res:
1710             if r.has_key('time'):
1711                 newestTime = int(r['time'])
1712         if newestTime is None:
1713             die("\"describe -s\" on newest change %d did not give a time")
1714         details["time"] = newestTime
1716         self.updateOptionDict(details)
1717         try:
1718             self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1719         except IOError:
1720             print "IO error with git fast-import. Is your git version recent enough?"
1721             print self.gitError.read()
1724     def getClientSpec(self):
1725         specList = p4CmdList( "client -o" )
1726         temp = {}
1727         for entry in specList:
1728             for k,v in entry.iteritems():
1729                 if k.startswith("View"):
1731                     # p4 has these %%1 to %%9 arguments in specs to
1732                     # reorder paths; which we can't handle (yet :)
1733                     if re.match('%%\d', v) != None:
1734                         print "Sorry, can't handle %%n arguments in client specs"
1735                         sys.exit(1)
1737                     if v.startswith('"'):
1738                         start = 1
1739                     else:
1740                         start = 0
1741                     index = v.find("...")
1743                     # save the "client view"; i.e the RHS of the view
1744                     # line that tells the client where to put the
1745                     # files for this view.
1746                     cv = v[index+3:].strip() # +3 to remove previous '...'
1748                     # if the client view doesn't end with a
1749                     # ... wildcard, then we're going to mess up the
1750                     # output directory, so fail gracefully.
1751                     if not cv.endswith('...'):
1752                         print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1753                         sys.exit(1)
1754                     cv=cv[:-3]
1756                     # now save the view; +index means included, -index
1757                     # means it should be filtered out.
1758                     v = v[start:index]
1759                     if v.startswith("-"):
1760                         v = v[1:]
1761                         include = -len(v)
1762                     else:
1763                         include = len(v)
1765                     temp[v] = (include, cv)
1767         self.clientSpecDirs = temp.items()
1768         self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
1770     def run(self, args):
1771         self.depotPaths = []
1772         self.changeRange = ""
1773         self.initialParent = ""
1774         self.previousDepotPaths = []
1776         # map from branch depot path to parent branch
1777         self.knownBranches = {}
1778         self.initialParents = {}
1779         self.hasOrigin = originP4BranchesExist()
1780         if not self.syncWithOrigin:
1781             self.hasOrigin = False
1783         if self.importIntoRemotes:
1784             self.refPrefix = "refs/remotes/p4/"
1785         else:
1786             self.refPrefix = "refs/heads/p4/"
1788         if self.syncWithOrigin and self.hasOrigin:
1789             if not self.silent:
1790                 print "Syncing with origin first by calling git fetch origin"
1791             system("git fetch origin")
1793         if len(self.branch) == 0:
1794             self.branch = self.refPrefix + "master"
1795             if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
1796                 system("git update-ref %s refs/heads/p4" % self.branch)
1797                 system("git branch -D p4");
1798             # create it /after/ importing, when master exists
1799             if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
1800                 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
1802         if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
1803             self.getClientSpec()
1805         # TODO: should always look at previous commits,
1806         # merge with previous imports, if possible.
1807         if args == []:
1808             if self.hasOrigin:
1809                 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
1810             self.listExistingP4GitBranches()
1812             if len(self.p4BranchesInGit) > 1:
1813                 if not self.silent:
1814                     print "Importing from/into multiple branches"
1815                 self.detectBranches = True
1817             if self.verbose:
1818                 print "branches: %s" % self.p4BranchesInGit
1820             p4Change = 0
1821             for branch in self.p4BranchesInGit:
1822                 logMsg =  extractLogMessageFromGitCommit(self.refPrefix + branch)
1824                 settings = extractSettingsGitLog(logMsg)
1826                 self.readOptions(settings)
1827                 if (settings.has_key('depot-paths')
1828                     and settings.has_key ('change')):
1829                     change = int(settings['change']) + 1
1830                     p4Change = max(p4Change, change)
1832                     depotPaths = sorted(settings['depot-paths'])
1833                     if self.previousDepotPaths == []:
1834                         self.previousDepotPaths = depotPaths
1835                     else:
1836                         paths = []
1837                         for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
1838                             prev_list = prev.split("/")
1839                             cur_list = cur.split("/")
1840                             for i in range(0, min(len(cur_list), len(prev_list))):
1841                                 if cur_list[i] <> prev_list[i]:
1842                                     i = i - 1
1843                                     break
1845                             paths.append ("/".join(cur_list[:i + 1]))
1847                         self.previousDepotPaths = paths
1849             if p4Change > 0:
1850                 self.depotPaths = sorted(self.previousDepotPaths)
1851                 self.changeRange = "@%s,#head" % p4Change
1852                 if not self.detectBranches:
1853                     self.initialParent = parseRevision(self.branch)
1854                 if not self.silent and not self.detectBranches:
1855                     print "Performing incremental import into %s git branch" % self.branch
1857         if not self.branch.startswith("refs/"):
1858             self.branch = "refs/heads/" + self.branch
1860         if len(args) == 0 and self.depotPaths:
1861             if not self.silent:
1862                 print "Depot paths: %s" % ' '.join(self.depotPaths)
1863         else:
1864             if self.depotPaths and self.depotPaths != args:
1865                 print ("previous import used depot path %s and now %s was specified. "
1866                        "This doesn't work!" % (' '.join (self.depotPaths),
1867                                                ' '.join (args)))
1868                 sys.exit(1)
1870             self.depotPaths = sorted(args)
1872         revision = ""
1873         self.users = {}
1875         newPaths = []
1876         for p in self.depotPaths:
1877             if p.find("@") != -1:
1878                 atIdx = p.index("@")
1879                 self.changeRange = p[atIdx:]
1880                 if self.changeRange == "@all":
1881                     self.changeRange = ""
1882                 elif ',' not in self.changeRange:
1883                     revision = self.changeRange
1884                     self.changeRange = ""
1885                 p = p[:atIdx]
1886             elif p.find("#") != -1:
1887                 hashIdx = p.index("#")
1888                 revision = p[hashIdx:]
1889                 p = p[:hashIdx]
1890             elif self.previousDepotPaths == []:
1891                 revision = "#head"
1893             p = re.sub ("\.\.\.$", "", p)
1894             if not p.endswith("/"):
1895                 p += "/"
1897             newPaths.append(p)
1899         self.depotPaths = newPaths
1902         self.loadUserMapFromCache()
1903         self.labels = {}
1904         if self.detectLabels:
1905             self.getLabels();
1907         if self.detectBranches:
1908             ## FIXME - what's a P4 projectName ?
1909             self.projectName = self.guessProjectName()
1911             if self.hasOrigin:
1912                 self.getBranchMappingFromGitBranches()
1913             else:
1914                 self.getBranchMapping()
1915             if self.verbose:
1916                 print "p4-git branches: %s" % self.p4BranchesInGit
1917                 print "initial parents: %s" % self.initialParents
1918             for b in self.p4BranchesInGit:
1919                 if b != "master":
1921                     ## FIXME
1922                     b = b[len(self.projectName):]
1923                 self.createdBranches.add(b)
1925         self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
1927         importProcess = subprocess.Popen(["git", "fast-import"],
1928                                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1929                                          stderr=subprocess.PIPE);
1930         self.gitOutput = importProcess.stdout
1931         self.gitStream = importProcess.stdin
1932         self.gitError = importProcess.stderr
1934         if revision:
1935             self.importHeadRevision(revision)
1936         else:
1937             changes = []
1939             if len(self.changesFile) > 0:
1940                 output = open(self.changesFile).readlines()
1941                 changeSet = set()
1942                 for line in output:
1943                     changeSet.add(int(line))
1945                 for change in changeSet:
1946                     changes.append(change)
1948                 changes.sort()
1949             else:
1950                 # catch "git-p4 sync" with no new branches, in a repo that
1951                 # does not have any existing git-p4 branches
1952                 if len(args) == 0 and not self.p4BranchesInGit:
1953                     die("No remote p4 branches.  Perhaps you never did \"git p4 clone\" in here.");
1954                 if self.verbose:
1955                     print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
1956                                                               self.changeRange)
1957                 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
1959                 if len(self.maxChanges) > 0:
1960                     changes = changes[:min(int(self.maxChanges), len(changes))]
1962             if len(changes) == 0:
1963                 if not self.silent:
1964                     print "No changes to import!"
1965                 return True
1967             if not self.silent and not self.detectBranches:
1968                 print "Import destination: %s" % self.branch
1970             self.updatedBranches = set()
1972             self.importChanges(changes)
1974             if not self.silent:
1975                 print ""
1976                 if len(self.updatedBranches) > 0:
1977                     sys.stdout.write("Updated branches: ")
1978                     for b in self.updatedBranches:
1979                         sys.stdout.write("%s " % b)
1980                     sys.stdout.write("\n")
1982         self.gitStream.close()
1983         if importProcess.wait() != 0:
1984             die("fast-import failed: %s" % self.gitError.read())
1985         self.gitOutput.close()
1986         self.gitError.close()
1988         return True
1990 class P4Rebase(Command):
1991     def __init__(self):
1992         Command.__init__(self)
1993         self.options = [ ]
1994         self.description = ("Fetches the latest revision from perforce and "
1995                             + "rebases the current work (branch) against it")
1996         self.verbose = False
1998     def run(self, args):
1999         sync = P4Sync()
2000         sync.run([])
2002         return self.rebase()
2004     def rebase(self):
2005         if os.system("git update-index --refresh") != 0:
2006             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.");
2007         if len(read_pipe("git diff-index HEAD --")) > 0:
2008             die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2010         [upstream, settings] = findUpstreamBranchPoint()
2011         if len(upstream) == 0:
2012             die("Cannot find upstream branchpoint for rebase")
2014         # the branchpoint may be p4/foo~3, so strip off the parent
2015         upstream = re.sub("~[0-9]+$", "", upstream)
2017         print "Rebasing the current branch onto %s" % upstream
2018         oldHead = read_pipe("git rev-parse HEAD").strip()
2019         system("git rebase %s" % upstream)
2020         system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
2021         return True
2023 class P4Clone(P4Sync):
2024     def __init__(self):
2025         P4Sync.__init__(self)
2026         self.description = "Creates a new git repository and imports from Perforce into it"
2027         self.usage = "usage: %prog [options] //depot/path[@revRange]"
2028         self.options += [
2029             optparse.make_option("--destination", dest="cloneDestination",
2030                                  action='store', default=None,
2031                                  help="where to leave result of the clone"),
2032             optparse.make_option("-/", dest="cloneExclude",
2033                                  action="append", type="string",
2034                                  help="exclude depot path"),
2035             optparse.make_option("--bare", dest="cloneBare",
2036                                  action="store_true", default=False),
2037         ]
2038         self.cloneDestination = None
2039         self.needsGit = False
2040         self.cloneBare = False
2042     # This is required for the "append" cloneExclude action
2043     def ensure_value(self, attr, value):
2044         if not hasattr(self, attr) or getattr(self, attr) is None:
2045             setattr(self, attr, value)
2046         return getattr(self, attr)
2048     def defaultDestination(self, args):
2049         ## TODO: use common prefix of args?
2050         depotPath = args[0]
2051         depotDir = re.sub("(@[^@]*)$", "", depotPath)
2052         depotDir = re.sub("(#[^#]*)$", "", depotDir)
2053         depotDir = re.sub(r"\.\.\.$", "", depotDir)
2054         depotDir = re.sub(r"/$", "", depotDir)
2055         return os.path.split(depotDir)[1]
2057     def run(self, args):
2058         if len(args) < 1:
2059             return False
2061         if self.keepRepoPath and not self.cloneDestination:
2062             sys.stderr.write("Must specify destination for --keep-path\n")
2063             sys.exit(1)
2065         depotPaths = args
2067         if not self.cloneDestination and len(depotPaths) > 1:
2068             self.cloneDestination = depotPaths[-1]
2069             depotPaths = depotPaths[:-1]
2071         self.cloneExclude = ["/"+p for p in self.cloneExclude]
2072         for p in depotPaths:
2073             if not p.startswith("//"):
2074                 return False
2076         if not self.cloneDestination:
2077             self.cloneDestination = self.defaultDestination(args)
2079         print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
2081         if not os.path.exists(self.cloneDestination):
2082             os.makedirs(self.cloneDestination)
2083         chdir(self.cloneDestination)
2085         init_cmd = [ "git", "init" ]
2086         if self.cloneBare:
2087             init_cmd.append("--bare")
2088         subprocess.check_call(init_cmd)
2090         if not P4Sync.run(self, depotPaths):
2091             return False
2092         if self.branch != "master":
2093             if self.importIntoRemotes:
2094                 masterbranch = "refs/remotes/p4/master"
2095             else:
2096                 masterbranch = "refs/heads/p4/master"
2097             if gitBranchExists(masterbranch):
2098                 system("git branch master %s" % masterbranch)
2099                 if not self.cloneBare:
2100                     system("git checkout -f")
2101             else:
2102                 print "Could not detect main branch. No checkout/master branch created."
2104         return True
2106 class P4Branches(Command):
2107     def __init__(self):
2108         Command.__init__(self)
2109         self.options = [ ]
2110         self.description = ("Shows the git branches that hold imports and their "
2111                             + "corresponding perforce depot paths")
2112         self.verbose = False
2114     def run(self, args):
2115         if originP4BranchesExist():
2116             createOrUpdateBranchesFromOrigin()
2118         cmdline = "git rev-parse --symbolic "
2119         cmdline += " --remotes"
2121         for line in read_pipe_lines(cmdline):
2122             line = line.strip()
2124             if not line.startswith('p4/') or line == "p4/HEAD":
2125                 continue
2126             branch = line
2128             log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2129             settings = extractSettingsGitLog(log)
2131             print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2132         return True
2134 class HelpFormatter(optparse.IndentedHelpFormatter):
2135     def __init__(self):
2136         optparse.IndentedHelpFormatter.__init__(self)
2138     def format_description(self, description):
2139         if description:
2140             return description + "\n"
2141         else:
2142             return ""
2144 def printUsage(commands):
2145     print "usage: %s <command> [options]" % sys.argv[0]
2146     print ""
2147     print "valid commands: %s" % ", ".join(commands)
2148     print ""
2149     print "Try %s <command> --help for command specific help." % sys.argv[0]
2150     print ""
2152 commands = {
2153     "debug" : P4Debug,
2154     "submit" : P4Submit,
2155     "commit" : P4Submit,
2156     "sync" : P4Sync,
2157     "rebase" : P4Rebase,
2158     "clone" : P4Clone,
2159     "rollback" : P4RollBack,
2160     "branches" : P4Branches
2164 def main():
2165     if len(sys.argv[1:]) == 0:
2166         printUsage(commands.keys())
2167         sys.exit(2)
2169     cmd = ""
2170     cmdName = sys.argv[1]
2171     try:
2172         klass = commands[cmdName]
2173         cmd = klass()
2174     except KeyError:
2175         print "unknown command %s" % cmdName
2176         print ""
2177         printUsage(commands.keys())
2178         sys.exit(2)
2180     options = cmd.options
2181     cmd.gitdir = os.environ.get("GIT_DIR", None)
2183     args = sys.argv[2:]
2185     if len(options) > 0:
2186         options.append(optparse.make_option("--git-dir", dest="gitdir"))
2188         parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2189                                        options,
2190                                        description = cmd.description,
2191                                        formatter = HelpFormatter())
2193         (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2194     global verbose
2195     verbose = cmd.verbose
2196     if cmd.needsGit:
2197         if cmd.gitdir == None:
2198             cmd.gitdir = os.path.abspath(".git")
2199             if not isValidGitDir(cmd.gitdir):
2200                 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2201                 if os.path.exists(cmd.gitdir):
2202                     cdup = read_pipe("git rev-parse --show-cdup").strip()
2203                     if len(cdup) > 0:
2204                         chdir(cdup);
2206         if not isValidGitDir(cmd.gitdir):
2207             if isValidGitDir(cmd.gitdir + "/.git"):
2208                 cmd.gitdir += "/.git"
2209             else:
2210                 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
2212         os.environ["GIT_DIR"] = cmd.gitdir
2214     if not cmd.run(args):
2215         parser.print_help()
2218 if __name__ == '__main__':
2219     main()