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