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