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