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