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