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