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