Sean | 1d84a60 | 2006-05-24 18:04:38 -0400 | [diff] [blame] | 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 | |
| 11 | import marshal |
| 12 | import os |
| 13 | import sys |
| 14 | import time |
| 15 | import getopt |
| 16 | |
| 17 | from signal import signal, \ |
| 18 | SIGPIPE, SIGINT, SIG_DFL, \ |
| 19 | default_int_handler |
| 20 | |
| 21 | signal(SIGPIPE, SIG_DFL) |
| 22 | s = signal(SIGINT, SIG_DFL) |
| 23 | if s != default_int_handler: |
| 24 | signal(SIGINT, s) |
| 25 | |
Sean | 1d84a60 | 2006-05-24 18:04:38 -0400 | [diff] [blame] | 26 | def 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 | |
| 32 | def usage(): |
| 33 | print "USAGE: git-p4import [-q|-v] [--authors=<file>] [-t <timezone>] [//p4repo/path <branch>]" |
| 34 | sys.exit(1) |
| 35 | |
| 36 | verbosity = 1 |
| 37 | logfile = "/dev/null" |
| 38 | ignore_warnings = False |
| 39 | stitch = 0 |
Sean | ada7781 | 2006-06-15 17:26:21 -0400 | [diff] [blame] | 40 | tagall = True |
Sean | 1d84a60 | 2006-05-24 18:04:38 -0400 | [diff] [blame] | 41 | |
| 42 | def 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 | |
| 53 | class 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 | |
| 156 | class 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-db\" ?") |
| 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("repo-config --get %s" % variable)[0].rstrip() |
| 197 | except: |
| 198 | return None |
| 199 | |
| 200 | def set_config(self, variable, value): |
| 201 | try: |
| 202 | self.git("repo-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 | |
Sean | 1d84a60 | 2006-05-24 18:04:38 -0400 | [diff] [blame] | 264 | try: |
| 265 | opts, args = getopt.getopt(sys.argv[1:], "qhvt:", |
Sean | ada7781 | 2006-06-15 17:26:21 -0400 | [diff] [blame] | 266 | ["authors=","help","stitch=","timezone=","log=","ignore","notags"]) |
Sean | 1d84a60 | 2006-05-24 18:04:38 -0400 | [diff] [blame] | 267 | except getopt.GetoptError: |
| 268 | usage() |
| 269 | |
| 270 | for 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 |
Sean | ada7781 | 2006-06-15 17:26:21 -0400 | [diff] [blame] | 277 | if o in ("--notags"): |
| 278 | tagall = False |
Sean | 1d84a60 | 2006-05-24 18:04:38 -0400 | [diff] [blame] | 279 | if o in ("-h", "--help"): |
| 280 | usage() |
| 281 | if o in ("--ignore"): |
| 282 | ignore_warnings = True |
| 283 | |
| 284 | git = git_command() |
| 285 | branch=git.current_branch() |
| 286 | |
| 287 | for 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 | |
| 294 | if 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]) |
| 301 | elif len(args) != 0: |
| 302 | die("You must specify the perforce //depot/path and git branch") |
| 303 | |
| 304 | p4path = git.get_config("perforce.%s.path" % branch) |
| 305 | if p4path == None: |
| 306 | die("Do not know Perforce //depot/path for git branch", branch) |
| 307 | |
| 308 | p4 = p4_command(p4path) |
| 309 | |
| 310 | for o, a in opts: |
| 311 | if o in ("-a", "--authors"): |
| 312 | p4.authors(a) |
| 313 | |
| 314 | localdir = git.basedir() |
| 315 | if 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 | |
| 321 | if stitch == 0: |
| 322 | top = git.top_change(branch) |
| 323 | else: |
| 324 | top = 0 |
| 325 | changes = p4.changes(top) |
| 326 | count = len(changes) |
| 327 | if count == 0: |
| 328 | report(1, "Already up to date...") |
| 329 | sys.exit(0) |
| 330 | |
| 331 | ptz = git.get_config("perforce.timezone") |
| 332 | if ptz: |
| 333 | report(1, "Setting timezone to", ptz) |
| 334 | os.environ['TZ'] = ptz |
| 335 | time.tzset() |
| 336 | |
| 337 | if stitch == 1: |
| 338 | git.remove_files() |
| 339 | git.clean_directories() |
| 340 | p4.sync(changes[0], force=True) |
| 341 | elif 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) |
| 346 | else: |
| 347 | p4.sync(changes[0], trick=True) |
| 348 | |
| 349 | report(1, "processing %s changes from p4 (%s) to git (%s)" % (count, p4.repopath, branch)) |
| 350 | for id in changes: |
| 351 | report(1, "Importing changeset", id) |
| 352 | change = p4.describe(id) |
| 353 | p4.sync(id) |
Sean | ada7781 | 2006-06-15 17:26:21 -0400 | [diff] [blame] | 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") |
Sean | 1d84a60 | 2006-05-24 18:04:38 -0400 | [diff] [blame] | 358 | if stitch == 1: |
| 359 | git.clean_directories() |
| 360 | stitch = 0 |
| 361 | |