]> git.ipfire.org Git - thirdparty/git.git/blame - contrib/fast-import/git-p4
git-p4: Teach gitConfig method about arguments.
[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)
b932705b
LD
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:
b4b0ba06
PW
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
b984733c
SH
459class Command:
460 def __init__(self):
461 self.usage = "usage: %prog [options]"
8910ac0e 462 self.needsGit = True
b984733c
SH
463
464class P4Debug(Command):
86949eef 465 def __init__(self):
6ae8de88 466 Command.__init__(self)
86949eef 467 self.options = [
b1ce9447
HWN
468 optparse.make_option("--verbose", dest="verbose", action="store_true",
469 default=False),
4addad22 470 ]
c8c39116 471 self.description = "A tool to debug the output of p4 -G."
8910ac0e 472 self.needsGit = False
b1ce9447 473 self.verbose = False
86949eef
SH
474
475 def run(self, args):
b1ce9447 476 j = 0
86949eef 477 for output in p4CmdList(" ".join(args)):
b1ce9447
HWN
478 print 'Element: %d' % j
479 j += 1
86949eef 480 print output
b984733c 481 return True
86949eef 482
5834684d
SH
483class P4RollBack(Command):
484 def __init__(self):
485 Command.__init__(self)
486 self.options = [
0c66a783
SH
487 optparse.make_option("--verbose", dest="verbose", action="store_true"),
488 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
5834684d
SH
489 ]
490 self.description = "A tool to debug the multi-branch import. Don't use :)"
52102d47 491 self.verbose = False
0c66a783 492 self.rollbackLocalBranches = False
5834684d
SH
493
494 def run(self, args):
495 if len(args) != 1:
496 return False
497 maxChange = int(args[0])
0c66a783 498
ad192f28 499 if "p4ExitCode" in p4Cmd("changes -m 1"):
66a2f523
SH
500 die("Problems executing p4");
501
0c66a783
SH
502 if self.rollbackLocalBranches:
503 refPrefix = "refs/heads/"
b016d397 504 lines = read_pipe_lines("git rev-parse --symbolic --branches")
0c66a783
SH
505 else:
506 refPrefix = "refs/remotes/"
b016d397 507 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
0c66a783
SH
508
509 for line in lines:
510 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
b25b2065
HWN
511 line = line.strip()
512 ref = refPrefix + line
5834684d 513 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
514 settings = extractSettingsGitLog(log)
515
516 depotPaths = settings['depot-paths']
517 change = settings['change']
518
5834684d 519 changed = False
52102d47 520
6326aa58
HWN
521 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
522 for p in depotPaths]))) == 0:
52102d47
SH
523 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
524 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
525 continue
526
bb6e09b2 527 while change and int(change) > maxChange:
5834684d 528 changed = True
52102d47
SH
529 if self.verbose:
530 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
5834684d
SH
531 system("git update-ref %s \"%s^\"" % (ref, ref))
532 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
533 settings = extractSettingsGitLog(log)
534
535
536 depotPaths = settings['depot-paths']
537 change = settings['change']
5834684d
SH
538
539 if changed:
52102d47 540 print "%s rewound to %s" % (ref, change)
5834684d
SH
541
542 return True
543
711544b0 544class P4Submit(Command):
4f5cf76a 545 def __init__(self):
b984733c 546 Command.__init__(self)
4f5cf76a 547 self.options = [
4addad22 548 optparse.make_option("--verbose", dest="verbose", action="store_true"),
4f5cf76a 549 optparse.make_option("--origin", dest="origin"),
ae901090 550 optparse.make_option("-M", dest="detectRenames", action="store_true"),
4f5cf76a
SH
551 ]
552 self.description = "Submit changes from git to the perforce depot."
c9b50e63 553 self.usage += " [name of git branch to submit into perforce depot]"
4f5cf76a 554 self.interactive = True
9512497b 555 self.origin = ""
ae901090 556 self.detectRenames = False
b0d10df7 557 self.verbose = False
f7baba8b 558 self.isWindows = (platform.system() == "Windows")
4f5cf76a 559
4f5cf76a
SH
560 def check(self):
561 if len(p4CmdList("opened ...")) > 0:
562 die("You have files opened with perforce! Close them before starting the sync.")
563
edae1e2f
SH
564 # replaces everything between 'Description:' and the next P4 submit template field with the
565 # commit message
4f5cf76a
SH
566 def prepareLogMessage(self, template, message):
567 result = ""
568
edae1e2f
SH
569 inDescriptionSection = False
570
4f5cf76a
SH
571 for line in template.split("\n"):
572 if line.startswith("#"):
573 result += line + "\n"
574 continue
575
edae1e2f 576 if inDescriptionSection:
c9dbab04 577 if line.startswith("Files:") or line.startswith("Jobs:"):
edae1e2f
SH
578 inDescriptionSection = False
579 else:
580 continue
581 else:
582 if line.startswith("Description:"):
583 inDescriptionSection = True
584 line += "\n"
585 for messageLine in message.split("\n"):
586 line += "\t" + messageLine + "\n"
587
588 result += line + "\n"
4f5cf76a
SH
589
590 return result
591
ea99c3ae
SH
592 def prepareSubmitTemplate(self):
593 # remove lines in the Files section that show changes to files outside the depot path we're committing into
594 template = ""
595 inFilesSection = False
b340fa43 596 for line in p4_read_pipe_lines("change -o"):
f3e5ae4f
MSO
597 if line.endswith("\r\n"):
598 line = line[:-2] + "\n"
ea99c3ae
SH
599 if inFilesSection:
600 if line.startswith("\t"):
601 # path starts and ends with a tab
602 path = line[1:]
603 lastTab = path.rfind("\t")
604 if lastTab != -1:
605 path = path[:lastTab]
606 if not path.startswith(self.depotPath):
607 continue
608 else:
609 inFilesSection = False
610 else:
611 if line.startswith("Files:"):
612 inFilesSection = True
613
614 template += line
615
616 return template
617
7cb5cbef 618 def applyCommit(self, id):
0e36f2d7 619 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
ae901090
VA
620
621 if not self.detectRenames:
622 # If not explicitly set check the config variable
623 self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
624
625 if self.detectRenames:
626 diffOpts = "-M"
627 else:
628 diffOpts = ""
629
4fddb41b
VA
630 if gitConfig("git-p4.detectCopies").lower() == "true":
631 diffOpts += " -C"
632
633 if gitConfig("git-p4.detectCopiesHarder").lower() == "true":
634 diffOpts += " --find-copies-harder"
635
0e36f2d7 636 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
4f5cf76a
SH
637 filesToAdd = set()
638 filesToDelete = set()
d336c158 639 editedFiles = set()
c65b670e 640 filesToChangeExecBit = {}
4f5cf76a 641 for line in diff:
b43b0a3c
CP
642 diff = parseDiffTreeEntry(line)
643 modifier = diff['status']
644 path = diff['src']
4f5cf76a 645 if modifier == "M":
87b611d5 646 p4_system("edit \"%s\"" % path)
c65b670e
CP
647 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
648 filesToChangeExecBit[path] = diff['dst_mode']
d336c158 649 editedFiles.add(path)
4f5cf76a
SH
650 elif modifier == "A":
651 filesToAdd.add(path)
c65b670e 652 filesToChangeExecBit[path] = diff['dst_mode']
4f5cf76a
SH
653 if path in filesToDelete:
654 filesToDelete.remove(path)
655 elif modifier == "D":
656 filesToDelete.add(path)
657 if path in filesToAdd:
658 filesToAdd.remove(path)
4fddb41b
VA
659 elif modifier == "C":
660 src, dest = diff['src'], diff['dst']
661 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
662 if diff['src_sha1'] != diff['dst_sha1']:
663 p4_system("edit \"%s\"" % (dest))
664 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
665 p4_system("edit \"%s\"" % (dest))
666 filesToChangeExecBit[dest] = diff['dst_mode']
667 os.unlink(dest)
668 editedFiles.add(dest)
d9a5f25b 669 elif modifier == "R":
b43b0a3c 670 src, dest = diff['src'], diff['dst']
87b611d5 671 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
ae901090
VA
672 if diff['src_sha1'] != diff['dst_sha1']:
673 p4_system("edit \"%s\"" % (dest))
c65b670e 674 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
ae901090 675 p4_system("edit \"%s\"" % (dest))
c65b670e 676 filesToChangeExecBit[dest] = diff['dst_mode']
d9a5f25b
CP
677 os.unlink(dest)
678 editedFiles.add(dest)
679 filesToDelete.add(src)
4f5cf76a
SH
680 else:
681 die("unknown modifier %s for %s" % (modifier, path))
682
0e36f2d7 683 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
47a130b7 684 patchcmd = diffcmd + " | git apply "
c1b296b9
SH
685 tryPatchCmd = patchcmd + "--check -"
686 applyPatchCmd = patchcmd + "--check --apply -"
51a2640a 687
47a130b7 688 if os.system(tryPatchCmd) != 0:
51a2640a
SH
689 print "Unfortunately applying the change failed!"
690 print "What do you want to do?"
691 response = "x"
692 while response != "s" and response != "a" and response != "w":
cebdf5af
HWN
693 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
694 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
51a2640a
SH
695 if response == "s":
696 print "Skipping! Good luck with the next patches..."
20947149 697 for f in editedFiles:
87b611d5 698 p4_system("revert \"%s\"" % f);
20947149
SH
699 for f in filesToAdd:
700 system("rm %s" %f)
51a2640a
SH
701 return
702 elif response == "a":
47a130b7 703 os.system(applyPatchCmd)
51a2640a
SH
704 if len(filesToAdd) > 0:
705 print "You may also want to call p4 add on the following files:"
706 print " ".join(filesToAdd)
707 if len(filesToDelete):
708 print "The following files should be scheduled for deletion with p4 delete:"
709 print " ".join(filesToDelete)
cebdf5af
HWN
710 die("Please resolve and submit the conflict manually and "
711 + "continue afterwards with git-p4 submit --continue")
51a2640a
SH
712 elif response == "w":
713 system(diffcmd + " > patch.txt")
714 print "Patch saved to patch.txt in %s !" % self.clientPath
cebdf5af
HWN
715 die("Please resolve and submit the conflict manually and "
716 "continue afterwards with git-p4 submit --continue")
51a2640a 717
47a130b7 718 system(applyPatchCmd)
4f5cf76a
SH
719
720 for f in filesToAdd:
87b611d5 721 p4_system("add \"%s\"" % f)
4f5cf76a 722 for f in filesToDelete:
87b611d5
AK
723 p4_system("revert \"%s\"" % f)
724 p4_system("delete \"%s\"" % f)
4f5cf76a 725
c65b670e
CP
726 # Set/clear executable bits
727 for f in filesToChangeExecBit.keys():
728 mode = filesToChangeExecBit[f]
729 setP4ExecBit(f, mode)
730
0e36f2d7 731 logMessage = extractLogMessageFromGitCommit(id)
0e36f2d7 732 logMessage = logMessage.strip()
4f5cf76a 733
ea99c3ae 734 template = self.prepareSubmitTemplate()
4f5cf76a
SH
735
736 if self.interactive:
737 submitTemplate = self.prepareLogMessage(template, logMessage)
67abd417
SB
738 if os.environ.has_key("P4DIFF"):
739 del(os.environ["P4DIFF"])
8b130262
AW
740 diff = ""
741 for editedFile in editedFiles:
742 diff += p4_read_pipe("diff -du %r" % editedFile)
4f5cf76a 743
f3e5ae4f 744 newdiff = ""
4f5cf76a 745 for newFile in filesToAdd:
f3e5ae4f
MSO
746 newdiff += "==== new file ====\n"
747 newdiff += "--- /dev/null\n"
748 newdiff += "+++ %s\n" % newFile
4f5cf76a
SH
749 f = open(newFile, "r")
750 for line in f.readlines():
f3e5ae4f 751 newdiff += "+" + line
4f5cf76a
SH
752 f.close()
753
f3e5ae4f 754 separatorLine = "######## everything below this line is just the diff #######\n"
4f5cf76a 755
e96e400f
SH
756 [handle, fileName] = tempfile.mkstemp()
757 tmpFile = os.fdopen(handle, "w+")
f3e5ae4f
MSO
758 if self.isWindows:
759 submitTemplate = submitTemplate.replace("\n", "\r\n")
760 separatorLine = separatorLine.replace("\n", "\r\n")
761 newdiff = newdiff.replace("\n", "\r\n")
762 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
e96e400f 763 tmpFile.close()
cdc7e388 764 mtime = os.stat(fileName).st_mtime
82cea9ff
SB
765 if os.environ.has_key("P4EDITOR"):
766 editor = os.environ.get("P4EDITOR")
767 else:
8b187e6b 768 editor = read_pipe("git var GIT_EDITOR").strip()
e96e400f 769 system(editor + " " + fileName)
e96e400f 770
cdc7e388
SH
771 response = "y"
772 if os.stat(fileName).st_mtime <= mtime:
773 response = "x"
774 while response != "y" and response != "n":
775 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
776
777 if response == "y":
778 tmpFile = open(fileName, "rb")
779 message = tmpFile.read()
780 tmpFile.close()
781 submitTemplate = message[:message.index(separatorLine)]
782 if self.isWindows:
783 submitTemplate = submitTemplate.replace("\r\n", "\n")
784 p4_write_pipe("submit -i", submitTemplate)
785 else:
786 for f in editedFiles:
787 p4_system("revert \"%s\"" % f);
788 for f in filesToAdd:
789 p4_system("revert \"%s\"" % f);
790 system("rm %s" %f)
791
792 os.remove(fileName)
4f5cf76a
SH
793 else:
794 fileName = "submit.txt"
795 file = open(fileName, "w+")
796 file.write(self.prepareLogMessage(template, logMessage))
797 file.close()
cebdf5af
HWN
798 print ("Perforce submit template written as %s. "
799 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
800 % (fileName, fileName))
4f5cf76a
SH
801
802 def run(self, args):
c9b50e63
SH
803 if len(args) == 0:
804 self.master = currentGitBranch()
4280e533 805 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
c9b50e63
SH
806 die("Detecting current git branch failed!")
807 elif len(args) == 1:
808 self.master = args[0]
809 else:
810 return False
811
4c2d5d72
JX
812 allowSubmit = gitConfig("git-p4.allowSubmit")
813 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
814 die("%s is not in git-p4.allowSubmit" % self.master)
815
27d2d811 816 [upstream, settings] = findUpstreamBranchPoint()
ea99c3ae 817 self.depotPath = settings['depot-paths'][0]
27d2d811
SH
818 if len(self.origin) == 0:
819 self.origin = upstream
a3fdd579
SH
820
821 if self.verbose:
822 print "Origin branch is " + self.origin
9512497b 823
ea99c3ae 824 if len(self.depotPath) == 0:
9512497b
SH
825 print "Internal error: cannot locate perforce depot path from existing branches"
826 sys.exit(128)
827
ea99c3ae 828 self.clientPath = p4Where(self.depotPath)
9512497b 829
51a2640a 830 if len(self.clientPath) == 0:
ea99c3ae 831 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
9512497b
SH
832 sys.exit(128)
833
ea99c3ae 834 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
7944f142 835 self.oldWorkingDirectory = os.getcwd()
c1b296b9 836
053fd0c1 837 chdir(self.clientPath)
6a01298a 838 print "Synchronizing p4 checkout..."
87b611d5 839 p4_system("sync ...")
9512497b 840
4f5cf76a 841 self.check()
4f5cf76a 842
4c750c0d
SH
843 commits = []
844 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
845 commits.append(line.strip())
846 commits.reverse()
4f5cf76a
SH
847
848 while len(commits) > 0:
4f5cf76a
SH
849 commit = commits[0]
850 commits = commits[1:]
7cb5cbef 851 self.applyCommit(commit)
4f5cf76a
SH
852 if not self.interactive:
853 break
854
4f5cf76a 855 if len(commits) == 0:
4c750c0d 856 print "All changes applied!"
053fd0c1 857 chdir(self.oldWorkingDirectory)
14594f4b 858
4c750c0d
SH
859 sync = P4Sync()
860 sync.run([])
14594f4b 861
4c750c0d
SH
862 rebase = P4Rebase()
863 rebase.rebase()
4f5cf76a 864
b984733c
SH
865 return True
866
711544b0 867class P4Sync(Command):
56c09345
PW
868 delete_actions = ( "delete", "move/delete", "purge" )
869
b984733c
SH
870 def __init__(self):
871 Command.__init__(self)
872 self.options = [
873 optparse.make_option("--branch", dest="branch"),
874 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
875 optparse.make_option("--changesfile", dest="changesFile"),
876 optparse.make_option("--silent", dest="silent", action="store_true"),
ef48f909 877 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
a028a98e 878 optparse.make_option("--verbose", dest="verbose", action="store_true"),
d2c6dd30
HWN
879 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
880 help="Import into refs/heads/ , not refs/remotes"),
8b41a97f 881 optparse.make_option("--max-changes", dest="maxChanges"),
86dff6b6 882 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
3a70cdfa
TAL
883 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
884 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
885 help="Only sync files that are included in the Perforce Client Spec")
b984733c
SH
886 ]
887 self.description = """Imports from Perforce into a git repository.\n
888 example:
889 //depot/my/project/ -- to import the current head
890 //depot/my/project/@all -- to import everything
891 //depot/my/project/@1,6 -- to import only from revision 1 to 6
892
893 (a ... is not needed in the path p4 specification, it's added implicitly)"""
894
895 self.usage += " //depot/path[@revRange]"
b984733c 896 self.silent = False
1d7367dc
RG
897 self.createdBranches = set()
898 self.committedChanges = set()
569d1bd4 899 self.branch = ""
b984733c 900 self.detectBranches = False
cb53e1f8 901 self.detectLabels = False
b984733c 902 self.changesFile = ""
01265103 903 self.syncWithOrigin = True
4b97ffb1 904 self.verbose = False
a028a98e 905 self.importIntoRemotes = True
01a9c9c5 906 self.maxChanges = ""
c1f9197f 907 self.isWindows = (platform.system() == "Windows")
8b41a97f 908 self.keepRepoPath = False
6326aa58 909 self.depotPaths = None
3c699645 910 self.p4BranchesInGit = []
354081d5 911 self.cloneExclude = []
3a70cdfa
TAL
912 self.useClientSpec = False
913 self.clientSpecDirs = []
b984733c 914
01265103
SH
915 if gitConfig("git-p4.syncFromOrigin") == "false":
916 self.syncWithOrigin = False
917
084f6306
PW
918 #
919 # P4 wildcards are not allowed in filenames. P4 complains
920 # if you simply add them, but you can force it with "-f", in
921 # which case it translates them into %xx encoding internally.
922 # Search for and fix just these four characters. Do % last so
923 # that fixing it does not inadvertently create new %-escapes.
924 #
925 def wildcard_decode(self, path):
926 # Cannot have * in a filename in windows; untested as to
927 # what p4 would do in such a case.
928 if not self.isWindows:
929 path = path.replace("%2A", "*")
930 path = path.replace("%23", "#") \
931 .replace("%40", "@") \
932 .replace("%25", "%")
933 return path
934
b984733c 935 def extractFilesFromCommit(self, commit):
354081d5
TT
936 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
937 for path in self.cloneExclude]
b984733c
SH
938 files = []
939 fnum = 0
940 while commit.has_key("depotFile%s" % fnum):
941 path = commit["depotFile%s" % fnum]
6326aa58 942
354081d5
TT
943 if [p for p in self.cloneExclude
944 if path.startswith (p)]:
945 found = False
946 else:
947 found = [p for p in self.depotPaths
948 if path.startswith (p)]
6326aa58 949 if not found:
b984733c
SH
950 fnum = fnum + 1
951 continue
952
953 file = {}
954 file["path"] = path
955 file["rev"] = commit["rev%s" % fnum]
956 file["action"] = commit["action%s" % fnum]
957 file["type"] = commit["type%s" % fnum]
958 files.append(file)
959 fnum = fnum + 1
960 return files
961
6326aa58 962 def stripRepoPath(self, path, prefixes):
3952710b
IW
963 if self.useClientSpec:
964
965 # if using the client spec, we use the output directory
966 # specified in the client. For example, a view
967 # //depot/foo/branch/... //client/branch/foo/...
968 # will end up putting all foo/branch files into
969 # branch/foo/
970 for val in self.clientSpecDirs:
971 if path.startswith(val[0]):
972 # replace the depot path with the client path
973 path = path.replace(val[0], val[1][1])
974 # now strip out the client (//client/...)
975 path = re.sub("^(//[^/]+/)", '', path)
976 # the rest is all path
977 return path
978
8b41a97f 979 if self.keepRepoPath:
6326aa58
HWN
980 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
981
982 for p in prefixes:
983 if path.startswith(p):
984 path = path[len(p):]
8b41a97f 985
6326aa58 986 return path
6754a299 987
71b112d4 988 def splitFilesIntoBranches(self, commit):
d5904674 989 branches = {}
71b112d4
SH
990 fnum = 0
991 while commit.has_key("depotFile%s" % fnum):
992 path = commit["depotFile%s" % fnum]
6326aa58
HWN
993 found = [p for p in self.depotPaths
994 if path.startswith (p)]
995 if not found:
71b112d4
SH
996 fnum = fnum + 1
997 continue
998
999 file = {}
1000 file["path"] = path
1001 file["rev"] = commit["rev%s" % fnum]
1002 file["action"] = commit["action%s" % fnum]
1003 file["type"] = commit["type%s" % fnum]
1004 fnum = fnum + 1
1005
6326aa58 1006 relPath = self.stripRepoPath(path, self.depotPaths)
b984733c 1007
4b97ffb1 1008 for branch in self.knownBranches.keys():
6754a299
HWN
1009
1010 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
1011 if relPath.startswith(branch + "/"):
d5904674
SH
1012 if branch not in branches:
1013 branches[branch] = []
71b112d4 1014 branches[branch].append(file)
6555b2cc 1015 break
b984733c
SH
1016
1017 return branches
1018
b932705b
LD
1019 # output one file from the P4 stream
1020 # - helper for streamP4Files
1021
1022 def streamOneP4File(self, file, contents):
1023 if file["type"] == "apple":
1024 print "\nfile %s is a strange apple file that forks. Ignoring" % \
1025 file['depotFile']
1026 return
1027
1028 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
084f6306 1029 relPath = self.wildcard_decode(relPath)
b932705b
LD
1030 if verbose:
1031 sys.stderr.write("%s\n" % relPath)
1032
1033 mode = "644"
1034 if isP4Exec(file["type"]):
1035 mode = "755"
1036 elif file["type"] == "symlink":
1037 mode = "120000"
1038 # p4 print on a symlink contains "target\n", so strip it off
b39c3612
EP
1039 data = ''.join(contents)
1040 contents = [data[:-1]]
b932705b
LD
1041
1042 if self.isWindows and file["type"].endswith("text"):
1043 mangled = []
1044 for data in contents:
1045 data = data.replace("\r\n", "\n")
1046 mangled.append(data)
1047 contents = mangled
1048
1049 if file['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
1050 contents = map(lambda text: re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text), contents)
1051 elif file['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
1052 contents = map(lambda text: re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$\n]*\$',r'$\1$', text), contents)
1053
1054 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
1055
1056 # total length...
1057 length = 0
1058 for d in contents:
1059 length = length + len(d)
1060
1061 self.gitStream.write("data %d\n" % length)
1062 for d in contents:
1063 self.gitStream.write(d)
1064 self.gitStream.write("\n")
1065
1066 def streamOneP4Deletion(self, file):
1067 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
1068 if verbose:
1069 sys.stderr.write("delete %s\n" % relPath)
1070 self.gitStream.write("D %s\n" % relPath)
1071
1072 # handle another chunk of streaming data
1073 def streamP4FilesCb(self, marshalled):
1074
1075 if marshalled.has_key('depotFile') and self.stream_have_file_info:
1076 # start of a new file - output the old one first
1077 self.streamOneP4File(self.stream_file, self.stream_contents)
1078 self.stream_file = {}
1079 self.stream_contents = []
1080 self.stream_have_file_info = False
1081
1082 # pick up the new file information... for the
1083 # 'data' field we need to append to our array
1084 for k in marshalled.keys():
1085 if k == 'data':
1086 self.stream_contents.append(marshalled['data'])
1087 else:
1088 self.stream_file[k] = marshalled[k]
1089
1090 self.stream_have_file_info = True
1091
1092 # Stream directly from "p4 files" into "git fast-import"
1093 def streamP4Files(self, files):
30b5940b
SH
1094 filesForCommit = []
1095 filesToRead = []
b932705b 1096 filesToDelete = []
30b5940b 1097
3a70cdfa 1098 for f in files:
30b5940b 1099 includeFile = True
3a70cdfa
TAL
1100 for val in self.clientSpecDirs:
1101 if f['path'].startswith(val[0]):
3952710b 1102 if val[1][0] <= 0:
30b5940b 1103 includeFile = False
3a70cdfa
TAL
1104 break
1105
30b5940b
SH
1106 if includeFile:
1107 filesForCommit.append(f)
56c09345 1108 if f['action'] in self.delete_actions:
b932705b 1109 filesToDelete.append(f)
56c09345
PW
1110 else:
1111 filesToRead.append(f)
6a49f8e2 1112
b932705b
LD
1113 # deleted files...
1114 for f in filesToDelete:
1115 self.streamOneP4Deletion(f)
1b9a4684 1116
b932705b
LD
1117 if len(filesToRead) > 0:
1118 self.stream_file = {}
1119 self.stream_contents = []
1120 self.stream_have_file_info = False
8ff45f2a 1121
b932705b
LD
1122 # curry self argument
1123 def streamP4FilesCbSelf(entry):
1124 self.streamP4FilesCb(entry)
6a49f8e2 1125
b932705b
LD
1126 p4CmdList("-x - print",
1127 '\n'.join(['%s#%s' % (f['path'], f['rev'])
1128 for f in filesToRead]),
1129 cb=streamP4FilesCbSelf)
30b5940b 1130
b932705b
LD
1131 # do the last chunk
1132 if self.stream_file.has_key('depotFile'):
1133 self.streamOneP4File(self.stream_file, self.stream_contents)
6a49f8e2 1134
6326aa58 1135 def commit(self, details, files, branch, branchPrefixes, parent = ""):
b984733c
SH
1136 epoch = details["time"]
1137 author = details["user"]
b932705b 1138 self.branchPrefixes = branchPrefixes
b984733c 1139
4b97ffb1
SH
1140 if self.verbose:
1141 print "commit into %s" % branch
1142
96e07dd2
HWN
1143 # start with reading files; if that fails, we should not
1144 # create a commit.
1145 new_files = []
1146 for f in files:
1147 if [p for p in branchPrefixes if f['path'].startswith(p)]:
1148 new_files.append (f)
1149 else:
1150 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
96e07dd2 1151
b984733c 1152 self.gitStream.write("commit %s\n" % branch)
6a49f8e2 1153# gitStream.write("mark :%s\n" % details["change"])
b984733c
SH
1154 self.committedChanges.add(int(details["change"]))
1155 committer = ""
b607e71e
SH
1156 if author not in self.users:
1157 self.getUserMapFromPerforceServer()
b984733c 1158 if author in self.users:
0828ab14 1159 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
b984733c 1160 else:
0828ab14 1161 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
b984733c
SH
1162
1163 self.gitStream.write("committer %s\n" % committer)
1164
1165 self.gitStream.write("data <<EOT\n")
1166 self.gitStream.write(details["desc"])
6581de09
SH
1167 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
1168 % (','.join (branchPrefixes), details["change"]))
1169 if len(details['options']) > 0:
1170 self.gitStream.write(": options = %s" % details['options'])
1171 self.gitStream.write("]\nEOT\n\n")
b984733c
SH
1172
1173 if len(parent) > 0:
4b97ffb1
SH
1174 if self.verbose:
1175 print "parent %s" % parent
b984733c
SH
1176 self.gitStream.write("from %s\n" % parent)
1177
b932705b 1178 self.streamP4Files(new_files)
b984733c
SH
1179 self.gitStream.write("\n")
1180
1f4ba1cb
SH
1181 change = int(details["change"])
1182
9bda3a85 1183 if self.labels.has_key(change):
1f4ba1cb
SH
1184 label = self.labels[change]
1185 labelDetails = label[0]
1186 labelRevisions = label[1]
71b112d4
SH
1187 if self.verbose:
1188 print "Change %s is labelled %s" % (change, labelDetails)
1f4ba1cb 1189
6326aa58
HWN
1190 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1191 for p in branchPrefixes]))
1f4ba1cb
SH
1192
1193 if len(files) == len(labelRevisions):
1194
1195 cleanedFiles = {}
1196 for info in files:
56c09345 1197 if info["action"] in self.delete_actions:
1f4ba1cb
SH
1198 continue
1199 cleanedFiles[info["depotFile"]] = info["rev"]
1200
1201 if cleanedFiles == labelRevisions:
1202 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1203 self.gitStream.write("from %s\n" % branch)
1204
1205 owner = labelDetails["Owner"]
1206 tagger = ""
1207 if author in self.users:
1208 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1209 else:
1210 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1211 self.gitStream.write("tagger %s\n" % tagger)
1212 self.gitStream.write("data <<EOT\n")
1213 self.gitStream.write(labelDetails["Description"])
1214 self.gitStream.write("EOT\n\n")
1215
1216 else:
a46668fa 1217 if not self.silent:
cebdf5af
HWN
1218 print ("Tag %s does not match with change %s: files do not match."
1219 % (labelDetails["label"], change))
1f4ba1cb
SH
1220
1221 else:
a46668fa 1222 if not self.silent:
cebdf5af
HWN
1223 print ("Tag %s does not match with change %s: file count is different."
1224 % (labelDetails["label"], change))
b984733c 1225
183b8ef8 1226 def getUserCacheFilename(self):
b2d2d16a
SH
1227 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1228 return home + "/.gitp4-usercache.txt"
183b8ef8 1229
b607e71e 1230 def getUserMapFromPerforceServer(self):
ebd81168
SH
1231 if self.userMapFromPerforceServer:
1232 return
b984733c
SH
1233 self.users = {}
1234
1235 for output in p4CmdList("users"):
1236 if not output.has_key("User"):
1237 continue
1238 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1239
183b8ef8
HWN
1240
1241 s = ''
1242 for (key, val) in self.users.items():
3b167396 1243 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
183b8ef8
HWN
1244
1245 open(self.getUserCacheFilename(), "wb").write(s)
ebd81168 1246 self.userMapFromPerforceServer = True
b607e71e
SH
1247
1248 def loadUserMapFromCache(self):
1249 self.users = {}
ebd81168 1250 self.userMapFromPerforceServer = False
b607e71e 1251 try:
183b8ef8 1252 cache = open(self.getUserCacheFilename(), "rb")
b607e71e
SH
1253 lines = cache.readlines()
1254 cache.close()
1255 for line in lines:
b25b2065 1256 entry = line.strip().split("\t")
b607e71e
SH
1257 self.users[entry[0]] = entry[1]
1258 except IOError:
1259 self.getUserMapFromPerforceServer()
1260
1f4ba1cb
SH
1261 def getLabels(self):
1262 self.labels = {}
1263
6326aa58 1264 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
10c3211b 1265 if len(l) > 0 and not self.silent:
183f8436 1266 print "Finding files belonging to labels in %s" % `self.depotPaths`
01ce1fe9
SH
1267
1268 for output in l:
1f4ba1cb
SH
1269 label = output["label"]
1270 revisions = {}
1271 newestChange = 0
71b112d4
SH
1272 if self.verbose:
1273 print "Querying files for label %s" % label
6326aa58
HWN
1274 for file in p4CmdList("files "
1275 + ' '.join (["%s...@%s" % (p, label)
1276 for p in self.depotPaths])):
1f4ba1cb
SH
1277 revisions[file["depotFile"]] = file["rev"]
1278 change = int(file["change"])
1279 if change > newestChange:
1280 newestChange = change
1281
9bda3a85
SH
1282 self.labels[newestChange] = [output, revisions]
1283
1284 if self.verbose:
1285 print "Label changes: %s" % self.labels.keys()
1f4ba1cb 1286
86dff6b6
HWN
1287 def guessProjectName(self):
1288 for p in self.depotPaths:
6e5295c4
SH
1289 if p.endswith("/"):
1290 p = p[:-1]
1291 p = p[p.strip().rfind("/") + 1:]
1292 if not p.endswith("/"):
1293 p += "/"
1294 return p
86dff6b6 1295
4b97ffb1 1296 def getBranchMapping(self):
6555b2cc
SH
1297 lostAndFoundBranches = set()
1298
4b97ffb1
SH
1299 for info in p4CmdList("branches"):
1300 details = p4Cmd("branch -o %s" % info["branch"])
1301 viewIdx = 0
1302 while details.has_key("View%s" % viewIdx):
1303 paths = details["View%s" % viewIdx].split(" ")
1304 viewIdx = viewIdx + 1
1305 # require standard //depot/foo/... //depot/bar/... mapping
1306 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1307 continue
1308 source = paths[0]
1309 destination = paths[1]
6509e19c
SH
1310 ## HACK
1311 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1312 source = source[len(self.depotPaths[0]):-4]
1313 destination = destination[len(self.depotPaths[0]):-4]
6555b2cc 1314
1a2edf4e
SH
1315 if destination in self.knownBranches:
1316 if not self.silent:
1317 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1318 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1319 continue
1320
6555b2cc
SH
1321 self.knownBranches[destination] = source
1322
1323 lostAndFoundBranches.discard(destination)
1324
29bdbac1 1325 if source not in self.knownBranches:
6555b2cc
SH
1326 lostAndFoundBranches.add(source)
1327
1328
1329 for branch in lostAndFoundBranches:
1330 self.knownBranches[branch] = branch
29bdbac1 1331
38f9f5ec
SH
1332 def getBranchMappingFromGitBranches(self):
1333 branches = p4BranchesInGit(self.importIntoRemotes)
1334 for branch in branches.keys():
1335 if branch == "master":
1336 branch = "main"
1337 else:
1338 branch = branch[len(self.projectName):]
1339 self.knownBranches[branch] = branch
1340
29bdbac1 1341 def listExistingP4GitBranches(self):
144ff46b
SH
1342 # branches holds mapping from name to commit
1343 branches = p4BranchesInGit(self.importIntoRemotes)
1344 self.p4BranchesInGit = branches.keys()
1345 for branch in branches.keys():
1346 self.initialParents[self.refPrefix + branch] = branches[branch]
4b97ffb1 1347
bb6e09b2
HWN
1348 def updateOptionDict(self, d):
1349 option_keys = {}
1350 if self.keepRepoPath:
1351 option_keys['keepRepoPath'] = 1
1352
1353 d["options"] = ' '.join(sorted(option_keys.keys()))
1354
1355 def readOptions(self, d):
1356 self.keepRepoPath = (d.has_key('options')
1357 and ('keepRepoPath' in d['options']))
6326aa58 1358
8134f69c
SH
1359 def gitRefForBranch(self, branch):
1360 if branch == "main":
1361 return self.refPrefix + "master"
1362
1363 if len(branch) <= 0:
1364 return branch
1365
1366 return self.refPrefix + self.projectName + branch
1367
1ca3d710
SH
1368 def gitCommitByP4Change(self, ref, change):
1369 if self.verbose:
1370 print "looking in ref " + ref + " for change %s using bisect..." % change
1371
1372 earliestCommit = ""
1373 latestCommit = parseRevision(ref)
1374
1375 while True:
1376 if self.verbose:
1377 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1378 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1379 if len(next) == 0:
1380 if self.verbose:
1381 print "argh"
1382 return ""
1383 log = extractLogMessageFromGitCommit(next)
1384 settings = extractSettingsGitLog(log)
1385 currentChange = int(settings['change'])
1386 if self.verbose:
1387 print "current change %s" % currentChange
1388
1389 if currentChange == change:
1390 if self.verbose:
1391 print "found %s" % next
1392 return next
1393
1394 if currentChange < change:
1395 earliestCommit = "^%s" % next
1396 else:
1397 latestCommit = "%s" % next
1398
1399 return ""
1400
1401 def importNewBranch(self, branch, maxChange):
1402 # make fast-import flush all changes to disk and update the refs using the checkpoint
1403 # command so that we can try to find the branch parent in the git history
1404 self.gitStream.write("checkpoint\n\n");
1405 self.gitStream.flush();
1406 branchPrefix = self.depotPaths[0] + branch + "/"
1407 range = "@1,%s" % maxChange
1408 #print "prefix" + branchPrefix
1409 changes = p4ChangesForPaths([branchPrefix], range)
1410 if len(changes) <= 0:
1411 return False
1412 firstChange = changes[0]
1413 #print "first change in branch: %s" % firstChange
1414 sourceBranch = self.knownBranches[branch]
1415 sourceDepotPath = self.depotPaths[0] + sourceBranch
1416 sourceRef = self.gitRefForBranch(sourceBranch)
1417 #print "source " + sourceBranch
1418
1419 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1420 #print "branch parent: %s" % branchParentChange
1421 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1422 if len(gitParent) > 0:
1423 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1424 #print "parent git commit: %s" % gitParent
1425
1426 self.importChanges(changes)
1427 return True
1428
e87f37ae
SH
1429 def importChanges(self, changes):
1430 cnt = 1
1431 for change in changes:
1432 description = p4Cmd("describe %s" % change)
1433 self.updateOptionDict(description)
1434
1435 if not self.silent:
1436 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1437 sys.stdout.flush()
1438 cnt = cnt + 1
1439
1440 try:
1441 if self.detectBranches:
1442 branches = self.splitFilesIntoBranches(description)
1443 for branch in branches.keys():
1444 ## HACK --hwn
1445 branchPrefix = self.depotPaths[0] + branch + "/"
1446
1447 parent = ""
1448
1449 filesForCommit = branches[branch]
1450
1451 if self.verbose:
1452 print "branch is %s" % branch
1453
1454 self.updatedBranches.add(branch)
1455
1456 if branch not in self.createdBranches:
1457 self.createdBranches.add(branch)
1458 parent = self.knownBranches[branch]
1459 if parent == branch:
1460 parent = ""
1ca3d710
SH
1461 else:
1462 fullBranch = self.projectName + branch
1463 if fullBranch not in self.p4BranchesInGit:
1464 if not self.silent:
1465 print("\n Importing new branch %s" % fullBranch);
1466 if self.importNewBranch(branch, change - 1):
1467 parent = ""
1468 self.p4BranchesInGit.append(fullBranch)
1469 if not self.silent:
1470 print("\n Resuming with change %s" % change);
1471
1472 if self.verbose:
1473 print "parent determined through known branches: %s" % parent
e87f37ae 1474
8134f69c
SH
1475 branch = self.gitRefForBranch(branch)
1476 parent = self.gitRefForBranch(parent)
e87f37ae
SH
1477
1478 if self.verbose:
1479 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1480
1481 if len(parent) == 0 and branch in self.initialParents:
1482 parent = self.initialParents[branch]
1483 del self.initialParents[branch]
1484
1485 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1486 else:
1487 files = self.extractFilesFromCommit(description)
1488 self.commit(description, files, self.branch, self.depotPaths,
1489 self.initialParent)
1490 self.initialParent = ""
1491 except IOError:
1492 print self.gitError.read()
1493 sys.exit(1)
1494
c208a243
SH
1495 def importHeadRevision(self, revision):
1496 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1497
1498 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1494fcbb 1499 details["desc"] = ("Initial import of %s from the state at revision %s\n"
c208a243
SH
1500 % (' '.join(self.depotPaths), revision))
1501 details["change"] = revision
1502 newestRevision = 0
1503
1504 fileCnt = 0
1505 for info in p4CmdList("files "
1506 + ' '.join(["%s...%s"
1507 % (p, revision)
1508 for p in self.depotPaths])):
1509
68b28593 1510 if 'code' in info and info['code'] == 'error':
c208a243
SH
1511 sys.stderr.write("p4 returned an error: %s\n"
1512 % info['data'])
d88e707f
PW
1513 if info['data'].find("must refer to client") >= 0:
1514 sys.stderr.write("This particular p4 error is misleading.\n")
1515 sys.stderr.write("Perhaps the depot path was misspelled.\n");
1516 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
c208a243 1517 sys.exit(1)
68b28593
PW
1518 if 'p4ExitCode' in info:
1519 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
c208a243
SH
1520 sys.exit(1)
1521
1522
1523 change = int(info["change"])
1524 if change > newestRevision:
1525 newestRevision = change
1526
56c09345 1527 if info["action"] in self.delete_actions:
c208a243
SH
1528 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1529 #fileCnt = fileCnt + 1
1530 continue
1531
1532 for prop in ["depotFile", "rev", "action", "type" ]:
1533 details["%s%s" % (prop, fileCnt)] = info[prop]
1534
1535 fileCnt = fileCnt + 1
1536
1537 details["change"] = newestRevision
1538 self.updateOptionDict(details)
1539 try:
1540 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1541 except IOError:
1542 print "IO error with git fast-import. Is your git version recent enough?"
1543 print self.gitError.read()
1544
1545
3a70cdfa
TAL
1546 def getClientSpec(self):
1547 specList = p4CmdList( "client -o" )
1548 temp = {}
1549 for entry in specList:
1550 for k,v in entry.iteritems():
1551 if k.startswith("View"):
3952710b
IW
1552
1553 # p4 has these %%1 to %%9 arguments in specs to
1554 # reorder paths; which we can't handle (yet :)
1555 if re.match('%%\d', v) != None:
1556 print "Sorry, can't handle %%n arguments in client specs"
1557 sys.exit(1)
1558
3a70cdfa
TAL
1559 if v.startswith('"'):
1560 start = 1
1561 else:
1562 start = 0
1563 index = v.find("...")
3952710b
IW
1564
1565 # save the "client view"; i.e the RHS of the view
1566 # line that tells the client where to put the
1567 # files for this view.
1568 cv = v[index+3:].strip() # +3 to remove previous '...'
1569
1570 # if the client view doesn't end with a
1571 # ... wildcard, then we're going to mess up the
1572 # output directory, so fail gracefully.
1573 if not cv.endswith('...'):
1574 print 'Sorry, client view in "%s" needs to end with wildcard' % (k)
1575 sys.exit(1)
1576 cv=cv[:-3]
1577
1578 # now save the view; +index means included, -index
1579 # means it should be filtered out.
3a70cdfa
TAL
1580 v = v[start:index]
1581 if v.startswith("-"):
1582 v = v[1:]
3952710b 1583 include = -len(v)
3a70cdfa 1584 else:
3952710b
IW
1585 include = len(v)
1586
1587 temp[v] = (include, cv)
1588
3a70cdfa 1589 self.clientSpecDirs = temp.items()
3952710b 1590 self.clientSpecDirs.sort( lambda x, y: abs( y[1][0] ) - abs( x[1][0] ) )
3a70cdfa 1591
b984733c 1592 def run(self, args):
6326aa58 1593 self.depotPaths = []
179caebf
SH
1594 self.changeRange = ""
1595 self.initialParent = ""
6326aa58 1596 self.previousDepotPaths = []
ce6f33c8 1597
29bdbac1
SH
1598 # map from branch depot path to parent branch
1599 self.knownBranches = {}
1600 self.initialParents = {}
5ca44617 1601 self.hasOrigin = originP4BranchesExist()
a43ff00c
SH
1602 if not self.syncWithOrigin:
1603 self.hasOrigin = False
29bdbac1 1604
a028a98e
SH
1605 if self.importIntoRemotes:
1606 self.refPrefix = "refs/remotes/p4/"
1607 else:
db775559 1608 self.refPrefix = "refs/heads/p4/"
a028a98e 1609
cebdf5af
HWN
1610 if self.syncWithOrigin and self.hasOrigin:
1611 if not self.silent:
1612 print "Syncing with origin first by calling git fetch origin"
1613 system("git fetch origin")
10f880f8 1614
569d1bd4 1615 if len(self.branch) == 0:
db775559 1616 self.branch = self.refPrefix + "master"
a028a98e 1617 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
48df6fd8 1618 system("git update-ref %s refs/heads/p4" % self.branch)
48df6fd8 1619 system("git branch -D p4");
faf1bd20 1620 # create it /after/ importing, when master exists
0058a33a 1621 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
a3c55c09 1622 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
967f72e2 1623
3cafb7d8 1624 if self.useClientSpec or gitConfig("git-p4.useclientspec") == "true":
3a70cdfa
TAL
1625 self.getClientSpec()
1626
6a49f8e2
HWN
1627 # TODO: should always look at previous commits,
1628 # merge with previous imports, if possible.
1629 if args == []:
d414c74a 1630 if self.hasOrigin:
5ca44617 1631 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
abcd790f
SH
1632 self.listExistingP4GitBranches()
1633
1634 if len(self.p4BranchesInGit) > 1:
1635 if not self.silent:
1636 print "Importing from/into multiple branches"
1637 self.detectBranches = True
967f72e2 1638
29bdbac1
SH
1639 if self.verbose:
1640 print "branches: %s" % self.p4BranchesInGit
1641
1642 p4Change = 0
1643 for branch in self.p4BranchesInGit:
cebdf5af 1644 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
bb6e09b2
HWN
1645
1646 settings = extractSettingsGitLog(logMsg)
29bdbac1 1647
bb6e09b2
HWN
1648 self.readOptions(settings)
1649 if (settings.has_key('depot-paths')
1650 and settings.has_key ('change')):
1651 change = int(settings['change']) + 1
29bdbac1
SH
1652 p4Change = max(p4Change, change)
1653
bb6e09b2
HWN
1654 depotPaths = sorted(settings['depot-paths'])
1655 if self.previousDepotPaths == []:
6326aa58 1656 self.previousDepotPaths = depotPaths
29bdbac1 1657 else:
6326aa58
HWN
1658 paths = []
1659 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
583e1707 1660 for i in range(0, min(len(cur), len(prev))):
6326aa58 1661 if cur[i] <> prev[i]:
583e1707 1662 i = i - 1
6326aa58
HWN
1663 break
1664
583e1707 1665 paths.append (cur[:i + 1])
6326aa58
HWN
1666
1667 self.previousDepotPaths = paths
29bdbac1
SH
1668
1669 if p4Change > 0:
bb6e09b2 1670 self.depotPaths = sorted(self.previousDepotPaths)
d5904674 1671 self.changeRange = "@%s,#head" % p4Change
330f53b8
SH
1672 if not self.detectBranches:
1673 self.initialParent = parseRevision(self.branch)
341dc1c1 1674 if not self.silent and not self.detectBranches:
967f72e2 1675 print "Performing incremental import into %s git branch" % self.branch
569d1bd4 1676
f9162f6a
SH
1677 if not self.branch.startswith("refs/"):
1678 self.branch = "refs/heads/" + self.branch
179caebf 1679
6326aa58 1680 if len(args) == 0 and self.depotPaths:
b984733c 1681 if not self.silent:
6326aa58 1682 print "Depot paths: %s" % ' '.join(self.depotPaths)
b984733c 1683 else:
6326aa58 1684 if self.depotPaths and self.depotPaths != args:
cebdf5af 1685 print ("previous import used depot path %s and now %s was specified. "
6326aa58
HWN
1686 "This doesn't work!" % (' '.join (self.depotPaths),
1687 ' '.join (args)))
b984733c 1688 sys.exit(1)
6326aa58 1689
bb6e09b2 1690 self.depotPaths = sorted(args)
b984733c 1691
1c49fc19 1692 revision = ""
b984733c 1693 self.users = {}
b984733c 1694
6326aa58
HWN
1695 newPaths = []
1696 for p in self.depotPaths:
1697 if p.find("@") != -1:
1698 atIdx = p.index("@")
1699 self.changeRange = p[atIdx:]
1700 if self.changeRange == "@all":
1701 self.changeRange = ""
6a49f8e2 1702 elif ',' not in self.changeRange:
1c49fc19 1703 revision = self.changeRange
6326aa58 1704 self.changeRange = ""
7fcff9de 1705 p = p[:atIdx]
6326aa58
HWN
1706 elif p.find("#") != -1:
1707 hashIdx = p.index("#")
1c49fc19 1708 revision = p[hashIdx:]
7fcff9de 1709 p = p[:hashIdx]
6326aa58 1710 elif self.previousDepotPaths == []:
1c49fc19 1711 revision = "#head"
6326aa58
HWN
1712
1713 p = re.sub ("\.\.\.$", "", p)
1714 if not p.endswith("/"):
1715 p += "/"
1716
1717 newPaths.append(p)
1718
1719 self.depotPaths = newPaths
1720
b984733c 1721
b607e71e 1722 self.loadUserMapFromCache()
cb53e1f8
SH
1723 self.labels = {}
1724 if self.detectLabels:
1725 self.getLabels();
b984733c 1726
4b97ffb1 1727 if self.detectBranches:
df450923
SH
1728 ## FIXME - what's a P4 projectName ?
1729 self.projectName = self.guessProjectName()
1730
38f9f5ec
SH
1731 if self.hasOrigin:
1732 self.getBranchMappingFromGitBranches()
1733 else:
1734 self.getBranchMapping()
29bdbac1
SH
1735 if self.verbose:
1736 print "p4-git branches: %s" % self.p4BranchesInGit
1737 print "initial parents: %s" % self.initialParents
1738 for b in self.p4BranchesInGit:
1739 if b != "master":
6326aa58
HWN
1740
1741 ## FIXME
29bdbac1
SH
1742 b = b[len(self.projectName):]
1743 self.createdBranches.add(b)
4b97ffb1 1744
f291b4e3 1745 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
b984733c 1746
cebdf5af 1747 importProcess = subprocess.Popen(["git", "fast-import"],
6326aa58
HWN
1748 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1749 stderr=subprocess.PIPE);
08483580
SH
1750 self.gitOutput = importProcess.stdout
1751 self.gitStream = importProcess.stdin
1752 self.gitError = importProcess.stderr
b984733c 1753
1c49fc19 1754 if revision:
c208a243 1755 self.importHeadRevision(revision)
b984733c
SH
1756 else:
1757 changes = []
1758
0828ab14 1759 if len(self.changesFile) > 0:
b984733c 1760 output = open(self.changesFile).readlines()
1d7367dc 1761 changeSet = set()
b984733c
SH
1762 for line in output:
1763 changeSet.add(int(line))
1764
1765 for change in changeSet:
1766 changes.append(change)
1767
1768 changes.sort()
1769 else:
ac34efcf 1770 if not isinstance(self, P4Clone) and not self.p4BranchesInGit:
e32e00dc 1771 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
29bdbac1 1772 if self.verbose:
86dff6b6 1773 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
6326aa58 1774 self.changeRange)
4f6432d8 1775 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
b984733c 1776
01a9c9c5 1777 if len(self.maxChanges) > 0:
7fcff9de 1778 changes = changes[:min(int(self.maxChanges), len(changes))]
01a9c9c5 1779
b984733c 1780 if len(changes) == 0:
0828ab14 1781 if not self.silent:
341dc1c1 1782 print "No changes to import!"
1f52af6c 1783 return True
b984733c 1784
a9d1a27a
SH
1785 if not self.silent and not self.detectBranches:
1786 print "Import destination: %s" % self.branch
1787
341dc1c1
SH
1788 self.updatedBranches = set()
1789
e87f37ae 1790 self.importChanges(changes)
b984733c 1791
341dc1c1
SH
1792 if not self.silent:
1793 print ""
1794 if len(self.updatedBranches) > 0:
1795 sys.stdout.write("Updated branches: ")
1796 for b in self.updatedBranches:
1797 sys.stdout.write("%s " % b)
1798 sys.stdout.write("\n")
b984733c 1799
b984733c 1800 self.gitStream.close()
29bdbac1
SH
1801 if importProcess.wait() != 0:
1802 die("fast-import failed: %s" % self.gitError.read())
b984733c
SH
1803 self.gitOutput.close()
1804 self.gitError.close()
1805
b984733c
SH
1806 return True
1807
01ce1fe9
SH
1808class P4Rebase(Command):
1809 def __init__(self):
1810 Command.__init__(self)
01265103 1811 self.options = [ ]
cebdf5af
HWN
1812 self.description = ("Fetches the latest revision from perforce and "
1813 + "rebases the current work (branch) against it")
68c42153 1814 self.verbose = False
01ce1fe9
SH
1815
1816 def run(self, args):
1817 sync = P4Sync()
1818 sync.run([])
d7e3868c 1819
14594f4b
SH
1820 return self.rebase()
1821
1822 def rebase(self):
36ee4ee4
SH
1823 if os.system("git update-index --refresh") != 0:
1824 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.");
1825 if len(read_pipe("git diff-index HEAD --")) > 0:
1826 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1827
d7e3868c
SH
1828 [upstream, settings] = findUpstreamBranchPoint()
1829 if len(upstream) == 0:
1830 die("Cannot find upstream branchpoint for rebase")
1831
1832 # the branchpoint may be p4/foo~3, so strip off the parent
1833 upstream = re.sub("~[0-9]+$", "", upstream)
1834
1835 print "Rebasing the current branch onto %s" % upstream
b25b2065 1836 oldHead = read_pipe("git rev-parse HEAD").strip()
d7e3868c 1837 system("git rebase %s" % upstream)
1f52af6c 1838 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
01ce1fe9
SH
1839 return True
1840
f9a3a4f7
SH
1841class P4Clone(P4Sync):
1842 def __init__(self):
1843 P4Sync.__init__(self)
1844 self.description = "Creates a new git repository and imports from Perforce into it"
bb6e09b2 1845 self.usage = "usage: %prog [options] //depot/path[@revRange]"
354081d5 1846 self.options += [
bb6e09b2
HWN
1847 optparse.make_option("--destination", dest="cloneDestination",
1848 action='store', default=None,
354081d5
TT
1849 help="where to leave result of the clone"),
1850 optparse.make_option("-/", dest="cloneExclude",
1851 action="append", type="string",
38200076
PW
1852 help="exclude depot path"),
1853 optparse.make_option("--bare", dest="cloneBare",
1854 action="store_true", default=False),
354081d5 1855 ]
bb6e09b2 1856 self.cloneDestination = None
f9a3a4f7 1857 self.needsGit = False
38200076 1858 self.cloneBare = False
f9a3a4f7 1859
354081d5
TT
1860 # This is required for the "append" cloneExclude action
1861 def ensure_value(self, attr, value):
1862 if not hasattr(self, attr) or getattr(self, attr) is None:
1863 setattr(self, attr, value)
1864 return getattr(self, attr)
1865
6a49f8e2
HWN
1866 def defaultDestination(self, args):
1867 ## TODO: use common prefix of args?
1868 depotPath = args[0]
1869 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1870 depotDir = re.sub("(#[^#]*)$", "", depotDir)
053d9e43 1871 depotDir = re.sub(r"\.\.\.$", "", depotDir)
6a49f8e2
HWN
1872 depotDir = re.sub(r"/$", "", depotDir)
1873 return os.path.split(depotDir)[1]
1874
f9a3a4f7
SH
1875 def run(self, args):
1876 if len(args) < 1:
1877 return False
bb6e09b2
HWN
1878
1879 if self.keepRepoPath and not self.cloneDestination:
1880 sys.stderr.write("Must specify destination for --keep-path\n")
1881 sys.exit(1)
f9a3a4f7 1882
6326aa58 1883 depotPaths = args
5e100b5c
SH
1884
1885 if not self.cloneDestination and len(depotPaths) > 1:
1886 self.cloneDestination = depotPaths[-1]
1887 depotPaths = depotPaths[:-1]
1888
354081d5 1889 self.cloneExclude = ["/"+p for p in self.cloneExclude]
6326aa58
HWN
1890 for p in depotPaths:
1891 if not p.startswith("//"):
1892 return False
f9a3a4f7 1893
bb6e09b2 1894 if not self.cloneDestination:
98ad4faf 1895 self.cloneDestination = self.defaultDestination(args)
f9a3a4f7 1896
86dff6b6 1897 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
38200076 1898
c3bf3f13
KG
1899 if not os.path.exists(self.cloneDestination):
1900 os.makedirs(self.cloneDestination)
053fd0c1 1901 chdir(self.cloneDestination)
38200076
PW
1902
1903 init_cmd = [ "git", "init" ]
1904 if self.cloneBare:
1905 init_cmd.append("--bare")
1906 subprocess.check_call(init_cmd)
1907
6326aa58 1908 if not P4Sync.run(self, depotPaths):
f9a3a4f7 1909 return False
f9a3a4f7 1910 if self.branch != "master":
e9905013
TAL
1911 if self.importIntoRemotes:
1912 masterbranch = "refs/remotes/p4/master"
1913 else:
1914 masterbranch = "refs/heads/p4/master"
1915 if gitBranchExists(masterbranch):
1916 system("git branch master %s" % masterbranch)
38200076
PW
1917 if not self.cloneBare:
1918 system("git checkout -f")
8f9b2e08
SH
1919 else:
1920 print "Could not detect main branch. No checkout/master branch created."
86dff6b6 1921
f9a3a4f7
SH
1922 return True
1923
09d89de2
SH
1924class P4Branches(Command):
1925 def __init__(self):
1926 Command.__init__(self)
1927 self.options = [ ]
1928 self.description = ("Shows the git branches that hold imports and their "
1929 + "corresponding perforce depot paths")
1930 self.verbose = False
1931
1932 def run(self, args):
5ca44617
SH
1933 if originP4BranchesExist():
1934 createOrUpdateBranchesFromOrigin()
1935
09d89de2
SH
1936 cmdline = "git rev-parse --symbolic "
1937 cmdline += " --remotes"
1938
1939 for line in read_pipe_lines(cmdline):
1940 line = line.strip()
1941
1942 if not line.startswith('p4/') or line == "p4/HEAD":
1943 continue
1944 branch = line
1945
1946 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1947 settings = extractSettingsGitLog(log)
1948
1949 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1950 return True
1951
b984733c
SH
1952class HelpFormatter(optparse.IndentedHelpFormatter):
1953 def __init__(self):
1954 optparse.IndentedHelpFormatter.__init__(self)
1955
1956 def format_description(self, description):
1957 if description:
1958 return description + "\n"
1959 else:
1960 return ""
4f5cf76a 1961
86949eef
SH
1962def printUsage(commands):
1963 print "usage: %s <command> [options]" % sys.argv[0]
1964 print ""
1965 print "valid commands: %s" % ", ".join(commands)
1966 print ""
1967 print "Try %s <command> --help for command specific help." % sys.argv[0]
1968 print ""
1969
1970commands = {
b86f7378
HWN
1971 "debug" : P4Debug,
1972 "submit" : P4Submit,
a9834f58 1973 "commit" : P4Submit,
b86f7378
HWN
1974 "sync" : P4Sync,
1975 "rebase" : P4Rebase,
1976 "clone" : P4Clone,
09d89de2
SH
1977 "rollback" : P4RollBack,
1978 "branches" : P4Branches
86949eef
SH
1979}
1980
86949eef 1981
bb6e09b2
HWN
1982def main():
1983 if len(sys.argv[1:]) == 0:
1984 printUsage(commands.keys())
1985 sys.exit(2)
4f5cf76a 1986
bb6e09b2
HWN
1987 cmd = ""
1988 cmdName = sys.argv[1]
1989 try:
b86f7378
HWN
1990 klass = commands[cmdName]
1991 cmd = klass()
bb6e09b2
HWN
1992 except KeyError:
1993 print "unknown command %s" % cmdName
1994 print ""
1995 printUsage(commands.keys())
1996 sys.exit(2)
1997
1998 options = cmd.options
b86f7378 1999 cmd.gitdir = os.environ.get("GIT_DIR", None)
bb6e09b2
HWN
2000
2001 args = sys.argv[2:]
2002
2003 if len(options) > 0:
2004 options.append(optparse.make_option("--git-dir", dest="gitdir"))
2005
2006 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
2007 options,
2008 description = cmd.description,
2009 formatter = HelpFormatter())
2010
2011 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
2012 global verbose
2013 verbose = cmd.verbose
2014 if cmd.needsGit:
b86f7378
HWN
2015 if cmd.gitdir == None:
2016 cmd.gitdir = os.path.abspath(".git")
2017 if not isValidGitDir(cmd.gitdir):
2018 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
2019 if os.path.exists(cmd.gitdir):
bb6e09b2
HWN
2020 cdup = read_pipe("git rev-parse --show-cdup").strip()
2021 if len(cdup) > 0:
053fd0c1 2022 chdir(cdup);
e20a9e53 2023
b86f7378
HWN
2024 if not isValidGitDir(cmd.gitdir):
2025 if isValidGitDir(cmd.gitdir + "/.git"):
2026 cmd.gitdir += "/.git"
bb6e09b2 2027 else:
b86f7378 2028 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
e20a9e53 2029
b86f7378 2030 os.environ["GIT_DIR"] = cmd.gitdir
86949eef 2031
bb6e09b2
HWN
2032 if not cmd.run(args):
2033 parser.print_help()
4f5cf76a 2034
4f5cf76a 2035
bb6e09b2
HWN
2036if __name__ == '__main__':
2037 main()