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