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