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