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