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