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