Switch to using 'p4_build_cmd'
[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
08483580 11import optparse, sys, os, marshal, popen2, subprocess, shelve
25df95cc 12import tempfile, getopt, sha, os.path, time, platform
ce6f33c8 13import re
8b41a97f 14
b984733c 15from sets import Set;
4f5cf76a 16
4addad22 17verbose = False
86949eef 18
21a50753
AK
19
20def p4_build_cmd(cmd):
21 """Build a suitable p4 command line.
22
23 This consolidates building and returning a p4 command line into one
24 location. It means that hooking into the environment, or other configuration
25 can be done more easily.
26 """
27 real_cmd = "%s %s" % ("p4", cmd)
ee06427a
AK
28 if verbose:
29 print real_cmd
21a50753
AK
30 return real_cmd
31
86dff6b6
HWN
32def die(msg):
33 if verbose:
34 raise Exception(msg)
35 else:
36 sys.stderr.write(msg + "\n")
37 sys.exit(1)
38
bce4c5fc 39def write_pipe(c, str):
4addad22 40 if verbose:
86dff6b6 41 sys.stderr.write('Writing pipe: %s\n' % c)
b016d397 42
bce4c5fc 43 pipe = os.popen(c, 'w')
b016d397 44 val = pipe.write(str)
bce4c5fc 45 if pipe.close():
86dff6b6 46 die('Command failed: %s' % c)
b016d397
HWN
47
48 return val
49
4addad22
HWN
50def read_pipe(c, ignore_error=False):
51 if verbose:
86dff6b6 52 sys.stderr.write('Reading pipe: %s\n' % c)
8b41a97f 53
bce4c5fc 54 pipe = os.popen(c, 'rb')
b016d397 55 val = pipe.read()
4addad22 56 if pipe.close() and not ignore_error:
86dff6b6 57 die('Command failed: %s' % c)
b016d397
HWN
58
59 return val
60
61
bce4c5fc 62def read_pipe_lines(c):
4addad22 63 if verbose:
86dff6b6 64 sys.stderr.write('Reading pipe: %s\n' % c)
b016d397 65 ## todo: check return status
bce4c5fc 66 pipe = os.popen(c, 'rb')
b016d397 67 val = pipe.readlines()
bce4c5fc 68 if pipe.close():
86dff6b6 69 die('Command failed: %s' % c)
b016d397
HWN
70
71 return val
caace111 72
2318121b
AK
73def p4_read_pipe_lines(c):
74 """Specifically invoke p4 on the command supplied. """
155af834 75 real_cmd = p4_build_cmd(c)
2318121b
AK
76 return read_pipe_lines(real_cmd)
77
6754a299 78def system(cmd):
4addad22 79 if verbose:
bb6e09b2 80 sys.stderr.write("executing %s\n" % cmd)
6754a299
HWN
81 if os.system(cmd) != 0:
82 die("command failed: %s" % cmd)
83
bf9320f1
AK
84def p4_system(cmd):
85 """Specifically invoke p4 as the system command. """
155af834 86 real_cmd = p4_build_cmd(cmd)
bf9320f1
AK
87 return system(real_cmd)
88
b9fc6ea9
DB
89def isP4Exec(kind):
90 """Determine if a Perforce 'kind' should have execute permission
91
92 'p4 help filetypes' gives a list of the types. If it starts with 'x',
93 or x follows one of a few letters. Otherwise, if there is an 'x' after
94 a plus sign, it is also executable"""
95 return (re.search(r"(^[cku]?x)|\+.*x", kind) != None)
96
c65b670e
CP
97def setP4ExecBit(file, mode):
98 # Reopens an already open file and changes the execute bit to match
99 # the execute bit setting in the passed in mode.
100
101 p4Type = "+x"
102
103 if not isModeExec(mode):
104 p4Type = getP4OpenedType(file)
105 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
106 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
107 if p4Type[-1] == "+":
108 p4Type = p4Type[0:-1]
109
87b611d5 110 p4_system("reopen -t %s %s" % (p4Type, file))
c65b670e
CP
111
112def getP4OpenedType(file):
113 # Returns the perforce file type for the given file.
114
115 result = read_pipe("p4 opened %s" % file)
f3e5ae4f 116 match = re.match(".*\((.+)\)\r?$", result)
c65b670e
CP
117 if match:
118 return match.group(1)
119 else:
f3e5ae4f 120 die("Could not determine file type for %s (result: '%s')" % (file, result))
c65b670e 121
b43b0a3c
CP
122def diffTreePattern():
123 # This is a simple generator for the diff tree regex pattern. This could be
124 # a class variable if this and parseDiffTreeEntry were a part of a class.
125 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
126 while True:
127 yield pattern
128
129def parseDiffTreeEntry(entry):
130 """Parses a single diff tree entry into its component elements.
131
132 See git-diff-tree(1) manpage for details about the format of the diff
133 output. This method returns a dictionary with the following elements:
134
135 src_mode - The mode of the source file
136 dst_mode - The mode of the destination file
137 src_sha1 - The sha1 for the source file
138 dst_sha1 - The sha1 fr the destination file
139 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
140 status_score - The score for the status (applicable for 'C' and 'R'
141 statuses). This is None if there is no score.
142 src - The path for the source file.
143 dst - The path for the destination file. This is only present for
144 copy or renames. If it is not present, this is None.
145
146 If the pattern is not matched, None is returned."""
147
148 match = diffTreePattern().next().match(entry)
149 if match:
150 return {
151 'src_mode': match.group(1),
152 'dst_mode': match.group(2),
153 'src_sha1': match.group(3),
154 'dst_sha1': match.group(4),
155 'status': match.group(5),
156 'status_score': match.group(6),
157 'src': match.group(7),
158 'dst': match.group(10)
159 }
160 return None
161
c65b670e
CP
162def isModeExec(mode):
163 # Returns True if the given git mode represents an executable file,
164 # otherwise False.
165 return mode[-3:] == "755"
166
167def isModeExecChanged(src_mode, dst_mode):
168 return isModeExec(src_mode) != isModeExec(dst_mode)
169
9f90c733 170def p4CmdList(cmd, stdin=None, stdin_mode='w+b'):
155af834 171 cmd = p4_build_cmd("-G %s" % (cmd))
6a49f8e2
HWN
172 if verbose:
173 sys.stderr.write("Opening pipe: %s\n" % cmd)
9f90c733
SL
174
175 # Use a temporary file to avoid deadlocks without
176 # subprocess.communicate(), which would put another copy
177 # of stdout into memory.
178 stdin_file = None
179 if stdin is not None:
180 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
181 stdin_file.write(stdin)
182 stdin_file.flush()
183 stdin_file.seek(0)
184
185 p4 = subprocess.Popen(cmd, shell=True,
186 stdin=stdin_file,
187 stdout=subprocess.PIPE)
86949eef
SH
188
189 result = []
190 try:
191 while True:
9f90c733 192 entry = marshal.load(p4.stdout)
86949eef
SH
193 result.append(entry)
194 except EOFError:
195 pass
9f90c733
SL
196 exitCode = p4.wait()
197 if exitCode != 0:
ac3e0d79
SH
198 entry = {}
199 entry["p4ExitCode"] = exitCode
200 result.append(entry)
86949eef
SH
201
202 return result
203
204def p4Cmd(cmd):
205 list = p4CmdList(cmd)
206 result = {}
207 for entry in list:
208 result.update(entry)
209 return result;
210
cb2c9db5
SH
211def p4Where(depotPath):
212 if not depotPath.endswith("/"):
213 depotPath += "/"
214 output = p4Cmd("where %s..." % depotPath)
dc524036
SH
215 if output["code"] == "error":
216 return ""
cb2c9db5
SH
217 clientPath = ""
218 if "path" in output:
219 clientPath = output.get("path")
220 elif "data" in output:
221 data = output.get("data")
222 lastSpace = data.rfind(" ")
223 clientPath = data[lastSpace + 1:]
224
225 if clientPath.endswith("..."):
226 clientPath = clientPath[:-3]
227 return clientPath
228
86949eef 229def currentGitBranch():
b25b2065 230 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
86949eef 231
4f5cf76a 232def isValidGitDir(path):
bb6e09b2
HWN
233 if (os.path.exists(path + "/HEAD")
234 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
4f5cf76a
SH
235 return True;
236 return False
237
463e8af6 238def parseRevision(ref):
b25b2065 239 return read_pipe("git rev-parse %s" % ref).strip()
463e8af6 240
6ae8de88
SH
241def extractLogMessageFromGitCommit(commit):
242 logMessage = ""
b016d397
HWN
243
244 ## fixme: title is first line of commit, not 1st paragraph.
6ae8de88 245 foundTitle = False
b016d397 246 for log in read_pipe_lines("git cat-file commit %s" % commit):
6ae8de88
SH
247 if not foundTitle:
248 if len(log) == 1:
1c094184 249 foundTitle = True
6ae8de88
SH
250 continue
251
252 logMessage += log
253 return logMessage
254
bb6e09b2 255def extractSettingsGitLog(log):
6ae8de88
SH
256 values = {}
257 for line in log.split("\n"):
258 line = line.strip()
6326aa58
HWN
259 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
260 if not m:
261 continue
262
263 assignments = m.group(1).split (':')
264 for a in assignments:
265 vals = a.split ('=')
266 key = vals[0].strip()
267 val = ('='.join (vals[1:])).strip()
268 if val.endswith ('\"') and val.startswith('"'):
269 val = val[1:-1]
270
271 values[key] = val
272
845b42cb
SH
273 paths = values.get("depot-paths")
274 if not paths:
275 paths = values.get("depot-path")
a3fdd579
SH
276 if paths:
277 values['depot-paths'] = paths.split(',')
bb6e09b2 278 return values
6ae8de88 279
8136a639 280def gitBranchExists(branch):
bb6e09b2
HWN
281 proc = subprocess.Popen(["git", "rev-parse", branch],
282 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
caace111 283 return proc.wait() == 0;
8136a639 284
01265103 285def gitConfig(key):
4addad22 286 return read_pipe("git config %s" % key, ignore_error=True).strip()
01265103 287
062410bb
SH
288def p4BranchesInGit(branchesAreInRemotes = True):
289 branches = {}
290
291 cmdline = "git rev-parse --symbolic "
292 if branchesAreInRemotes:
293 cmdline += " --remotes"
294 else:
295 cmdline += " --branches"
296
297 for line in read_pipe_lines(cmdline):
298 line = line.strip()
299
300 ## only import to p4/
301 if not line.startswith('p4/') or line == "p4/HEAD":
302 continue
303 branch = line
304
305 # strip off p4
306 branch = re.sub ("^p4/", "", line)
307
308 branches[branch] = parseRevision(line)
309 return branches
310
9ceab363 311def findUpstreamBranchPoint(head = "HEAD"):
86506fe5
SH
312 branches = p4BranchesInGit()
313 # map from depot-path to branch name
314 branchByDepotPath = {}
315 for branch in branches.keys():
316 tip = branches[branch]
317 log = extractLogMessageFromGitCommit(tip)
318 settings = extractSettingsGitLog(log)
319 if settings.has_key("depot-paths"):
320 paths = ",".join(settings["depot-paths"])
321 branchByDepotPath[paths] = "remotes/p4/" + branch
322
27d2d811 323 settings = None
27d2d811
SH
324 parent = 0
325 while parent < 65535:
9ceab363 326 commit = head + "~%s" % parent
27d2d811
SH
327 log = extractLogMessageFromGitCommit(commit)
328 settings = extractSettingsGitLog(log)
86506fe5
SH
329 if settings.has_key("depot-paths"):
330 paths = ",".join(settings["depot-paths"])
331 if branchByDepotPath.has_key(paths):
332 return [branchByDepotPath[paths], settings]
27d2d811 333
86506fe5 334 parent = parent + 1
27d2d811 335
86506fe5 336 return ["", settings]
27d2d811 337
5ca44617
SH
338def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
339 if not silent:
340 print ("Creating/updating branch(es) in %s based on origin branch(es)"
341 % localRefPrefix)
342
343 originPrefix = "origin/p4/"
344
345 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
346 line = line.strip()
347 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
348 continue
349
350 headName = line[len(originPrefix):]
351 remoteHead = localRefPrefix + headName
352 originHead = line
353
354 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
355 if (not original.has_key('depot-paths')
356 or not original.has_key('change')):
357 continue
358
359 update = False
360 if not gitBranchExists(remoteHead):
361 if verbose:
362 print "creating %s" % remoteHead
363 update = True
364 else:
365 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
366 if settings.has_key('change') > 0:
367 if settings['depot-paths'] == original['depot-paths']:
368 originP4Change = int(original['change'])
369 p4Change = int(settings['change'])
370 if originP4Change > p4Change:
371 print ("%s (%s) is newer than %s (%s). "
372 "Updating p4 branch from origin."
373 % (originHead, originP4Change,
374 remoteHead, p4Change))
375 update = True
376 else:
377 print ("Ignoring: %s was imported from %s while "
378 "%s was imported from %s"
379 % (originHead, ','.join(original['depot-paths']),
380 remoteHead, ','.join(settings['depot-paths'])))
381
382 if update:
383 system("git update-ref %s %s" % (remoteHead, originHead))
384
385def originP4BranchesExist():
386 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
387
4f6432d8
SH
388def p4ChangesForPaths(depotPaths, changeRange):
389 assert depotPaths
b340fa43 390 output = p4_read_pipe_lines("changes " + ' '.join (["%s...%s" % (p, changeRange)
4f6432d8
SH
391 for p in depotPaths]))
392
393 changes = []
394 for line in output:
395 changeNum = line.split(" ")[1]
396 changes.append(int(changeNum))
397
398 changes.sort()
399 return changes
400
b984733c
SH
401class Command:
402 def __init__(self):
403 self.usage = "usage: %prog [options]"
8910ac0e 404 self.needsGit = True
b984733c
SH
405
406class P4Debug(Command):
86949eef 407 def __init__(self):
6ae8de88 408 Command.__init__(self)
86949eef 409 self.options = [
b1ce9447
HWN
410 optparse.make_option("--verbose", dest="verbose", action="store_true",
411 default=False),
4addad22 412 ]
c8c39116 413 self.description = "A tool to debug the output of p4 -G."
8910ac0e 414 self.needsGit = False
b1ce9447 415 self.verbose = False
86949eef
SH
416
417 def run(self, args):
b1ce9447 418 j = 0
86949eef 419 for output in p4CmdList(" ".join(args)):
b1ce9447
HWN
420 print 'Element: %d' % j
421 j += 1
86949eef 422 print output
b984733c 423 return True
86949eef 424
5834684d
SH
425class P4RollBack(Command):
426 def __init__(self):
427 Command.__init__(self)
428 self.options = [
0c66a783
SH
429 optparse.make_option("--verbose", dest="verbose", action="store_true"),
430 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
5834684d
SH
431 ]
432 self.description = "A tool to debug the multi-branch import. Don't use :)"
52102d47 433 self.verbose = False
0c66a783 434 self.rollbackLocalBranches = False
5834684d
SH
435
436 def run(self, args):
437 if len(args) != 1:
438 return False
439 maxChange = int(args[0])
0c66a783 440
ad192f28 441 if "p4ExitCode" in p4Cmd("changes -m 1"):
66a2f523
SH
442 die("Problems executing p4");
443
0c66a783
SH
444 if self.rollbackLocalBranches:
445 refPrefix = "refs/heads/"
b016d397 446 lines = read_pipe_lines("git rev-parse --symbolic --branches")
0c66a783
SH
447 else:
448 refPrefix = "refs/remotes/"
b016d397 449 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
0c66a783
SH
450
451 for line in lines:
452 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
b25b2065
HWN
453 line = line.strip()
454 ref = refPrefix + line
5834684d 455 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
456 settings = extractSettingsGitLog(log)
457
458 depotPaths = settings['depot-paths']
459 change = settings['change']
460
5834684d 461 changed = False
52102d47 462
6326aa58
HWN
463 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
464 for p in depotPaths]))) == 0:
52102d47
SH
465 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
466 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
467 continue
468
bb6e09b2 469 while change and int(change) > maxChange:
5834684d 470 changed = True
52102d47
SH
471 if self.verbose:
472 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
5834684d
SH
473 system("git update-ref %s \"%s^\"" % (ref, ref))
474 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
475 settings = extractSettingsGitLog(log)
476
477
478 depotPaths = settings['depot-paths']
479 change = settings['change']
5834684d
SH
480
481 if changed:
52102d47 482 print "%s rewound to %s" % (ref, change)
5834684d
SH
483
484 return True
485
711544b0 486class P4Submit(Command):
4f5cf76a 487 def __init__(self):
b984733c 488 Command.__init__(self)
4f5cf76a 489 self.options = [
4addad22 490 optparse.make_option("--verbose", dest="verbose", action="store_true"),
4f5cf76a 491 optparse.make_option("--origin", dest="origin"),
d9a5f25b 492 optparse.make_option("-M", dest="detectRename", action="store_true"),
4f5cf76a
SH
493 ]
494 self.description = "Submit changes from git to the perforce depot."
c9b50e63 495 self.usage += " [name of git branch to submit into perforce depot]"
4f5cf76a 496 self.interactive = True
9512497b 497 self.origin = ""
d9a5f25b 498 self.detectRename = False
b0d10df7 499 self.verbose = False
f7baba8b 500 self.isWindows = (platform.system() == "Windows")
4f5cf76a 501
4f5cf76a
SH
502 def check(self):
503 if len(p4CmdList("opened ...")) > 0:
504 die("You have files opened with perforce! Close them before starting the sync.")
505
edae1e2f
SH
506 # replaces everything between 'Description:' and the next P4 submit template field with the
507 # commit message
4f5cf76a
SH
508 def prepareLogMessage(self, template, message):
509 result = ""
510
edae1e2f
SH
511 inDescriptionSection = False
512
4f5cf76a
SH
513 for line in template.split("\n"):
514 if line.startswith("#"):
515 result += line + "\n"
516 continue
517
edae1e2f
SH
518 if inDescriptionSection:
519 if line.startswith("Files:"):
520 inDescriptionSection = False
521 else:
522 continue
523 else:
524 if line.startswith("Description:"):
525 inDescriptionSection = True
526 line += "\n"
527 for messageLine in message.split("\n"):
528 line += "\t" + messageLine + "\n"
529
530 result += line + "\n"
4f5cf76a
SH
531
532 return result
533
ea99c3ae
SH
534 def prepareSubmitTemplate(self):
535 # remove lines in the Files section that show changes to files outside the depot path we're committing into
536 template = ""
537 inFilesSection = False
b340fa43 538 for line in p4_read_pipe_lines("change -o"):
f3e5ae4f
MSO
539 if line.endswith("\r\n"):
540 line = line[:-2] + "\n"
ea99c3ae
SH
541 if inFilesSection:
542 if line.startswith("\t"):
543 # path starts and ends with a tab
544 path = line[1:]
545 lastTab = path.rfind("\t")
546 if lastTab != -1:
547 path = path[:lastTab]
548 if not path.startswith(self.depotPath):
549 continue
550 else:
551 inFilesSection = False
552 else:
553 if line.startswith("Files:"):
554 inFilesSection = True
555
556 template += line
557
558 return template
559
7cb5cbef 560 def applyCommit(self, id):
0e36f2d7
SH
561 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
562 diffOpts = ("", "-M")[self.detectRename]
563 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (diffOpts, id, id))
4f5cf76a
SH
564 filesToAdd = set()
565 filesToDelete = set()
d336c158 566 editedFiles = set()
c65b670e 567 filesToChangeExecBit = {}
4f5cf76a 568 for line in diff:
b43b0a3c
CP
569 diff = parseDiffTreeEntry(line)
570 modifier = diff['status']
571 path = diff['src']
4f5cf76a 572 if modifier == "M":
87b611d5 573 p4_system("edit \"%s\"" % path)
c65b670e
CP
574 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
575 filesToChangeExecBit[path] = diff['dst_mode']
d336c158 576 editedFiles.add(path)
4f5cf76a
SH
577 elif modifier == "A":
578 filesToAdd.add(path)
c65b670e 579 filesToChangeExecBit[path] = diff['dst_mode']
4f5cf76a
SH
580 if path in filesToDelete:
581 filesToDelete.remove(path)
582 elif modifier == "D":
583 filesToDelete.add(path)
584 if path in filesToAdd:
585 filesToAdd.remove(path)
d9a5f25b 586 elif modifier == "R":
b43b0a3c 587 src, dest = diff['src'], diff['dst']
87b611d5
AK
588 p4_system("integrate -Dt \"%s\" \"%s\"" % (src, dest))
589 p4_system("edit \"%s\"" % (dest))
c65b670e
CP
590 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
591 filesToChangeExecBit[dest] = diff['dst_mode']
d9a5f25b
CP
592 os.unlink(dest)
593 editedFiles.add(dest)
594 filesToDelete.add(src)
4f5cf76a
SH
595 else:
596 die("unknown modifier %s for %s" % (modifier, path))
597
0e36f2d7 598 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
47a130b7 599 patchcmd = diffcmd + " | git apply "
c1b296b9
SH
600 tryPatchCmd = patchcmd + "--check -"
601 applyPatchCmd = patchcmd + "--check --apply -"
51a2640a 602
47a130b7 603 if os.system(tryPatchCmd) != 0:
51a2640a
SH
604 print "Unfortunately applying the change failed!"
605 print "What do you want to do?"
606 response = "x"
607 while response != "s" and response != "a" and response != "w":
cebdf5af
HWN
608 response = raw_input("[s]kip this patch / [a]pply the patch forcibly "
609 "and with .rej files / [w]rite the patch to a file (patch.txt) ")
51a2640a
SH
610 if response == "s":
611 print "Skipping! Good luck with the next patches..."
20947149 612 for f in editedFiles:
87b611d5 613 p4_system("revert \"%s\"" % f);
20947149
SH
614 for f in filesToAdd:
615 system("rm %s" %f)
51a2640a
SH
616 return
617 elif response == "a":
47a130b7 618 os.system(applyPatchCmd)
51a2640a
SH
619 if len(filesToAdd) > 0:
620 print "You may also want to call p4 add on the following files:"
621 print " ".join(filesToAdd)
622 if len(filesToDelete):
623 print "The following files should be scheduled for deletion with p4 delete:"
624 print " ".join(filesToDelete)
cebdf5af
HWN
625 die("Please resolve and submit the conflict manually and "
626 + "continue afterwards with git-p4 submit --continue")
51a2640a
SH
627 elif response == "w":
628 system(diffcmd + " > patch.txt")
629 print "Patch saved to patch.txt in %s !" % self.clientPath
cebdf5af
HWN
630 die("Please resolve and submit the conflict manually and "
631 "continue afterwards with git-p4 submit --continue")
51a2640a 632
47a130b7 633 system(applyPatchCmd)
4f5cf76a
SH
634
635 for f in filesToAdd:
87b611d5 636 p4_system("add \"%s\"" % f)
4f5cf76a 637 for f in filesToDelete:
87b611d5
AK
638 p4_system("revert \"%s\"" % f)
639 p4_system("delete \"%s\"" % f)
4f5cf76a 640
c65b670e
CP
641 # Set/clear executable bits
642 for f in filesToChangeExecBit.keys():
643 mode = filesToChangeExecBit[f]
644 setP4ExecBit(f, mode)
645
0e36f2d7 646 logMessage = extractLogMessageFromGitCommit(id)
0e36f2d7 647 logMessage = logMessage.strip()
4f5cf76a 648
ea99c3ae 649 template = self.prepareSubmitTemplate()
4f5cf76a
SH
650
651 if self.interactive:
652 submitTemplate = self.prepareLogMessage(template, logMessage)
67abd417
SB
653 if os.environ.has_key("P4DIFF"):
654 del(os.environ["P4DIFF"])
b016d397 655 diff = read_pipe("p4 diff -du ...")
4f5cf76a 656
f3e5ae4f 657 newdiff = ""
4f5cf76a 658 for newFile in filesToAdd:
f3e5ae4f
MSO
659 newdiff += "==== new file ====\n"
660 newdiff += "--- /dev/null\n"
661 newdiff += "+++ %s\n" % newFile
4f5cf76a
SH
662 f = open(newFile, "r")
663 for line in f.readlines():
f3e5ae4f 664 newdiff += "+" + line
4f5cf76a
SH
665 f.close()
666
f3e5ae4f 667 separatorLine = "######## everything below this line is just the diff #######\n"
4f5cf76a 668
e96e400f
SH
669 [handle, fileName] = tempfile.mkstemp()
670 tmpFile = os.fdopen(handle, "w+")
f3e5ae4f
MSO
671 if self.isWindows:
672 submitTemplate = submitTemplate.replace("\n", "\r\n")
673 separatorLine = separatorLine.replace("\n", "\r\n")
674 newdiff = newdiff.replace("\n", "\r\n")
675 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
e96e400f
SH
676 tmpFile.close()
677 defaultEditor = "vi"
678 if platform.system() == "Windows":
679 defaultEditor = "notepad"
82cea9ff
SB
680 if os.environ.has_key("P4EDITOR"):
681 editor = os.environ.get("P4EDITOR")
682 else:
683 editor = os.environ.get("EDITOR", defaultEditor);
e96e400f
SH
684 system(editor + " " + fileName)
685 tmpFile = open(fileName, "rb")
686 message = tmpFile.read()
687 tmpFile.close()
688 os.remove(fileName)
689 submitTemplate = message[:message.index(separatorLine)]
690 if self.isWindows:
691 submitTemplate = submitTemplate.replace("\r\n", "\n")
692
e96e400f 693 write_pipe("p4 submit -i", submitTemplate)
4f5cf76a
SH
694 else:
695 fileName = "submit.txt"
696 file = open(fileName, "w+")
697 file.write(self.prepareLogMessage(template, logMessage))
698 file.close()
cebdf5af
HWN
699 print ("Perforce submit template written as %s. "
700 + "Please review/edit and then use p4 submit -i < %s to submit directly!"
701 % (fileName, fileName))
4f5cf76a
SH
702
703 def run(self, args):
c9b50e63
SH
704 if len(args) == 0:
705 self.master = currentGitBranch()
4280e533 706 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
c9b50e63
SH
707 die("Detecting current git branch failed!")
708 elif len(args) == 1:
709 self.master = args[0]
710 else:
711 return False
712
4c2d5d72
JX
713 allowSubmit = gitConfig("git-p4.allowSubmit")
714 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
715 die("%s is not in git-p4.allowSubmit" % self.master)
716
27d2d811 717 [upstream, settings] = findUpstreamBranchPoint()
ea99c3ae 718 self.depotPath = settings['depot-paths'][0]
27d2d811
SH
719 if len(self.origin) == 0:
720 self.origin = upstream
a3fdd579
SH
721
722 if self.verbose:
723 print "Origin branch is " + self.origin
9512497b 724
ea99c3ae 725 if len(self.depotPath) == 0:
9512497b
SH
726 print "Internal error: cannot locate perforce depot path from existing branches"
727 sys.exit(128)
728
ea99c3ae 729 self.clientPath = p4Where(self.depotPath)
9512497b 730
51a2640a 731 if len(self.clientPath) == 0:
ea99c3ae 732 print "Error: Cannot locate perforce checkout of %s in client view" % self.depotPath
9512497b
SH
733 sys.exit(128)
734
ea99c3ae 735 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
7944f142 736 self.oldWorkingDirectory = os.getcwd()
c1b296b9 737
51a2640a 738 os.chdir(self.clientPath)
31f9ec12 739 print "Syncronizing p4 checkout..."
87b611d5 740 p4_system("sync ...")
9512497b 741
4f5cf76a 742 self.check()
4f5cf76a 743
4c750c0d
SH
744 commits = []
745 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
746 commits.append(line.strip())
747 commits.reverse()
4f5cf76a
SH
748
749 while len(commits) > 0:
4f5cf76a
SH
750 commit = commits[0]
751 commits = commits[1:]
7cb5cbef 752 self.applyCommit(commit)
4f5cf76a
SH
753 if not self.interactive:
754 break
755
4f5cf76a 756 if len(commits) == 0:
4c750c0d
SH
757 print "All changes applied!"
758 os.chdir(self.oldWorkingDirectory)
14594f4b 759
4c750c0d
SH
760 sync = P4Sync()
761 sync.run([])
14594f4b 762
4c750c0d
SH
763 rebase = P4Rebase()
764 rebase.rebase()
4f5cf76a 765
b984733c
SH
766 return True
767
711544b0 768class P4Sync(Command):
b984733c
SH
769 def __init__(self):
770 Command.__init__(self)
771 self.options = [
772 optparse.make_option("--branch", dest="branch"),
773 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
774 optparse.make_option("--changesfile", dest="changesFile"),
775 optparse.make_option("--silent", dest="silent", action="store_true"),
ef48f909 776 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
a028a98e 777 optparse.make_option("--verbose", dest="verbose", action="store_true"),
d2c6dd30
HWN
778 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
779 help="Import into refs/heads/ , not refs/remotes"),
8b41a97f 780 optparse.make_option("--max-changes", dest="maxChanges"),
86dff6b6 781 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
3a70cdfa
TAL
782 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
783 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
784 help="Only sync files that are included in the Perforce Client Spec")
b984733c
SH
785 ]
786 self.description = """Imports from Perforce into a git repository.\n
787 example:
788 //depot/my/project/ -- to import the current head
789 //depot/my/project/@all -- to import everything
790 //depot/my/project/@1,6 -- to import only from revision 1 to 6
791
792 (a ... is not needed in the path p4 specification, it's added implicitly)"""
793
794 self.usage += " //depot/path[@revRange]"
b984733c 795 self.silent = False
b984733c
SH
796 self.createdBranches = Set()
797 self.committedChanges = Set()
569d1bd4 798 self.branch = ""
b984733c 799 self.detectBranches = False
cb53e1f8 800 self.detectLabels = False
b984733c 801 self.changesFile = ""
01265103 802 self.syncWithOrigin = True
4b97ffb1 803 self.verbose = False
a028a98e 804 self.importIntoRemotes = True
01a9c9c5 805 self.maxChanges = ""
c1f9197f 806 self.isWindows = (platform.system() == "Windows")
8b41a97f 807 self.keepRepoPath = False
6326aa58 808 self.depotPaths = None
3c699645 809 self.p4BranchesInGit = []
354081d5 810 self.cloneExclude = []
3a70cdfa
TAL
811 self.useClientSpec = False
812 self.clientSpecDirs = []
b984733c 813
01265103
SH
814 if gitConfig("git-p4.syncFromOrigin") == "false":
815 self.syncWithOrigin = False
816
b984733c 817 def extractFilesFromCommit(self, commit):
354081d5
TT
818 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
819 for path in self.cloneExclude]
b984733c
SH
820 files = []
821 fnum = 0
822 while commit.has_key("depotFile%s" % fnum):
823 path = commit["depotFile%s" % fnum]
6326aa58 824
354081d5
TT
825 if [p for p in self.cloneExclude
826 if path.startswith (p)]:
827 found = False
828 else:
829 found = [p for p in self.depotPaths
830 if path.startswith (p)]
6326aa58 831 if not found:
b984733c
SH
832 fnum = fnum + 1
833 continue
834
835 file = {}
836 file["path"] = path
837 file["rev"] = commit["rev%s" % fnum]
838 file["action"] = commit["action%s" % fnum]
839 file["type"] = commit["type%s" % fnum]
840 files.append(file)
841 fnum = fnum + 1
842 return files
843
6326aa58 844 def stripRepoPath(self, path, prefixes):
8b41a97f 845 if self.keepRepoPath:
6326aa58
HWN
846 prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
847
848 for p in prefixes:
849 if path.startswith(p):
850 path = path[len(p):]
8b41a97f 851
6326aa58 852 return path
6754a299 853
71b112d4 854 def splitFilesIntoBranches(self, commit):
d5904674 855 branches = {}
71b112d4
SH
856 fnum = 0
857 while commit.has_key("depotFile%s" % fnum):
858 path = commit["depotFile%s" % fnum]
6326aa58
HWN
859 found = [p for p in self.depotPaths
860 if path.startswith (p)]
861 if not found:
71b112d4
SH
862 fnum = fnum + 1
863 continue
864
865 file = {}
866 file["path"] = path
867 file["rev"] = commit["rev%s" % fnum]
868 file["action"] = commit["action%s" % fnum]
869 file["type"] = commit["type%s" % fnum]
870 fnum = fnum + 1
871
6326aa58 872 relPath = self.stripRepoPath(path, self.depotPaths)
b984733c 873
4b97ffb1 874 for branch in self.knownBranches.keys():
6754a299
HWN
875
876 # add a trailing slash so that a commit into qt/4.2foo doesn't end up in qt/4.2
877 if relPath.startswith(branch + "/"):
d5904674
SH
878 if branch not in branches:
879 branches[branch] = []
71b112d4 880 branches[branch].append(file)
6555b2cc 881 break
b984733c
SH
882
883 return branches
884
6a49f8e2
HWN
885 ## Should move this out, doesn't use SELF.
886 def readP4Files(self, files):
30b5940b
SH
887 filesForCommit = []
888 filesToRead = []
889
3a70cdfa 890 for f in files:
30b5940b 891 includeFile = True
3a70cdfa
TAL
892 for val in self.clientSpecDirs:
893 if f['path'].startswith(val[0]):
30b5940b
SH
894 if val[1] <= 0:
895 includeFile = False
3a70cdfa
TAL
896 break
897
30b5940b
SH
898 if includeFile:
899 filesForCommit.append(f)
900 if f['action'] != 'delete':
901 filesToRead.append(f)
6a49f8e2 902
30b5940b
SH
903 filedata = []
904 if len(filesToRead) > 0:
905 filedata = p4CmdList('-x - print',
906 stdin='\n'.join(['%s#%s' % (f['path'], f['rev'])
907 for f in filesToRead]),
908 stdin_mode='w+')
f2eda79f 909
30b5940b
SH
910 if "p4ExitCode" in filedata[0]:
911 die("Problems executing p4. Error: [%d]."
912 % (filedata[0]['p4ExitCode']));
6a49f8e2 913
d2c6dd30
HWN
914 j = 0;
915 contents = {}
b1ce9447 916 while j < len(filedata):
d2c6dd30 917 stat = filedata[j]
b1ce9447 918 j += 1
8ff45f2a 919 text = [];
f3e9512b 920 while j < len(filedata) and filedata[j]['code'] in ('text', 'unicode', 'binary'):
8ff45f2a 921 text.append(filedata[j]['data'])
b1ce9447 922 j += 1
8ff45f2a 923 text = ''.join(text)
1b9a4684
HWN
924
925 if not stat.has_key('depotFile'):
926 sys.stderr.write("p4 print fails with: %s\n" % repr(stat))
927 continue
928
8ff45f2a
MSO
929 if stat['type'] in ('text+ko', 'unicode+ko', 'binary+ko'):
930 text = re.sub(r'(?i)\$(Id|Header):[^$]*\$',r'$\1$', text)
931 elif stat['type'] in ('text+k', 'ktext', 'kxtext', 'unicode+k', 'binary+k'):
2d71bde2 932 text = re.sub(r'\$(Id|Header|Author|Date|DateTime|Change|File|Revision):[^$]*\$',r'$\1$', text)
8ff45f2a 933
b1ce9447 934 contents[stat['depotFile']] = text
6a49f8e2 935
30b5940b
SH
936 for f in filesForCommit:
937 path = f['path']
938 if contents.has_key(path):
939 f['data'] = contents[path]
940
941 return filesForCommit
6a49f8e2 942
6326aa58 943 def commit(self, details, files, branch, branchPrefixes, parent = ""):
b984733c
SH
944 epoch = details["time"]
945 author = details["user"]
946
4b97ffb1
SH
947 if self.verbose:
948 print "commit into %s" % branch
949
96e07dd2
HWN
950 # start with reading files; if that fails, we should not
951 # create a commit.
952 new_files = []
953 for f in files:
954 if [p for p in branchPrefixes if f['path'].startswith(p)]:
955 new_files.append (f)
956 else:
957 sys.stderr.write("Ignoring file outside of prefix: %s\n" % path)
3a70cdfa 958 files = self.readP4Files(new_files)
96e07dd2 959
b984733c 960 self.gitStream.write("commit %s\n" % branch)
6a49f8e2 961# gitStream.write("mark :%s\n" % details["change"])
b984733c
SH
962 self.committedChanges.add(int(details["change"]))
963 committer = ""
b607e71e
SH
964 if author not in self.users:
965 self.getUserMapFromPerforceServer()
b984733c 966 if author in self.users:
0828ab14 967 committer = "%s %s %s" % (self.users[author], epoch, self.tz)
b984733c 968 else:
0828ab14 969 committer = "%s <a@b> %s %s" % (author, epoch, self.tz)
b984733c
SH
970
971 self.gitStream.write("committer %s\n" % committer)
972
973 self.gitStream.write("data <<EOT\n")
974 self.gitStream.write(details["desc"])
6581de09
SH
975 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s"
976 % (','.join (branchPrefixes), details["change"]))
977 if len(details['options']) > 0:
978 self.gitStream.write(": options = %s" % details['options'])
979 self.gitStream.write("]\nEOT\n\n")
b984733c
SH
980
981 if len(parent) > 0:
4b97ffb1
SH
982 if self.verbose:
983 print "parent %s" % parent
b984733c
SH
984 self.gitStream.write("from %s\n" % parent)
985
6a49f8e2 986 for file in files:
b984733c 987 if file["type"] == "apple":
6a49f8e2 988 print "\nfile %s is a strange apple file that forks. Ignoring!" % file['path']
b984733c
SH
989 continue
990
6a49f8e2
HWN
991 relPath = self.stripRepoPath(file['path'], branchPrefixes)
992 if file["action"] == "delete":
b984733c
SH
993 self.gitStream.write("D %s\n" % relPath)
994 else:
6a49f8e2 995 data = file['data']
b984733c 996
74276ec6 997 mode = "644"
b9fc6ea9 998 if isP4Exec(file["type"]):
74276ec6
SH
999 mode = "755"
1000 elif file["type"] == "symlink":
1001 mode = "120000"
1002 # p4 print on a symlink contains "target\n", so strip it off
1003 data = data[:-1]
1004
c1f9197f
MSO
1005 if self.isWindows and file["type"].endswith("text"):
1006 data = data.replace("\r\n", "\n")
1007
74276ec6 1008 self.gitStream.write("M %s inline %s\n" % (mode, relPath))
b984733c
SH
1009 self.gitStream.write("data %s\n" % len(data))
1010 self.gitStream.write(data)
1011 self.gitStream.write("\n")
1012
1013 self.gitStream.write("\n")
1014
1f4ba1cb
SH
1015 change = int(details["change"])
1016
9bda3a85 1017 if self.labels.has_key(change):
1f4ba1cb
SH
1018 label = self.labels[change]
1019 labelDetails = label[0]
1020 labelRevisions = label[1]
71b112d4
SH
1021 if self.verbose:
1022 print "Change %s is labelled %s" % (change, labelDetails)
1f4ba1cb 1023
6326aa58
HWN
1024 files = p4CmdList("files " + ' '.join (["%s...@%s" % (p, change)
1025 for p in branchPrefixes]))
1f4ba1cb
SH
1026
1027 if len(files) == len(labelRevisions):
1028
1029 cleanedFiles = {}
1030 for info in files:
1031 if info["action"] == "delete":
1032 continue
1033 cleanedFiles[info["depotFile"]] = info["rev"]
1034
1035 if cleanedFiles == labelRevisions:
1036 self.gitStream.write("tag tag_%s\n" % labelDetails["label"])
1037 self.gitStream.write("from %s\n" % branch)
1038
1039 owner = labelDetails["Owner"]
1040 tagger = ""
1041 if author in self.users:
1042 tagger = "%s %s %s" % (self.users[owner], epoch, self.tz)
1043 else:
1044 tagger = "%s <a@b> %s %s" % (owner, epoch, self.tz)
1045 self.gitStream.write("tagger %s\n" % tagger)
1046 self.gitStream.write("data <<EOT\n")
1047 self.gitStream.write(labelDetails["Description"])
1048 self.gitStream.write("EOT\n\n")
1049
1050 else:
a46668fa 1051 if not self.silent:
cebdf5af
HWN
1052 print ("Tag %s does not match with change %s: files do not match."
1053 % (labelDetails["label"], change))
1f4ba1cb
SH
1054
1055 else:
a46668fa 1056 if not self.silent:
cebdf5af
HWN
1057 print ("Tag %s does not match with change %s: file count is different."
1058 % (labelDetails["label"], change))
b984733c 1059
183b8ef8 1060 def getUserCacheFilename(self):
b2d2d16a
SH
1061 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1062 return home + "/.gitp4-usercache.txt"
183b8ef8 1063
b607e71e 1064 def getUserMapFromPerforceServer(self):
ebd81168
SH
1065 if self.userMapFromPerforceServer:
1066 return
b984733c
SH
1067 self.users = {}
1068
1069 for output in p4CmdList("users"):
1070 if not output.has_key("User"):
1071 continue
1072 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1073
183b8ef8
HWN
1074
1075 s = ''
1076 for (key, val) in self.users.items():
1077 s += "%s\t%s\n" % (key, val)
1078
1079 open(self.getUserCacheFilename(), "wb").write(s)
ebd81168 1080 self.userMapFromPerforceServer = True
b607e71e
SH
1081
1082 def loadUserMapFromCache(self):
1083 self.users = {}
ebd81168 1084 self.userMapFromPerforceServer = False
b607e71e 1085 try:
183b8ef8 1086 cache = open(self.getUserCacheFilename(), "rb")
b607e71e
SH
1087 lines = cache.readlines()
1088 cache.close()
1089 for line in lines:
b25b2065 1090 entry = line.strip().split("\t")
b607e71e
SH
1091 self.users[entry[0]] = entry[1]
1092 except IOError:
1093 self.getUserMapFromPerforceServer()
1094
1f4ba1cb
SH
1095 def getLabels(self):
1096 self.labels = {}
1097
6326aa58 1098 l = p4CmdList("labels %s..." % ' '.join (self.depotPaths))
10c3211b 1099 if len(l) > 0 and not self.silent:
183f8436 1100 print "Finding files belonging to labels in %s" % `self.depotPaths`
01ce1fe9
SH
1101
1102 for output in l:
1f4ba1cb
SH
1103 label = output["label"]
1104 revisions = {}
1105 newestChange = 0
71b112d4
SH
1106 if self.verbose:
1107 print "Querying files for label %s" % label
6326aa58
HWN
1108 for file in p4CmdList("files "
1109 + ' '.join (["%s...@%s" % (p, label)
1110 for p in self.depotPaths])):
1f4ba1cb
SH
1111 revisions[file["depotFile"]] = file["rev"]
1112 change = int(file["change"])
1113 if change > newestChange:
1114 newestChange = change
1115
9bda3a85
SH
1116 self.labels[newestChange] = [output, revisions]
1117
1118 if self.verbose:
1119 print "Label changes: %s" % self.labels.keys()
1f4ba1cb 1120
86dff6b6
HWN
1121 def guessProjectName(self):
1122 for p in self.depotPaths:
6e5295c4
SH
1123 if p.endswith("/"):
1124 p = p[:-1]
1125 p = p[p.strip().rfind("/") + 1:]
1126 if not p.endswith("/"):
1127 p += "/"
1128 return p
86dff6b6 1129
4b97ffb1 1130 def getBranchMapping(self):
6555b2cc
SH
1131 lostAndFoundBranches = set()
1132
4b97ffb1
SH
1133 for info in p4CmdList("branches"):
1134 details = p4Cmd("branch -o %s" % info["branch"])
1135 viewIdx = 0
1136 while details.has_key("View%s" % viewIdx):
1137 paths = details["View%s" % viewIdx].split(" ")
1138 viewIdx = viewIdx + 1
1139 # require standard //depot/foo/... //depot/bar/... mapping
1140 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
1141 continue
1142 source = paths[0]
1143 destination = paths[1]
6509e19c
SH
1144 ## HACK
1145 if source.startswith(self.depotPaths[0]) and destination.startswith(self.depotPaths[0]):
1146 source = source[len(self.depotPaths[0]):-4]
1147 destination = destination[len(self.depotPaths[0]):-4]
6555b2cc 1148
1a2edf4e
SH
1149 if destination in self.knownBranches:
1150 if not self.silent:
1151 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
1152 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
1153 continue
1154
6555b2cc
SH
1155 self.knownBranches[destination] = source
1156
1157 lostAndFoundBranches.discard(destination)
1158
29bdbac1 1159 if source not in self.knownBranches:
6555b2cc
SH
1160 lostAndFoundBranches.add(source)
1161
1162
1163 for branch in lostAndFoundBranches:
1164 self.knownBranches[branch] = branch
29bdbac1 1165
38f9f5ec
SH
1166 def getBranchMappingFromGitBranches(self):
1167 branches = p4BranchesInGit(self.importIntoRemotes)
1168 for branch in branches.keys():
1169 if branch == "master":
1170 branch = "main"
1171 else:
1172 branch = branch[len(self.projectName):]
1173 self.knownBranches[branch] = branch
1174
29bdbac1 1175 def listExistingP4GitBranches(self):
144ff46b
SH
1176 # branches holds mapping from name to commit
1177 branches = p4BranchesInGit(self.importIntoRemotes)
1178 self.p4BranchesInGit = branches.keys()
1179 for branch in branches.keys():
1180 self.initialParents[self.refPrefix + branch] = branches[branch]
4b97ffb1 1181
bb6e09b2
HWN
1182 def updateOptionDict(self, d):
1183 option_keys = {}
1184 if self.keepRepoPath:
1185 option_keys['keepRepoPath'] = 1
1186
1187 d["options"] = ' '.join(sorted(option_keys.keys()))
1188
1189 def readOptions(self, d):
1190 self.keepRepoPath = (d.has_key('options')
1191 and ('keepRepoPath' in d['options']))
6326aa58 1192
8134f69c
SH
1193 def gitRefForBranch(self, branch):
1194 if branch == "main":
1195 return self.refPrefix + "master"
1196
1197 if len(branch) <= 0:
1198 return branch
1199
1200 return self.refPrefix + self.projectName + branch
1201
1ca3d710
SH
1202 def gitCommitByP4Change(self, ref, change):
1203 if self.verbose:
1204 print "looking in ref " + ref + " for change %s using bisect..." % change
1205
1206 earliestCommit = ""
1207 latestCommit = parseRevision(ref)
1208
1209 while True:
1210 if self.verbose:
1211 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
1212 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
1213 if len(next) == 0:
1214 if self.verbose:
1215 print "argh"
1216 return ""
1217 log = extractLogMessageFromGitCommit(next)
1218 settings = extractSettingsGitLog(log)
1219 currentChange = int(settings['change'])
1220 if self.verbose:
1221 print "current change %s" % currentChange
1222
1223 if currentChange == change:
1224 if self.verbose:
1225 print "found %s" % next
1226 return next
1227
1228 if currentChange < change:
1229 earliestCommit = "^%s" % next
1230 else:
1231 latestCommit = "%s" % next
1232
1233 return ""
1234
1235 def importNewBranch(self, branch, maxChange):
1236 # make fast-import flush all changes to disk and update the refs using the checkpoint
1237 # command so that we can try to find the branch parent in the git history
1238 self.gitStream.write("checkpoint\n\n");
1239 self.gitStream.flush();
1240 branchPrefix = self.depotPaths[0] + branch + "/"
1241 range = "@1,%s" % maxChange
1242 #print "prefix" + branchPrefix
1243 changes = p4ChangesForPaths([branchPrefix], range)
1244 if len(changes) <= 0:
1245 return False
1246 firstChange = changes[0]
1247 #print "first change in branch: %s" % firstChange
1248 sourceBranch = self.knownBranches[branch]
1249 sourceDepotPath = self.depotPaths[0] + sourceBranch
1250 sourceRef = self.gitRefForBranch(sourceBranch)
1251 #print "source " + sourceBranch
1252
1253 branchParentChange = int(p4Cmd("changes -m 1 %s...@1,%s" % (sourceDepotPath, firstChange))["change"])
1254 #print "branch parent: %s" % branchParentChange
1255 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
1256 if len(gitParent) > 0:
1257 self.initialParents[self.gitRefForBranch(branch)] = gitParent
1258 #print "parent git commit: %s" % gitParent
1259
1260 self.importChanges(changes)
1261 return True
1262
e87f37ae
SH
1263 def importChanges(self, changes):
1264 cnt = 1
1265 for change in changes:
1266 description = p4Cmd("describe %s" % change)
1267 self.updateOptionDict(description)
1268
1269 if not self.silent:
1270 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
1271 sys.stdout.flush()
1272 cnt = cnt + 1
1273
1274 try:
1275 if self.detectBranches:
1276 branches = self.splitFilesIntoBranches(description)
1277 for branch in branches.keys():
1278 ## HACK --hwn
1279 branchPrefix = self.depotPaths[0] + branch + "/"
1280
1281 parent = ""
1282
1283 filesForCommit = branches[branch]
1284
1285 if self.verbose:
1286 print "branch is %s" % branch
1287
1288 self.updatedBranches.add(branch)
1289
1290 if branch not in self.createdBranches:
1291 self.createdBranches.add(branch)
1292 parent = self.knownBranches[branch]
1293 if parent == branch:
1294 parent = ""
1ca3d710
SH
1295 else:
1296 fullBranch = self.projectName + branch
1297 if fullBranch not in self.p4BranchesInGit:
1298 if not self.silent:
1299 print("\n Importing new branch %s" % fullBranch);
1300 if self.importNewBranch(branch, change - 1):
1301 parent = ""
1302 self.p4BranchesInGit.append(fullBranch)
1303 if not self.silent:
1304 print("\n Resuming with change %s" % change);
1305
1306 if self.verbose:
1307 print "parent determined through known branches: %s" % parent
e87f37ae 1308
8134f69c
SH
1309 branch = self.gitRefForBranch(branch)
1310 parent = self.gitRefForBranch(parent)
e87f37ae
SH
1311
1312 if self.verbose:
1313 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
1314
1315 if len(parent) == 0 and branch in self.initialParents:
1316 parent = self.initialParents[branch]
1317 del self.initialParents[branch]
1318
1319 self.commit(description, filesForCommit, branch, [branchPrefix], parent)
1320 else:
1321 files = self.extractFilesFromCommit(description)
1322 self.commit(description, files, self.branch, self.depotPaths,
1323 self.initialParent)
1324 self.initialParent = ""
1325 except IOError:
1326 print self.gitError.read()
1327 sys.exit(1)
1328
c208a243
SH
1329 def importHeadRevision(self, revision):
1330 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
1331
1332 details = { "user" : "git perforce import user", "time" : int(time.time()) }
1333 details["desc"] = ("Initial import of %s from the state at revision %s"
1334 % (' '.join(self.depotPaths), revision))
1335 details["change"] = revision
1336 newestRevision = 0
1337
1338 fileCnt = 0
1339 for info in p4CmdList("files "
1340 + ' '.join(["%s...%s"
1341 % (p, revision)
1342 for p in self.depotPaths])):
1343
1344 if info['code'] == 'error':
1345 sys.stderr.write("p4 returned an error: %s\n"
1346 % info['data'])
1347 sys.exit(1)
1348
1349
1350 change = int(info["change"])
1351 if change > newestRevision:
1352 newestRevision = change
1353
1354 if info["action"] == "delete":
1355 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
1356 #fileCnt = fileCnt + 1
1357 continue
1358
1359 for prop in ["depotFile", "rev", "action", "type" ]:
1360 details["%s%s" % (prop, fileCnt)] = info[prop]
1361
1362 fileCnt = fileCnt + 1
1363
1364 details["change"] = newestRevision
1365 self.updateOptionDict(details)
1366 try:
1367 self.commit(details, self.extractFilesFromCommit(details), self.branch, self.depotPaths)
1368 except IOError:
1369 print "IO error with git fast-import. Is your git version recent enough?"
1370 print self.gitError.read()
1371
1372
3a70cdfa
TAL
1373 def getClientSpec(self):
1374 specList = p4CmdList( "client -o" )
1375 temp = {}
1376 for entry in specList:
1377 for k,v in entry.iteritems():
1378 if k.startswith("View"):
1379 if v.startswith('"'):
1380 start = 1
1381 else:
1382 start = 0
1383 index = v.find("...")
1384 v = v[start:index]
1385 if v.startswith("-"):
1386 v = v[1:]
1387 temp[v] = -len(v)
1388 else:
1389 temp[v] = len(v)
1390 self.clientSpecDirs = temp.items()
1391 self.clientSpecDirs.sort( lambda x, y: abs( y[1] ) - abs( x[1] ) )
1392
b984733c 1393 def run(self, args):
6326aa58 1394 self.depotPaths = []
179caebf
SH
1395 self.changeRange = ""
1396 self.initialParent = ""
6326aa58 1397 self.previousDepotPaths = []
ce6f33c8 1398
29bdbac1
SH
1399 # map from branch depot path to parent branch
1400 self.knownBranches = {}
1401 self.initialParents = {}
5ca44617 1402 self.hasOrigin = originP4BranchesExist()
a43ff00c
SH
1403 if not self.syncWithOrigin:
1404 self.hasOrigin = False
29bdbac1 1405
a028a98e
SH
1406 if self.importIntoRemotes:
1407 self.refPrefix = "refs/remotes/p4/"
1408 else:
db775559 1409 self.refPrefix = "refs/heads/p4/"
a028a98e 1410
cebdf5af
HWN
1411 if self.syncWithOrigin and self.hasOrigin:
1412 if not self.silent:
1413 print "Syncing with origin first by calling git fetch origin"
1414 system("git fetch origin")
10f880f8 1415
569d1bd4 1416 if len(self.branch) == 0:
db775559 1417 self.branch = self.refPrefix + "master"
a028a98e 1418 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
48df6fd8 1419 system("git update-ref %s refs/heads/p4" % self.branch)
48df6fd8 1420 system("git branch -D p4");
faf1bd20 1421 # create it /after/ importing, when master exists
0058a33a 1422 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
a3c55c09 1423 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
967f72e2 1424
3a70cdfa
TAL
1425 if self.useClientSpec or gitConfig("p4.useclientspec") == "true":
1426 self.getClientSpec()
1427
6a49f8e2
HWN
1428 # TODO: should always look at previous commits,
1429 # merge with previous imports, if possible.
1430 if args == []:
d414c74a 1431 if self.hasOrigin:
5ca44617 1432 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
abcd790f
SH
1433 self.listExistingP4GitBranches()
1434
1435 if len(self.p4BranchesInGit) > 1:
1436 if not self.silent:
1437 print "Importing from/into multiple branches"
1438 self.detectBranches = True
967f72e2 1439
29bdbac1
SH
1440 if self.verbose:
1441 print "branches: %s" % self.p4BranchesInGit
1442
1443 p4Change = 0
1444 for branch in self.p4BranchesInGit:
cebdf5af 1445 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
bb6e09b2
HWN
1446
1447 settings = extractSettingsGitLog(logMsg)
29bdbac1 1448
bb6e09b2
HWN
1449 self.readOptions(settings)
1450 if (settings.has_key('depot-paths')
1451 and settings.has_key ('change')):
1452 change = int(settings['change']) + 1
29bdbac1
SH
1453 p4Change = max(p4Change, change)
1454
bb6e09b2
HWN
1455 depotPaths = sorted(settings['depot-paths'])
1456 if self.previousDepotPaths == []:
6326aa58 1457 self.previousDepotPaths = depotPaths
29bdbac1 1458 else:
6326aa58
HWN
1459 paths = []
1460 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
583e1707 1461 for i in range(0, min(len(cur), len(prev))):
6326aa58 1462 if cur[i] <> prev[i]:
583e1707 1463 i = i - 1
6326aa58
HWN
1464 break
1465
583e1707 1466 paths.append (cur[:i + 1])
6326aa58
HWN
1467
1468 self.previousDepotPaths = paths
29bdbac1
SH
1469
1470 if p4Change > 0:
bb6e09b2 1471 self.depotPaths = sorted(self.previousDepotPaths)
d5904674 1472 self.changeRange = "@%s,#head" % p4Change
330f53b8
SH
1473 if not self.detectBranches:
1474 self.initialParent = parseRevision(self.branch)
341dc1c1 1475 if not self.silent and not self.detectBranches:
967f72e2 1476 print "Performing incremental import into %s git branch" % self.branch
569d1bd4 1477
f9162f6a
SH
1478 if not self.branch.startswith("refs/"):
1479 self.branch = "refs/heads/" + self.branch
179caebf 1480
6326aa58 1481 if len(args) == 0 and self.depotPaths:
b984733c 1482 if not self.silent:
6326aa58 1483 print "Depot paths: %s" % ' '.join(self.depotPaths)
b984733c 1484 else:
6326aa58 1485 if self.depotPaths and self.depotPaths != args:
cebdf5af 1486 print ("previous import used depot path %s and now %s was specified. "
6326aa58
HWN
1487 "This doesn't work!" % (' '.join (self.depotPaths),
1488 ' '.join (args)))
b984733c 1489 sys.exit(1)
6326aa58 1490
bb6e09b2 1491 self.depotPaths = sorted(args)
b984733c 1492
1c49fc19 1493 revision = ""
b984733c 1494 self.users = {}
b984733c 1495
6326aa58
HWN
1496 newPaths = []
1497 for p in self.depotPaths:
1498 if p.find("@") != -1:
1499 atIdx = p.index("@")
1500 self.changeRange = p[atIdx:]
1501 if self.changeRange == "@all":
1502 self.changeRange = ""
6a49f8e2 1503 elif ',' not in self.changeRange:
1c49fc19 1504 revision = self.changeRange
6326aa58 1505 self.changeRange = ""
7fcff9de 1506 p = p[:atIdx]
6326aa58
HWN
1507 elif p.find("#") != -1:
1508 hashIdx = p.index("#")
1c49fc19 1509 revision = p[hashIdx:]
7fcff9de 1510 p = p[:hashIdx]
6326aa58 1511 elif self.previousDepotPaths == []:
1c49fc19 1512 revision = "#head"
6326aa58
HWN
1513
1514 p = re.sub ("\.\.\.$", "", p)
1515 if not p.endswith("/"):
1516 p += "/"
1517
1518 newPaths.append(p)
1519
1520 self.depotPaths = newPaths
1521
b984733c 1522
b607e71e 1523 self.loadUserMapFromCache()
cb53e1f8
SH
1524 self.labels = {}
1525 if self.detectLabels:
1526 self.getLabels();
b984733c 1527
4b97ffb1 1528 if self.detectBranches:
df450923
SH
1529 ## FIXME - what's a P4 projectName ?
1530 self.projectName = self.guessProjectName()
1531
38f9f5ec
SH
1532 if self.hasOrigin:
1533 self.getBranchMappingFromGitBranches()
1534 else:
1535 self.getBranchMapping()
29bdbac1
SH
1536 if self.verbose:
1537 print "p4-git branches: %s" % self.p4BranchesInGit
1538 print "initial parents: %s" % self.initialParents
1539 for b in self.p4BranchesInGit:
1540 if b != "master":
6326aa58
HWN
1541
1542 ## FIXME
29bdbac1
SH
1543 b = b[len(self.projectName):]
1544 self.createdBranches.add(b)
4b97ffb1 1545
f291b4e3 1546 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
b984733c 1547
cebdf5af 1548 importProcess = subprocess.Popen(["git", "fast-import"],
6326aa58
HWN
1549 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1550 stderr=subprocess.PIPE);
08483580
SH
1551 self.gitOutput = importProcess.stdout
1552 self.gitStream = importProcess.stdin
1553 self.gitError = importProcess.stderr
b984733c 1554
1c49fc19 1555 if revision:
c208a243 1556 self.importHeadRevision(revision)
b984733c
SH
1557 else:
1558 changes = []
1559
0828ab14 1560 if len(self.changesFile) > 0:
b984733c
SH
1561 output = open(self.changesFile).readlines()
1562 changeSet = Set()
1563 for line in output:
1564 changeSet.add(int(line))
1565
1566 for change in changeSet:
1567 changes.append(change)
1568
1569 changes.sort()
1570 else:
29bdbac1 1571 if self.verbose:
86dff6b6 1572 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
6326aa58 1573 self.changeRange)
4f6432d8 1574 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
b984733c 1575
01a9c9c5 1576 if len(self.maxChanges) > 0:
7fcff9de 1577 changes = changes[:min(int(self.maxChanges), len(changes))]
01a9c9c5 1578
b984733c 1579 if len(changes) == 0:
0828ab14 1580 if not self.silent:
341dc1c1 1581 print "No changes to import!"
1f52af6c 1582 return True
b984733c 1583
a9d1a27a
SH
1584 if not self.silent and not self.detectBranches:
1585 print "Import destination: %s" % self.branch
1586
341dc1c1
SH
1587 self.updatedBranches = set()
1588
e87f37ae 1589 self.importChanges(changes)
b984733c 1590
341dc1c1
SH
1591 if not self.silent:
1592 print ""
1593 if len(self.updatedBranches) > 0:
1594 sys.stdout.write("Updated branches: ")
1595 for b in self.updatedBranches:
1596 sys.stdout.write("%s " % b)
1597 sys.stdout.write("\n")
b984733c 1598
b984733c 1599 self.gitStream.close()
29bdbac1
SH
1600 if importProcess.wait() != 0:
1601 die("fast-import failed: %s" % self.gitError.read())
b984733c
SH
1602 self.gitOutput.close()
1603 self.gitError.close()
1604
b984733c
SH
1605 return True
1606
01ce1fe9
SH
1607class P4Rebase(Command):
1608 def __init__(self):
1609 Command.__init__(self)
01265103 1610 self.options = [ ]
cebdf5af
HWN
1611 self.description = ("Fetches the latest revision from perforce and "
1612 + "rebases the current work (branch) against it")
68c42153 1613 self.verbose = False
01ce1fe9
SH
1614
1615 def run(self, args):
1616 sync = P4Sync()
1617 sync.run([])
d7e3868c 1618
14594f4b
SH
1619 return self.rebase()
1620
1621 def rebase(self):
36ee4ee4
SH
1622 if os.system("git update-index --refresh") != 0:
1623 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.");
1624 if len(read_pipe("git diff-index HEAD --")) > 0:
1625 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
1626
d7e3868c
SH
1627 [upstream, settings] = findUpstreamBranchPoint()
1628 if len(upstream) == 0:
1629 die("Cannot find upstream branchpoint for rebase")
1630
1631 # the branchpoint may be p4/foo~3, so strip off the parent
1632 upstream = re.sub("~[0-9]+$", "", upstream)
1633
1634 print "Rebasing the current branch onto %s" % upstream
b25b2065 1635 oldHead = read_pipe("git rev-parse HEAD").strip()
d7e3868c 1636 system("git rebase %s" % upstream)
1f52af6c 1637 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
01ce1fe9
SH
1638 return True
1639
f9a3a4f7
SH
1640class P4Clone(P4Sync):
1641 def __init__(self):
1642 P4Sync.__init__(self)
1643 self.description = "Creates a new git repository and imports from Perforce into it"
bb6e09b2 1644 self.usage = "usage: %prog [options] //depot/path[@revRange]"
354081d5 1645 self.options += [
bb6e09b2
HWN
1646 optparse.make_option("--destination", dest="cloneDestination",
1647 action='store', default=None,
354081d5
TT
1648 help="where to leave result of the clone"),
1649 optparse.make_option("-/", dest="cloneExclude",
1650 action="append", type="string",
1651 help="exclude depot path")
1652 ]
bb6e09b2 1653 self.cloneDestination = None
f9a3a4f7 1654 self.needsGit = False
f9a3a4f7 1655
354081d5
TT
1656 # This is required for the "append" cloneExclude action
1657 def ensure_value(self, attr, value):
1658 if not hasattr(self, attr) or getattr(self, attr) is None:
1659 setattr(self, attr, value)
1660 return getattr(self, attr)
1661
6a49f8e2
HWN
1662 def defaultDestination(self, args):
1663 ## TODO: use common prefix of args?
1664 depotPath = args[0]
1665 depotDir = re.sub("(@[^@]*)$", "", depotPath)
1666 depotDir = re.sub("(#[^#]*)$", "", depotDir)
053d9e43 1667 depotDir = re.sub(r"\.\.\.$", "", depotDir)
6a49f8e2
HWN
1668 depotDir = re.sub(r"/$", "", depotDir)
1669 return os.path.split(depotDir)[1]
1670
f9a3a4f7
SH
1671 def run(self, args):
1672 if len(args) < 1:
1673 return False
bb6e09b2
HWN
1674
1675 if self.keepRepoPath and not self.cloneDestination:
1676 sys.stderr.write("Must specify destination for --keep-path\n")
1677 sys.exit(1)
f9a3a4f7 1678
6326aa58 1679 depotPaths = args
5e100b5c
SH
1680
1681 if not self.cloneDestination and len(depotPaths) > 1:
1682 self.cloneDestination = depotPaths[-1]
1683 depotPaths = depotPaths[:-1]
1684
354081d5 1685 self.cloneExclude = ["/"+p for p in self.cloneExclude]
6326aa58
HWN
1686 for p in depotPaths:
1687 if not p.startswith("//"):
1688 return False
f9a3a4f7 1689
bb6e09b2 1690 if not self.cloneDestination:
98ad4faf 1691 self.cloneDestination = self.defaultDestination(args)
f9a3a4f7 1692
86dff6b6 1693 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
c3bf3f13
KG
1694 if not os.path.exists(self.cloneDestination):
1695 os.makedirs(self.cloneDestination)
bb6e09b2 1696 os.chdir(self.cloneDestination)
f9a3a4f7 1697 system("git init")
b86f7378 1698 self.gitdir = os.getcwd() + "/.git"
6326aa58 1699 if not P4Sync.run(self, depotPaths):
f9a3a4f7 1700 return False
f9a3a4f7 1701 if self.branch != "master":
8f9b2e08
SH
1702 if gitBranchExists("refs/remotes/p4/master"):
1703 system("git branch master refs/remotes/p4/master")
1704 system("git checkout -f")
1705 else:
1706 print "Could not detect main branch. No checkout/master branch created."
86dff6b6 1707
f9a3a4f7
SH
1708 return True
1709
09d89de2
SH
1710class P4Branches(Command):
1711 def __init__(self):
1712 Command.__init__(self)
1713 self.options = [ ]
1714 self.description = ("Shows the git branches that hold imports and their "
1715 + "corresponding perforce depot paths")
1716 self.verbose = False
1717
1718 def run(self, args):
5ca44617
SH
1719 if originP4BranchesExist():
1720 createOrUpdateBranchesFromOrigin()
1721
09d89de2
SH
1722 cmdline = "git rev-parse --symbolic "
1723 cmdline += " --remotes"
1724
1725 for line in read_pipe_lines(cmdline):
1726 line = line.strip()
1727
1728 if not line.startswith('p4/') or line == "p4/HEAD":
1729 continue
1730 branch = line
1731
1732 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
1733 settings = extractSettingsGitLog(log)
1734
1735 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
1736 return True
1737
b984733c
SH
1738class HelpFormatter(optparse.IndentedHelpFormatter):
1739 def __init__(self):
1740 optparse.IndentedHelpFormatter.__init__(self)
1741
1742 def format_description(self, description):
1743 if description:
1744 return description + "\n"
1745 else:
1746 return ""
4f5cf76a 1747
86949eef
SH
1748def printUsage(commands):
1749 print "usage: %s <command> [options]" % sys.argv[0]
1750 print ""
1751 print "valid commands: %s" % ", ".join(commands)
1752 print ""
1753 print "Try %s <command> --help for command specific help." % sys.argv[0]
1754 print ""
1755
1756commands = {
b86f7378
HWN
1757 "debug" : P4Debug,
1758 "submit" : P4Submit,
a9834f58 1759 "commit" : P4Submit,
b86f7378
HWN
1760 "sync" : P4Sync,
1761 "rebase" : P4Rebase,
1762 "clone" : P4Clone,
09d89de2
SH
1763 "rollback" : P4RollBack,
1764 "branches" : P4Branches
86949eef
SH
1765}
1766
86949eef 1767
bb6e09b2
HWN
1768def main():
1769 if len(sys.argv[1:]) == 0:
1770 printUsage(commands.keys())
1771 sys.exit(2)
4f5cf76a 1772
bb6e09b2
HWN
1773 cmd = ""
1774 cmdName = sys.argv[1]
1775 try:
b86f7378
HWN
1776 klass = commands[cmdName]
1777 cmd = klass()
bb6e09b2
HWN
1778 except KeyError:
1779 print "unknown command %s" % cmdName
1780 print ""
1781 printUsage(commands.keys())
1782 sys.exit(2)
1783
1784 options = cmd.options
b86f7378 1785 cmd.gitdir = os.environ.get("GIT_DIR", None)
bb6e09b2
HWN
1786
1787 args = sys.argv[2:]
1788
1789 if len(options) > 0:
1790 options.append(optparse.make_option("--git-dir", dest="gitdir"))
1791
1792 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
1793 options,
1794 description = cmd.description,
1795 formatter = HelpFormatter())
1796
1797 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
1798 global verbose
1799 verbose = cmd.verbose
1800 if cmd.needsGit:
b86f7378
HWN
1801 if cmd.gitdir == None:
1802 cmd.gitdir = os.path.abspath(".git")
1803 if not isValidGitDir(cmd.gitdir):
1804 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
1805 if os.path.exists(cmd.gitdir):
bb6e09b2
HWN
1806 cdup = read_pipe("git rev-parse --show-cdup").strip()
1807 if len(cdup) > 0:
1808 os.chdir(cdup);
e20a9e53 1809
b86f7378
HWN
1810 if not isValidGitDir(cmd.gitdir):
1811 if isValidGitDir(cmd.gitdir + "/.git"):
1812 cmd.gitdir += "/.git"
bb6e09b2 1813 else:
b86f7378 1814 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
e20a9e53 1815
b86f7378 1816 os.environ["GIT_DIR"] = cmd.gitdir
86949eef 1817
bb6e09b2
HWN
1818 if not cmd.run(args):
1819 parser.print_help()
4f5cf76a 1820
4f5cf76a 1821
bb6e09b2
HWN
1822if __name__ == '__main__':
1823 main()