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