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