git p4: catch p4 describe errors
[git/git.git] / git-p4.py
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
60df071c 13import re, shutil
8b41a97f 14
4addad22 15verbose = False
86949eef 16
06804c76 17# Only labels/tags matching this will be imported/exported
c8942a22 18defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
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 """
6de040df 27 real_cmd = ["p4"]
abcaf073
AK
28
29 user = gitConfig("git-p4.user")
30 if len(user) > 0:
6de040df 31 real_cmd += ["-u",user]
abcaf073
AK
32
33 password = gitConfig("git-p4.password")
34 if len(password) > 0:
6de040df 35 real_cmd += ["-P", password]
abcaf073
AK
36
37 port = gitConfig("git-p4.port")
38 if len(port) > 0:
6de040df 39 real_cmd += ["-p", port]
abcaf073
AK
40
41 host = gitConfig("git-p4.host")
42 if len(host) > 0:
41799aa2 43 real_cmd += ["-H", host]
abcaf073
AK
44
45 client = gitConfig("git-p4.client")
46 if len(client) > 0:
6de040df 47 real_cmd += ["-c", client]
abcaf073 48
6de040df
LD
49
50 if isinstance(cmd,basestring):
51 real_cmd = ' '.join(real_cmd) + ' ' + cmd
52 else:
53 real_cmd += cmd
21a50753
AK
54 return real_cmd
55
053fd0c1 56def chdir(dir):
6de040df 57 # P4 uses the PWD environment variable rather than getcwd(). Since we're
bf1d68ff
GG
58 # not using the shell, we have to set it ourselves. This path could
59 # be relative, so go there first, then figure out where we ended up.
053fd0c1 60 os.chdir(dir)
bf1d68ff 61 os.environ['PWD'] = os.getcwd()
053fd0c1 62
86dff6b6
HWN
63def die(msg):
64 if verbose:
65 raise Exception(msg)
66 else:
67 sys.stderr.write(msg + "\n")
68 sys.exit(1)
69
6de040df 70def write_pipe(c, stdin):
4addad22 71 if verbose:
6de040df 72 sys.stderr.write('Writing pipe: %s\n' % str(c))
b016d397 73
6de040df
LD
74 expand = isinstance(c,basestring)
75 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
76 pipe = p.stdin
77 val = pipe.write(stdin)
78 pipe.close()
79 if p.wait():
80 die('Command failed: %s' % str(c))
b016d397
HWN
81
82 return val
83
6de040df 84def p4_write_pipe(c, stdin):
d9429194 85 real_cmd = p4_build_cmd(c)
6de040df 86 return write_pipe(real_cmd, stdin)
d9429194 87
4addad22
HWN
88def read_pipe(c, ignore_error=False):
89 if verbose:
6de040df 90 sys.stderr.write('Reading pipe: %s\n' % str(c))
8b41a97f 91
6de040df
LD
92 expand = isinstance(c,basestring)
93 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
94 pipe = p.stdout
b016d397 95 val = pipe.read()
6de040df
LD
96 if p.wait() and not ignore_error:
97 die('Command failed: %s' % str(c))
b016d397
HWN
98
99 return val
100
d9429194
AK
101def p4_read_pipe(c, ignore_error=False):
102 real_cmd = p4_build_cmd(c)
103 return read_pipe(real_cmd, ignore_error)
b016d397 104
bce4c5fc 105def read_pipe_lines(c):
4addad22 106 if verbose:
6de040df
LD
107 sys.stderr.write('Reading pipe: %s\n' % str(c))
108
109 expand = isinstance(c, basestring)
110 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
111 pipe = p.stdout
b016d397 112 val = pipe.readlines()
6de040df
LD
113 if pipe.close() or p.wait():
114 die('Command failed: %s' % str(c))
b016d397
HWN
115
116 return val
caace111 117
2318121b
AK
118def p4_read_pipe_lines(c):
119 """Specifically invoke p4 on the command supplied. """
155af834 120 real_cmd = p4_build_cmd(c)
2318121b
AK
121 return read_pipe_lines(real_cmd)
122
8e9497c2
GG
123def p4_has_command(cmd):
124 """Ask p4 for help on this command. If it returns an error, the
125 command does not exist in this version of p4."""
126 real_cmd = p4_build_cmd(["help", cmd])
127 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
128 stderr=subprocess.PIPE)
129 p.communicate()
130 return p.returncode == 0
131
6754a299 132def system(cmd):
6de040df 133 expand = isinstance(cmd,basestring)
4addad22 134 if verbose:
6de040df
LD
135 sys.stderr.write("executing %s\n" % str(cmd))
136 subprocess.check_call(cmd, shell=expand)
6754a299 137
bf9320f1
AK
138def p4_system(cmd):
139 """Specifically invoke p4 as the system command. """
155af834 140 real_cmd = p4_build_cmd(cmd)
6de040df
LD
141 expand = isinstance(real_cmd, basestring)
142 subprocess.check_call(real_cmd, shell=expand)
143
144def p4_integrate(src, dest):
9d7d446a 145 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
6de040df 146
8d7ec362 147def p4_sync(f, *options):
9d7d446a 148 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
6de040df
LD
149
150def p4_add(f):
9d7d446a
PW
151 # forcibly add file names with wildcards
152 if wildcard_present(f):
153 p4_system(["add", "-f", f])
154 else:
155 p4_system(["add", f])
6de040df
LD
156
157def p4_delete(f):
9d7d446a 158 p4_system(["delete", wildcard_encode(f)])
6de040df
LD
159
160def p4_edit(f):
9d7d446a 161 p4_system(["edit", wildcard_encode(f)])
6de040df
LD
162
163def p4_revert(f):
9d7d446a 164 p4_system(["revert", wildcard_encode(f)])
6de040df 165
9d7d446a
PW
166def p4_reopen(type, f):
167 p4_system(["reopen", "-t", type, wildcard_encode(f)])
bf9320f1 168
8e9497c2
GG
169def p4_move(src, dest):
170 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
171
18fa13d0
PW
172def p4_describe(change):
173 """Make sure it returns a valid result by checking for
174 the presence of field "time". Return a dict of the
175 results."""
176
177 ds = p4CmdList(["describe", "-s", str(change)])
178 if len(ds) != 1:
179 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
180
181 d = ds[0]
182
183 if "p4ExitCode" in d:
184 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
185 str(d)))
186 if "code" in d:
187 if d["code"] == "error":
188 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
189
190 if "time" not in d:
191 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
192
193 return d
194
9cffb8c8
PW
195#
196# Canonicalize the p4 type and return a tuple of the
197# base type, plus any modifiers. See "p4 help filetypes"
198# for a list and explanation.
199#
200def split_p4_type(p4type):
201
202 p4_filetypes_historical = {
203 "ctempobj": "binary+Sw",
204 "ctext": "text+C",
205 "cxtext": "text+Cx",
206 "ktext": "text+k",
207 "kxtext": "text+kx",
208 "ltext": "text+F",
209 "tempobj": "binary+FSw",
210 "ubinary": "binary+F",
211 "uresource": "resource+F",
212 "uxbinary": "binary+Fx",
213 "xbinary": "binary+x",
214 "xltext": "text+Fx",
215 "xtempobj": "binary+Swx",
216 "xtext": "text+x",
217 "xunicode": "unicode+x",
218 "xutf16": "utf16+x",
219 }
220 if p4type in p4_filetypes_historical:
221 p4type = p4_filetypes_historical[p4type]
222 mods = ""
223 s = p4type.split("+")
224 base = s[0]
225 mods = ""
226 if len(s) > 1:
227 mods = s[1]
228 return (base, mods)
b9fc6ea9 229
60df071c
LD
230#
231# return the raw p4 type of a file (text, text+ko, etc)
232#
233def p4_type(file):
234 results = p4CmdList(["fstat", "-T", "headType", file])
235 return results[0]['headType']
236
237#
238# Given a type base and modifier, return a regexp matching
239# the keywords that can be expanded in the file
240#
241def p4_keywords_regexp_for_type(base, type_mods):
242 if base in ("text", "unicode", "binary"):
243 kwords = None
244 if "ko" in type_mods:
245 kwords = 'Id|Header'
246 elif "k" in type_mods:
247 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
248 else:
249 return None
250 pattern = r"""
251 \$ # Starts with a dollar, followed by...
252 (%s) # one of the keywords, followed by...
6b2bf41e 253 (:[^$\n]+)? # possibly an old expansion, followed by...
60df071c
LD
254 \$ # another dollar
255 """ % kwords
256 return pattern
257 else:
258 return None
259
260#
261# Given a file, return a regexp matching the possible
262# RCS keywords that will be expanded, or None for files
263# with kw expansion turned off.
264#
265def p4_keywords_regexp_for_file(file):
266 if not os.path.exists(file):
267 return None
268 else:
269 (type_base, type_mods) = split_p4_type(p4_type(file))
270 return p4_keywords_regexp_for_type(type_base, type_mods)
b9fc6ea9 271
c65b670e
CP
272def setP4ExecBit(file, mode):
273 # Reopens an already open file and changes the execute bit to match
274 # the execute bit setting in the passed in mode.
275
276 p4Type = "+x"
277
278 if not isModeExec(mode):
279 p4Type = getP4OpenedType(file)
280 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
281 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
282 if p4Type[-1] == "+":
283 p4Type = p4Type[0:-1]
284
6de040df 285 p4_reopen(p4Type, file)
c65b670e
CP
286
287def getP4OpenedType(file):
288 # Returns the perforce file type for the given file.
289
9d7d446a 290 result = p4_read_pipe(["opened", wildcard_encode(file)])
f3e5ae4f 291 match = re.match(".*\((.+)\)\r?$", result)
c65b670e
CP
292 if match:
293 return match.group(1)
294 else:
f3e5ae4f 295 die("Could not determine file type for %s (result: '%s')" % (file, result))
c65b670e 296
06804c76
LD
297# Return the set of all p4 labels
298def getP4Labels(depotPaths):
299 labels = set()
300 if isinstance(depotPaths,basestring):
301 depotPaths = [depotPaths]
302
303 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
304 label = l['label']
305 labels.add(label)
306
307 return labels
308
309# Return the set of all git tags
310def getGitTags():
311 gitTags = set()
312 for line in read_pipe_lines(["git", "tag"]):
313 tag = line.strip()
314 gitTags.add(tag)
315 return gitTags
316
b43b0a3c
CP
317def diffTreePattern():
318 # This is a simple generator for the diff tree regex pattern. This could be
319 # a class variable if this and parseDiffTreeEntry were a part of a class.
320 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
321 while True:
322 yield pattern
323
324def parseDiffTreeEntry(entry):
325 """Parses a single diff tree entry into its component elements.
326
327 See git-diff-tree(1) manpage for details about the format of the diff
328 output. This method returns a dictionary with the following elements:
329
330 src_mode - The mode of the source file
331 dst_mode - The mode of the destination file
332 src_sha1 - The sha1 for the source file
333 dst_sha1 - The sha1 fr the destination file
334 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
335 status_score - The score for the status (applicable for 'C' and 'R'
336 statuses). This is None if there is no score.
337 src - The path for the source file.
338 dst - The path for the destination file. This is only present for
339 copy or renames. If it is not present, this is None.
340
341 If the pattern is not matched, None is returned."""
342
343 match = diffTreePattern().next().match(entry)
344 if match:
345 return {
346 'src_mode': match.group(1),
347 'dst_mode': match.group(2),
348 'src_sha1': match.group(3),
349 'dst_sha1': match.group(4),
350 'status': match.group(5),
351 'status_score': match.group(6),
352 'src': match.group(7),
353 'dst': match.group(10)
354 }
355 return None
356
c65b670e
CP
357def isModeExec(mode):
358 # Returns True if the given git mode represents an executable file,
359 # otherwise False.
360 return mode[-3:] == "755"
361
362def isModeExecChanged(src_mode, dst_mode):
363 return isModeExec(src_mode) != isModeExec(dst_mode)
364
b932705b 365def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
6de040df
LD
366
367 if isinstance(cmd,basestring):
368 cmd = "-G " + cmd
369 expand = True
370 else:
371 cmd = ["-G"] + cmd
372 expand = False
373
374 cmd = p4_build_cmd(cmd)
6a49f8e2 375 if verbose:
6de040df 376 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
9f90c733
SL
377
378 # Use a temporary file to avoid deadlocks without
379 # subprocess.communicate(), which would put another copy
380 # of stdout into memory.
381 stdin_file = None
382 if stdin is not None:
383 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
6de040df
LD
384 if isinstance(stdin,basestring):
385 stdin_file.write(stdin)
386 else:
387 for i in stdin:
388 stdin_file.write(i + '\n')
9f90c733
SL
389 stdin_file.flush()
390 stdin_file.seek(0)
391
6de040df
LD
392 p4 = subprocess.Popen(cmd,
393 shell=expand,
9f90c733
SL
394 stdin=stdin_file,
395 stdout=subprocess.PIPE)
86949eef
SH
396
397 result = []
398 try:
399 while True:
9f90c733 400 entry = marshal.load(p4.stdout)
c3f6163b
AG
401 if cb is not None:
402 cb(entry)
403 else:
404 result.append(entry)
86949eef
SH
405 except EOFError:
406 pass
9f90c733
SL
407 exitCode = p4.wait()
408 if exitCode != 0:
ac3e0d79
SH
409 entry = {}
410 entry["p4ExitCode"] = exitCode
411 result.append(entry)
86949eef
SH
412
413 return result
414
415def p4Cmd(cmd):
416 list = p4CmdList(cmd)
417 result = {}
418 for entry in list:
419 result.update(entry)
420 return result;
421
cb2c9db5
SH
422def p4Where(depotPath):
423 if not depotPath.endswith("/"):
424 depotPath += "/"
7f705dc3 425 depotPath = depotPath + "..."
6de040df 426 outputList = p4CmdList(["where", depotPath])
7f705dc3
TAL
427 output = None
428 for entry in outputList:
75bc9573
TAL
429 if "depotFile" in entry:
430 if entry["depotFile"] == depotPath:
431 output = entry
432 break
433 elif "data" in entry:
434 data = entry.get("data")
435 space = data.find(" ")
436 if data[:space] == depotPath:
437 output = entry
438 break
7f705dc3
TAL
439 if output == None:
440 return ""
dc524036
SH
441 if output["code"] == "error":
442 return ""
cb2c9db5
SH
443 clientPath = ""
444 if "path" in output:
445 clientPath = output.get("path")
446 elif "data" in output:
447 data = output.get("data")
448 lastSpace = data.rfind(" ")
449 clientPath = data[lastSpace + 1:]
450
451 if clientPath.endswith("..."):
452 clientPath = clientPath[:-3]
453 return clientPath
454
86949eef 455def currentGitBranch():
b25b2065 456 return read_pipe("git name-rev HEAD").split(" ")[1].strip()
86949eef 457
4f5cf76a 458def isValidGitDir(path):
bb6e09b2
HWN
459 if (os.path.exists(path + "/HEAD")
460 and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
4f5cf76a
SH
461 return True;
462 return False
463
463e8af6 464def parseRevision(ref):
b25b2065 465 return read_pipe("git rev-parse %s" % ref).strip()
463e8af6 466
28755dba
PW
467def branchExists(ref):
468 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
469 ignore_error=True)
470 return len(rev) > 0
471
6ae8de88
SH
472def extractLogMessageFromGitCommit(commit):
473 logMessage = ""
b016d397
HWN
474
475 ## fixme: title is first line of commit, not 1st paragraph.
6ae8de88 476 foundTitle = False
b016d397 477 for log in read_pipe_lines("git cat-file commit %s" % commit):
6ae8de88
SH
478 if not foundTitle:
479 if len(log) == 1:
1c094184 480 foundTitle = True
6ae8de88
SH
481 continue
482
483 logMessage += log
484 return logMessage
485
bb6e09b2 486def extractSettingsGitLog(log):
6ae8de88
SH
487 values = {}
488 for line in log.split("\n"):
489 line = line.strip()
6326aa58
HWN
490 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
491 if not m:
492 continue
493
494 assignments = m.group(1).split (':')
495 for a in assignments:
496 vals = a.split ('=')
497 key = vals[0].strip()
498 val = ('='.join (vals[1:])).strip()
499 if val.endswith ('\"') and val.startswith('"'):
500 val = val[1:-1]
501
502 values[key] = val
503
845b42cb
SH
504 paths = values.get("depot-paths")
505 if not paths:
506 paths = values.get("depot-path")
a3fdd579
SH
507 if paths:
508 values['depot-paths'] = paths.split(',')
bb6e09b2 509 return values
6ae8de88 510
8136a639 511def gitBranchExists(branch):
bb6e09b2
HWN
512 proc = subprocess.Popen(["git", "rev-parse", branch],
513 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
caace111 514 return proc.wait() == 0;
8136a639 515
36bd8446 516_gitConfig = {}
99f790f2 517def gitConfig(key, args = None): # set args to "--bool", for instance
36bd8446 518 if not _gitConfig.has_key(key):
99f790f2
TAL
519 argsFilter = ""
520 if args != None:
521 argsFilter = "%s " % args
522 cmd = "git config %s%s" % (argsFilter, key)
523 _gitConfig[key] = read_pipe(cmd, ignore_error=True).strip()
36bd8446 524 return _gitConfig[key]
01265103 525
7199cf13
VA
526def gitConfigList(key):
527 if not _gitConfig.has_key(key):
528 _gitConfig[key] = read_pipe("git config --get-all %s" % key, ignore_error=True).strip().split(os.linesep)
529 return _gitConfig[key]
530
062410bb
SH
531def p4BranchesInGit(branchesAreInRemotes = True):
532 branches = {}
533
534 cmdline = "git rev-parse --symbolic "
535 if branchesAreInRemotes:
536 cmdline += " --remotes"
537 else:
538 cmdline += " --branches"
539
540 for line in read_pipe_lines(cmdline):
541 line = line.strip()
542
543 ## only import to p4/
544 if not line.startswith('p4/') or line == "p4/HEAD":
545 continue
546 branch = line
547
548 # strip off p4
549 branch = re.sub ("^p4/", "", line)
550
551 branches[branch] = parseRevision(line)
552 return branches
553
9ceab363 554def findUpstreamBranchPoint(head = "HEAD"):
86506fe5
SH
555 branches = p4BranchesInGit()
556 # map from depot-path to branch name
557 branchByDepotPath = {}
558 for branch in branches.keys():
559 tip = branches[branch]
560 log = extractLogMessageFromGitCommit(tip)
561 settings = extractSettingsGitLog(log)
562 if settings.has_key("depot-paths"):
563 paths = ",".join(settings["depot-paths"])
564 branchByDepotPath[paths] = "remotes/p4/" + branch
565
27d2d811 566 settings = None
27d2d811
SH
567 parent = 0
568 while parent < 65535:
9ceab363 569 commit = head + "~%s" % parent
27d2d811
SH
570 log = extractLogMessageFromGitCommit(commit)
571 settings = extractSettingsGitLog(log)
86506fe5
SH
572 if settings.has_key("depot-paths"):
573 paths = ",".join(settings["depot-paths"])
574 if branchByDepotPath.has_key(paths):
575 return [branchByDepotPath[paths], settings]
27d2d811 576
86506fe5 577 parent = parent + 1
27d2d811 578
86506fe5 579 return ["", settings]
27d2d811 580
5ca44617
SH
581def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
582 if not silent:
583 print ("Creating/updating branch(es) in %s based on origin branch(es)"
584 % localRefPrefix)
585
586 originPrefix = "origin/p4/"
587
588 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
589 line = line.strip()
590 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
591 continue
592
593 headName = line[len(originPrefix):]
594 remoteHead = localRefPrefix + headName
595 originHead = line
596
597 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
598 if (not original.has_key('depot-paths')
599 or not original.has_key('change')):
600 continue
601
602 update = False
603 if not gitBranchExists(remoteHead):
604 if verbose:
605 print "creating %s" % remoteHead
606 update = True
607 else:
608 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
609 if settings.has_key('change') > 0:
610 if settings['depot-paths'] == original['depot-paths']:
611 originP4Change = int(original['change'])
612 p4Change = int(settings['change'])
613 if originP4Change > p4Change:
614 print ("%s (%s) is newer than %s (%s). "
615 "Updating p4 branch from origin."
616 % (originHead, originP4Change,
617 remoteHead, p4Change))
618 update = True
619 else:
620 print ("Ignoring: %s was imported from %s while "
621 "%s was imported from %s"
622 % (originHead, ','.join(original['depot-paths']),
623 remoteHead, ','.join(settings['depot-paths'])))
624
625 if update:
626 system("git update-ref %s %s" % (remoteHead, originHead))
627
628def originP4BranchesExist():
629 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
630
4f6432d8
SH
631def p4ChangesForPaths(depotPaths, changeRange):
632 assert depotPaths
6de040df
LD
633 cmd = ['changes']
634 for p in depotPaths:
635 cmd += ["%s...%s" % (p, changeRange)]
636 output = p4_read_pipe_lines(cmd)
4f6432d8 637
b4b0ba06 638 changes = {}
4f6432d8 639 for line in output:
c3f6163b
AG
640 changeNum = int(line.split(" ")[1])
641 changes[changeNum] = True
4f6432d8 642
b4b0ba06
PW
643 changelist = changes.keys()
644 changelist.sort()
645 return changelist
4f6432d8 646
d53de8b9
TAL
647def p4PathStartsWith(path, prefix):
648 # This method tries to remedy a potential mixed-case issue:
649 #
650 # If UserA adds //depot/DirA/file1
651 # and UserB adds //depot/dira/file2
652 #
653 # we may or may not have a problem. If you have core.ignorecase=true,
654 # we treat DirA and dira as the same directory
655 ignorecase = gitConfig("core.ignorecase", "--bool") == "true"
656 if ignorecase:
657 return path.lower().startswith(prefix.lower())
658 return path.startswith(prefix)
659
543987bd
PW
660def getClientSpec():
661 """Look at the p4 client spec, create a View() object that contains
662 all the mappings, and return it."""
663
664 specList = p4CmdList("client -o")
665 if len(specList) != 1:
666 die('Output from "client -o" is %d lines, expecting 1' %
667 len(specList))
668
669 # dictionary of all client parameters
670 entry = specList[0]
671
672 # just the keys that start with "View"
673 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
674
675 # hold this new View
676 view = View()
677
678 # append the lines, in order, to the view
679 for view_num in range(len(view_keys)):
680 k = "View%d" % view_num
681 if k not in view_keys:
682 die("Expected view key %s missing" % k)
683 view.append(entry[k])
684
685 return view
686
687def getClientRoot():
688 """Grab the client directory."""
689
690 output = p4CmdList("client -o")
691 if len(output) != 1:
692 die('Output from "client -o" is %d lines, expecting 1' % len(output))
693
694 entry = output[0]
695 if "Root" not in entry:
696 die('Client has no "Root"')
697
698 return entry["Root"]
699
9d7d446a
PW
700#
701# P4 wildcards are not allowed in filenames. P4 complains
702# if you simply add them, but you can force it with "-f", in
703# which case it translates them into %xx encoding internally.
704#
705def wildcard_decode(path):
706 # Search for and fix just these four characters. Do % last so
707 # that fixing it does not inadvertently create new %-escapes.
708 # Cannot have * in a filename in windows; untested as to
709 # what p4 would do in such a case.
710 if not platform.system() == "Windows":
711 path = path.replace("%2A", "*")
712 path = path.replace("%23", "#") \
713 .replace("%40", "@") \
714 .replace("%25", "%")
715 return path
716
717def wildcard_encode(path):
718 # do % first to avoid double-encoding the %s introduced here
719 path = path.replace("%", "%25") \
720 .replace("*", "%2A") \
721 .replace("#", "%23") \
722 .replace("@", "%40")
723 return path
724
725def wildcard_present(path):
726 return path.translate(None, "*#@%") != path
727
b984733c
SH
728class Command:
729 def __init__(self):
730 self.usage = "usage: %prog [options]"
8910ac0e 731 self.needsGit = True
6a10b6aa 732 self.verbose = False
b984733c 733
3ea2cfd4
LD
734class P4UserMap:
735 def __init__(self):
736 self.userMapFromPerforceServer = False
affb474f
LD
737 self.myP4UserId = None
738
739 def p4UserId(self):
740 if self.myP4UserId:
741 return self.myP4UserId
742
743 results = p4CmdList("user -o")
744 for r in results:
745 if r.has_key('User'):
746 self.myP4UserId = r['User']
747 return r['User']
748 die("Could not find your p4 user id")
749
750 def p4UserIsMe(self, p4User):
751 # return True if the given p4 user is actually me
752 me = self.p4UserId()
753 if not p4User or p4User != me:
754 return False
755 else:
756 return True
3ea2cfd4
LD
757
758 def getUserCacheFilename(self):
759 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
760 return home + "/.gitp4-usercache.txt"
761
762 def getUserMapFromPerforceServer(self):
763 if self.userMapFromPerforceServer:
764 return
765 self.users = {}
766 self.emails = {}
767
768 for output in p4CmdList("users"):
769 if not output.has_key("User"):
770 continue
771 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
772 self.emails[output["Email"]] = output["User"]
773
774
775 s = ''
776 for (key, val) in self.users.items():
777 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
778
779 open(self.getUserCacheFilename(), "wb").write(s)
780 self.userMapFromPerforceServer = True
781
782 def loadUserMapFromCache(self):
783 self.users = {}
784 self.userMapFromPerforceServer = False
785 try:
786 cache = open(self.getUserCacheFilename(), "rb")
787 lines = cache.readlines()
788 cache.close()
789 for line in lines:
790 entry = line.strip().split("\t")
791 self.users[entry[0]] = entry[1]
792 except IOError:
793 self.getUserMapFromPerforceServer()
794
b984733c 795class P4Debug(Command):
86949eef 796 def __init__(self):
6ae8de88 797 Command.__init__(self)
6a10b6aa 798 self.options = []
c8c39116 799 self.description = "A tool to debug the output of p4 -G."
8910ac0e 800 self.needsGit = False
86949eef
SH
801
802 def run(self, args):
b1ce9447 803 j = 0
6de040df 804 for output in p4CmdList(args):
b1ce9447
HWN
805 print 'Element: %d' % j
806 j += 1
86949eef 807 print output
b984733c 808 return True
86949eef 809
5834684d
SH
810class P4RollBack(Command):
811 def __init__(self):
812 Command.__init__(self)
813 self.options = [
0c66a783 814 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
5834684d
SH
815 ]
816 self.description = "A tool to debug the multi-branch import. Don't use :)"
0c66a783 817 self.rollbackLocalBranches = False
5834684d
SH
818
819 def run(self, args):
820 if len(args) != 1:
821 return False
822 maxChange = int(args[0])
0c66a783 823
ad192f28 824 if "p4ExitCode" in p4Cmd("changes -m 1"):
66a2f523
SH
825 die("Problems executing p4");
826
0c66a783
SH
827 if self.rollbackLocalBranches:
828 refPrefix = "refs/heads/"
b016d397 829 lines = read_pipe_lines("git rev-parse --symbolic --branches")
0c66a783
SH
830 else:
831 refPrefix = "refs/remotes/"
b016d397 832 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
0c66a783
SH
833
834 for line in lines:
835 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
b25b2065
HWN
836 line = line.strip()
837 ref = refPrefix + line
5834684d 838 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
839 settings = extractSettingsGitLog(log)
840
841 depotPaths = settings['depot-paths']
842 change = settings['change']
843
5834684d 844 changed = False
52102d47 845
6326aa58
HWN
846 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
847 for p in depotPaths]))) == 0:
52102d47
SH
848 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
849 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
850 continue
851
bb6e09b2 852 while change and int(change) > maxChange:
5834684d 853 changed = True
52102d47
SH
854 if self.verbose:
855 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
5834684d
SH
856 system("git update-ref %s \"%s^\"" % (ref, ref))
857 log = extractLogMessageFromGitCommit(ref)
bb6e09b2
HWN
858 settings = extractSettingsGitLog(log)
859
860
861 depotPaths = settings['depot-paths']
862 change = settings['change']
5834684d
SH
863
864 if changed:
52102d47 865 print "%s rewound to %s" % (ref, change)
5834684d
SH
866
867 return True
868
3ea2cfd4 869class P4Submit(Command, P4UserMap):
6bbfd137
PW
870
871 conflict_behavior_choices = ("ask", "skip", "quit")
872
4f5cf76a 873 def __init__(self):
b984733c 874 Command.__init__(self)
3ea2cfd4 875 P4UserMap.__init__(self)
4f5cf76a 876 self.options = [
4f5cf76a 877 optparse.make_option("--origin", dest="origin"),
ae901090 878 optparse.make_option("-M", dest="detectRenames", action="store_true"),
3ea2cfd4
LD
879 # preserve the user, requires relevant p4 permissions
880 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
06804c76 881 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
ef739f08 882 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
728b7ad8 883 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
6bbfd137
PW
884 optparse.make_option("--conflict", dest="conflict_behavior",
885 choices=self.conflict_behavior_choices)
4f5cf76a
SH
886 ]
887 self.description = "Submit changes from git to the perforce depot."
c9b50e63 888 self.usage += " [name of git branch to submit into perforce depot]"
9512497b 889 self.origin = ""
ae901090 890 self.detectRenames = False
3ea2cfd4 891 self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
ef739f08 892 self.dry_run = False
728b7ad8 893 self.prepare_p4_only = False
6bbfd137 894 self.conflict_behavior = None
f7baba8b 895 self.isWindows = (platform.system() == "Windows")
06804c76 896 self.exportLabels = False
8e9497c2 897 self.p4HasMoveCommand = p4_has_command("move")
4f5cf76a 898
4f5cf76a
SH
899 def check(self):
900 if len(p4CmdList("opened ...")) > 0:
901 die("You have files opened with perforce! Close them before starting the sync.")
902
f19cb0a0
PW
903 def separate_jobs_from_description(self, message):
904 """Extract and return a possible Jobs field in the commit
905 message. It goes into a separate section in the p4 change
906 specification.
907
908 A jobs line starts with "Jobs:" and looks like a new field
909 in a form. Values are white-space separated on the same
910 line or on following lines that start with a tab.
911
912 This does not parse and extract the full git commit message
913 like a p4 form. It just sees the Jobs: line as a marker
914 to pass everything from then on directly into the p4 form,
915 but outside the description section.
916
917 Return a tuple (stripped log message, jobs string)."""
918
919 m = re.search(r'^Jobs:', message, re.MULTILINE)
920 if m is None:
921 return (message, None)
922
923 jobtext = message[m.start():]
924 stripped_message = message[:m.start()].rstrip()
925 return (stripped_message, jobtext)
926
927 def prepareLogMessage(self, template, message, jobs):
928 """Edits the template returned from "p4 change -o" to insert
929 the message in the Description field, and the jobs text in
930 the Jobs field."""
4f5cf76a
SH
931 result = ""
932
edae1e2f
SH
933 inDescriptionSection = False
934
4f5cf76a
SH
935 for line in template.split("\n"):
936 if line.startswith("#"):
937 result += line + "\n"
938 continue
939
edae1e2f 940 if inDescriptionSection:
c9dbab04 941 if line.startswith("Files:") or line.startswith("Jobs:"):
edae1e2f 942 inDescriptionSection = False
f19cb0a0
PW
943 # insert Jobs section
944 if jobs:
945 result += jobs + "\n"
edae1e2f
SH
946 else:
947 continue
948 else:
949 if line.startswith("Description:"):
950 inDescriptionSection = True
951 line += "\n"
952 for messageLine in message.split("\n"):
953 line += "\t" + messageLine + "\n"
954
955 result += line + "\n"
4f5cf76a
SH
956
957 return result
958
60df071c
LD
959 def patchRCSKeywords(self, file, pattern):
960 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
961 (handle, outFileName) = tempfile.mkstemp(dir='.')
962 try:
963 outFile = os.fdopen(handle, "w+")
964 inFile = open(file, "r")
965 regexp = re.compile(pattern, re.VERBOSE)
966 for line in inFile.readlines():
967 line = regexp.sub(r'$\1$', line)
968 outFile.write(line)
969 inFile.close()
970 outFile.close()
971 # Forcibly overwrite the original file
972 os.unlink(file)
973 shutil.move(outFileName, file)
974 except:
975 # cleanup our temporary file
976 os.unlink(outFileName)
977 print "Failed to strip RCS keywords in %s" % file
978 raise
979
980 print "Patched up RCS keywords in %s" % file
981
3ea2cfd4
LD
982 def p4UserForCommit(self,id):
983 # Return the tuple (perforce user,git email) for a given git commit id
984 self.getUserMapFromPerforceServer()
985 gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
986 gitEmail = gitEmail.strip()
987 if not self.emails.has_key(gitEmail):
988 return (None,gitEmail)
989 else:
990 return (self.emails[gitEmail],gitEmail)
991
992 def checkValidP4Users(self,commits):
993 # check if any git authors cannot be mapped to p4 users
994 for id in commits:
995 (user,email) = self.p4UserForCommit(id)
996 if not user:
997 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
998 if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
999 print "%s" % msg
1000 else:
1001 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1002
1003 def lastP4Changelist(self):
1004 # Get back the last changelist number submitted in this client spec. This
1005 # then gets used to patch up the username in the change. If the same
1006 # client spec is being used by multiple processes then this might go
1007 # wrong.
1008 results = p4CmdList("client -o") # find the current client
1009 client = None
1010 for r in results:
1011 if r.has_key('Client'):
1012 client = r['Client']
1013 break
1014 if not client:
1015 die("could not get client spec")
6de040df 1016 results = p4CmdList(["changes", "-c", client, "-m", "1"])
3ea2cfd4
LD
1017 for r in results:
1018 if r.has_key('change'):
1019 return r['change']
1020 die("Could not get changelist number for last submit - cannot patch up user details")
1021
1022 def modifyChangelistUser(self, changelist, newUser):
1023 # fixup the user field of a changelist after it has been submitted.
1024 changes = p4CmdList("change -o %s" % changelist)
ecdba36d
LD
1025 if len(changes) != 1:
1026 die("Bad output from p4 change modifying %s to user %s" %
1027 (changelist, newUser))
1028
1029 c = changes[0]
1030 if c['User'] == newUser: return # nothing to do
1031 c['User'] = newUser
1032 input = marshal.dumps(c)
1033
3ea2cfd4
LD
1034 result = p4CmdList("change -f -i", stdin=input)
1035 for r in result:
1036 if r.has_key('code'):
1037 if r['code'] == 'error':
1038 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1039 if r.has_key('data'):
1040 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1041 return
1042 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1043
1044 def canChangeChangelists(self):
1045 # check to see if we have p4 admin or super-user permissions, either of
1046 # which are required to modify changelists.
52a4880b 1047 results = p4CmdList(["protects", self.depotPath])
3ea2cfd4
LD
1048 for r in results:
1049 if r.has_key('perm'):
1050 if r['perm'] == 'admin':
1051 return 1
1052 if r['perm'] == 'super':
1053 return 1
1054 return 0
1055
ea99c3ae 1056 def prepareSubmitTemplate(self):
f19cb0a0
PW
1057 """Run "p4 change -o" to grab a change specification template.
1058 This does not use "p4 -G", as it is nice to keep the submission
1059 template in original order, since a human might edit it.
1060
1061 Remove lines in the Files section that show changes to files
1062 outside the depot path we're committing into."""
1063
ea99c3ae
SH
1064 template = ""
1065 inFilesSection = False
6de040df 1066 for line in p4_read_pipe_lines(['change', '-o']):
f3e5ae4f
MSO
1067 if line.endswith("\r\n"):
1068 line = line[:-2] + "\n"
ea99c3ae
SH
1069 if inFilesSection:
1070 if line.startswith("\t"):
1071 # path starts and ends with a tab
1072 path = line[1:]
1073 lastTab = path.rfind("\t")
1074 if lastTab != -1:
1075 path = path[:lastTab]
d53de8b9 1076 if not p4PathStartsWith(path, self.depotPath):
ea99c3ae
SH
1077 continue
1078 else:
1079 inFilesSection = False
1080 else:
1081 if line.startswith("Files:"):
1082 inFilesSection = True
1083
1084 template += line
1085
1086 return template
1087
7c766e57
PW
1088 def edit_template(self, template_file):
1089 """Invoke the editor to let the user change the submission
1090 message. Return true if okay to continue with the submit."""
1091
1092 # if configured to skip the editing part, just submit
1093 if gitConfig("git-p4.skipSubmitEdit") == "true":
1094 return True
1095
1096 # look at the modification time, to check later if the user saved
1097 # the file
1098 mtime = os.stat(template_file).st_mtime
1099
1100 # invoke the editor
f95ceaf0 1101 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
7c766e57
PW
1102 editor = os.environ.get("P4EDITOR")
1103 else:
1104 editor = read_pipe("git var GIT_EDITOR").strip()
1105 system(editor + " " + template_file)
1106
1107 # If the file was not saved, prompt to see if this patch should
1108 # be skipped. But skip this verification step if configured so.
1109 if gitConfig("git-p4.skipSubmitEditCheck") == "true":
1110 return True
1111
d1652049
PW
1112 # modification time updated means user saved the file
1113 if os.stat(template_file).st_mtime > mtime:
1114 return True
1115
1116 while True:
1117 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1118 if response == 'y':
1119 return True
1120 if response == 'n':
1121 return False
7c766e57 1122
7cb5cbef 1123 def applyCommit(self, id):
67b0fe2e
PW
1124 """Apply one commit, return True if it succeeded."""
1125
1126 print "Applying", read_pipe(["git", "show", "-s",
1127 "--format=format:%h %s", id])
ae901090 1128
848de9c3 1129 (p4User, gitEmail) = self.p4UserForCommit(id)
3ea2cfd4 1130
84cb0003 1131 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
4f5cf76a
SH
1132 filesToAdd = set()
1133 filesToDelete = set()
d336c158 1134 editedFiles = set()
b6ad6dcc 1135 pureRenameCopy = set()
c65b670e 1136 filesToChangeExecBit = {}
60df071c 1137
4f5cf76a 1138 for line in diff:
b43b0a3c
CP
1139 diff = parseDiffTreeEntry(line)
1140 modifier = diff['status']
1141 path = diff['src']
4f5cf76a 1142 if modifier == "M":
6de040df 1143 p4_edit(path)
c65b670e
CP
1144 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1145 filesToChangeExecBit[path] = diff['dst_mode']
d336c158 1146 editedFiles.add(path)
4f5cf76a
SH
1147 elif modifier == "A":
1148 filesToAdd.add(path)
c65b670e 1149 filesToChangeExecBit[path] = diff['dst_mode']
4f5cf76a
SH
1150 if path in filesToDelete:
1151 filesToDelete.remove(path)
1152 elif modifier == "D":
1153 filesToDelete.add(path)
1154 if path in filesToAdd:
1155 filesToAdd.remove(path)
4fddb41b
VA
1156 elif modifier == "C":
1157 src, dest = diff['src'], diff['dst']
6de040df 1158 p4_integrate(src, dest)
b6ad6dcc 1159 pureRenameCopy.add(dest)
4fddb41b 1160 if diff['src_sha1'] != diff['dst_sha1']:
6de040df 1161 p4_edit(dest)
b6ad6dcc 1162 pureRenameCopy.discard(dest)
4fddb41b 1163 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
6de040df 1164 p4_edit(dest)
b6ad6dcc 1165 pureRenameCopy.discard(dest)
4fddb41b
VA
1166 filesToChangeExecBit[dest] = diff['dst_mode']
1167 os.unlink(dest)
1168 editedFiles.add(dest)
d9a5f25b 1169 elif modifier == "R":
b43b0a3c 1170 src, dest = diff['src'], diff['dst']
8e9497c2
GG
1171 if self.p4HasMoveCommand:
1172 p4_edit(src) # src must be open before move
1173 p4_move(src, dest) # opens for (move/delete, move/add)
b6ad6dcc 1174 else:
8e9497c2
GG
1175 p4_integrate(src, dest)
1176 if diff['src_sha1'] != diff['dst_sha1']:
1177 p4_edit(dest)
1178 else:
1179 pureRenameCopy.add(dest)
c65b670e 1180 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
8e9497c2
GG
1181 if not self.p4HasMoveCommand:
1182 p4_edit(dest) # with move: already open, writable
c65b670e 1183 filesToChangeExecBit[dest] = diff['dst_mode']
8e9497c2
GG
1184 if not self.p4HasMoveCommand:
1185 os.unlink(dest)
1186 filesToDelete.add(src)
d9a5f25b 1187 editedFiles.add(dest)
4f5cf76a
SH
1188 else:
1189 die("unknown modifier %s for %s" % (modifier, path))
1190
0e36f2d7 1191 diffcmd = "git format-patch -k --stdout \"%s^\"..\"%s\"" % (id, id)
47a130b7 1192 patchcmd = diffcmd + " | git apply "
c1b296b9
SH
1193 tryPatchCmd = patchcmd + "--check -"
1194 applyPatchCmd = patchcmd + "--check --apply -"
60df071c 1195 patch_succeeded = True
51a2640a 1196
47a130b7 1197 if os.system(tryPatchCmd) != 0:
60df071c
LD
1198 fixed_rcs_keywords = False
1199 patch_succeeded = False
51a2640a 1200 print "Unfortunately applying the change failed!"
60df071c
LD
1201
1202 # Patch failed, maybe it's just RCS keyword woes. Look through
1203 # the patch to see if that's possible.
1204 if gitConfig("git-p4.attemptRCSCleanup","--bool") == "true":
1205 file = None
1206 pattern = None
1207 kwfiles = {}
1208 for file in editedFiles | filesToDelete:
1209 # did this file's delta contain RCS keywords?
1210 pattern = p4_keywords_regexp_for_file(file)
1211
1212 if pattern:
1213 # this file is a possibility...look for RCS keywords.
1214 regexp = re.compile(pattern, re.VERBOSE)
1215 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1216 if regexp.search(line):
1217 if verbose:
1218 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1219 kwfiles[file] = pattern
1220 break
1221
1222 for file in kwfiles:
1223 if verbose:
1224 print "zapping %s with %s" % (line,pattern)
1225 self.patchRCSKeywords(file, kwfiles[file])
1226 fixed_rcs_keywords = True
1227
1228 if fixed_rcs_keywords:
1229 print "Retrying the patch with RCS keywords cleaned up"
1230 if os.system(tryPatchCmd) == 0:
1231 patch_succeeded = True
1232
1233 if not patch_succeeded:
7e5dd9f2
PW
1234 for f in editedFiles:
1235 p4_revert(f)
7e5dd9f2 1236 return False
51a2640a 1237
55ac2ed6
PW
1238 #
1239 # Apply the patch for real, and do add/delete/+x handling.
1240 #
47a130b7 1241 system(applyPatchCmd)
4f5cf76a
SH
1242
1243 for f in filesToAdd:
6de040df 1244 p4_add(f)
4f5cf76a 1245 for f in filesToDelete:
6de040df
LD
1246 p4_revert(f)
1247 p4_delete(f)
4f5cf76a 1248
c65b670e
CP
1249 # Set/clear executable bits
1250 for f in filesToChangeExecBit.keys():
1251 mode = filesToChangeExecBit[f]
1252 setP4ExecBit(f, mode)
1253
55ac2ed6
PW
1254 #
1255 # Build p4 change description, starting with the contents
1256 # of the git commit message.
1257 #
0e36f2d7 1258 logMessage = extractLogMessageFromGitCommit(id)
0e36f2d7 1259 logMessage = logMessage.strip()
f19cb0a0 1260 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
4f5cf76a 1261
ea99c3ae 1262 template = self.prepareSubmitTemplate()
f19cb0a0 1263 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
ecdba36d 1264
c47178d4 1265 if self.preserveUser:
55ac2ed6 1266 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
c47178d4 1267
55ac2ed6
PW
1268 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1269 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1270 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1271 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
c47178d4 1272
55ac2ed6
PW
1273 separatorLine = "######## everything below this line is just the diff #######\n"
1274
1275 # diff
c47178d4
PW
1276 if os.environ.has_key("P4DIFF"):
1277 del(os.environ["P4DIFF"])
1278 diff = ""
1279 for editedFile in editedFiles:
1280 diff += p4_read_pipe(['diff', '-du',
1281 wildcard_encode(editedFile)])
1282
55ac2ed6 1283 # new file diff
c47178d4
PW
1284 newdiff = ""
1285 for newFile in filesToAdd:
1286 newdiff += "==== new file ====\n"
1287 newdiff += "--- /dev/null\n"
1288 newdiff += "+++ %s\n" % newFile
1289 f = open(newFile, "r")
1290 for line in f.readlines():
1291 newdiff += "+" + line
1292 f.close()
1293
55ac2ed6 1294 # change description file: submitTemplate, separatorLine, diff, newdiff
c47178d4
PW
1295 (handle, fileName) = tempfile.mkstemp()
1296 tmpFile = os.fdopen(handle, "w+")
1297 if self.isWindows:
1298 submitTemplate = submitTemplate.replace("\n", "\r\n")
1299 separatorLine = separatorLine.replace("\n", "\r\n")
1300 newdiff = newdiff.replace("\n", "\r\n")
1301 tmpFile.write(submitTemplate + separatorLine + diff + newdiff)
1302 tmpFile.close()
1303
728b7ad8
PW
1304 if self.prepare_p4_only:
1305 #
1306 # Leave the p4 tree prepared, and the submit template around
1307 # and let the user decide what to do next
1308 #
1309 print
1310 print "P4 workspace prepared for submission."
1311 print "To submit or revert, go to client workspace"
1312 print " " + self.clientPath
1313 print
1314 print "To submit, use \"p4 submit\" to write a new description,"
1315 print "or \"p4 submit -i %s\" to use the one prepared by" \
1316 " \"git p4\"." % fileName
1317 print "You can delete the file \"%s\" when finished." % fileName
1318
1319 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1320 print "To preserve change ownership by user %s, you must\n" \
1321 "do \"p4 change -f <change>\" after submitting and\n" \
1322 "edit the User field."
1323 if pureRenameCopy:
1324 print "After submitting, renamed files must be re-synced."
1325 print "Invoke \"p4 sync -f\" on each of these files:"
1326 for f in pureRenameCopy:
1327 print " " + f
1328
1329 print
1330 print "To revert the changes, use \"p4 revert ...\", and delete"
1331 print "the submit template file \"%s\"" % fileName
1332 if filesToAdd:
1333 print "Since the commit adds new files, they must be deleted:"
1334 for f in filesToAdd:
1335 print " " + f
1336 print
1337 return True
1338
55ac2ed6
PW
1339 #
1340 # Let the user edit the change description, then submit it.
1341 #
c47178d4
PW
1342 if self.edit_template(fileName):
1343 # read the edited message and submit
5a41c16a 1344 ret = True
c47178d4
PW
1345 tmpFile = open(fileName, "rb")
1346 message = tmpFile.read()
e96e400f 1347 tmpFile.close()
c47178d4
PW
1348 submitTemplate = message[:message.index(separatorLine)]
1349 if self.isWindows:
1350 submitTemplate = submitTemplate.replace("\r\n", "\n")
1351 p4_write_pipe(['submit', '-i'], submitTemplate)
e96e400f 1352
c47178d4
PW
1353 if self.preserveUser:
1354 if p4User:
1355 # Get last changelist number. Cannot easily get it from
1356 # the submit command output as the output is
1357 # unmarshalled.
1358 changelist = self.lastP4Changelist()
1359 self.modifyChangelistUser(changelist, p4User)
1360
1361 # The rename/copy happened by applying a patch that created a
1362 # new file. This leaves it writable, which confuses p4.
1363 for f in pureRenameCopy:
1364 p4_sync(f, "-f")
cdc7e388 1365
4f5cf76a 1366 else:
c47178d4 1367 # skip this patch
5a41c16a 1368 ret = False
c47178d4
PW
1369 print "Submission cancelled, undoing p4 changes."
1370 for f in editedFiles:
1371 p4_revert(f)
1372 for f in filesToAdd:
1373 p4_revert(f)
1374 os.remove(f)
df9c5453
PW
1375 for f in filesToDelete:
1376 p4_revert(f)
c47178d4
PW
1377
1378 os.remove(fileName)
5a41c16a 1379 return ret
4f5cf76a 1380
06804c76
LD
1381 # Export git tags as p4 labels. Create a p4 label and then tag
1382 # with that.
1383 def exportGitTags(self, gitTags):
c8942a22
LD
1384 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1385 if len(validLabelRegexp) == 0:
1386 validLabelRegexp = defaultLabelRegexp
1387 m = re.compile(validLabelRegexp)
06804c76
LD
1388
1389 for name in gitTags:
1390
1391 if not m.match(name):
1392 if verbose:
05a3cec5 1393 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
06804c76
LD
1394 continue
1395
1396 # Get the p4 commit this corresponds to
c8942a22
LD
1397 logMessage = extractLogMessageFromGitCommit(name)
1398 values = extractSettingsGitLog(logMessage)
06804c76 1399
c8942a22 1400 if not values.has_key('change'):
06804c76
LD
1401 # a tag pointing to something not sent to p4; ignore
1402 if verbose:
1403 print "git tag %s does not give a p4 commit" % name
1404 continue
c8942a22
LD
1405 else:
1406 changelist = values['change']
06804c76
LD
1407
1408 # Get the tag details.
1409 inHeader = True
1410 isAnnotated = False
1411 body = []
1412 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1413 l = l.strip()
1414 if inHeader:
1415 if re.match(r'tag\s+', l):
1416 isAnnotated = True
1417 elif re.match(r'\s*$', l):
1418 inHeader = False
1419 continue
1420 else:
1421 body.append(l)
1422
1423 if not isAnnotated:
1424 body = ["lightweight tag imported by git p4\n"]
1425
1426 # Create the label - use the same view as the client spec we are using
1427 clientSpec = getClientSpec()
1428
1429 labelTemplate = "Label: %s\n" % name
1430 labelTemplate += "Description:\n"
1431 for b in body:
1432 labelTemplate += "\t" + b + "\n"
1433 labelTemplate += "View:\n"
1434 for mapping in clientSpec.mappings:
1435 labelTemplate += "\t%s\n" % mapping.depot_side.path
1436
ef739f08
PW
1437 if self.dry_run:
1438 print "Would create p4 label %s for tag" % name
728b7ad8
PW
1439 elif self.prepare_p4_only:
1440 print "Not creating p4 label %s for tag due to option" \
1441 " --prepare-p4-only" % name
ef739f08
PW
1442 else:
1443 p4_write_pipe(["label", "-i"], labelTemplate)
06804c76 1444
ef739f08
PW
1445 # Use the label
1446 p4_system(["tag", "-l", name] +
1447 ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
06804c76 1448
ef739f08
PW
1449 if verbose:
1450 print "created p4 label for tag %s" % name
06804c76 1451
4f5cf76a 1452 def run(self, args):
c9b50e63
SH
1453 if len(args) == 0:
1454 self.master = currentGitBranch()
4280e533 1455 if len(self.master) == 0 or not gitBranchExists("refs/heads/%s" % self.master):
c9b50e63
SH
1456 die("Detecting current git branch failed!")
1457 elif len(args) == 1:
1458 self.master = args[0]
28755dba
PW
1459 if not branchExists(self.master):
1460 die("Branch %s does not exist" % self.master)
c9b50e63
SH
1461 else:
1462 return False
1463
4c2d5d72
JX
1464 allowSubmit = gitConfig("git-p4.allowSubmit")
1465 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1466 die("%s is not in git-p4.allowSubmit" % self.master)
1467
27d2d811 1468 [upstream, settings] = findUpstreamBranchPoint()
ea99c3ae 1469 self.depotPath = settings['depot-paths'][0]
27d2d811
SH
1470 if len(self.origin) == 0:
1471 self.origin = upstream
a3fdd579 1472
3ea2cfd4
LD
1473 if self.preserveUser:
1474 if not self.canChangeChangelists():
1475 die("Cannot preserve user names without p4 super-user or admin permissions")
1476
6bbfd137
PW
1477 # if not set from the command line, try the config file
1478 if self.conflict_behavior is None:
1479 val = gitConfig("git-p4.conflict")
1480 if val:
1481 if val not in self.conflict_behavior_choices:
1482 die("Invalid value '%s' for config git-p4.conflict" % val)
1483 else:
1484 val = "ask"
1485 self.conflict_behavior = val
1486
a3fdd579
SH
1487 if self.verbose:
1488 print "Origin branch is " + self.origin
9512497b 1489
ea99c3ae 1490 if len(self.depotPath) == 0:
9512497b
SH
1491 print "Internal error: cannot locate perforce depot path from existing branches"
1492 sys.exit(128)
1493
543987bd
PW
1494 self.useClientSpec = False
1495 if gitConfig("git-p4.useclientspec", "--bool") == "true":
1496 self.useClientSpec = True
1497 if self.useClientSpec:
1498 self.clientSpecDirs = getClientSpec()
9512497b 1499
543987bd
PW
1500 if self.useClientSpec:
1501 # all files are relative to the client spec
1502 self.clientPath = getClientRoot()
1503 else:
1504 self.clientPath = p4Where(self.depotPath)
9512497b 1505
543987bd
PW
1506 if self.clientPath == "":
1507 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
9512497b 1508
ea99c3ae 1509 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
7944f142 1510 self.oldWorkingDirectory = os.getcwd()
c1b296b9 1511
0591cfa8 1512 # ensure the clientPath exists
8d7ec362 1513 new_client_dir = False
0591cfa8 1514 if not os.path.exists(self.clientPath):
8d7ec362 1515 new_client_dir = True
0591cfa8
GG
1516 os.makedirs(self.clientPath)
1517
053fd0c1 1518 chdir(self.clientPath)
ef739f08
PW
1519 if self.dry_run:
1520 print "Would synchronize p4 checkout in %s" % self.clientPath
8d7ec362 1521 else:
ef739f08
PW
1522 print "Synchronizing p4 checkout..."
1523 if new_client_dir:
1524 # old one was destroyed, and maybe nobody told p4
1525 p4_sync("...", "-f")
1526 else:
1527 p4_sync("...")
4f5cf76a 1528 self.check()
4f5cf76a 1529
4c750c0d
SH
1530 commits = []
1531 for line in read_pipe_lines("git rev-list --no-merges %s..%s" % (self.origin, self.master)):
1532 commits.append(line.strip())
1533 commits.reverse()
4f5cf76a 1534
848de9c3
LD
1535 if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"):
1536 self.checkAuthorship = False
1537 else:
1538 self.checkAuthorship = True
1539
3ea2cfd4
LD
1540 if self.preserveUser:
1541 self.checkValidP4Users(commits)
1542
84cb0003
GG
1543 #
1544 # Build up a set of options to be passed to diff when
1545 # submitting each commit to p4.
1546 #
1547 if self.detectRenames:
1548 # command-line -M arg
1549 self.diffOpts = "-M"
1550 else:
1551 # If not explicitly set check the config variable
1552 detectRenames = gitConfig("git-p4.detectRenames")
1553
1554 if detectRenames.lower() == "false" or detectRenames == "":
1555 self.diffOpts = ""
1556 elif detectRenames.lower() == "true":
1557 self.diffOpts = "-M"
1558 else:
1559 self.diffOpts = "-M%s" % detectRenames
1560
1561 # no command-line arg for -C or --find-copies-harder, just
1562 # config variables
1563 detectCopies = gitConfig("git-p4.detectCopies")
1564 if detectCopies.lower() == "false" or detectCopies == "":
1565 pass
1566 elif detectCopies.lower() == "true":
1567 self.diffOpts += " -C"
1568 else:
1569 self.diffOpts += " -C%s" % detectCopies
1570
1571 if gitConfig("git-p4.detectCopiesHarder", "--bool") == "true":
1572 self.diffOpts += " --find-copies-harder"
1573
7e5dd9f2
PW
1574 #
1575 # Apply the commits, one at a time. On failure, ask if should
1576 # continue to try the rest of the patches, or quit.
1577 #
ef739f08
PW
1578 if self.dry_run:
1579 print "Would apply"
67b0fe2e 1580 applied = []
7e5dd9f2
PW
1581 last = len(commits) - 1
1582 for i, commit in enumerate(commits):
ef739f08
PW
1583 if self.dry_run:
1584 print " ", read_pipe(["git", "show", "-s",
1585 "--format=format:%h %s", commit])
1586 ok = True
1587 else:
1588 ok = self.applyCommit(commit)
67b0fe2e
PW
1589 if ok:
1590 applied.append(commit)
7e5dd9f2 1591 else:
728b7ad8
PW
1592 if self.prepare_p4_only and i < last:
1593 print "Processing only the first commit due to option" \
1594 " --prepare-p4-only"
1595 break
7e5dd9f2
PW
1596 if i < last:
1597 quit = False
1598 while True:
6bbfd137
PW
1599 # prompt for what to do, or use the option/variable
1600 if self.conflict_behavior == "ask":
1601 print "What do you want to do?"
1602 response = raw_input("[s]kip this commit but apply"
1603 " the rest, or [q]uit? ")
1604 if not response:
1605 continue
1606 elif self.conflict_behavior == "skip":
1607 response = "s"
1608 elif self.conflict_behavior == "quit":
1609 response = "q"
1610 else:
1611 die("Unknown conflict_behavior '%s'" %
1612 self.conflict_behavior)
1613
7e5dd9f2
PW
1614 if response[0] == "s":
1615 print "Skipping this commit, but applying the rest"
1616 break
1617 if response[0] == "q":
1618 print "Quitting"
1619 quit = True
1620 break
1621 if quit:
1622 break
4f5cf76a 1623
67b0fe2e 1624 chdir(self.oldWorkingDirectory)
4f5cf76a 1625
ef739f08
PW
1626 if self.dry_run:
1627 pass
728b7ad8
PW
1628 elif self.prepare_p4_only:
1629 pass
ef739f08 1630 elif len(commits) == len(applied):
67b0fe2e 1631 print "All commits applied!"
14594f4b 1632
4c750c0d
SH
1633 sync = P4Sync()
1634 sync.run([])
14594f4b 1635
4c750c0d
SH
1636 rebase = P4Rebase()
1637 rebase.rebase()
4f5cf76a 1638
67b0fe2e
PW
1639 else:
1640 if len(applied) == 0:
1641 print "No commits applied."
1642 else:
1643 print "Applied only the commits marked with '*':"
1644 for c in commits:
1645 if c in applied:
1646 star = "*"
1647 else:
1648 star = " "
1649 print star, read_pipe(["git", "show", "-s",
1650 "--format=format:%h %s", c])
1651 print "You will have to do 'git p4 sync' and rebase."
1652
06804c76 1653 if gitConfig("git-p4.exportLabels", "--bool") == "true":
06dcd152 1654 self.exportLabels = True
06804c76
LD
1655
1656 if self.exportLabels:
1657 p4Labels = getP4Labels(self.depotPath)
1658 gitTags = getGitTags()
1659
1660 missingGitTags = gitTags - p4Labels
1661 self.exportGitTags(missingGitTags)
1662
67b0fe2e
PW
1663 # exit with error unless everything applied perfecly
1664 if len(commits) != len(applied):
1665 sys.exit(1)
1666
b984733c
SH
1667 return True
1668
ecb7cf98
PW
1669class View(object):
1670 """Represent a p4 view ("p4 help views"), and map files in a
1671 repo according to the view."""
1672
1673 class Path(object):
1674 """A depot or client path, possibly containing wildcards.
1675 The only one supported is ... at the end, currently.
1676 Initialize with the full path, with //depot or //client."""
1677
1678 def __init__(self, path, is_depot):
1679 self.path = path
1680 self.is_depot = is_depot
1681 self.find_wildcards()
1682 # remember the prefix bit, useful for relative mappings
1683 m = re.match("(//[^/]+/)", self.path)
1684 if not m:
1685 die("Path %s does not start with //prefix/" % self.path)
1686 prefix = m.group(1)
1687 if not self.is_depot:
1688 # strip //client/ on client paths
1689 self.path = self.path[len(prefix):]
1690
1691 def find_wildcards(self):
1692 """Make sure wildcards are valid, and set up internal
1693 variables."""
1694
1695 self.ends_triple_dot = False
1696 # There are three wildcards allowed in p4 views
1697 # (see "p4 help views"). This code knows how to
1698 # handle "..." (only at the end), but cannot deal with
1699 # "%%n" or "*". Only check the depot_side, as p4 should
1700 # validate that the client_side matches too.
1701 if re.search(r'%%[1-9]', self.path):
1702 die("Can't handle %%n wildcards in view: %s" % self.path)
1703 if self.path.find("*") >= 0:
1704 die("Can't handle * wildcards in view: %s" % self.path)
1705 triple_dot_index = self.path.find("...")
1706 if triple_dot_index >= 0:
43b82bd9
PW
1707 if triple_dot_index != len(self.path) - 3:
1708 die("Can handle only single ... wildcard, at end: %s" %
ecb7cf98
PW
1709 self.path)
1710 self.ends_triple_dot = True
1711
1712 def ensure_compatible(self, other_path):
1713 """Make sure the wildcards agree."""
1714 if self.ends_triple_dot != other_path.ends_triple_dot:
1715 die("Both paths must end with ... if either does;\n" +
1716 "paths: %s %s" % (self.path, other_path.path))
1717
1718 def match_wildcards(self, test_path):
1719 """See if this test_path matches us, and fill in the value
1720 of the wildcards if so. Returns a tuple of
1721 (True|False, wildcards[]). For now, only the ... at end
1722 is supported, so at most one wildcard."""
1723 if self.ends_triple_dot:
1724 dotless = self.path[:-3]
1725 if test_path.startswith(dotless):
1726 wildcard = test_path[len(dotless):]
1727 return (True, [ wildcard ])
1728 else:
1729 if test_path == self.path:
1730 return (True, [])
1731 return (False, [])
1732
1733 def match(self, test_path):
1734 """Just return if it matches; don't bother with the wildcards."""
1735 b, _ = self.match_wildcards(test_path)
1736 return b
1737
1738 def fill_in_wildcards(self, wildcards):
1739 """Return the relative path, with the wildcards filled in
1740 if there are any."""
1741 if self.ends_triple_dot:
1742 return self.path[:-3] + wildcards[0]
1743 else:
1744 return self.path
1745
1746 class Mapping(object):
1747 def __init__(self, depot_side, client_side, overlay, exclude):
1748 # depot_side is without the trailing /... if it had one
1749 self.depot_side = View.Path(depot_side, is_depot=True)
1750 self.client_side = View.Path(client_side, is_depot=False)
1751 self.overlay = overlay # started with "+"
1752 self.exclude = exclude # started with "-"
1753 assert not (self.overlay and self.exclude)
1754 self.depot_side.ensure_compatible(self.client_side)
1755
1756 def __str__(self):
1757 c = " "
1758 if self.overlay:
1759 c = "+"
1760 if self.exclude:
1761 c = "-"
1762 return "View.Mapping: %s%s -> %s" % \
329afb8e 1763 (c, self.depot_side.path, self.client_side.path)
ecb7cf98
PW
1764
1765 def map_depot_to_client(self, depot_path):
1766 """Calculate the client path if using this mapping on the
1767 given depot path; does not consider the effect of other
1768 mappings in a view. Even excluded mappings are returned."""
1769 matches, wildcards = self.depot_side.match_wildcards(depot_path)
1770 if not matches:
1771 return ""
1772 client_path = self.client_side.fill_in_wildcards(wildcards)
1773 return client_path
1774
1775 #
1776 # View methods
1777 #
1778 def __init__(self):
1779 self.mappings = []
1780
1781 def append(self, view_line):
1782 """Parse a view line, splitting it into depot and client
1783 sides. Append to self.mappings, preserving order."""
1784
1785 # Split the view line into exactly two words. P4 enforces
1786 # structure on these lines that simplifies this quite a bit.
1787 #
1788 # Either or both words may be double-quoted.
1789 # Single quotes do not matter.
1790 # Double-quote marks cannot occur inside the words.
1791 # A + or - prefix is also inside the quotes.
1792 # There are no quotes unless they contain a space.
1793 # The line is already white-space stripped.
1794 # The two words are separated by a single space.
1795 #
1796 if view_line[0] == '"':
1797 # First word is double quoted. Find its end.
1798 close_quote_index = view_line.find('"', 1)
1799 if close_quote_index <= 0:
1800 die("No first-word closing quote found: %s" % view_line)
1801 depot_side = view_line[1:close_quote_index]
1802 # skip closing quote and space
1803 rhs_index = close_quote_index + 1 + 1
1804 else:
1805 space_index = view_line.find(" ")
1806 if space_index <= 0:
1807 die("No word-splitting space found: %s" % view_line)
1808 depot_side = view_line[0:space_index]
1809 rhs_index = space_index + 1
1810
1811 if view_line[rhs_index] == '"':
1812 # Second word is double quoted. Make sure there is a
1813 # double quote at the end too.
1814 if not view_line.endswith('"'):
1815 die("View line with rhs quote should end with one: %s" %
1816 view_line)
1817 # skip the quotes
1818 client_side = view_line[rhs_index+1:-1]
1819 else:
1820 client_side = view_line[rhs_index:]
1821
1822 # prefix + means overlay on previous mapping
1823 overlay = False
1824 if depot_side.startswith("+"):
1825 overlay = True
1826 depot_side = depot_side[1:]
1827
1828 # prefix - means exclude this path
1829 exclude = False
1830 if depot_side.startswith("-"):
1831 exclude = True
1832 depot_side = depot_side[1:]
1833
1834 m = View.Mapping(depot_side, client_side, overlay, exclude)
1835 self.mappings.append(m)
1836
1837 def map_in_client(self, depot_path):
1838 """Return the relative location in the client where this
1839 depot file should live. Returns "" if the file should
1840 not be mapped in the client."""
1841
1842 paths_filled = []
1843 client_path = ""
1844
1845 # look at later entries first
1846 for m in self.mappings[::-1]:
1847
1848 # see where will this path end up in the client
1849 p = m.map_depot_to_client(depot_path)
1850
1851 if p == "":
1852 # Depot path does not belong in client. Must remember
1853 # this, as previous items should not cause files to
1854 # exist in this path either. Remember that the list is
1855 # being walked from the end, which has higher precedence.
1856 # Overlap mappings do not exclude previous mappings.
1857 if not m.overlay:
1858 paths_filled.append(m.client_side)
1859
1860 else:
1861 # This mapping matched; no need to search any further.
1862 # But, the mapping could be rejected if the client path
6ee9a999
PW
1863 # has already been claimed by an earlier mapping (i.e.
1864 # one later in the list, which we are walking backwards).
ecb7cf98
PW
1865 already_mapped_in_client = False
1866 for f in paths_filled:
1867 # this is View.Path.match
1868 if f.match(p):
1869 already_mapped_in_client = True
1870 break
1871 if not already_mapped_in_client:
1872 # Include this file, unless it is from a line that
1873 # explicitly said to exclude it.
1874 if not m.exclude:
1875 client_path = p
1876
1877 # a match, even if rejected, always stops the search
1878 break
1879
1880 return client_path
1881
3ea2cfd4 1882class P4Sync(Command, P4UserMap):
56c09345
PW
1883 delete_actions = ( "delete", "move/delete", "purge" )
1884
b984733c
SH
1885 def __init__(self):
1886 Command.__init__(self)
3ea2cfd4 1887 P4UserMap.__init__(self)
b984733c
SH
1888 self.options = [
1889 optparse.make_option("--branch", dest="branch"),
1890 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
1891 optparse.make_option("--changesfile", dest="changesFile"),
1892 optparse.make_option("--silent", dest="silent", action="store_true"),
ef48f909 1893 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
06804c76 1894 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
d2c6dd30
HWN
1895 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
1896 help="Import into refs/heads/ , not refs/remotes"),
8b41a97f 1897 optparse.make_option("--max-changes", dest="maxChanges"),
86dff6b6 1898 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
3a70cdfa
TAL
1899 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
1900 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
1901 help="Only sync files that are included in the Perforce Client Spec")
b984733c
SH
1902 ]
1903 self.description = """Imports from Perforce into a git repository.\n
1904 example:
1905 //depot/my/project/ -- to import the current head
1906 //depot/my/project/@all -- to import everything
1907 //depot/my/project/@1,6 -- to import only from revision 1 to 6
1908
1909 (a ... is not needed in the path p4 specification, it's added implicitly)"""
1910
1911 self.usage += " //depot/path[@revRange]"
b984733c 1912 self.silent = False
1d7367dc
RG
1913 self.createdBranches = set()
1914 self.committedChanges = set()
569d1bd4 1915 self.branch = ""
b984733c 1916 self.detectBranches = False
cb53e1f8 1917 self.detectLabels = False
06804c76 1918 self.importLabels = False
b984733c 1919 self.changesFile = ""
01265103 1920 self.syncWithOrigin = True
a028a98e 1921 self.importIntoRemotes = True
01a9c9c5 1922 self.maxChanges = ""
c1f9197f 1923 self.isWindows = (platform.system() == "Windows")
8b41a97f 1924 self.keepRepoPath = False
6326aa58 1925 self.depotPaths = None
3c699645 1926 self.p4BranchesInGit = []
354081d5 1927 self.cloneExclude = []
3a70cdfa 1928 self.useClientSpec = False
a93d33ee 1929 self.useClientSpec_from_options = False
ecb7cf98 1930 self.clientSpecDirs = None
fed23693
VA
1931 self.tempBranches = []
1932 self.tempBranchLocation = "git-p4-tmp"
b984733c 1933
01265103
SH
1934 if gitConfig("git-p4.syncFromOrigin") == "false":
1935 self.syncWithOrigin = False
1936
fed23693
VA
1937 # Force a checkpoint in fast-import and wait for it to finish
1938 def checkpoint(self):
1939 self.gitStream.write("checkpoint\n\n")
1940 self.gitStream.write("progress checkpoint\n\n")
1941 out = self.gitOutput.readline()
1942 if self.verbose:
1943 print "checkpoint finished: " + out
1944
b984733c 1945 def extractFilesFromCommit(self, commit):
354081d5
TT
1946 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
1947 for path in self.cloneExclude]
b984733c
SH
1948 files = []
1949 fnum = 0
1950 while commit.has_key("depotFile%s" % fnum):
1951 path = commit["depotFile%s" % fnum]
6326aa58 1952
354081d5 1953 if [p for p in self.cloneExclude
d53de8b9 1954 if p4PathStartsWith(path, p)]:
354081d5
TT
1955 found = False
1956 else:
1957 found = [p for p in self.depotPaths
d53de8b9 1958 if p4PathStartsWith(path, p)]
6326aa58 1959 if not found:
b984733c
SH
1960 fnum = fnum + 1
1961 continue
1962
1963 file = {}
1964 file["path"] = path
1965 file["rev"] = commit["rev%s" % fnum]
1966 file["action"] = commit["action%s" % fnum]
1967 file["type"] = commit["type%s" % fnum]
1968 files.append(file)
1969 fnum = fnum + 1
1970 return files
1971
6326aa58 1972 def stripRepoPath(self, path, prefixes):
21ef5df4
PW
1973 """When streaming files, this is called to map a p4 depot path
1974 to where it should go in git. The prefixes are either
1975 self.depotPaths, or self.branchPrefixes in the case of
1976 branch detection."""
1977
3952710b 1978 if self.useClientSpec:
21ef5df4
PW
1979 # branch detection moves files up a level (the branch name)
1980 # from what client spec interpretation gives
0d1696ef 1981 path = self.clientSpecDirs.map_in_client(path)
21ef5df4
PW
1982 if self.detectBranches:
1983 for b in self.knownBranches:
1984 if path.startswith(b + "/"):
1985 path = path[len(b)+1:]
1986
1987 elif self.keepRepoPath:
1988 # Preserve everything in relative path name except leading
1989 # //depot/; just look at first prefix as they all should
1990 # be in the same depot.
1991 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
1992 if p4PathStartsWith(path, depot):
1993 path = path[len(depot):]
3952710b 1994
0d1696ef 1995 else:
0d1696ef
PW
1996 for p in prefixes:
1997 if p4PathStartsWith(path, p):
1998 path = path[len(p):]
21ef5df4 1999 break
8b41a97f 2000
0d1696ef 2001 path = wildcard_decode(path)
6326aa58 2002 return path
6754a299 2003
71b112d4 2004 def splitFilesIntoBranches(self, commit):
21ef5df4
PW
2005 """Look at each depotFile in the commit to figure out to what
2006 branch it belongs."""
2007
d5904674 2008 branches = {}
71b112d4
SH
2009 fnum = 0
2010 while commit.has_key("depotFile%s" % fnum):
2011 path = commit["depotFile%s" % fnum]
6326aa58 2012 found = [p for p in self.depotPaths
d53de8b9 2013 if p4PathStartsWith(path, p)]
6326aa58 2014 if not found:
71b112d4
SH
2015 fnum = fnum + 1
2016 continue
2017
2018 file = {}
2019 file["path"] = path
2020 file["rev"] = commit["rev%s" % fnum]
2021 file["action"] = commit["action%s" % fnum]
2022 file["type"] = commit["type%s" % fnum]
2023 fnum = fnum + 1
2024
21ef5df4
PW
2025 # start with the full relative path where this file would
2026 # go in a p4 client
2027 if self.useClientSpec:
2028 relPath = self.clientSpecDirs.map_in_client(path)
2029 else:
2030 relPath = self.stripRepoPath(path, self.depotPaths)
b984733c 2031
4b97ffb1 2032 for branch in self.knownBranches.keys():
21ef5df4
PW
2033 # add a trailing slash so that a commit into qt/4.2foo
2034 # doesn't end up in qt/4.2, e.g.
6754a299 2035 if relPath.startswith(branch + "/"):
d5904674
SH
2036 if branch not in branches:
2037 branches[branch] = []
71b112d4 2038 branches[branch].append(file)
6555b2cc 2039 break
b984733c
SH
2040
2041 return branches
2042
b932705b
LD
2043 # output one file from the P4 stream
2044 # - helper for streamP4Files
2045
2046 def streamOneP4File(self, file, contents):
b932705b
LD
2047 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
2048 if verbose:
2049 sys.stderr.write("%s\n" % relPath)
2050
9cffb8c8
PW
2051 (type_base, type_mods) = split_p4_type(file["type"])
2052
2053 git_mode = "100644"
2054 if "x" in type_mods:
2055 git_mode = "100755"
2056 if type_base == "symlink":
2057 git_mode = "120000"
2058 # p4 print on a symlink contains "target\n"; remove the newline
b39c3612
EP
2059 data = ''.join(contents)
2060 contents = [data[:-1]]
b932705b 2061
9cffb8c8 2062 if type_base == "utf16":
55aa5714
PW
2063 # p4 delivers different text in the python output to -G
2064 # than it does when using "print -o", or normal p4 client
2065 # operations. utf16 is converted to ascii or utf8, perhaps.
2066 # But ascii text saved as -t utf16 is completely mangled.
2067 # Invoke print -o to get the real contents.
6de040df 2068 text = p4_read_pipe(['print', '-q', '-o', '-', file['depotFile']])
55aa5714
PW
2069 contents = [ text ]
2070
9f7ef0ea
PW
2071 if type_base == "apple":
2072 # Apple filetype files will be streamed as a concatenation of
2073 # its appledouble header and the contents. This is useless
2074 # on both macs and non-macs. If using "print -q -o xx", it
2075 # will create "xx" with the data, and "%xx" with the header.
2076 # This is also not very useful.
2077 #
2078 # Ideally, someday, this script can learn how to generate
2079 # appledouble files directly and import those to git, but
2080 # non-mac machines can never find a use for apple filetype.
2081 print "\nIgnoring apple filetype file %s" % file['depotFile']
2082 return
2083
9cffb8c8
PW
2084 # Perhaps windows wants unicode, utf16 newlines translated too;
2085 # but this is not doing it.
2086 if self.isWindows and type_base == "text":
b932705b
LD
2087 mangled = []
2088 for data in contents:
2089 data = data.replace("\r\n", "\n")
2090 mangled.append(data)
2091 contents = mangled
2092
55aa5714
PW
2093 # Note that we do not try to de-mangle keywords on utf16 files,
2094 # even though in theory somebody may want that.
60df071c
LD
2095 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2096 if pattern:
2097 regexp = re.compile(pattern, re.VERBOSE)
2098 text = ''.join(contents)
2099 text = regexp.sub(r'$\1$', text)
2100 contents = [ text ]
b932705b 2101
9cffb8c8 2102 self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
b932705b
LD
2103
2104 # total length...
2105 length = 0
2106 for d in contents:
2107 length = length + len(d)
2108
2109 self.gitStream.write("data %d\n" % length)
2110 for d in contents:
2111 self.gitStream.write(d)
2112 self.gitStream.write("\n")
2113
2114 def streamOneP4Deletion(self, file):
2115 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
2116 if verbose:
2117 sys.stderr.write("delete %s\n" % relPath)
2118 self.gitStream.write("D %s\n" % relPath)
2119
2120 # handle another chunk of streaming data
2121 def streamP4FilesCb(self, marshalled):
2122
c3f6163b
AG
2123 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2124 # start of a new file - output the old one first
2125 self.streamOneP4File(self.stream_file, self.stream_contents)
2126 self.stream_file = {}
2127 self.stream_contents = []
2128 self.stream_have_file_info = False
b932705b 2129
c3f6163b
AG
2130 # pick up the new file information... for the
2131 # 'data' field we need to append to our array
2132 for k in marshalled.keys():
2133 if k == 'data':
2134 self.stream_contents.append(marshalled['data'])
2135 else:
2136 self.stream_file[k] = marshalled[k]
b932705b 2137
c3f6163b 2138 self.stream_have_file_info = True
b932705b
LD
2139
2140 # Stream directly from "p4 files" into "git fast-import"
2141 def streamP4Files(self, files):
30b5940b
SH
2142 filesForCommit = []
2143 filesToRead = []
b932705b 2144 filesToDelete = []
30b5940b 2145
3a70cdfa 2146 for f in files:
ecb7cf98
PW
2147 # if using a client spec, only add the files that have
2148 # a path in the client
2149 if self.clientSpecDirs:
2150 if self.clientSpecDirs.map_in_client(f['path']) == "":
2151 continue
3a70cdfa 2152
ecb7cf98
PW
2153 filesForCommit.append(f)
2154 if f['action'] in self.delete_actions:
2155 filesToDelete.append(f)
2156 else:
2157 filesToRead.append(f)
6a49f8e2 2158
b932705b
LD
2159 # deleted files...
2160 for f in filesToDelete:
2161 self.streamOneP4Deletion(f)
1b9a4684 2162
b932705b
LD
2163 if len(filesToRead) > 0:
2164 self.stream_file = {}
2165 self.stream_contents = []
2166 self.stream_have_file_info = False
8ff45f2a 2167
c3f6163b
AG
2168 # curry self argument
2169 def streamP4FilesCbSelf(entry):
2170 self.streamP4FilesCb(entry)
6a49f8e2 2171
6de040df
LD
2172 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2173
2174 p4CmdList(["-x", "-", "print"],
2175 stdin=fileArgs,
2176 cb=streamP4FilesCbSelf)
30b5940b 2177
b932705b
LD
2178 # do the last chunk
2179 if self.stream_file.has_key('depotFile'):
2180 self.streamOneP4File(self.stream_file, self.stream_contents)
6a49f8e2 2181
affb474f
LD
2182 def make_email(self, userid):
2183 if userid in self.users:
2184 return self.users[userid]
2185 else:
2186 return "%s <a@b>" % userid
2187
06804c76
LD
2188 # Stream a p4 tag
2189 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
2190 if verbose:
2191 print "writing tag %s for commit %s" % (labelName, commit)
2192 gitStream.write("tag %s\n" % labelName)
2193 gitStream.write("from %s\n" % commit)
2194
2195 if labelDetails.has_key('Owner'):
2196 owner = labelDetails["Owner"]
2197 else:
2198 owner = None
2199
2200 # Try to use the owner of the p4 label, or failing that,
2201 # the current p4 user id.
2202 if owner:
2203 email = self.make_email(owner)
2204 else:
2205 email = self.make_email(self.p4UserId())
2206 tagger = "%s %s %s" % (email, epoch, self.tz)
2207
2208 gitStream.write("tagger %s\n" % tagger)
2209
2210 print "labelDetails=",labelDetails
2211 if labelDetails.has_key('Description'):
2212 description = labelDetails['Description']
2213 else:
2214 description = 'Label from git p4'
2215
2216 gitStream.write("data %d\n" % len(description))
2217 gitStream.write(description)
2218 gitStream.write("\n")
2219
e63231e5 2220 def commit(self, details, files, branch, parent = ""):
b984733c
SH
2221 epoch = details["time"]
2222 author = details["user"]
2223
4b97ffb1
SH
2224 if self.verbose:
2225 print "commit into %s" % branch
2226
96e07dd2
HWN
2227 # start with reading files; if that fails, we should not
2228 # create a commit.
2229 new_files = []
2230 for f in files:
e63231e5 2231 if [p for p in self.branchPrefixes if p4PathStartsWith(f['path'], p)]:
96e07dd2
HWN
2232 new_files.append (f)
2233 else:
afa1dd9a 2234 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
96e07dd2 2235
b984733c 2236 self.gitStream.write("commit %s\n" % branch)
6a49f8e2 2237# gitStream.write("mark :%s\n" % details["change"])
b984733c
SH
2238 self.committedChanges.add(int(details["change"]))
2239 committer = ""
b607e71e
SH
2240 if author not in self.users:
2241 self.getUserMapFromPerforceServer()
affb474f 2242 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
b984733c
SH
2243
2244 self.gitStream.write("committer %s\n" % committer)
2245
2246 self.gitStream.write("data <<EOT\n")
2247 self.gitStream.write(details["desc"])
e63231e5
PW
2248 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2249 (','.join(self.branchPrefixes), details["change"]))
6581de09
SH
2250 if len(details['options']) > 0:
2251 self.gitStream.write(": options = %s" % details['options'])
2252 self.gitStream.write("]\nEOT\n\n")
b984733c
SH
2253
2254 if len(parent) > 0:
4b97ffb1
SH
2255 if self.verbose:
2256 print "parent %s" % parent
b984733c
SH
2257 self.gitStream.write("from %s\n" % parent)
2258
b932705b 2259 self.streamP4Files(new_files)
b984733c
SH
2260 self.gitStream.write("\n")
2261
1f4ba1cb
SH
2262 change = int(details["change"])
2263
9bda3a85 2264 if self.labels.has_key(change):
1f4ba1cb
SH
2265 label = self.labels[change]
2266 labelDetails = label[0]
2267 labelRevisions = label[1]
71b112d4
SH
2268 if self.verbose:
2269 print "Change %s is labelled %s" % (change, labelDetails)
1f4ba1cb 2270
6de040df 2271 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
e63231e5 2272 for p in self.branchPrefixes])
1f4ba1cb
SH
2273
2274 if len(files) == len(labelRevisions):
2275
2276 cleanedFiles = {}
2277 for info in files:
56c09345 2278 if info["action"] in self.delete_actions:
1f4ba1cb
SH
2279 continue
2280 cleanedFiles[info["depotFile"]] = info["rev"]
2281
2282 if cleanedFiles == labelRevisions:
06804c76 2283 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
1f4ba1cb
SH
2284
2285 else:
a46668fa 2286 if not self.silent:
cebdf5af
HWN
2287 print ("Tag %s does not match with change %s: files do not match."
2288 % (labelDetails["label"], change))
1f4ba1cb
SH
2289
2290 else:
a46668fa 2291 if not self.silent:
cebdf5af
HWN
2292 print ("Tag %s does not match with change %s: file count is different."
2293 % (labelDetails["label"], change))
b984733c 2294
06804c76 2295 # Build a dictionary of changelists and labels, for "detect-labels" option.
1f4ba1cb
SH
2296 def getLabels(self):
2297 self.labels = {}
2298
52a4880b 2299 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
10c3211b 2300 if len(l) > 0 and not self.silent:
183f8436 2301 print "Finding files belonging to labels in %s" % `self.depotPaths`
01ce1fe9
SH
2302
2303 for output in l:
1f4ba1cb
SH
2304 label = output["label"]
2305 revisions = {}
2306 newestChange = 0
71b112d4
SH
2307 if self.verbose:
2308 print "Querying files for label %s" % label
6de040df
LD
2309 for file in p4CmdList(["files"] +
2310 ["%s...@%s" % (p, label)
2311 for p in self.depotPaths]):
1f4ba1cb
SH
2312 revisions[file["depotFile"]] = file["rev"]
2313 change = int(file["change"])
2314 if change > newestChange:
2315 newestChange = change
2316
9bda3a85
SH
2317 self.labels[newestChange] = [output, revisions]
2318
2319 if self.verbose:
2320 print "Label changes: %s" % self.labels.keys()
1f4ba1cb 2321
06804c76
LD
2322 # Import p4 labels as git tags. A direct mapping does not
2323 # exist, so assume that if all the files are at the same revision
2324 # then we can use that, or it's something more complicated we should
2325 # just ignore.
2326 def importP4Labels(self, stream, p4Labels):
2327 if verbose:
2328 print "import p4 labels: " + ' '.join(p4Labels)
2329
2330 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
c8942a22 2331 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
06804c76
LD
2332 if len(validLabelRegexp) == 0:
2333 validLabelRegexp = defaultLabelRegexp
2334 m = re.compile(validLabelRegexp)
2335
2336 for name in p4Labels:
2337 commitFound = False
2338
2339 if not m.match(name):
2340 if verbose:
2341 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2342 continue
2343
2344 if name in ignoredP4Labels:
2345 continue
2346
2347 labelDetails = p4CmdList(['label', "-o", name])[0]
2348
2349 # get the most recent changelist for each file in this label
2350 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2351 for p in self.depotPaths])
2352
2353 if change.has_key('change'):
2354 # find the corresponding git commit; take the oldest commit
2355 changelist = int(change['change'])
2356 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2357 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
2358 if len(gitCommit) == 0:
2359 print "could not find git commit for changelist %d" % changelist
2360 else:
2361 gitCommit = gitCommit.strip()
2362 commitFound = True
2363 # Convert from p4 time format
2364 try:
2365 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2366 except ValueError:
2367 print "Could not convert label time %s" % labelDetail['Update']
2368 tmwhen = 1
2369
2370 when = int(time.mktime(tmwhen))
2371 self.streamTag(stream, name, labelDetails, gitCommit, when)
2372 if verbose:
2373 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2374 else:
2375 if verbose:
2376 print "Label %s has no changelists - possibly deleted?" % name
2377
2378 if not commitFound:
2379 # We can't import this label; don't try again as it will get very
2380 # expensive repeatedly fetching all the files for labels that will
2381 # never be imported. If the label is moved in the future, the
2382 # ignore will need to be removed manually.
2383 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2384
86dff6b6
HWN
2385 def guessProjectName(self):
2386 for p in self.depotPaths:
6e5295c4
SH
2387 if p.endswith("/"):
2388 p = p[:-1]
2389 p = p[p.strip().rfind("/") + 1:]
2390 if not p.endswith("/"):
2391 p += "/"
2392 return p
86dff6b6 2393
4b97ffb1 2394 def getBranchMapping(self):
6555b2cc
SH
2395 lostAndFoundBranches = set()
2396
8ace74c0
VA
2397 user = gitConfig("git-p4.branchUser")
2398 if len(user) > 0:
2399 command = "branches -u %s" % user
2400 else:
2401 command = "branches"
2402
2403 for info in p4CmdList(command):
52a4880b 2404 details = p4Cmd(["branch", "-o", info["branch"]])
4b97ffb1
SH
2405 viewIdx = 0
2406 while details.has_key("View%s" % viewIdx):
2407 paths = details["View%s" % viewIdx].split(" ")
2408 viewIdx = viewIdx + 1
2409 # require standard //depot/foo/... //depot/bar/... mapping
2410 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2411 continue
2412 source = paths[0]
2413 destination = paths[1]
6509e19c 2414 ## HACK
d53de8b9 2415 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
6509e19c
SH
2416 source = source[len(self.depotPaths[0]):-4]
2417 destination = destination[len(self.depotPaths[0]):-4]
6555b2cc 2418
1a2edf4e
SH
2419 if destination in self.knownBranches:
2420 if not self.silent:
2421 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2422 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2423 continue
2424
6555b2cc
SH
2425 self.knownBranches[destination] = source
2426
2427 lostAndFoundBranches.discard(destination)
2428
29bdbac1 2429 if source not in self.knownBranches:
6555b2cc
SH
2430 lostAndFoundBranches.add(source)
2431
7199cf13
VA
2432 # Perforce does not strictly require branches to be defined, so we also
2433 # check git config for a branch list.
2434 #
2435 # Example of branch definition in git config file:
2436 # [git-p4]
2437 # branchList=main:branchA
2438 # branchList=main:branchB
2439 # branchList=branchA:branchC
2440 configBranches = gitConfigList("git-p4.branchList")
2441 for branch in configBranches:
2442 if branch:
2443 (source, destination) = branch.split(":")
2444 self.knownBranches[destination] = source
2445
2446 lostAndFoundBranches.discard(destination)
2447
2448 if source not in self.knownBranches:
2449 lostAndFoundBranches.add(source)
2450
6555b2cc
SH
2451
2452 for branch in lostAndFoundBranches:
2453 self.knownBranches[branch] = branch
29bdbac1 2454
38f9f5ec
SH
2455 def getBranchMappingFromGitBranches(self):
2456 branches = p4BranchesInGit(self.importIntoRemotes)
2457 for branch in branches.keys():
2458 if branch == "master":
2459 branch = "main"
2460 else:
2461 branch = branch[len(self.projectName):]
2462 self.knownBranches[branch] = branch
2463
29bdbac1 2464 def listExistingP4GitBranches(self):
144ff46b
SH
2465 # branches holds mapping from name to commit
2466 branches = p4BranchesInGit(self.importIntoRemotes)
2467 self.p4BranchesInGit = branches.keys()
2468 for branch in branches.keys():
2469 self.initialParents[self.refPrefix + branch] = branches[branch]
4b97ffb1 2470
bb6e09b2
HWN
2471 def updateOptionDict(self, d):
2472 option_keys = {}
2473 if self.keepRepoPath:
2474 option_keys['keepRepoPath'] = 1
2475
2476 d["options"] = ' '.join(sorted(option_keys.keys()))
2477
2478 def readOptions(self, d):
2479 self.keepRepoPath = (d.has_key('options')
2480 and ('keepRepoPath' in d['options']))
6326aa58 2481
8134f69c
SH
2482 def gitRefForBranch(self, branch):
2483 if branch == "main":
2484 return self.refPrefix + "master"
2485
2486 if len(branch) <= 0:
2487 return branch
2488
2489 return self.refPrefix + self.projectName + branch
2490
1ca3d710
SH
2491 def gitCommitByP4Change(self, ref, change):
2492 if self.verbose:
2493 print "looking in ref " + ref + " for change %s using bisect..." % change
2494
2495 earliestCommit = ""
2496 latestCommit = parseRevision(ref)
2497
2498 while True:
2499 if self.verbose:
2500 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
2501 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
2502 if len(next) == 0:
2503 if self.verbose:
2504 print "argh"
2505 return ""
2506 log = extractLogMessageFromGitCommit(next)
2507 settings = extractSettingsGitLog(log)
2508 currentChange = int(settings['change'])
2509 if self.verbose:
2510 print "current change %s" % currentChange
2511
2512 if currentChange == change:
2513 if self.verbose:
2514 print "found %s" % next
2515 return next
2516
2517 if currentChange < change:
2518 earliestCommit = "^%s" % next
2519 else:
2520 latestCommit = "%s" % next
2521
2522 return ""
2523
2524 def importNewBranch(self, branch, maxChange):
2525 # make fast-import flush all changes to disk and update the refs using the checkpoint
2526 # command so that we can try to find the branch parent in the git history
2527 self.gitStream.write("checkpoint\n\n");
2528 self.gitStream.flush();
2529 branchPrefix = self.depotPaths[0] + branch + "/"
2530 range = "@1,%s" % maxChange
2531 #print "prefix" + branchPrefix
2532 changes = p4ChangesForPaths([branchPrefix], range)
2533 if len(changes) <= 0:
2534 return False
2535 firstChange = changes[0]
2536 #print "first change in branch: %s" % firstChange
2537 sourceBranch = self.knownBranches[branch]
2538 sourceDepotPath = self.depotPaths[0] + sourceBranch
2539 sourceRef = self.gitRefForBranch(sourceBranch)
2540 #print "source " + sourceBranch
2541
52a4880b 2542 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
1ca3d710
SH
2543 #print "branch parent: %s" % branchParentChange
2544 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
2545 if len(gitParent) > 0:
2546 self.initialParents[self.gitRefForBranch(branch)] = gitParent
2547 #print "parent git commit: %s" % gitParent
2548
2549 self.importChanges(changes)
2550 return True
2551
fed23693
VA
2552 def searchParent(self, parent, branch, target):
2553 parentFound = False
2554 for blob in read_pipe_lines(["git", "rev-list", "--reverse", "--no-merges", parent]):
2555 blob = blob.strip()
2556 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
2557 parentFound = True
2558 if self.verbose:
2559 print "Found parent of %s in commit %s" % (branch, blob)
2560 break
2561 if parentFound:
2562 return blob
2563 else:
2564 return None
2565
e87f37ae
SH
2566 def importChanges(self, changes):
2567 cnt = 1
2568 for change in changes:
18fa13d0 2569 description = p4_describe(change)
e87f37ae
SH
2570 self.updateOptionDict(description)
2571
2572 if not self.silent:
2573 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
2574 sys.stdout.flush()
2575 cnt = cnt + 1
2576
2577 try:
2578 if self.detectBranches:
2579 branches = self.splitFilesIntoBranches(description)
2580 for branch in branches.keys():
2581 ## HACK --hwn
2582 branchPrefix = self.depotPaths[0] + branch + "/"
e63231e5 2583 self.branchPrefixes = [ branchPrefix ]
e87f37ae
SH
2584
2585 parent = ""
2586
2587 filesForCommit = branches[branch]
2588
2589 if self.verbose:
2590 print "branch is %s" % branch
2591
2592 self.updatedBranches.add(branch)
2593
2594 if branch not in self.createdBranches:
2595 self.createdBranches.add(branch)
2596 parent = self.knownBranches[branch]
2597 if parent == branch:
2598 parent = ""
1ca3d710
SH
2599 else:
2600 fullBranch = self.projectName + branch
2601 if fullBranch not in self.p4BranchesInGit:
2602 if not self.silent:
2603 print("\n Importing new branch %s" % fullBranch);
2604 if self.importNewBranch(branch, change - 1):
2605 parent = ""
2606 self.p4BranchesInGit.append(fullBranch)
2607 if not self.silent:
2608 print("\n Resuming with change %s" % change);
2609
2610 if self.verbose:
2611 print "parent determined through known branches: %s" % parent
e87f37ae 2612
8134f69c
SH
2613 branch = self.gitRefForBranch(branch)
2614 parent = self.gitRefForBranch(parent)
e87f37ae
SH
2615
2616 if self.verbose:
2617 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
2618
2619 if len(parent) == 0 and branch in self.initialParents:
2620 parent = self.initialParents[branch]
2621 del self.initialParents[branch]
2622
fed23693
VA
2623 blob = None
2624 if len(parent) > 0:
2625 tempBranch = os.path.join(self.tempBranchLocation, "%d" % (change))
2626 if self.verbose:
2627 print "Creating temporary branch: " + tempBranch
e63231e5 2628 self.commit(description, filesForCommit, tempBranch)
fed23693
VA
2629 self.tempBranches.append(tempBranch)
2630 self.checkpoint()
2631 blob = self.searchParent(parent, branch, tempBranch)
2632 if blob:
e63231e5 2633 self.commit(description, filesForCommit, branch, blob)
fed23693
VA
2634 else:
2635 if self.verbose:
2636 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
e63231e5 2637 self.commit(description, filesForCommit, branch, parent)
e87f37ae
SH
2638 else:
2639 files = self.extractFilesFromCommit(description)
e63231e5 2640 self.commit(description, files, self.branch,
e87f37ae
SH
2641 self.initialParent)
2642 self.initialParent = ""
2643 except IOError:
2644 print self.gitError.read()
2645 sys.exit(1)
2646
c208a243
SH
2647 def importHeadRevision(self, revision):
2648 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
2649
4e2e6ce4
PW
2650 details = {}
2651 details["user"] = "git perforce import user"
1494fcbb 2652 details["desc"] = ("Initial import of %s from the state at revision %s\n"
c208a243
SH
2653 % (' '.join(self.depotPaths), revision))
2654 details["change"] = revision
2655 newestRevision = 0
2656
2657 fileCnt = 0
6de040df
LD
2658 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
2659
2660 for info in p4CmdList(["files"] + fileArgs):
c208a243 2661
68b28593 2662 if 'code' in info and info['code'] == 'error':
c208a243
SH
2663 sys.stderr.write("p4 returned an error: %s\n"
2664 % info['data'])
d88e707f
PW
2665 if info['data'].find("must refer to client") >= 0:
2666 sys.stderr.write("This particular p4 error is misleading.\n")
2667 sys.stderr.write("Perhaps the depot path was misspelled.\n");
2668 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
c208a243 2669 sys.exit(1)
68b28593
PW
2670 if 'p4ExitCode' in info:
2671 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
c208a243
SH
2672 sys.exit(1)
2673
2674
2675 change = int(info["change"])
2676 if change > newestRevision:
2677 newestRevision = change
2678
56c09345 2679 if info["action"] in self.delete_actions:
c208a243
SH
2680 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
2681 #fileCnt = fileCnt + 1
2682 continue
2683
2684 for prop in ["depotFile", "rev", "action", "type" ]:
2685 details["%s%s" % (prop, fileCnt)] = info[prop]
2686
2687 fileCnt = fileCnt + 1
2688
2689 details["change"] = newestRevision
4e2e6ce4 2690
9dcb9f24 2691 # Use time from top-most change so that all git p4 clones of
4e2e6ce4 2692 # the same p4 repo have the same commit SHA1s.
18fa13d0
PW
2693 res = p4_describe(newestRevision)
2694 details["time"] = res["time"]
4e2e6ce4 2695
c208a243
SH
2696 self.updateOptionDict(details)
2697 try:
e63231e5 2698 self.commit(details, self.extractFilesFromCommit(details), self.branch)
c208a243
SH
2699 except IOError:
2700 print "IO error with git fast-import. Is your git version recent enough?"
2701 print self.gitError.read()
2702
2703
b984733c 2704 def run(self, args):
6326aa58 2705 self.depotPaths = []
179caebf
SH
2706 self.changeRange = ""
2707 self.initialParent = ""
6326aa58 2708 self.previousDepotPaths = []
ce6f33c8 2709
29bdbac1
SH
2710 # map from branch depot path to parent branch
2711 self.knownBranches = {}
2712 self.initialParents = {}
5ca44617 2713 self.hasOrigin = originP4BranchesExist()
a43ff00c
SH
2714 if not self.syncWithOrigin:
2715 self.hasOrigin = False
29bdbac1 2716
a028a98e
SH
2717 if self.importIntoRemotes:
2718 self.refPrefix = "refs/remotes/p4/"
2719 else:
db775559 2720 self.refPrefix = "refs/heads/p4/"
a028a98e 2721
cebdf5af
HWN
2722 if self.syncWithOrigin and self.hasOrigin:
2723 if not self.silent:
2724 print "Syncing with origin first by calling git fetch origin"
2725 system("git fetch origin")
10f880f8 2726
569d1bd4 2727 if len(self.branch) == 0:
db775559 2728 self.branch = self.refPrefix + "master"
a028a98e 2729 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
48df6fd8 2730 system("git update-ref %s refs/heads/p4" % self.branch)
48df6fd8 2731 system("git branch -D p4");
faf1bd20 2732 # create it /after/ importing, when master exists
0058a33a 2733 if not gitBranchExists(self.refPrefix + "HEAD") and self.importIntoRemotes and gitBranchExists(self.branch):
a3c55c09 2734 system("git symbolic-ref %sHEAD %s" % (self.refPrefix, self.branch))
967f72e2 2735
a93d33ee
PW
2736 # accept either the command-line option, or the configuration variable
2737 if self.useClientSpec:
2738 # will use this after clone to set the variable
2739 self.useClientSpec_from_options = True
2740 else:
09fca77b
PW
2741 if gitConfig("git-p4.useclientspec", "--bool") == "true":
2742 self.useClientSpec = True
2743 if self.useClientSpec:
543987bd 2744 self.clientSpecDirs = getClientSpec()
3a70cdfa 2745
6a49f8e2
HWN
2746 # TODO: should always look at previous commits,
2747 # merge with previous imports, if possible.
2748 if args == []:
d414c74a 2749 if self.hasOrigin:
5ca44617 2750 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
abcd790f
SH
2751 self.listExistingP4GitBranches()
2752
2753 if len(self.p4BranchesInGit) > 1:
2754 if not self.silent:
2755 print "Importing from/into multiple branches"
2756 self.detectBranches = True
967f72e2 2757
29bdbac1
SH
2758 if self.verbose:
2759 print "branches: %s" % self.p4BranchesInGit
2760
2761 p4Change = 0
2762 for branch in self.p4BranchesInGit:
cebdf5af 2763 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
bb6e09b2
HWN
2764
2765 settings = extractSettingsGitLog(logMsg)
29bdbac1 2766
bb6e09b2
HWN
2767 self.readOptions(settings)
2768 if (settings.has_key('depot-paths')
2769 and settings.has_key ('change')):
2770 change = int(settings['change']) + 1
29bdbac1
SH
2771 p4Change = max(p4Change, change)
2772
bb6e09b2
HWN
2773 depotPaths = sorted(settings['depot-paths'])
2774 if self.previousDepotPaths == []:
6326aa58 2775 self.previousDepotPaths = depotPaths
29bdbac1 2776 else:
6326aa58
HWN
2777 paths = []
2778 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
04d277b3
VA
2779 prev_list = prev.split("/")
2780 cur_list = cur.split("/")
2781 for i in range(0, min(len(cur_list), len(prev_list))):
2782 if cur_list[i] <> prev_list[i]:
583e1707 2783 i = i - 1
6326aa58
HWN
2784 break
2785
04d277b3 2786 paths.append ("/".join(cur_list[:i + 1]))
6326aa58
HWN
2787
2788 self.previousDepotPaths = paths
29bdbac1
SH
2789
2790 if p4Change > 0:
bb6e09b2 2791 self.depotPaths = sorted(self.previousDepotPaths)
d5904674 2792 self.changeRange = "@%s,#head" % p4Change
330f53b8
SH
2793 if not self.detectBranches:
2794 self.initialParent = parseRevision(self.branch)
341dc1c1 2795 if not self.silent and not self.detectBranches:
967f72e2 2796 print "Performing incremental import into %s git branch" % self.branch
569d1bd4 2797
f9162f6a
SH
2798 if not self.branch.startswith("refs/"):
2799 self.branch = "refs/heads/" + self.branch
179caebf 2800
6326aa58 2801 if len(args) == 0 and self.depotPaths:
b984733c 2802 if not self.silent:
6326aa58 2803 print "Depot paths: %s" % ' '.join(self.depotPaths)
b984733c 2804 else:
6326aa58 2805 if self.depotPaths and self.depotPaths != args:
cebdf5af 2806 print ("previous import used depot path %s and now %s was specified. "
6326aa58
HWN
2807 "This doesn't work!" % (' '.join (self.depotPaths),
2808 ' '.join (args)))
b984733c 2809 sys.exit(1)
6326aa58 2810
bb6e09b2 2811 self.depotPaths = sorted(args)
b984733c 2812
1c49fc19 2813 revision = ""
b984733c 2814 self.users = {}
b984733c 2815
58c8bc7c
PW
2816 # Make sure no revision specifiers are used when --changesfile
2817 # is specified.
2818 bad_changesfile = False
2819 if len(self.changesFile) > 0:
2820 for p in self.depotPaths:
2821 if p.find("@") >= 0 or p.find("#") >= 0:
2822 bad_changesfile = True
2823 break
2824 if bad_changesfile:
2825 die("Option --changesfile is incompatible with revision specifiers")
2826
6326aa58
HWN
2827 newPaths = []
2828 for p in self.depotPaths:
2829 if p.find("@") != -1:
2830 atIdx = p.index("@")
2831 self.changeRange = p[atIdx:]
2832 if self.changeRange == "@all":
2833 self.changeRange = ""
6a49f8e2 2834 elif ',' not in self.changeRange:
1c49fc19 2835 revision = self.changeRange
6326aa58 2836 self.changeRange = ""
7fcff9de 2837 p = p[:atIdx]
6326aa58
HWN
2838 elif p.find("#") != -1:
2839 hashIdx = p.index("#")
1c49fc19 2840 revision = p[hashIdx:]
7fcff9de 2841 p = p[:hashIdx]
6326aa58 2842 elif self.previousDepotPaths == []:
58c8bc7c
PW
2843 # pay attention to changesfile, if given, else import
2844 # the entire p4 tree at the head revision
2845 if len(self.changesFile) == 0:
2846 revision = "#head"
6326aa58
HWN
2847
2848 p = re.sub ("\.\.\.$", "", p)
2849 if not p.endswith("/"):
2850 p += "/"
2851
2852 newPaths.append(p)
2853
2854 self.depotPaths = newPaths
2855
e63231e5
PW
2856 # --detect-branches may change this for each branch
2857 self.branchPrefixes = self.depotPaths
2858
b607e71e 2859 self.loadUserMapFromCache()
cb53e1f8
SH
2860 self.labels = {}
2861 if self.detectLabels:
2862 self.getLabels();
b984733c 2863
4b97ffb1 2864 if self.detectBranches:
df450923
SH
2865 ## FIXME - what's a P4 projectName ?
2866 self.projectName = self.guessProjectName()
2867
38f9f5ec
SH
2868 if self.hasOrigin:
2869 self.getBranchMappingFromGitBranches()
2870 else:
2871 self.getBranchMapping()
29bdbac1
SH
2872 if self.verbose:
2873 print "p4-git branches: %s" % self.p4BranchesInGit
2874 print "initial parents: %s" % self.initialParents
2875 for b in self.p4BranchesInGit:
2876 if b != "master":
6326aa58
HWN
2877
2878 ## FIXME
29bdbac1
SH
2879 b = b[len(self.projectName):]
2880 self.createdBranches.add(b)
4b97ffb1 2881
f291b4e3 2882 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
b984733c 2883
cebdf5af 2884 importProcess = subprocess.Popen(["git", "fast-import"],
6326aa58
HWN
2885 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2886 stderr=subprocess.PIPE);
08483580
SH
2887 self.gitOutput = importProcess.stdout
2888 self.gitStream = importProcess.stdin
2889 self.gitError = importProcess.stderr
b984733c 2890
1c49fc19 2891 if revision:
c208a243 2892 self.importHeadRevision(revision)
b984733c
SH
2893 else:
2894 changes = []
2895
0828ab14 2896 if len(self.changesFile) > 0:
b984733c 2897 output = open(self.changesFile).readlines()
1d7367dc 2898 changeSet = set()
b984733c
SH
2899 for line in output:
2900 changeSet.add(int(line))
2901
2902 for change in changeSet:
2903 changes.append(change)
2904
2905 changes.sort()
2906 else:
9dcb9f24
PW
2907 # catch "git p4 sync" with no new branches, in a repo that
2908 # does not have any existing p4 branches
accad8e0 2909 if len(args) == 0 and not self.p4BranchesInGit:
e32e00dc 2910 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.");
29bdbac1 2911 if self.verbose:
86dff6b6 2912 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
6326aa58 2913 self.changeRange)
4f6432d8 2914 changes = p4ChangesForPaths(self.depotPaths, self.changeRange)
b984733c 2915
01a9c9c5 2916 if len(self.maxChanges) > 0:
7fcff9de 2917 changes = changes[:min(int(self.maxChanges), len(changes))]
01a9c9c5 2918
b984733c 2919 if len(changes) == 0:
0828ab14 2920 if not self.silent:
341dc1c1 2921 print "No changes to import!"
06804c76
LD
2922 else:
2923 if not self.silent and not self.detectBranches:
2924 print "Import destination: %s" % self.branch
2925
2926 self.updatedBranches = set()
b984733c 2927
06804c76 2928 self.importChanges(changes)
a9d1a27a 2929
06804c76
LD
2930 if not self.silent:
2931 print ""
2932 if len(self.updatedBranches) > 0:
2933 sys.stdout.write("Updated branches: ")
2934 for b in self.updatedBranches:
2935 sys.stdout.write("%s " % b)
2936 sys.stdout.write("\n")
341dc1c1 2937
06804c76 2938 if gitConfig("git-p4.importLabels", "--bool") == "true":
06dcd152 2939 self.importLabels = True
b984733c 2940
06804c76
LD
2941 if self.importLabels:
2942 p4Labels = getP4Labels(self.depotPaths)
2943 gitTags = getGitTags()
2944
2945 missingP4Labels = p4Labels - gitTags
2946 self.importP4Labels(self.gitStream, missingP4Labels)
b984733c 2947
b984733c 2948 self.gitStream.close()
29bdbac1
SH
2949 if importProcess.wait() != 0:
2950 die("fast-import failed: %s" % self.gitError.read())
b984733c
SH
2951 self.gitOutput.close()
2952 self.gitError.close()
2953
fed23693
VA
2954 # Cleanup temporary branches created during import
2955 if self.tempBranches != []:
2956 for branch in self.tempBranches:
2957 read_pipe("git update-ref -d %s" % branch)
2958 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
2959
b984733c
SH
2960 return True
2961
01ce1fe9
SH
2962class P4Rebase(Command):
2963 def __init__(self):
2964 Command.__init__(self)
06804c76
LD
2965 self.options = [
2966 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
06804c76 2967 ]
06804c76 2968 self.importLabels = False
cebdf5af
HWN
2969 self.description = ("Fetches the latest revision from perforce and "
2970 + "rebases the current work (branch) against it")
01ce1fe9
SH
2971
2972 def run(self, args):
2973 sync = P4Sync()
06804c76 2974 sync.importLabels = self.importLabels
01ce1fe9 2975 sync.run([])
d7e3868c 2976
14594f4b
SH
2977 return self.rebase()
2978
2979 def rebase(self):
36ee4ee4
SH
2980 if os.system("git update-index --refresh") != 0:
2981 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.");
2982 if len(read_pipe("git diff-index HEAD --")) > 0:
2983 die("You have uncommited changes. Please commit them before rebasing or stash them away with git stash.");
2984
d7e3868c
SH
2985 [upstream, settings] = findUpstreamBranchPoint()
2986 if len(upstream) == 0:
2987 die("Cannot find upstream branchpoint for rebase")
2988
2989 # the branchpoint may be p4/foo~3, so strip off the parent
2990 upstream = re.sub("~[0-9]+$", "", upstream)
2991
2992 print "Rebasing the current branch onto %s" % upstream
b25b2065 2993 oldHead = read_pipe("git rev-parse HEAD").strip()
d7e3868c 2994 system("git rebase %s" % upstream)
1f52af6c 2995 system("git diff-tree --stat --summary -M %s HEAD" % oldHead)
01ce1fe9
SH
2996 return True
2997
f9a3a4f7
SH
2998class P4Clone(P4Sync):
2999 def __init__(self):
3000 P4Sync.__init__(self)
3001 self.description = "Creates a new git repository and imports from Perforce into it"
bb6e09b2 3002 self.usage = "usage: %prog [options] //depot/path[@revRange]"
354081d5 3003 self.options += [
bb6e09b2
HWN
3004 optparse.make_option("--destination", dest="cloneDestination",
3005 action='store', default=None,
354081d5
TT
3006 help="where to leave result of the clone"),
3007 optparse.make_option("-/", dest="cloneExclude",
3008 action="append", type="string",
38200076
PW
3009 help="exclude depot path"),
3010 optparse.make_option("--bare", dest="cloneBare",
3011 action="store_true", default=False),
354081d5 3012 ]
bb6e09b2 3013 self.cloneDestination = None
f9a3a4f7 3014 self.needsGit = False
38200076 3015 self.cloneBare = False
f9a3a4f7 3016
354081d5
TT
3017 # This is required for the "append" cloneExclude action
3018 def ensure_value(self, attr, value):
3019 if not hasattr(self, attr) or getattr(self, attr) is None:
3020 setattr(self, attr, value)
3021 return getattr(self, attr)
3022
6a49f8e2
HWN
3023 def defaultDestination(self, args):
3024 ## TODO: use common prefix of args?
3025 depotPath = args[0]
3026 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3027 depotDir = re.sub("(#[^#]*)$", "", depotDir)
053d9e43 3028 depotDir = re.sub(r"\.\.\.$", "", depotDir)
6a49f8e2
HWN
3029 depotDir = re.sub(r"/$", "", depotDir)
3030 return os.path.split(depotDir)[1]
3031
f9a3a4f7
SH
3032 def run(self, args):
3033 if len(args) < 1:
3034 return False
bb6e09b2
HWN
3035
3036 if self.keepRepoPath and not self.cloneDestination:
3037 sys.stderr.write("Must specify destination for --keep-path\n")
3038 sys.exit(1)
f9a3a4f7 3039
6326aa58 3040 depotPaths = args
5e100b5c
SH
3041
3042 if not self.cloneDestination and len(depotPaths) > 1:
3043 self.cloneDestination = depotPaths[-1]
3044 depotPaths = depotPaths[:-1]
3045
354081d5 3046 self.cloneExclude = ["/"+p for p in self.cloneExclude]
6326aa58
HWN
3047 for p in depotPaths:
3048 if not p.startswith("//"):
3049 return False
f9a3a4f7 3050
bb6e09b2 3051 if not self.cloneDestination:
98ad4faf 3052 self.cloneDestination = self.defaultDestination(args)
f9a3a4f7 3053
86dff6b6 3054 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
38200076 3055
c3bf3f13
KG
3056 if not os.path.exists(self.cloneDestination):
3057 os.makedirs(self.cloneDestination)
053fd0c1 3058 chdir(self.cloneDestination)
38200076
PW
3059
3060 init_cmd = [ "git", "init" ]
3061 if self.cloneBare:
3062 init_cmd.append("--bare")
3063 subprocess.check_call(init_cmd)
3064
6326aa58 3065 if not P4Sync.run(self, depotPaths):
f9a3a4f7 3066 return False
f9a3a4f7 3067 if self.branch != "master":
e9905013
TAL
3068 if self.importIntoRemotes:
3069 masterbranch = "refs/remotes/p4/master"
3070 else:
3071 masterbranch = "refs/heads/p4/master"
3072 if gitBranchExists(masterbranch):
3073 system("git branch master %s" % masterbranch)
38200076
PW
3074 if not self.cloneBare:
3075 system("git checkout -f")
8f9b2e08
SH
3076 else:
3077 print "Could not detect main branch. No checkout/master branch created."
86dff6b6 3078
a93d33ee
PW
3079 # auto-set this variable if invoked with --use-client-spec
3080 if self.useClientSpec_from_options:
3081 system("git config --bool git-p4.useclientspec true")
3082
f9a3a4f7
SH
3083 return True
3084
09d89de2
SH
3085class P4Branches(Command):
3086 def __init__(self):
3087 Command.__init__(self)
3088 self.options = [ ]
3089 self.description = ("Shows the git branches that hold imports and their "
3090 + "corresponding perforce depot paths")
3091 self.verbose = False
3092
3093 def run(self, args):
5ca44617
SH
3094 if originP4BranchesExist():
3095 createOrUpdateBranchesFromOrigin()
3096
09d89de2
SH
3097 cmdline = "git rev-parse --symbolic "
3098 cmdline += " --remotes"
3099
3100 for line in read_pipe_lines(cmdline):
3101 line = line.strip()
3102
3103 if not line.startswith('p4/') or line == "p4/HEAD":
3104 continue
3105 branch = line
3106
3107 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3108 settings = extractSettingsGitLog(log)
3109
3110 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3111 return True
3112
b984733c
SH
3113class HelpFormatter(optparse.IndentedHelpFormatter):
3114 def __init__(self):
3115 optparse.IndentedHelpFormatter.__init__(self)
3116
3117 def format_description(self, description):
3118 if description:
3119 return description + "\n"
3120 else:
3121 return ""
4f5cf76a 3122
86949eef
SH
3123def printUsage(commands):
3124 print "usage: %s <command> [options]" % sys.argv[0]
3125 print ""
3126 print "valid commands: %s" % ", ".join(commands)
3127 print ""
3128 print "Try %s <command> --help for command specific help." % sys.argv[0]
3129 print ""
3130
3131commands = {
b86f7378
HWN
3132 "debug" : P4Debug,
3133 "submit" : P4Submit,
a9834f58 3134 "commit" : P4Submit,
b86f7378
HWN
3135 "sync" : P4Sync,
3136 "rebase" : P4Rebase,
3137 "clone" : P4Clone,
09d89de2
SH
3138 "rollback" : P4RollBack,
3139 "branches" : P4Branches
86949eef
SH
3140}
3141
86949eef 3142
bb6e09b2
HWN
3143def main():
3144 if len(sys.argv[1:]) == 0:
3145 printUsage(commands.keys())
3146 sys.exit(2)
4f5cf76a 3147
bb6e09b2
HWN
3148 cmd = ""
3149 cmdName = sys.argv[1]
3150 try:
b86f7378
HWN
3151 klass = commands[cmdName]
3152 cmd = klass()
bb6e09b2
HWN
3153 except KeyError:
3154 print "unknown command %s" % cmdName
3155 print ""
3156 printUsage(commands.keys())
3157 sys.exit(2)
3158
3159 options = cmd.options
b86f7378 3160 cmd.gitdir = os.environ.get("GIT_DIR", None)
bb6e09b2
HWN
3161
3162 args = sys.argv[2:]
3163
b0ccc80d 3164 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
6a10b6aa
LD
3165 if cmd.needsGit:
3166 options.append(optparse.make_option("--git-dir", dest="gitdir"))
bb6e09b2 3167
6a10b6aa
LD
3168 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3169 options,
3170 description = cmd.description,
3171 formatter = HelpFormatter())
bb6e09b2 3172
6a10b6aa 3173 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
bb6e09b2
HWN
3174 global verbose
3175 verbose = cmd.verbose
3176 if cmd.needsGit:
b86f7378
HWN
3177 if cmd.gitdir == None:
3178 cmd.gitdir = os.path.abspath(".git")
3179 if not isValidGitDir(cmd.gitdir):
3180 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3181 if os.path.exists(cmd.gitdir):
bb6e09b2
HWN
3182 cdup = read_pipe("git rev-parse --show-cdup").strip()
3183 if len(cdup) > 0:
053fd0c1 3184 chdir(cdup);
e20a9e53 3185
b86f7378
HWN
3186 if not isValidGitDir(cmd.gitdir):
3187 if isValidGitDir(cmd.gitdir + "/.git"):
3188 cmd.gitdir += "/.git"
bb6e09b2 3189 else:
b86f7378 3190 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
e20a9e53 3191
b86f7378 3192 os.environ["GIT_DIR"] = cmd.gitdir
86949eef 3193
bb6e09b2
HWN
3194 if not cmd.run(args):
3195 parser.print_help()
09fca77b 3196 sys.exit(2)
4f5cf76a 3197
4f5cf76a 3198
bb6e09b2
HWN
3199if __name__ == '__main__':
3200 main()