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