git-p4import.pyon commit git-svn: add support for --stat in the log command (488a63e)
   1#!/usr/bin/python
   2#
   3# This tool is copyright (c) 2006, Sean Estabrooks.
   4# It is released under the Gnu Public License, version 2.
   5#
   6# Import Perforce branches into Git repositories.
   7# Checking out the files is done by calling the standard p4
   8# client which you must have properly configured yourself
   9#
  10
  11import marshal
  12import os
  13import sys
  14import time
  15import getopt
  16
  17from signal import signal, \
  18   SIGPIPE, SIGINT, SIG_DFL, \
  19   default_int_handler
  20
  21signal(SIGPIPE, SIG_DFL)
  22s = signal(SIGINT, SIG_DFL)
  23if s != default_int_handler:
  24   signal(SIGINT, s)
  25
  26def die(msg, *args):
  27    for a in args:
  28        msg = "%s %s" % (msg, a)
  29    print "git-p4import fatal error:", msg
  30    sys.exit(1)
  31
  32def usage():
  33    print "USAGE: git-p4import [-q|-v]  [--authors=<file>]  [-t <timezone>]  [//p4repo/path <branch>]"
  34    sys.exit(1)
  35
  36verbosity = 1
  37logfile = "/dev/null"
  38ignore_warnings = False
  39stitch = 0
  40tagall = True
  41
  42def report(level, msg, *args):
  43    global verbosity
  44    global logfile
  45    for a in args:
  46        msg = "%s %s" % (msg, a)
  47    fd = open(logfile, "a")
  48    fd.writelines(msg)
  49    fd.close()
  50    if level <= verbosity:
  51        print msg
  52
  53class p4_command:
  54    def __init__(self, _repopath):
  55        try:
  56            global logfile
  57            self.userlist = {}
  58            if _repopath[-1] == '/':
  59                self.repopath = _repopath[:-1]
  60            else:
  61                self.repopath = _repopath
  62            if self.repopath[-4:] != "/...":
  63                self.repopath= "%s/..." % self.repopath
  64            f=os.popen('p4 -V 2>>%s'%logfile, 'rb')
  65            a = f.readlines()
  66            if f.close():
  67                raise
  68        except:
  69                die("Could not find the \"p4\" command")
  70
  71    def p4(self, cmd, *args):
  72        global logfile
  73        cmd = "%s %s" % (cmd, ' '.join(args))
  74        report(2, "P4:", cmd)
  75        f=os.popen('p4 -G %s 2>>%s' % (cmd,logfile), 'rb')
  76        list = []
  77        while 1:
  78           try:
  79                list.append(marshal.load(f))
  80           except EOFError:
  81                break
  82        self.ret = f.close()
  83        return list
  84
  85    def sync(self, id, force=False, trick=False, test=False):
  86        if force:
  87            ret = self.p4("sync -f %s@%s"%(self.repopath, id))[0]
  88        elif trick:
  89            ret = self.p4("sync -k %s@%s"%(self.repopath, id))[0]
  90        elif test:
  91            ret = self.p4("sync -n %s@%s"%(self.repopath, id))[0]
  92        else:
  93            ret = self.p4("sync    %s@%s"%(self.repopath, id))[0]
  94        if ret['code'] == "error":
  95             data = ret['data'].upper()
  96             if data.find('VIEW') > 0:
  97                 die("Perforce reports %s is not in client view"% self.repopath)
  98             elif data.find('UP-TO-DATE') < 0:
  99                 die("Could not sync files from perforce", self.repopath)
 100
 101    def changes(self, since=0):
 102        try:
 103            list = []
 104            for rec in self.p4("changes %s@%s,#head" % (self.repopath, since+1)):
 105                list.append(rec['change'])
 106            list.reverse()
 107            return list
 108        except:
 109            return []
 110
 111    def authors(self, filename):
 112        f=open(filename)
 113        for l in f.readlines():
 114            self.userlist[l[:l.find('=')].rstrip()] = \
 115                    (l[l.find('=')+1:l.find('<')].rstrip(),l[l.find('<')+1:l.find('>')])
 116        f.close()
 117        for f,e in self.userlist.items():
 118                report(2, f, ":", e[0], "  <", e[1], ">")
 119
 120    def _get_user(self, id):
 121        if not self.userlist.has_key(id):
 122            try:
 123                user = self.p4("users", id)[0]
 124                self.userlist[id] = (user['FullName'], user['Email'])
 125            except:
 126                self.userlist[id] = (id, "")
 127        return self.userlist[id]
 128
 129    def _format_date(self, ticks):
 130        symbol='+'
 131        name = time.tzname[0]
 132        offset = time.timezone
 133        if ticks[8]:
 134            name = time.tzname[1]
 135            offset = time.altzone
 136        if offset < 0:
 137            offset *= -1
 138            symbol = '-'
 139        localo = "%s%02d%02d %s" % (symbol, offset / 3600, offset % 3600, name)
 140        tickso = time.strftime("%a %b %d %H:%M:%S %Y", ticks)
 141        return "%s %s" % (tickso, localo)
 142
 143    def where(self):
 144        try:
 145            return self.p4("where %s" % self.repopath)[-1]['path']
 146        except:
 147            return ""
 148
 149    def describe(self, num):
 150        desc = self.p4("describe -s", num)[0]
 151        self.msg = desc['desc']
 152        self.author, self.email = self._get_user(desc['user'])
 153        self.date = self._format_date(time.localtime(long(desc['time'])))
 154        return self
 155
 156class git_command:
 157    def __init__(self):
 158        try:
 159            self.version = self.git("--version")[0][12:].rstrip()
 160        except:
 161            die("Could not find the \"git\" command")
 162        try:
 163            self.gitdir = self.get_single("rev-parse --git-dir")
 164            report(2, "gdir:", self.gitdir)
 165        except:
 166            die("Not a git repository... did you forget to \"git init\" ?")
 167        try:
 168            self.cdup = self.get_single("rev-parse --show-cdup")
 169            if self.cdup != "":
 170                os.chdir(self.cdup)
 171            self.topdir = os.getcwd()
 172            report(2, "topdir:", self.topdir)
 173        except:
 174            die("Could not find top git directory")
 175
 176    def git(self, cmd):
 177        global logfile
 178        report(2, "GIT:", cmd)
 179        f=os.popen('git %s 2>>%s' % (cmd,logfile), 'rb')
 180        r=f.readlines()
 181        self.ret = f.close()
 182        return r
 183
 184    def get_single(self, cmd):
 185        return self.git(cmd)[0].rstrip()
 186
 187    def current_branch(self):
 188        try:
 189            testit = self.git("rev-parse --verify HEAD")[0]
 190            return self.git("symbolic-ref HEAD")[0][11:].rstrip()
 191        except:
 192            return None
 193
 194    def get_config(self, variable):
 195        try:
 196            return self.git("config --get %s" % variable)[0].rstrip()
 197        except:
 198            return None
 199
 200    def set_config(self, variable, value):
 201        try:
 202            self.git("config %s %s"%(variable, value) )
 203        except:
 204            die("Could not set %s to " % variable, value)
 205
 206    def make_tag(self, name, head):
 207        self.git("tag -f %s %s"%(name,head))
 208
 209    def top_change(self, branch):
 210        try:
 211            a=self.get_single("name-rev --tags refs/heads/%s" % branch)
 212            loc = a.find(' tags/') + 6
 213            if a[loc:loc+3] != "p4/":
 214                raise
 215            return int(a[loc+3:][:-2])
 216        except:
 217            return 0
 218
 219    def update_index(self):
 220        self.git("ls-files -m -d -o -z | git update-index --add --remove -z --stdin")
 221
 222    def checkout(self, branch):
 223        self.git("checkout %s" % branch)
 224
 225    def repoint_head(self, branch):
 226        self.git("symbolic-ref HEAD refs/heads/%s" % branch)
 227
 228    def remove_files(self):
 229        self.git("ls-files | xargs rm")
 230
 231    def clean_directories(self):
 232        self.git("clean -d")
 233
 234    def fresh_branch(self, branch):
 235        report(1, "Creating new branch", branch)
 236        self.git("ls-files | xargs rm")
 237        os.remove(".git/index")
 238        self.repoint_head(branch)
 239        self.git("clean -d")
 240
 241    def basedir(self):
 242        return self.topdir
 243
 244    def commit(self, author, email, date, msg, id):
 245        self.update_index()
 246        fd=open(".msg", "w")
 247        fd.writelines(msg)
 248        fd.close()
 249        try:
 250                current = self.get_single("rev-parse --verify HEAD")
 251                head = "-p HEAD"
 252        except:
 253                current = ""
 254                head = ""
 255        tree = self.get_single("write-tree")
 256        for r,l in [('DATE',date),('NAME',author),('EMAIL',email)]:
 257            os.environ['GIT_AUTHOR_%s'%r] = l
 258            os.environ['GIT_COMMITTER_%s'%r] = l
 259        commit = self.get_single("commit-tree %s %s < .msg" % (tree,head))
 260        os.remove(".msg")
 261        self.make_tag("p4/%s"%id, commit)
 262        self.git("update-ref HEAD %s %s" % (commit, current) )
 263
 264try:
 265    opts, args = getopt.getopt(sys.argv[1:], "qhvt:",
 266            ["authors=","help","stitch=","timezone=","log=","ignore","notags"])
 267except getopt.GetoptError:
 268    usage()
 269
 270for o, a in opts:
 271    if o == "-q":
 272        verbosity = 0
 273    if o == "-v":
 274        verbosity += 1
 275    if o in ("--log"):
 276        logfile = a
 277    if o in ("--notags"):
 278        tagall = False
 279    if o in ("-h", "--help"):
 280        usage()
 281    if o in ("--ignore"):
 282        ignore_warnings = True
 283
 284git = git_command()
 285branch=git.current_branch()
 286
 287for o, a in opts:
 288    if o in ("-t", "--timezone"):
 289        git.set_config("perforce.timezone", a)
 290    if o in ("--stitch"):
 291        git.set_config("perforce.%s.path" % branch, a)
 292        stitch = 1
 293
 294if len(args) == 2:
 295    branch = args[1]
 296    git.checkout(branch)
 297    if branch == git.current_branch():
 298        die("Branch %s already exists!" % branch)
 299    report(1, "Setting perforce to ", args[0])
 300    git.set_config("perforce.%s.path" % branch, args[0])
 301elif len(args) != 0:
 302    die("You must specify the perforce //depot/path and git branch")
 303
 304p4path = git.get_config("perforce.%s.path" % branch)
 305if p4path == None:
 306    die("Do not know Perforce //depot/path for git branch", branch)
 307
 308p4 = p4_command(p4path)
 309
 310for o, a in opts:
 311    if o in ("-a", "--authors"):
 312        p4.authors(a)
 313
 314localdir = git.basedir()
 315if p4.where()[:len(localdir)] != localdir:
 316    report(1, "**WARNING** Appears p4 client is misconfigured")
 317    report(1, "   for sync from %s to %s" % (p4.repopath, localdir))
 318    if ignore_warnings != True:
 319        die("Reconfigure or use \"--ignore\" on command line")
 320
 321if stitch == 0:
 322    top = git.top_change(branch)
 323else:
 324    top = 0
 325changes = p4.changes(top)
 326count = len(changes)
 327if count == 0:
 328    report(1, "Already up to date...")
 329    sys.exit(0)
 330
 331ptz = git.get_config("perforce.timezone")
 332if ptz:
 333    report(1, "Setting timezone to", ptz)
 334    os.environ['TZ'] = ptz
 335    time.tzset()
 336
 337if stitch == 1:
 338    git.remove_files()
 339    git.clean_directories()
 340    p4.sync(changes[0], force=True)
 341elif top == 0 and branch != git.current_branch():
 342    p4.sync(changes[0], test=True)
 343    report(1, "Creating new initial commit");
 344    git.fresh_branch(branch)
 345    p4.sync(changes[0], force=True)
 346else:
 347    p4.sync(changes[0], trick=True)
 348
 349report(1, "processing %s changes from p4 (%s) to git (%s)" % (count, p4.repopath, branch))
 350for id in changes:
 351    report(1, "Importing changeset", id)
 352    change = p4.describe(id)
 353    p4.sync(id)
 354    if tagall :
 355            git.commit(change.author, change.email, change.date, change.msg, id)
 356    else:
 357            git.commit(change.author, change.email, change.date, change.msg, "import")
 358    if stitch == 1:
 359        git.clean_directories()
 360        stitch = 0
 361