Add core.quotepath configuration variable.
[git] / git-p4import.py
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
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
40 tagall = True
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\" ?")
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
264 try:
265     opts, args = getopt.getopt(sys.argv[1:], "qhvt:",
266             ["authors=","help","stitch=","timezone=","log=","ignore","notags"])
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
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
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)
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