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