]> git.ipfire.org Git - thirdparty/git.git/blame - contrib/fast-import/git-p4
git-p4: small improvements to user-preservation
[thirdparty/git.git] / contrib / fast-import / git-p4
CommitLineData
86949eef
SH
1#!/usr/bin/env python
2#
3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
4#
c8cbbee9
SH
5# Author: Simon Hausmann <simon@lst.de>
6# Copyright: 2007 Simon Hausmann <simon@lst.de>
83dce55a 7# 2007 Trolltech ASA
86949eef
SH
8# License: MIT <http://www.opensource.org/licenses/mit-license.php>
9#
10
1d7367dc
RG
11import optparse, sys, os, marshal, subprocess, shelve
12import tempfile, getopt, os.path, time, platform
ce6f33c8 13import re
8b41a97f 14
4addad22 15verbose = False
86949eef 16
21a50753
AK
17
18def p4_build_cmd(cmd):
19 """Build a suitable p4 command line.
20
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 """
abcaf073
AK
25 real_cmd = "%s " % "p4"
26
27 user = gitConfig("git-p4.user")
28 if len(user) > 0:
29 real_cmd += "-u %s " % user
30
31 password = gitConfig("git-p4.password")
32 if len(password) > 0:
33 real_cmd += "-P %s " % password
34
35 port = gitConfig("git-p4.port")
36 if len(port) > 0:
37 real_cmd += "-p %s " % port
38
39 host = gitConfig("git-p4.host")
40 if len(host) > 0:
41 real_cmd += "-h %s " % host
42
43 client = gitConfig("git-p4.client")
44 if len(client) > 0:
45 real_cmd += "-c %s " % client
46
47 real_cmd += "%s" % (cmd)
ee06427a
AK
48 if verbose:
49 print real_cmd
21a50753
AK
50 return real_cmd
51
053fd0c1
RB
52def chdir(dir):
53 if os.name == 'nt':
54 os.environ['PWD']=dir
55 os.chdir(dir)
56
86dff6b6
HWN
57def die(msg):
58 if verbose:
59 raise Exception(msg)
60 else:
61 sys.stderr.write(msg + "\n")
62 sys.exit(1)
63
bce4c5fc 64def write_pipe(c, str):
4addad22 65 if verbose:
86dff6b6 66 sys.stderr.write('Writing pipe: %s\n' % c)
b016d397 67
bce4c5fc 68 pipe = os.popen(c, 'w')
b016d397 69 val = pipe.write(str)
bce4c5fc 70 if pipe.close():
86dff6b6 71 die('Command failed: %s' % c)
b016d397
HWN
72
73 return val
74
d9429194
AK
75def p4_write_pipe(c, str):
76 real_cmd = p4_build_cmd(c)
893d340f 77 return write_pipe(real_cmd, str)
d9429194 78
4addad22
HWN
79def read_pipe(c, ignore_error=False):
80 if verbose:
86dff6b6 81 sys.stderr.write('Reading pipe: %s\n' % c)
8b41a97f 82
bce4c5fc 83 pipe = os.popen(c, 'rb')
b016d397 84 val = pipe.read()
4addad22 85 if pipe.close() and not ignore_error:
86dff6b6 86 die('Command failed: %s' % c)
b016d397
HWN
87
88 return val
89
d9429194
AK
90def p4_read_pipe(c, ignore_error=False):
91 real_cmd = p4_build_cmd(c)
92 return read_pipe(real_cmd, ignore_error)
b016d397 93
bce4c5fc 94def read_pipe_lines(c):
4addad22 95 if verbose:
86dff6b6 96 sys.stderr.write('Reading pipe: %s\n' % c)
b016d397 97 ## todo: check return status
bce4c5fc 98 pipe = os.popen(c, 'rb')
b016d397 99 val = pipe.readlines()
bce4c5fc 100 if pipe.close():
86dff6b6 101 die('Command failed: %s' % c)
b016d397
HWN
102
103 return val
caace111 104
2318121b
AK
105def p4_read_pipe_lines(c):
106 """Specifically invoke p4 on the command supplied. """
155af834 107 real_cmd = p4_build_cmd(c)
2318121b
AK
108 return read_pipe_lines(real_cmd)
109
6754a299 110def system(cmd):
4addad22 111 if verbose:
bb6e09b2 112 sys.stderr.write("executing %s\n" % cmd)
6754a299
HWN
113 if os.system(cmd) != 0:
114 die("command failed: %s" % cmd)
115
bf9320f1
AK
116def p4_system(cmd):
117 """Specifically invoke p4 as the system command. """
155af834 118 real_cmd = p4_build_cmd(cmd)
bf9320f1
AK
119 return system(real_cmd)
120
b9fc6ea9
DB
121def isP4Exec(kind):
122 """Determine if a Perforce 'kind' should have execute permission
123
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)
128
c65b670e
CP
129def 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.
132
133 p4Type = "+x"
134
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]
141
87b611d5 142 p4_system("reopen -t %s %s" % (p4Type, file))
c65b670e
CP
143
144def getP4OpenedType(file):
145 # Returns the perforce file type for the given file.
146
a7d3ef9d 147 result = p4_read_pipe("opened %s" % file)
f3e5ae4f 148 match = re.match(".*\((.+)\)\r?$", result)
c65b670e
CP
149 if match:
150 return match.group(1)
151 else:
f3e5ae4f 152 die("Could not determine file type for %s (result: '%s')" % (file, result))
c65b670e 153
b43b0a3c
CP
154def 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
160
161def parseDiffTreeEntry(entry):
162 """Parses a single diff tree entry into its component elements.
163
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:
166
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.
177
178 If the pattern is not matched, None is returned."""
179
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
193
c65b670e
CP
194def isModeExec(mode):
195 # Returns True if the given git mode represents an executable file,
196 # otherwise False.
197 return mode[-3:] == "755"
198
199def isModeExecChanged(src_mode, dst_mode):
200 return isModeExec(src_mode) != isModeExec(dst_mode)
201
b932705b 202def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
155af834 203 cmd = p4_build_cmd("-G %s" % (cmd))
6a49f8e2
HWN
204 if verbose:
205 sys.stderr.write("Opening pipe: %s\n" % cmd)
9f90c733
SL
206
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)
216
217 p4 = subprocess.Popen(cmd, shell=True,
218 stdin=stdin_file,
219 stdout=subprocess.PIPE)
86949eef
SH
220
221 result = []
222 try:
223 while True:
9f90c733 224 entry = marshal.load(p4.stdout)
c3f6163b
AG
225 if cb is not None:
226 cb(entry)
227 else:
228 result.append(entry)
86949eef
SH
229 except EOFError:
230 pass
9f90c733
SL
231 exitCode = p4.wait()
232 if exitCode != 0:
ac3e0d79
SH
233 entry = {}
234 entry["p4ExitCode"] = exitCode
235 result.append(entry)
86949eef
SH
236
237 return result
238
239def p4Cmd(cmd):
240 list = p4CmdList(cmd)
241 result = {}
242 for entry in list:
243 result.update(entry)
244 return result;
245
cb2c9db5
SH
246def p4Where(depotPath):
247 if not depotPath.endswith("/"):
248 depotPath += "/"
7f705dc3
TAL
249 depotPath = depotPath + "..."
250 outputList = p4CmdList("where %s" % depotPath)
251 output = None
252 for entry in outputList:
75bc9573
TAL
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
7f705dc3
TAL
263 if output == None:
264 return ""
dc524036
SH
265 if output["code"] == "error":
266 return ""
cb2c9db5
SH
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:]
274
275 if clientPath.endswith("..."):
276 clientPath = clientPath[:-3]
277 return clientPath
278
86949eef 279def currentGitBranch():
b25b2065 280 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
86949eef 281
4f5cf76a 282def isValidGitDir(path):
bb6e09b2
HWN
283 if (os.path.exists(path + "/HEAD")
284 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
4f5cf76a
SH
285 return True;
286 return False
287
463e8af6 288def parseRevision(ref):
b25b2065 289 return read_pipe("git rev-parse %s" % ref).strip()
463e8af6 290
6ae8de88
SH
291def extractLogMessageFromGitCommit(commit):
292 logMessage = ""
b016d397
HWN
293
294 ## fixme: title is first line of commit, not 1st paragraph.
6ae8de88 295 foundTitle = False
b016d397 296 for log in read_pipe_lines("git cat-file commit %s" % commit):
6ae8de88
SH
297 if not foundTitle:
298 if len(log) == 1:
1c094184 299 foundTitle = True
6ae8de88
SH
300 continue
301
302 logMessage += log
303 return logMessage
304
bb6e09b2 305def extractSettingsGitLog(log):
6ae8de88
SH
306 values = {}
307 for line in log.split("\n"):
308 line = line.strip()
6326aa58
HWN
309 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
310 if not m:
311 continue
312
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]
320
321 values[key] = val
322
845b42cb
SH
323 paths = values.get("depot-paths")
324 if not paths:
325 paths = values.get("depot-path")
a3fdd579
SH
326 if paths:
327 values['depot-paths'] = paths.split(',')
bb6e09b2 328 return values
6ae8de88 329
8136a639 330def gitBranchExists(branch):
bb6e09b2
HWN
331 proc = subprocess.Popen(["git", "rev-parse", branch],
332 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
caace111 333 return proc.wait() == 0;
8136a639 334
36bd8446 335_gitConfig = {}
99f790f2 336def gitConfig(key, args = None): # set args to "--bool", for instance
36bd8446 337 if not _gitConfig.has_key(key):
99f790f2
TAL
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()
36bd8446 343 return _gitConfig[key]
01265103 344
062410bb
SH
345def p4BranchesInGit(branchesAreInRemotes = True):
346 branches = {}
347
348 cmdline = "git rev-parse --symbolic "
349 if branchesAreInRemotes:
350 cmdline += " --remotes"
351 else:
352 cmdline += " --branches"
353
354 for line in read_pipe_lines(cmdline):
355 line = line.strip()
356
357 ## only import to p4/
358 if not line.startswith('p4/') or line == "p4/HEAD":
359 continue
360 branch = line
361
362 # strip off p4
363 branch = re.sub ("^p4/", "", line)
364
365 branches[branch] = parseRevision(line)
366 return branches
367
9ceab363 368def findUpstreamBranchPoint(head = "HEAD"):
86506fe5
SH
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
379
27d2d811 380 settings = None
27d2d811
SH
381 parent = 0
382 while parent < 65535:
9ceab363 383 commit = head + "~%s" % parent
27d2d811
SH
384 log = extractLogMessageFromGitCommit(commit)
385 settings = extractSettingsGitLog(log)
86506fe5
SH
386 if settings.has_key("depot-paths"):
387 paths = ",".join(settings["depot-paths"])
388 if branchByDepotPath.has_key(paths):
389 return [branchByDepotPath[paths], settings]
27d2d811 390
86506fe5 391 parent = parent + 1
27d2d811 392
86506fe5 393 return ["", settings]
27d2d811 394
5ca44617
SH
395def 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)
399
400 originPrefix = "origin/p4/"
401
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
406
407 headName = line[len(originPrefix):]
408 remoteHead = localRefPrefix + headName
409 originHead = line
410
411 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
412 if (not original.has_key('depot-paths')
413 or not original.has_key('change')):
414 continue
415
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'])))
438
439 if update:
440 system("git update-ref %s %s" % (remoteHead, originHead))
441
442def originP4BranchesExist():
443 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
444
4f6432d8
SH
445def p4ChangesForPaths(depotPaths, changeRange):
446 assert depotPaths
b340fa43 447 output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
4f6432d8
SH
448 for p in depotPaths]))
449
b4b0ba06 450 changes = {}
4f6432d8 451 for line in output:
c3f6163b
AG
452 changeNum = int(line.split(" ")[1])
453 changes[changeNum] = True
4f6432d8 454
b4b0ba06
PW
455 changelist = changes.keys()
456 changelist.sort()
457 return changelist
4f6432d8 458
d53de8b9
TAL
459def 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)
471
b984733c
SH
472class Command:
473 def __init__(self):
474 self.usage = "usage: %prog [options]"
8910ac0e 475 self.needsGit = True
b984733c 476
3ea2cfd4
LD
477class P4UserMap:
478 def __init__(self):
479 self.userMapFromPerforceServer = False
480
481 def getUserCacheFilename(self):
482 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
483 return home + "/.gitp4-usercache.txt"
484
485 def getUserMapFromPerforceServer(self):
486 if self.userMapFromPerforceServer:
487 return
488 self.users = {}
489 self.emails = {}
490
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"]
496
497
498 s = ''
499 for (key, val) in self.users.items():
500 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
501
502 open(self.getUserCacheFilename(), "wb").write(s)
503 self.userMapFromPerforceServer = True
504
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()
517
b984733c 518class P4Debug(Command):
86949eef 519 def __init__(self):
6ae8de88 520 Command.__init__(self)
86949eef 521 self.options = [
b1ce9447
HWN
522 optparse.make_option("--verbose", dest="verbose", action="store_true",
523 default=False),
4addad22 524 ]
c8c39116 525 self.description = "A tool to debug the output of p4 -G."
8910ac0e 526 self.needsGit = False
b1ce9447 527 self.verbose = False
86949eef
SH
528
529 def run(self, args):
b1ce9447 530 j = 0
86949eef 531 for output in p4CmdList(" ".join(args)):
b1ce9447
HWN
532 print 'Element: %d' % j
533 j += 1
86949eef 534 print output
b984733c 535 return True
86949eef 536
5834684d
SH
537class P4RollBack(Command):
538 def __init__(self):
539 Command.__init__(self)
540 self.options = [
0c66a783
SH
541 optparse.make_option("--verbose", dest="verbose", action="store_true"),
542 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
5834684d
SH
543 ]
544 self.description = "A tool to debug the multi-branch import. Don't use :)"
52102d47 545 self.verbose = False
0c66a783 546 self.rollbackLocalBranches = False
5834684d
SH
547
548 def run(self, args):
549 if len(args) != 1:
550 return False
551 maxChange = int(args[0])
0c66a783 552
ad192f28 553 if "p4ExitCode" in p4Cmd("changes -m 1"):
66a2f523
SH
554 die("Problems executing p4");
555
0c66a783
SH
556 if self.rollbackLocalBranches:
557 refPrefix = "refs/heads/"
b016d397 558 lines = read_pipe_lines("git rev-parse --symbolic --branches")
0c66a783
SH
559 else:
560 refPrefix = "refs/remotes/"
b016d397 561 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
0c66a783
SH
562
563 for line in lines:
564 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
b25b2065
HWN
565 line = line.strip()
566 ref = refPrefix + line
5834684d 567 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
568 settings = extractSettingsGitLog(log)
569
570 depotPaths = settings['depot-paths']
571 change = settings['change']
572
5834684d 573 changed = False
52102d47 574
6326aa58
HWN
575 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
576 for p in depotPaths]))) == 0:
52102d47
SH
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
580
bb6e09b2 581 while change and int(change) > maxChange:
5834684d 582 changed = True
52102d47
SH
583 if self.verbose:
584 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
5834684d
SH
585 system("git update-ref %s \"%s^\"" % (ref, ref))
586 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
587 settings = extractSettingsGitLog(log)
588
589
590 depotPaths = settings['depot-paths']
591 change = settings['change']
5834684d
SH
592
593 if changed:
52102d47 594 print "%s rewound to %s" % (ref, change)
5834684d
SH
595
596 return True
597
3ea2cfd4 598class P4Submit(Command, P4UserMap):
4f5cf76a 599 def __init__(self):
b984733c 600 Command.__init__(self)
3ea2cfd4 601 P4UserMap.__init__(self)
4f5cf76a 602 self.options = [
4addad22 603 optparse.make_option("--verbose", dest="verbose", action="store_true"),
4f5cf76a 604 optparse.make_option("--origin", dest="origin"),
ae901090 605 optparse.make_option("-M", dest="detectRenames", action="store_true"),
3ea2cfd4
LD
606 # preserve the user, requires relevant p4 permissions
607 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
4f5cf76a
SH
608 ]
609 self.description = "Submit changes from git to the perforce depot."
c9b50e63 610 self.usage += " [name of git branch to submit into perforce depot]"
4f5cf76a 611 self.interactive = True
9512497b 612 self.origin = ""
ae901090 613 self.detectRenames = False
b0d10df7 614 self.verbose = False
3ea2cfd4 615 self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
f7baba8b 616 self.isWindows = (platform.system() == "Windows")
4f5cf76a 617
4f5cf76a
SH
618 def check(self):
619 if len(p4CmdList("opened ...")) > 0:
620 die("You have files opened with perforce! Close them before starting the sync.")
621
edae1e2f
SH
622 # replaces everything between 'Description:' and the next P4 submit template field with the
623 # commit message
4f5cf76a
SH
624 def prepareLogMessage(self, template, message):
625 result = ""
626
edae1e2f
SH
627 inDescriptionSection = False
628
4f5cf76a
SH
629 for line in template.split("\n"):
630 if line.startswith("#"):
631 result += line + "\n"
632 continue
633
edae1e2f 634 if inDescriptionSection:
c9dbab04 635 if line.startswith("Files:") or line.startswith("Jobs:"):
edae1e2f
SH
636 inDescriptionSection = False
637 else:
638 continue
639 else:
640 if line.startswith("Description:"):
641 inDescriptionSection = True
642 line += "\n"
643 for messageLine in message.split("\n"):
644 line += "\t" + messageLine + "\n"
645
646 result += line + "\n"
4f5cf76a
SH
647
648 return result
649
3ea2cfd4
LD
650 def p4UserForCommit(self,id):
651 # Return the tuple (perforce user,git email) for a given git commit id
652 self.getUserMapFromPerforceServer()
653 gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
654 gitEmail = gitEmail.strip()
655 if not self.emails.has_key(gitEmail):
656 return (None,gitEmail)
657 else:
658 return (self.emails[gitEmail],gitEmail)
659
660 def checkValidP4Users(self,commits):
661 # check if any git authors cannot be mapped to p4 users
662 for id in commits:
663 (user,email) = self.p4UserForCommit(id)
664 if not user:
665 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
666 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
667 print "%s" % msg
668 else:
669 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
670
671 def lastP4Changelist(self):
672 # Get back the last changelist number submitted in this client spec. This
673 # then gets used to patch up the username in the change. If the same
674 # client spec is being used by multiple processes then this might go
675 # wrong.
676 results = p4CmdList("client -o") # find the current client
677 client = None
678 for r in results:
679 if r.has_key('Client'):
680 client = r['Client']
681 break
682 if not client:
683 die("could not get client spec")
684 results = p4CmdList("changes -c %s -m 1" % client)
685 for r in results:
686 if r.has_key('change'):
687 return r['change']
688 die("Could not get changelist number for last submit - cannot patch up user details")
689
690 def modifyChangelistUser(self, changelist, newUser):
691 # fixup the user field of a changelist after it has been submitted.
692 changes = p4CmdList("change -o %s" % changelist)
ecdba36d
LD
693 if len(changes) != 1:
694 die("Bad output from p4 change modifying %s to user %s" %
695 (changelist, newUser))
696
697 c = changes[0]
698 if c['User'] == newUser: return # nothing to do
699 c['User'] = newUser
700 input = marshal.dumps(c)
701
3ea2cfd4
LD
702 result = p4CmdList("change -f -i", stdin=input)
703 for r in result:
704 if r.has_key('code'):
705 if r['code'] == 'error':
706 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
707 if r.has_key('data'):
708 print("Updated user field for changelist %s to %s" % (changelist, newUser))
709 return
710 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
711
712 def canChangeChangelists(self):
713 # check to see if we have p4 admin or super-user permissions, either of
714 # which are required to modify changelists.
ecdba36d 715 results = p4CmdList("protects %s" % self.depotPath)
3ea2cfd4
LD
716 for r in results:
717 if r.has_key('perm'):
718 if r['perm'] == 'admin':
719 return 1
720 if r['perm'] == 'super':
721 return 1
722 return 0
723
ea99c3ae
SH
724 def prepareSubmitTemplate(self):
725 # remove lines in the Files section that show changes to files outside the depot path we're committing into
726 template = ""
727 inFilesSection = False
b340fa43 728 for line in p4_read_pipe_lines("change -o"):
f3e5ae4f
MSO
729 if line.endswith("\r\n"):
730 line = line[:-2] + "\n"
ea99c3ae
SH
731 if inFilesSection:
732 if line.startswith("\t"):
733 # path starts and ends with a tab
734 path = line[1:]
735 lastTab = path.rfind("\t")
736 if lastTab != -1:
737 path = path[:lastTab]
d53de8b9 738 if not p4PathStartsWith(path, self.depotPath):
ea99c3ae
SH
739 continue
740 else:
741 inFilesSection = False
742 else:
743 if line.startswith("Files:"):
744 inFilesSection = True
745
746 template += line
747
748 return template
749
7cb5cbef 750 def applyCommit(self, id):
0e36f2d7 751 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
ae901090 752
3ea2cfd4
LD
753 if self.preserveUser:
754 (p4User, gitEmail) = self.p4UserForCommit(id)
755
ae901090
VA
756 if not self.detectRenames:
757 # If not explicitly set check the config variable
758 self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
759
760 if self.detectRenames:
761 diffOpts = "-M"
762 else:
763 diffOpts = ""
764
4fddb41b
VA
765 if gitConfig("git-p4.detectCopies").lower() == "true":
766 diffOpts += " -C"
767
768 if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
769 diffOpts += " --find-copies-harder"
770
0e36f2d7 771 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
4f5cf76a
SH
772 filesToAdd = set()
773 filesToDelete = set()
d336c158 774 editedFiles = set()
c65b670e 775 filesToChangeExecBit = {}
4f5cf76a 776 for line in diff:
b43b0a3c
CP
777 diff = parseDiffTreeEntry(line)
778 modifier = diff['status']
779 path = diff['src']
4f5cf76a 780 if modifier == "M":
87b611d5 781 p4_system("edit \"%s\"" % path)
c65b670e
CP
782 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
783 filesToChangeExecBit[path] = diff['dst_mode']
d336c158 784 editedFiles.add(path)
4f5cf76a
SH
785 elif modifier == "A":
786 filesToAdd.add(path)
c65b670e 787 filesToChangeExecBit[path] = diff['dst_mode']
4f5cf76a
SH
788 if path in filesToDelete:
789 filesToDelete.remove(path)
790 elif modifier == "D":
791 filesToDelete.add(path)
792 if path in filesToAdd:
793 filesToAdd.remove(path)
4fddb41b
VA
794 elif modifier == "C":
795 src, dest = diff['src'], diff['dst']
796 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
797 if diff['src_sha1'] != diff['dst_sha1']:
798 p4_system("edit \"%s\"" % (dest))
799 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
800 p4_system("edit \"%s\"" % (dest))
801 filesToChangeExecBit[dest] = diff['dst_mode']
802 os.unlink(dest)
803 editedFiles.add(dest)
d9a5f25b 804 elif modifier == "R":
b43b0a3c 805 src, dest = diff['src'], diff['dst']
87b611d5 806 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
ae901090
VA
807 if diff['src_sha1'] != diff['dst_sha1']:
808 p4_system("edit \"%s\"" % (dest))
c65b670e 809 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
ae901090 810 p4_system("edit \"%s\"" % (dest))
c65b670e 811 filesToChangeExecBit[dest] = diff['dst_mode']
d9a5f25b
CP
812 os.unlink(dest)
813 editedFiles.add(dest)
814 filesToDelete.add(src)
4f5cf76a
SH
815 else:
816 die("unknown modifier %s for %s" % (modifier, path))
817
0e36f2d7 818 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
47a130b7 819 patchcmd = diffcmd + " | git apply "
c1b296b9
SH
820 tryPatchCmd = patchcmd + "--check -"
821 applyPatchCmd = patchcmd + "--check --apply -"
51a2640a 822
47a130b7 823 if os.system(tryPatchCmd) != 0:
51a2640a
SH
824 print "Unfortunately applying the change failed!"
825 print "What do you want to do?"
826 response = "x"
827 while response != "s" and response != "a" and response != "w":
cebdf5af
HWN
828 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
829 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
51a2640a
SH
830 if response == "s":
831 print "Skipping! Good luck with the next patches..."
20947149 832 for f in editedFiles:
87b611d5 833 p4_system("revert \"%s\"" % f);
20947149
SH
834 for f in filesToAdd:
835 system("rm %s" %f)
51a2640a
SH
836 return
837 elif response == "a":
47a130b7 838 os.system(applyPatchCmd)
51a2640a
SH
839 if len(filesToAdd) > 0:
840 print "You may also want to call p4 add on the following files:"
841 print " ".join(filesToAdd)
842 if len(filesToDelete):
843 print "The following files should be scheduled for deletion with p4 delete:"
844 print " ".join(filesToDelete)
cebdf5af
HWN
845 die("Please resolve and submit the conflict manually and "
846 + "continue afterwards with git-p4 submit --continue")
51a2640a
SH
847 elif response == "w":
848 system(diffcmd + " > patch.txt")
849 print "Patch saved to patch.txt in %s !" % self.clientPath
cebdf5af
HWN
850 die("Please resolve and submit the conflict manually and "
851 "continue afterwards with git-p4 submit --continue")
51a2640a 852
47a130b7 853 system(applyPatchCmd)
4f5cf76a
SH
854
855 for f in filesToAdd:
87b611d5 856 p4_system("add \"%s\"" % f)
4f5cf76a 857 for f in filesToDelete:
87b611d5
AK
858 p4_system("revert \"%s\"" % f)
859 p4_system("delete \"%s\"" % f)
4f5cf76a 860
c65b670e
CP
861 # Set/clear executable bits
862 for f in filesToChangeExecBit.keys():
863 mode = filesToChangeExecBit[f]
864 setP4ExecBit(f, mode)
865
0e36f2d7 866 logMessage = extractLogMessageFromGitCommit(id)
0e36f2d7 867 logMessage = logMessage.strip()
4f5cf76a 868
ea99c3ae 869 template = self.prepareSubmitTemplate()
4f5cf76a
SH
870
871 if self.interactive:
872 submitTemplate = self.prepareLogMessage(template, logMessage)
ecdba36d
LD
873
874 if self.preserveUser:
875 submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User)
876
67abd417
SB
877 if os.environ.has_key("P4DIFF"):
878 del(os.environ["P4DIFF"])
8b130262
AW
879 diff = ""
880 for editedFile in editedFiles:
881 diff += p4_read_pipe("diff -du %r" % editedFile)
4f5cf76a 882
f3e5ae4f 883 newdiff = ""
4f5cf76a 884 for newFile in filesToAdd:
f3e5ae4f
MSO
885 newdiff += "==== new file ====\n"
886 newdiff += "--- /dev/null\n"
887 newdiff += "+++ %s\n" % newFile
4f5cf76a
SH
888 f = open(newFile, "r")
889 for line in f.readlines():
f3e5ae4f 890 newdiff += "+" + line
4f5cf76a
SH
891 f.close()
892
f3e5ae4f 893 separatorLine = "######## everything below this line is just the diff #######\n"
4f5cf76a 894
e96e400f
SH
895 [handle, fileName] = tempfile.mkstemp()
896 tmpFile = os.fdopen(handle, "w+")
f3e5ae4f
MSO
897 if self.isWindows:
898 submitTemplate = submitTemplate.replace("\n", "\r\n")
899 separatorLine = separatorLine.replace("\n", "\r\n")
900 newdiff = newdiff.replace("\n", "\r\n")
901 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
e96e400f 902 tmpFile.close()
cdc7e388 903 mtime = os.stat(fileName).st_mtime
82cea9ff
SB
904 if os.environ.has_key("P4EDITOR"):
905 editor = os.environ.get("P4EDITOR")
906 else:
8b187e6b 907 editor = read_pipe("git var GIT_EDITOR").strip()
e96e400f 908 system(editor + " " + fileName)
e96e400f 909
3ea2cfd4
LD
910 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
911 checkModTime = False
912 else:
913 checkModTime = True
914
cdc7e388 915 response = "y"
3ea2cfd4 916 if checkModTime and (os.stat(fileName).st_mtime <= mtime):
cdc7e388
SH
917 response = "x"
918 while response != "y" and response != "n":
919 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
920
921 if response == "y":
922 tmpFile = open(fileName, "rb")
923 message = tmpFile.read()
924 tmpFile.close()
925 submitTemplate = message[:message.index(separatorLine)]
926 if self.isWindows:
927 submitTemplate = submitTemplate.replace("\r\n", "\n")
928 p4_write_pipe("submit -i", submitTemplate)
3ea2cfd4
LD
929
930 if self.preserveUser:
931 if p4User:
932 # Get last changelist number. Cannot easily get it from
933 # the submit command output as the output is unmarshalled.
934 changelist = self.lastP4Changelist()
935 self.modifyChangelistUser(changelist, p4User)
936
cdc7e388
SH
937 else:
938 for f in editedFiles:
939 p4_system("revert \"%s\"" % f);
940 for f in filesToAdd:
941 p4_system("revert \"%s\"" % f);
942 system("rm %s" %f)
943
944 os.remove(fileName)
4f5cf76a
SH
945 else:
946 fileName = "submit.txt"
947 file = open(fileName, "w+")
948 file.write(self.prepareLogMessage(template, logMessage))
949 file.close()
cebdf5af
HWN
950 print ("Perforce submit template written as %s. "
951 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
952 % (fileName, fileName))
4f5cf76a
SH
953
954 def run(self, args):
c9b50e63
SH
955 if len(args) == 0:
956 self.master = currentGitBranch()
4280e533 957 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
c9b50e63
SH
958 die("Detecting current git branch failed!")
959 elif len(args) == 1:
960 self.master = args[0]
961 else:
962 return False
963
4c2d5d72
JX
964 allowSubmit = gitConfig("git-p4.allowSubmit")
965 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
966 die("%s is not in git-p4.allowSubmit" % self.master)
967
27d2d811 968 [upstream, settings] = findUpstreamBranchPoint()
ea99c3ae 969 self.depotPath = settings['depot-paths'][0]
27d2d811
SH
970 if len(self.origin) == 0:
971 self.origin = upstream
a3fdd579 972
3ea2cfd4
LD
973 if self.preserveUser:
974 if not self.canChangeChangelists():
975 die("Cannot preserve user names without p4 super-user or admin permissions")
976
a3fdd579
SH
977 if self.verbose:
978 print "Origin branch is " + self.origin
9512497b 979
ea99c3ae 980 if len(self.depotPath) == 0:
9512497b
SH
981 print "Internal error: cannot locate perforce depot path from existing branches"
982 sys.exit(128)
983
ea99c3ae 984 self.clientPath = p4Where(self.depotPath)
9512497b 985
51a2640a 986 if len(self.clientPath) == 0:
ea99c3ae 987 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
9512497b
SH
988 sys.exit(128)
989
ea99c3ae 990 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
7944f142 991 self.oldWorkingDirectory = os.getcwd()
c1b296b9 992
053fd0c1 993 chdir(self.clientPath)
6a01298a 994 print "Synchronizing p4 checkout..."
87b611d5 995 p4_system("sync ...")
9512497b 996
4f5cf76a 997 self.check()
4f5cf76a 998
4c750c0d
SH
999 commits = []
1000 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1001 commits.append(line.strip())
1002 commits.reverse()
4f5cf76a 1003
3ea2cfd4
LD
1004 if self.preserveUser:
1005 self.checkValidP4Users(commits)
1006
4f5cf76a 1007 while len(commits) > 0:
4f5cf76a
SH
1008 commit = commits[0]
1009 commits = commits[1:]
7cb5cbef 1010 self.applyCommit(commit)
4f5cf76a
SH
1011 if not self.interactive:
1012 break
1013
4f5cf76a 1014 if len(commits) == 0:
4c750c0d 1015 print "All changes applied!"
053fd0c1 1016 chdir(self.oldWorkingDirectory)
14594f4b 1017
4c750c0d
SH
1018 sync = P4Sync()
1019 sync.run([])
14594f4b 1020
4c750c0d
SH
1021 rebase = P4Rebase()
1022 rebase.rebase()
4f5cf76a 1023
b984733c
SH
1024 return True
1025
3ea2cfd4 1026class P4Sync(Command, P4UserMap):
56c09345
PW
1027 delete_actions = ( "delete", "move/delete", "purge" )
1028
b984733c
SH
1029 def __init__(self):
1030 Command.__init__(self)
3ea2cfd4 1031 P4UserMap.__init__(self)
b984733c
SH
1032 self.options = [
1033 optparse.make_option("--branch", dest="branch"),
1034 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1035 optparse.make_option("--changesfile", dest="changesFile"),
1036 optparse.make_option("--silent", dest="silent", action="store_true"),
ef48f909 1037 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
a028a98e 1038 optparse.make_option("--verbose", dest="verbose", action="store_true"),
d2c6dd30
HWN
1039 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1040 help="Import into refs/heads/ , not refs/remotes"),
8b41a97f 1041 optparse.make_option("--max-changes", dest="maxChanges"),
86dff6b6 1042 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
3a70cdfa
TAL
1043 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1044 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1045 help="Only sync files that are included in the Perforce Client Spec")
b984733c
SH
1046 ]
1047 self.description = """Imports from Perforce into a git repository.\n
1048 example:
1049 //depot/my/project/ -- to import the current head
1050 //depot/my/project/@all -- to import everything
1051 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1052
1053 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1054
1055 self.usage += " //depot/path[@revRange]"
b984733c 1056 self.silent = False
1d7367dc
RG
1057 self.createdBranches = set()
1058 self.committedChanges = set()
569d1bd4 1059 self.branch = ""
b984733c 1060 self.detectBranches = False
cb53e1f8 1061 self.detectLabels = False
b984733c 1062 self.changesFile = ""
01265103 1063 self.syncWithOrigin = True
4b97ffb1 1064 self.verbose = False
a028a98e 1065 self.importIntoRemotes = True
01a9c9c5 1066 self.maxChanges = ""
c1f9197f 1067 self.isWindows = (platform.system() == "Windows")
8b41a97f 1068 self.keepRepoPath = False
6326aa58 1069 self.depotPaths = None
3c699645 1070 self.p4BranchesInGit = []
354081d5 1071 self.cloneExclude = []
3a70cdfa
TAL
1072 self.useClientSpec = False
1073 self.clientSpecDirs = []
b984733c 1074
01265103
SH
1075 if gitConfig("git-p4.syncFromOrigin") == "false":
1076 self.syncWithOrigin = False
1077
084f6306
PW
1078 #
1079 # P4 wildcards are not allowed in filenames. P4 complains
1080 # if you simply add them, but you can force it with "-f", in
1081 # which case it translates them into %xx encoding internally.
1082 # Search for and fix just these four characters. Do % last so
1083 # that fixing it does not inadvertently create new %-escapes.
1084 #
1085 def wildcard_decode(self, path):
1086 # Cannot have * in a filename in windows; untested as to
1087 # what p4 would do in such a case.
1088 if not self.isWindows:
1089 path = path.replace("%2A", "*")
1090 path = path.replace("%23", "#") \
1091 .replace("%40", "@") \
1092 .replace("%25", "%")
1093 return path
1094
b984733c 1095 def extractFilesFromCommit(self, commit):
354081d5
TT
1096 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1097 for path in self.cloneExclude]
b984733c
SH
1098 files = []
1099 fnum = 0
1100 while commit.has_key("depotFile%s" % fnum):
1101 path = commit["depotFile%s" % fnum]
6326aa58 1102
354081d5 1103 if [p for p in self.cloneExclude
d53de8b9 1104 if p4PathStartsWith(path, p)]:
354081d5
TT
1105 found = False
1106 else:
1107 found = [p for p in self.depotPaths
d53de8b9 1108 if p4PathStartsWith(path, p)]
6326aa58 1109 if not found:
b984733c
SH
1110 fnum = fnum + 1
1111 continue
1112
1113 file = {}
1114 file["path"] = path
1115 file["rev"] = commit["rev%s" % fnum]
1116 file["action"] = commit["action%s" % fnum]
1117 file["type"] = commit["type%s" % fnum]
1118 files.append(file)
1119 fnum = fnum + 1
1120 return files
1121
6326aa58 1122 def stripRepoPath(self, path, prefixes):
3952710b
IW
1123 if self.useClientSpec:
1124
1125 # if using the client spec, we use the output directory
1126 # specified in the client. For example, a view
1127 # //depot/foo/branch/... //client/branch/foo/...
1128 # will end up putting all foo/branch files into
1129 # branch/foo/
1130 for val in self.clientSpecDirs:
1131 if path.startswith(val[0]):
1132 # replace the depot path with the client path
1133 path = path.replace(val[0], val[1][1])
1134 # now strip out the client (//client/...)
1135 path = re.sub("^(//[^/]+/)", '', path)
1136 # the rest is all path
1137 return path
1138
8b41a97f 1139 if self.keepRepoPath:
6326aa58
HWN
1140 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
1141
1142 for p in prefixes:
d53de8b9 1143 if p4PathStartsWith(path, p):
6326aa58 1144 path = path[len(p):]
8b41a97f 1145
6326aa58 1146 return path
6754a299 1147
71b112d4 1148 def splitFilesIntoBranches(self, commit):
d5904674 1149 branches = {}
71b112d4
SH
1150 fnum = 0
1151 while commit.has_key("depotFile%s" % fnum):
1152 path = commit["depotFile%s" % fnum]
6326aa58 1153 found = [p for p in self.depotPaths
d53de8b9 1154 if p4PathStartsWith(path, p)]
6326aa58 1155 if not found:
71b112d4
SH
1156 fnum = fnum + 1
1157 continue
1158
1159 file = {}
1160 file["path"] = path
1161 file["rev"] = commit["rev%s" % fnum]
1162 file["action"] = commit["action%s" % fnum]
1163 file["type"] = commit["type%s" % fnum]
1164 fnum = fnum + 1
1165
6326aa58 1166 relPath = self.stripRepoPath(path, self.depotPaths)
b984733c 1167
4b97ffb1 1168 for branch in self.knownBranches.keys():
6754a299
HWN
1169
1170 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1171 if relPath.startswith(branch + "/"):
d5904674
SH
1172 if branch not in branches:
1173 branches[branch] = []
71b112d4 1174 branches[branch].append(file)
6555b2cc 1175 break
b984733c
SH
1176
1177 return branches
1178
b932705b
LD
1179 # output one file from the P4 stream
1180 # - helper for streamP4Files
1181
1182 def streamOneP4File(self, file, contents):
c3f6163b
AG
1183 if file["type"] == "apple":
1184 print "\nfile %s is a strange apple file that forks. Ignoring" % \
1185 file['depotFile']
1186 return
b932705b
LD
1187
1188 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
084f6306 1189 relPath = self.wildcard_decode(relPath)
b932705b
LD
1190 if verbose:
1191 sys.stderr.write("%s\n" % relPath)
1192
1193 mode = "644"
1194 if isP4Exec(file["type"]):
1195 mode = "755"
1196 elif file["type"] == "symlink":
1197 mode = "120000"
1198 # p4 print on a symlink contains "target\n", so strip it off
b39c3612
EP
1199 data = ''.join(contents)
1200 contents = [data[:-1]]
b932705b
LD
1201
1202 if self.isWindows and file["type"].endswith("text"):
1203 mangled = []
1204 for data in contents:
1205 data = data.replace("\r\n", "\n")
1206 mangled.append(data)
1207 contents = mangled
1208
1209 if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1210 contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1211 elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1212 contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1213
1214 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1215
1216 # total length...
1217 length = 0
1218 for d in contents:
1219 length = length + len(d)
1220
1221 self.gitStream.write("data %d\n" % length)
1222 for d in contents:
1223 self.gitStream.write(d)
1224 self.gitStream.write("\n")
1225
1226 def streamOneP4Deletion(self, file):
1227 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1228 if verbose:
1229 sys.stderr.write("delete %s\n" % relPath)
1230 self.gitStream.write("D %s\n" % relPath)
1231
1232 # handle another chunk of streaming data
1233 def streamP4FilesCb(self, marshalled):
1234
c3f6163b
AG
1235 if marshalled.has_key('depotFile') and self.stream_have_file_info:
1236 # start of a new file - output the old one first
1237 self.streamOneP4File(self.stream_file, self.stream_contents)
1238 self.stream_file = {}
1239 self.stream_contents = []
1240 self.stream_have_file_info = False
b932705b 1241
c3f6163b
AG
1242 # pick up the new file information... for the
1243 # 'data' field we need to append to our array
1244 for k in marshalled.keys():
1245 if k == 'data':
1246 self.stream_contents.append(marshalled['data'])
1247 else:
1248 self.stream_file[k] = marshalled[k]
b932705b 1249
c3f6163b 1250 self.stream_have_file_info = True
b932705b
LD
1251
1252 # Stream directly from "p4 files" into "git fast-import"
1253 def streamP4Files(self, files):
30b5940b
SH
1254 filesForCommit = []
1255 filesToRead = []
b932705b 1256 filesToDelete = []
30b5940b 1257
3a70cdfa 1258 for f in files:
30b5940b 1259 includeFile = True
3a70cdfa
TAL
1260 for val in self.clientSpecDirs:
1261 if f['path'].startswith(val[0]):
3952710b 1262 if val[1][0] <= 0:
30b5940b 1263 includeFile = False
3a70cdfa
TAL
1264 break
1265
30b5940b
SH
1266 if includeFile:
1267 filesForCommit.append(f)
56c09345 1268 if f['action'] in self.delete_actions:
b932705b 1269 filesToDelete.append(f)
56c09345
PW
1270 else:
1271 filesToRead.append(f)
6a49f8e2 1272
b932705b
LD
1273 # deleted files...
1274 for f in filesToDelete:
1275 self.streamOneP4Deletion(f)
1b9a4684 1276
b932705b
LD
1277 if len(filesToRead) > 0:
1278 self.stream_file = {}
1279 self.stream_contents = []
1280 self.stream_have_file_info = False
8ff45f2a 1281
c3f6163b
AG
1282 # curry self argument
1283 def streamP4FilesCbSelf(entry):
1284 self.streamP4FilesCb(entry)
6a49f8e2 1285
c3f6163b
AG
1286 p4CmdList("-x - print",
1287 '\n'.join(['%s#%s' % (f['path'], f['rev'])
b932705b 1288 for f in filesToRead]),
c3f6163b 1289 cb=streamP4FilesCbSelf)
30b5940b 1290
b932705b
LD
1291 # do the last chunk
1292 if self.stream_file.has_key('depotFile'):
1293 self.streamOneP4File(self.stream_file, self.stream_contents)
6a49f8e2 1294
6326aa58 1295 def commit(self, details, files, branch, branchPrefixes, parent = ""):
b984733c
SH
1296 epoch = details["time"]
1297 author = details["user"]
c3f6163b 1298 self.branchPrefixes = branchPrefixes
b984733c 1299
4b97ffb1
SH
1300 if self.verbose:
1301 print "commit into %s" % branch
1302
96e07dd2
HWN
1303 # start with reading files; if that fails, we should not
1304 # create a commit.
1305 new_files = []
1306 for f in files:
d53de8b9 1307 if [p for p in branchPrefixes if p4PathStartsWith(f['path'], p)]:
96e07dd2
HWN
1308 new_files.append (f)
1309 else:
afa1dd9a 1310 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
96e07dd2 1311
b984733c 1312 self.gitStream.write("commit %s\n" % branch)
6a49f8e2 1313# gitStream.write("mark :%s\n" % details["change"])
b984733c
SH
1314 self.committedChanges.add(int(details["change"]))
1315 committer = ""
b607e71e
SH
1316 if author not in self.users:
1317 self.getUserMapFromPerforceServer()
b984733c 1318 if author in self.users:
0828ab14 1319 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
b984733c 1320 else:
0828ab14 1321 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
b984733c
SH
1322
1323 self.gitStream.write("committer %s\n" % committer)
1324
1325 self.gitStream.write("data <<EOT\n")
1326 self.gitStream.write(details["desc"])
6581de09
SH
1327 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1328 % (','.join (branchPrefixes), details["change"]))
1329 if len(details['options']) > 0:
1330 self.gitStream.write(": options = %s" % details['options'])
1331 self.gitStream.write("]\nEOT\n\n")
b984733c
SH
1332
1333 if len(parent) > 0:
4b97ffb1
SH
1334 if self.verbose:
1335 print "parent %s" % parent
b984733c
SH
1336 self.gitStream.write("from %s\n" % parent)
1337
b932705b 1338 self.streamP4Files(new_files)
b984733c
SH
1339 self.gitStream.write("\n")
1340
1f4ba1cb
SH
1341 change = int(details["change"])
1342
9bda3a85 1343 if self.labels.has_key(change):
1f4ba1cb
SH
1344 label = self.labels[change]
1345 labelDetails = label[0]
1346 labelRevisions = label[1]
71b112d4
SH
1347 if self.verbose:
1348 print "Change %s is labelled %s" % (change, labelDetails)
1f4ba1cb 1349
6326aa58
HWN
1350 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1351 for p in branchPrefixes]))
1f4ba1cb
SH
1352
1353 if len(files) == len(labelRevisions):
1354
1355 cleanedFiles = {}
1356 for info in files:
56c09345 1357 if info["action"] in self.delete_actions:
1f4ba1cb
SH
1358 continue
1359 cleanedFiles[info["depotFile"]] = info["rev"]
1360
1361 if cleanedFiles == labelRevisions:
1362 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1363 self.gitStream.write("from %s\n" % branch)
1364
1365 owner = labelDetails["Owner"]
1366 tagger = ""
1367 if author in self.users:
1368 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1369 else:
1370 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1371 self.gitStream.write("tagger %s\n" % tagger)
1372 self.gitStream.write("data <<EOT\n")
1373 self.gitStream.write(labelDetails["Description"])
1374 self.gitStream.write("EOT\n\n")
1375
1376 else:
a46668fa 1377 if not self.silent:
cebdf5af
HWN
1378 print ("Tag %s does not match with change %s: files do not match."
1379 % (labelDetails["label"], change))
1f4ba1cb
SH
1380
1381 else:
a46668fa 1382 if not self.silent:
cebdf5af
HWN
1383 print ("Tag %s does not match with change %s: file count is different."
1384 % (labelDetails["label"], change))
b984733c 1385
1f4ba1cb
SH
1386 def getLabels(self):
1387 self.labels = {}
1388
6326aa58 1389 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
10c3211b 1390 if len(l) > 0 and not self.silent:
183f8436 1391 print "Finding files belonging to labels in %s" % `self.depotPaths`
01ce1fe9
SH
1392
1393 for output in l:
1f4ba1cb
SH
1394 label = output["label"]
1395 revisions = {}
1396 newestChange = 0
71b112d4
SH
1397 if self.verbose:
1398 print "Querying files for label %s" % label
6326aa58
HWN
1399 for file in p4CmdList("files "
1400 + ' '.join (["%s...@%s" % (p, label)
1401 for p in self.depotPaths])):
1f4ba1cb
SH
1402 revisions[file["depotFile"]] = file["rev"]
1403 change = int(file["change"])
1404 if change > newestChange:
1405 newestChange = change
1406
9bda3a85
SH
1407 self.labels[newestChange] = [output, revisions]
1408
1409 if self.verbose:
1410 print "Label changes: %s" % self.labels.keys()
1f4ba1cb 1411
86dff6b6
HWN
1412 def guessProjectName(self):
1413 for p in self.depotPaths:
6e5295c4
SH
1414 if p.endswith("/"):
1415 p = p[:-1]
1416 p = p[p.strip().rfind("/") + 1:]
1417 if not p.endswith("/"):
1418 p += "/"
1419 return p
86dff6b6 1420
4b97ffb1 1421 def getBranchMapping(self):
6555b2cc
SH
1422 lostAndFoundBranches = set()
1423
4b97ffb1
SH
1424 for info in p4CmdList("branches"):
1425 details = p4Cmd("branch -o %s" % info["branch"])
1426 viewIdx = 0
1427 while details.has_key("View%s" % viewIdx):
1428 paths = details["View%s" % viewIdx].split(" ")
1429 viewIdx = viewIdx + 1
1430 # require standard //depot/foo/... //depot/bar/... mapping
1431 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1432 continue
1433 source = paths[0]
1434 destination = paths[1]
6509e19c 1435 ## HACK
d53de8b9 1436 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
6509e19c
SH
1437 source = source[len(self.depotPaths[0]):-4]
1438 destination = destination[len(self.depotPaths[0]):-4]
6555b2cc 1439
1a2edf4e
SH
1440 if destination in self.knownBranches:
1441 if not self.silent:
1442 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1443 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1444 continue
1445
6555b2cc
SH
1446 self.knownBranches[destination] = source
1447
1448 lostAndFoundBranches.discard(destination)
1449
29bdbac1 1450 if source not in self.knownBranches:
6555b2cc
SH
1451 lostAndFoundBranches.add(source)
1452
1453
1454 for branch in lostAndFoundBranches:
1455 self.knownBranches[branch] = branch
29bdbac1 1456
38f9f5ec
SH
1457 def getBranchMappingFromGitBranches(self):
1458 branches = p4BranchesInGit(self.importIntoRemotes)
1459 for branch in branches.keys():
1460 if branch == "master":
1461 branch = "main"
1462 else:
1463 branch = branch[len(self.projectName):]
1464 self.knownBranches[branch] = branch
1465
29bdbac1 1466 def listExistingP4GitBranches(self):
144ff46b
SH
1467 # branches holds mapping from name to commit
1468 branches = p4BranchesInGit(self.importIntoRemotes)
1469 self.p4BranchesInGit = branches.keys()
1470 for branch in branches.keys():
1471 self.initialParents[self.refPrefix + branch] = branches[branch]
4b97ffb1 1472
bb6e09b2
HWN
1473 def updateOptionDict(self, d):
1474 option_keys = {}
1475 if self.keepRepoPath:
1476 option_keys['keepRepoPath'] = 1
1477
1478 d["options"] = ' '.join(sorted(option_keys.keys()))
1479
1480 def readOptions(self, d):
1481 self.keepRepoPath = (d.has_key('options')
1482 and ('keepRepoPath' in d['options']))
6326aa58 1483
8134f69c
SH
1484 def gitRefForBranch(self, branch):
1485 if branch == "main":
1486 return self.refPrefix + "master"
1487
1488 if len(branch) <= 0:
1489 return branch
1490
1491 return self.refPrefix + self.projectName + branch
1492
1ca3d710
SH
1493 def gitCommitByP4Change(self, ref, change):
1494 if self.verbose:
1495 print "looking in ref " + ref + " for change %s using bisect..." % change
1496
1497 earliestCommit = ""
1498 latestCommit = parseRevision(ref)
1499
1500 while True:
1501 if self.verbose:
1502 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1503 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1504 if len(next) == 0:
1505 if self.verbose:
1506 print "argh"
1507 return ""
1508 log = extractLogMessageFromGitCommit(next)
1509 settings = extractSettingsGitLog(log)
1510 currentChange = int(settings['change'])
1511 if self.verbose:
1512 print "current change %s" % currentChange
1513
1514 if currentChange == change:
1515 if self.verbose:
1516 print "found %s" % next
1517 return next
1518
1519 if currentChange < change:
1520 earliestCommit = "^%s" % next
1521 else:
1522 latestCommit = "%s" % next
1523
1524 return ""
1525
1526 def importNewBranch(self, branch, maxChange):
1527 # make fast-import flush all changes to disk and update the refs using the checkpoint
1528 # command so that we can try to find the branch parent in the git history
1529 self.gitStream.write("checkpoint\n\n");
1530 self.gitStream.flush();
1531 branchPrefix = self.depotPaths[0] + branch + "/"
1532 range = "@1,%s" % maxChange
1533 #print "prefix" + branchPrefix
1534 changes = p4ChangesForPaths([branchPrefix], range)
1535 if len(changes) <= 0:
1536 return False
1537 firstChange = changes[0]
1538 #print "first change in branch: %s" % firstChange
1539 sourceBranch = self.knownBranches[branch]
1540 sourceDepotPath = self.depotPaths[0] + sourceBranch
1541 sourceRef = self.gitRefForBranch(sourceBranch)
1542 #print "source " + sourceBranch
1543
1544 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1545 #print "branch parent: %s" % branchParentChange
1546 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1547 if len(gitParent) > 0:
1548 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1549 #print "parent git commit: %s" % gitParent
1550
1551 self.importChanges(changes)
1552 return True
1553
e87f37ae
SH
1554 def importChanges(self, changes):
1555 cnt = 1
1556 for change in changes:
1557 description = p4Cmd("describe %s" % change)
1558 self.updateOptionDict(description)
1559
1560 if not self.silent:
1561 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1562 sys.stdout.flush()
1563 cnt = cnt + 1
1564
1565 try:
1566 if self.detectBranches:
1567 branches = self.splitFilesIntoBranches(description)
1568 for branch in branches.keys():
1569 ## HACK --hwn
1570 branchPrefix = self.depotPaths[0] + branch + "/"
1571
1572 parent = ""
1573
1574 filesForCommit = branches[branch]
1575
1576 if self.verbose:
1577 print "branch is %s" % branch
1578
1579 self.updatedBranches.add(branch)
1580
1581 if branch not in self.createdBranches:
1582 self.createdBranches.add(branch)
1583 parent = self.knownBranches[branch]
1584 if parent == branch:
1585 parent = ""
1ca3d710
SH
1586 else:
1587 fullBranch = self.projectName + branch
1588 if fullBranch not in self.p4BranchesInGit:
1589 if not self.silent:
1590 print("\n Importing new branch %s" % fullBranch);
1591 if self.importNewBranch(branch, change - 1):
1592 parent = ""
1593 self.p4BranchesInGit.append(fullBranch)
1594 if not self.silent:
1595 print("\n Resuming with change %s" % change);
1596
1597 if self.verbose:
1598 print "parent determined through known branches: %s" % parent
e87f37ae 1599
8134f69c
SH
1600 branch = self.gitRefForBranch(branch)
1601 parent = self.gitRefForBranch(parent)
e87f37ae
SH
1602
1603 if self.verbose:
1604 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1605
1606 if len(parent) == 0 and branch in self.initialParents:
1607 parent = self.initialParents[branch]
1608 del self.initialParents[branch]
1609
1610 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1611 else:
1612 files = self.extractFilesFromCommit(description)
1613 self.commit(description, files, self.branch, self.depotPaths,
1614 self.initialParent)
1615 self.initialParent = ""
1616 except IOError:
1617 print self.gitError.read()
1618 sys.exit(1)
1619
c208a243
SH
1620 def importHeadRevision(self, revision):
1621 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1622
1623 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1494fcbb 1624 details["desc"] = ("Initial import of %s from the state at revision %s\n"
c208a243
SH
1625 % (' '.join(self.depotPaths), revision))
1626 details["change"] = revision
1627 newestRevision = 0
1628
1629 fileCnt = 0
1630 for info in p4CmdList("files "
1631 + ' '.join(["%s...%s"
1632 % (p, revision)
1633 for p in self.depotPaths])):
1634
68b28593 1635 if 'code' in info and info['code'] == 'error':
c208a243
SH
1636 sys.stderr.write("p4 returned an error: %s\n"
1637 % info['data'])
d88e707f
PW
1638 if info['data'].find("must refer to client") >= 0:
1639 sys.stderr.write("This particular p4 error is misleading.\n")
1640 sys.stderr.write("Perhaps the depot path was misspelled.\n");
1641 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
c208a243 1642 sys.exit(1)
68b28593
PW
1643 if 'p4ExitCode' in info:
1644 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
c208a243
SH
1645 sys.exit(1)
1646
1647
1648 change = int(info["change"])
1649 if change > newestRevision:
1650 newestRevision = change
1651
56c09345 1652 if info["action"] in self.delete_actions:
c208a243
SH
1653 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1654 #fileCnt = fileCnt + 1
1655 continue
1656
1657 for prop in ["depotFile", "rev", "action", "type" ]:
1658 details["%s%s" % (prop, fileCnt)] = info[prop]
1659
1660 fileCnt = fileCnt + 1
1661
1662 details["change"] = newestRevision
1663 self.updateOptionDict(details)
1664 try:
1665 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1666 except IOError:
1667 print "IO error with git fast-import. Is your git version recent enough?"
1668 print self.gitError.read()
1669
1670
3a70cdfa
TAL
1671 def getClientSpec(self):
1672 specList = p4CmdList( "client -o" )
1673 temp = {}
1674 for entry in specList:
1675 for k,v in entry.iteritems():
1676 if k.startswith("View"):
3952710b
IW
1677
1678 # p4 has these %%1 to %%9 arguments in specs to
1679 # reorder paths; which we can't handle (yet :)
1680 if re.match('%%\d', v) != None:
1681 print "Sorry, can't handle %%n arguments in client specs"
1682 sys.exit(1)
1683
3a70cdfa
TAL
1684 if v.startswith('"'):
1685 start = 1
1686 else:
1687 start = 0
1688 index = v.find("...")
3952710b
IW
1689
1690 # save the "client view"; i.e the RHS of the view
1691 # line that tells the client where to put the
1692 # files for this view.
1693 cv = v[index+3:].strip() # +3 to remove previous '...'
1694
1695 # if the client view doesn't end with a
1696 # ... wildcard, then we're going to mess up the
1697 # output directory, so fail gracefully.
1698 if not cv.endswith('...'):
1699 print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1700 sys.exit(1)
1701 cv=cv[:-3]
1702
1703 # now save the view; +index means included, -index
1704 # means it should be filtered out.
3a70cdfa
TAL
1705 v = v[start:index]
1706 if v.startswith("-"):
1707 v = v[1:]
3952710b 1708 include = -len(v)
3a70cdfa 1709 else:
3952710b
IW
1710 include = len(v)
1711
1712 temp[v] = (include, cv)
1713
3a70cdfa 1714 self.clientSpecDirs = temp.items()
3952710b 1715 self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
3a70cdfa 1716
b984733c 1717 def run(self, args):
6326aa58 1718 self.depotPaths = []
179caebf
SH
1719 self.changeRange = ""
1720 self.initialParent = ""
6326aa58 1721 self.previousDepotPaths = []
ce6f33c8 1722
29bdbac1
SH
1723 # map from branch depot path to parent branch
1724 self.knownBranches = {}
1725 self.initialParents = {}
5ca44617 1726 self.hasOrigin = originP4BranchesExist()
a43ff00c
SH
1727 if not self.syncWithOrigin:
1728 self.hasOrigin = False
29bdbac1 1729
a028a98e
SH
1730 if self.importIntoRemotes:
1731 self.refPrefix = "refs/remotes/p4/"
1732 else:
db775559 1733 self.refPrefix = "refs/heads/p4/"
a028a98e 1734
cebdf5af
HWN
1735 if self.syncWithOrigin and self.hasOrigin:
1736 if not self.silent:
1737 print "Syncing with origin first by calling git fetch origin"
1738 system("git fetch origin")
10f880f8 1739
569d1bd4 1740 if len(self.branch) == 0:
db775559 1741 self.branch = self.refPrefix + "master"
a028a98e 1742 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
48df6fd8 1743 system("git update-ref %s refs/heads/p4" % self.branch)
48df6fd8 1744 system("git branch -D p4");
faf1bd20 1745 # create it /after/ importing, when master exists
0058a33a 1746 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
a3c55c09 1747 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
967f72e2 1748
3cafb7d8 1749 if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
3a70cdfa
TAL
1750 self.getClientSpec()
1751
6a49f8e2
HWN
1752 # TODO: should always look at previous commits,
1753 # merge with previous imports, if possible.
1754 if args == []:
d414c74a 1755 if self.hasOrigin:
5ca44617 1756 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
abcd790f
SH
1757 self.listExistingP4GitBranches()
1758
1759 if len(self.p4BranchesInGit) > 1:
1760 if not self.silent:
1761 print "Importing from/into multiple branches"
1762 self.detectBranches = True
967f72e2 1763
29bdbac1
SH
1764 if self.verbose:
1765 print "branches: %s" % self.p4BranchesInGit
1766
1767 p4Change = 0
1768 for branch in self.p4BranchesInGit:
cebdf5af 1769 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
bb6e09b2
HWN
1770
1771 settings = extractSettingsGitLog(logMsg)
29bdbac1 1772
bb6e09b2
HWN
1773 self.readOptions(settings)
1774 if (settings.has_key('depot-paths')
1775 and settings.has_key ('change')):
1776 change = int(settings['change']) + 1
29bdbac1
SH
1777 p4Change = max(p4Change, change)
1778
bb6e09b2
HWN
1779 depotPaths = sorted(settings['depot-paths'])
1780 if self.previousDepotPaths == []:
6326aa58 1781 self.previousDepotPaths = depotPaths
29bdbac1 1782 else:
6326aa58
HWN
1783 paths = []
1784 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
583e1707 1785 for i in range(0, min(len(cur), len(prev))):
6326aa58 1786 if cur[i] <> prev[i]:
583e1707 1787 i = i - 1
6326aa58
HWN
1788 break
1789
583e1707 1790 paths.append (cur[:i + 1])
6326aa58
HWN
1791
1792 self.previousDepotPaths = paths
29bdbac1
SH
1793
1794 if p4Change > 0:
bb6e09b2 1795 self.depotPaths = sorted(self.previousDepotPaths)
d5904674 1796 self.changeRange = "@%s,#head" % p4Change
330f53b8
SH
1797 if not self.detectBranches:
1798 self.initialParent = parseRevision(self.branch)
341dc1c1 1799 if not self.silent and not self.detectBranches:
967f72e2 1800 print "Performing incremental import into %s git branch" % self.branch
569d1bd4 1801
f9162f6a
SH
1802 if not self.branch.startswith("refs/"):
1803 self.branch = "refs/heads/" + self.branch
179caebf 1804
6326aa58 1805 if len(args) == 0 and self.depotPaths:
b984733c 1806 if not self.silent:
6326aa58 1807 print "Depot paths: %s" % ' '.join(self.depotPaths)
b984733c 1808 else:
6326aa58 1809 if self.depotPaths and self.depotPaths != args:
cebdf5af 1810 print ("previous import used depot path %s and now %s was specified. "
6326aa58
HWN
1811 "This doesn't work!" % (' '.join (self.depotPaths),
1812 ' '.join (args)))
b984733c 1813 sys.exit(1)
6326aa58 1814
bb6e09b2 1815 self.depotPaths = sorted(args)
b984733c 1816
1c49fc19 1817 revision = ""
b984733c 1818 self.users = {}
b984733c 1819
6326aa58
HWN
1820 newPaths = []
1821 for p in self.depotPaths:
1822 if p.find("@") != -1:
1823 atIdx = p.index("@")
1824 self.changeRange = p[atIdx:]
1825 if self.changeRange == "@all":
1826 self.changeRange = ""
6a49f8e2 1827 elif ',' not in self.changeRange:
1c49fc19 1828 revision = self.changeRange
6326aa58 1829 self.changeRange = ""
7fcff9de 1830 p = p[:atIdx]
6326aa58
HWN
1831 elif p.find("#") != -1:
1832 hashIdx = p.index("#")
1c49fc19 1833 revision = p[hashIdx:]
7fcff9de 1834 p = p[:hashIdx]
6326aa58 1835 elif self.previousDepotPaths == []:
1c49fc19 1836 revision = "#head"
6326aa58
HWN
1837
1838 p = re.sub ("\.\.\.$", "", p)
1839 if not p.endswith("/"):
1840 p += "/"
1841
1842 newPaths.append(p)
1843
1844 self.depotPaths = newPaths
1845
b984733c 1846
b607e71e 1847 self.loadUserMapFromCache()
cb53e1f8
SH
1848 self.labels = {}
1849 if self.detectLabels:
1850 self.getLabels();
b984733c 1851
4b97ffb1 1852 if self.detectBranches:
df450923
SH
1853 ## FIXME - what's a P4 projectName ?
1854 self.projectName = self.guessProjectName()
1855
38f9f5ec
SH
1856 if self.hasOrigin:
1857 self.getBranchMappingFromGitBranches()
1858 else:
1859 self.getBranchMapping()
29bdbac1
SH
1860 if self.verbose:
1861 print "p4-git branches: %s" % self.p4BranchesInGit
1862 print "initial parents: %s" % self.initialParents
1863 for b in self.p4BranchesInGit:
1864 if b != "master":
6326aa58
HWN
1865
1866 ## FIXME
29bdbac1
SH
1867 b = b[len(self.projectName):]
1868 self.createdBranches.add(b)
4b97ffb1 1869
f291b4e3 1870 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
b984733c 1871
cebdf5af 1872 importProcess = subprocess.Popen(["git", "fast-import"],
6326aa58
HWN
1873 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1874 stderr=subprocess.PIPE);
08483580
SH
1875 self.gitOutput = importProcess.stdout
1876 self.gitStream = importProcess.stdin
1877 self.gitError = importProcess.stderr
b984733c 1878
1c49fc19 1879 if revision:
c208a243 1880 self.importHeadRevision(revision)
b984733c
SH
1881 else:
1882 changes = []
1883
0828ab14 1884 if len(self.changesFile) > 0:
b984733c 1885 output = open(self.changesFile).readlines()
1d7367dc 1886 changeSet = set()
b984733c
SH
1887 for line in output:
1888 changeSet.add(int(line))
1889
1890 for change in changeSet:
1891 changes.append(change)
1892
1893 changes.sort()
1894 else:
accad8e0
PW
1895 # catch "git-p4 sync" with no new branches, in a repo that
1896 # does not have any existing git-p4 branches
1897 if len(args) == 0 and not self.p4BranchesInGit:
e32e00dc 1898 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
29bdbac1 1899 if self.verbose:
86dff6b6 1900 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
6326aa58 1901 self.changeRange)
4f6432d8 1902 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
b984733c 1903
01a9c9c5 1904 if len(self.maxChanges) > 0:
7fcff9de 1905 changes = changes[:min(int(self.maxChanges), len(changes))]
01a9c9c5 1906
b984733c 1907 if len(changes) == 0:
0828ab14 1908 if not self.silent:
341dc1c1 1909 print "No changes to import!"
1f52af6c 1910 return True
b984733c 1911
a9d1a27a
SH
1912 if not self.silent and not self.detectBranches:
1913 print "Import destination: %s" % self.branch
1914
341dc1c1
SH
1915 self.updatedBranches = set()
1916
e87f37ae 1917 self.importChanges(changes)
b984733c 1918
341dc1c1
SH
1919 if not self.silent:
1920 print ""
1921 if len(self.updatedBranches) > 0:
1922 sys.stdout.write("Updated branches: ")
1923 for b in self.updatedBranches:
1924 sys.stdout.write("%s " % b)
1925 sys.stdout.write("\n")
b984733c 1926
b984733c 1927 self.gitStream.close()
29bdbac1
SH
1928 if importProcess.wait() != 0:
1929 die("fast-import failed: %s" % self.gitError.read())
b984733c
SH
1930 self.gitOutput.close()
1931 self.gitError.close()
1932
b984733c
SH
1933 return True
1934
01ce1fe9
SH
1935class P4Rebase(Command):
1936 def __init__(self):
1937 Command.__init__(self)
01265103 1938 self.options = [ ]
cebdf5af
HWN
1939 self.description = ("Fetches the latest revision from perforce and "
1940 + "rebases the current work (branch) against it")
68c42153 1941 self.verbose = False
01ce1fe9
SH
1942
1943 def run(self, args):
1944 sync = P4Sync()
1945 sync.run([])
d7e3868c 1946
14594f4b
SH
1947 return self.rebase()
1948
1949 def rebase(self):
36ee4ee4
SH
1950 if os.system("git update-index --refresh") != 0:
1951 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.");
1952 if len(read_pipe("git diff-index HEAD --")) > 0:
1953 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1954
d7e3868c
SH
1955 [upstream, settings] = findUpstreamBranchPoint()
1956 if len(upstream) == 0:
1957 die("Cannot find upstream branchpoint for rebase")
1958
1959 # the branchpoint may be p4/foo~3, so strip off the parent
1960 upstream = re.sub("~[0-9]+$", "", upstream)
1961
1962 print "Rebasing the current branch onto %s" % upstream
b25b2065 1963 oldHead = read_pipe("git rev-parse HEAD").strip()
d7e3868c 1964 system("git rebase %s" % upstream)
1f52af6c 1965 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
01ce1fe9
SH
1966 return True
1967
f9a3a4f7
SH
1968class P4Clone(P4Sync):
1969 def __init__(self):
1970 P4Sync.__init__(self)
1971 self.description = "Creates a new git repository and imports from Perforce into it"
bb6e09b2 1972 self.usage = "usage: %prog [options] //depot/path[@revRange]"
354081d5 1973 self.options += [
bb6e09b2
HWN
1974 optparse.make_option("--destination", dest="cloneDestination",
1975 action='store', default=None,
354081d5
TT
1976 help="where to leave result of the clone"),
1977 optparse.make_option("-/", dest="cloneExclude",
1978 action="append", type="string",
38200076
PW
1979 help="exclude depot path"),
1980 optparse.make_option("--bare", dest="cloneBare",
1981 action="store_true", default=False),
354081d5 1982 ]
bb6e09b2 1983 self.cloneDestination = None
f9a3a4f7 1984 self.needsGit = False
38200076 1985 self.cloneBare = False
f9a3a4f7 1986
354081d5
TT
1987 # This is required for the "append" cloneExclude action
1988 def ensure_value(self, attr, value):
1989 if not hasattr(self, attr) or getattr(self, attr) is None:
1990 setattr(self, attr, value)
1991 return getattr(self, attr)
1992
6a49f8e2
HWN
1993 def defaultDestination(self, args):
1994 ## TODO: use common prefix of args?
1995 depotPath = args[0]
1996 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1997 depotDir = re.sub("(#[^#]*)$", "", depotDir)
053d9e43 1998 depotDir = re.sub(r"\.\.\.$", "", depotDir)
6a49f8e2
HWN
1999 depotDir = re.sub(r"/$", "", depotDir)
2000 return os.path.split(depotDir)[1]
2001
f9a3a4f7
SH
2002 def run(self, args):
2003 if len(args) < 1:
2004 return False
bb6e09b2
HWN
2005
2006 if self.keepRepoPath and not self.cloneDestination:
2007 sys.stderr.write("Must specify destination for --keep-path\n")
2008 sys.exit(1)
f9a3a4f7 2009
6326aa58 2010 depotPaths = args
5e100b5c
SH
2011
2012 if not self.cloneDestination and len(depotPaths) > 1:
2013 self.cloneDestination = depotPaths[-1]
2014 depotPaths = depotPaths[:-1]
2015
354081d5 2016 self.cloneExclude = ["/"+p for p in self.cloneExclude]
6326aa58
HWN
2017 for p in depotPaths:
2018 if not p.startswith("//"):
2019 return False
f9a3a4f7 2020
bb6e09b2 2021 if not self.cloneDestination:
98ad4faf 2022 self.cloneDestination = self.defaultDestination(args)
f9a3a4f7 2023
86dff6b6 2024 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
38200076 2025
c3bf3f13
KG
2026 if not os.path.exists(self.cloneDestination):
2027 os.makedirs(self.cloneDestination)
053fd0c1 2028 chdir(self.cloneDestination)
38200076
PW
2029
2030 init_cmd = [ "git", "init" ]
2031 if self.cloneBare:
2032 init_cmd.append("--bare")
2033 subprocess.check_call(init_cmd)
2034
6326aa58 2035 if not P4Sync.run(self, depotPaths):
f9a3a4f7 2036 return False
f9a3a4f7 2037 if self.branch != "master":
e9905013
TAL
2038 if self.importIntoRemotes:
2039 masterbranch = "refs/remotes/p4/master"
2040 else:
2041 masterbranch = "refs/heads/p4/master"
2042 if gitBranchExists(masterbranch):
2043 system("git branch master %s" % masterbranch)
38200076
PW
2044 if not self.cloneBare:
2045 system("git checkout -f")
8f9b2e08
SH
2046 else:
2047 print "Could not detect main branch. No checkout/master branch created."
86dff6b6 2048
f9a3a4f7
SH
2049 return True
2050
09d89de2
SH
2051class P4Branches(Command):
2052 def __init__(self):
2053 Command.__init__(self)
2054 self.options = [ ]
2055 self.description = ("Shows the git branches that hold imports and their "
2056 + "corresponding perforce depot paths")
2057 self.verbose = False
2058
2059 def run(self, args):
5ca44617
SH
2060 if originP4BranchesExist():
2061 createOrUpdateBranchesFromOrigin()
2062
09d89de2
SH
2063 cmdline = "git rev-parse --symbolic "
2064 cmdline += " --remotes"
2065
2066 for line in read_pipe_lines(cmdline):
2067 line = line.strip()
2068
2069 if not line.startswith('p4/') or line == "p4/HEAD":
2070 continue
2071 branch = line
2072
2073 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
2074 settings = extractSettingsGitLog(log)
2075
2076 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
2077 return True
2078
b984733c
SH
2079class HelpFormatter(optparse.IndentedHelpFormatter):
2080 def __init__(self):
2081 optparse.IndentedHelpFormatter.__init__(self)
2082
2083 def format_description(self, description):
2084 if description:
2085 return description + "\n"
2086 else:
2087 return ""
4f5cf76a 2088
86949eef
SH
2089def printUsage(commands):
2090 print "usage: %s <command> [options]" % sys.argv[0]
2091 print ""
2092 print "valid commands: %s" % ", ".join(commands)
2093 print ""
2094 print "Try %s <command> --help for command specific help." % sys.argv[0]
2095 print ""
2096
2097commands = {
b86f7378
HWN
2098 "debug" : P4Debug,
2099 "submit" : P4Submit,
a9834f58 2100 "commit" : P4Submit,
b86f7378
HWN
2101 "sync" : P4Sync,
2102 "rebase" : P4Rebase,
2103 "clone" : P4Clone,
09d89de2
SH
2104 "rollback" : P4RollBack,
2105 "branches" : P4Branches
86949eef
SH
2106}
2107
86949eef 2108
bb6e09b2
HWN
2109def main():
2110 if len(sys.argv[1:]) == 0:
2111 printUsage(commands.keys())
2112 sys.exit(2)
4f5cf76a 2113
bb6e09b2
HWN
2114 cmd = ""
2115 cmdName = sys.argv[1]
2116 try:
b86f7378
HWN
2117 klass = commands[cmdName]
2118 cmd = klass()
bb6e09b2
HWN
2119 except KeyError:
2120 print "unknown command %s" % cmdName
2121 print ""
2122 printUsage(commands.keys())
2123 sys.exit(2)
2124
2125 options = cmd.options
b86f7378 2126 cmd.gitdir = os.environ.get("GIT_DIR", None)
bb6e09b2
HWN
2127
2128 args = sys.argv[2:]
2129
2130 if len(options) > 0:
2131 options.append(optparse.make_option("--git-dir", dest="gitdir"))
2132
2133 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2134 options,
2135 description = cmd.description,
2136 formatter = HelpFormatter())
2137
2138 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2139 global verbose
2140 verbose = cmd.verbose
2141 if cmd.needsGit:
b86f7378
HWN
2142 if cmd.gitdir == None:
2143 cmd.gitdir = os.path.abspath(".git")
2144 if not isValidGitDir(cmd.gitdir):
2145 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2146 if os.path.exists(cmd.gitdir):
bb6e09b2
HWN
2147 cdup = read_pipe("git rev-parse --show-cdup").strip()
2148 if len(cdup) > 0:
053fd0c1 2149 chdir(cdup);
e20a9e53 2150
b86f7378
HWN
2151 if not isValidGitDir(cmd.gitdir):
2152 if isValidGitDir(cmd.gitdir + "/.git"):
2153 cmd.gitdir += "/.git"
bb6e09b2 2154 else:
b86f7378 2155 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
e20a9e53 2156
b86f7378 2157 os.environ["GIT_DIR"] = cmd.gitdir
86949eef 2158
bb6e09b2
HWN
2159 if not cmd.run(args):
2160 parser.print_help()
4f5cf76a 2161
4f5cf76a 2162
bb6e09b2
HWN
2163if __name__ == '__main__':
2164 main()