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