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