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