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