blob: 8fbf6eb1fe385090489f55c87913d0867ad8c349 [file] [log] [blame]
Simon Hausmann86949ee2007-03-19 20:59:12 +01001#!/usr/bin/env python
2#
3# git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
4#
Simon Hausmannc8cbbee2007-05-28 14:43:25 +02005# Author: Simon Hausmann <simon@lst.de>
6# Copyright: 2007 Simon Hausmann <simon@lst.de>
Simon Hausmann83dce552007-03-19 22:26:36 +01007# 2007 Trolltech ASA
Simon Hausmann86949ee2007-03-19 20:59:12 +01008# License: MIT <http://www.opensource.org/licenses/mit-license.php>
9#
Joel Holdsworth794bb282022-04-01 15:24:49 +010010# 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
Luke Diamand4c1d5862020-01-29 11:12:43 +000032#
Joel Holdsworth4ff01082022-04-01 15:25:04 +010033
Tao Klerksf7b5ff62022-04-30 19:26:52 +000034import struct
Eric S. Raymonda33faf22012-12-28 11:40:59 -050035import sys
Yang Zhao0b4396f2019-12-13 15:52:35 -080036if sys.version_info.major < 3 and sys.version_info.minor < 7:
37 sys.stderr.write("git-p4: requires Python 2.7 or later.\n")
Eric S. Raymonda33faf22012-12-28 11:40:59 -050038 sys.exit(1)
Joel Holdsworth4ff01082022-04-01 15:25:04 +010039
40import ctypes
41import errno
Yang Zhaoa6b13062019-12-13 15:52:44 -080042import functools
Joel Holdsworth4ff01082022-04-01 15:25:04 +010043import glob
Pete Wyckofff629fa52013-01-26 22:11:05 -050044import marshal
Joel Holdsworth4ff01082022-04-01 15:25:04 +010045import optparse
46import os
Pete Wyckofff629fa52013-01-26 22:11:05 -050047import platform
48import re
49import shutil
Pete Wyckoffd20f0f82013-01-26 22:11:19 -050050import stat
Joel Holdsworth4ff01082022-04-01 15:25:04 +010051import subprocess
52import tempfile
53import time
Lars Schneidera5db4b12015-09-26 09:55:03 +020054import zipfile
55import zlib
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -030056
Yang Zhao7575f4f2019-12-13 15:52:47 -080057# 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
Luke Diamandefdcc992018-06-19 09:04:09 +010060# support basestring in python3
61try:
Yang Zhao7575f4f2019-12-13 15:52:47 -080062 if raw_input and input:
63 input = raw_input
64except:
65 pass
Brandon Caseya235e852013-01-26 11:14:33 -080066
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -030067verbose = False
Simon Hausmann86949ee2007-03-19 20:59:12 +010068
Luke Diamand06804c72012-04-11 17:21:24 +020069# Only labels/tags matching this will be imported/exported
Luke Diamandc8942a22012-04-11 17:21:24 +020070defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
Anand Kumria21a50752008-08-10 19:26:28 +010071
Luke Diamand3deed5e2018-06-08 21:32:48 +010072# The block size is reduced automatically if required
Joel Holdsworth6febb9f2022-04-01 15:24:58 +010073defaultBlockSize = 1 << 20
Luke Diamand1051ef02015-06-10 08:30:59 +010074
Tao Klerksf7b5ff62022-04-30 19:26:52 +000075defaultMetadataDecodingStrategy = 'passthrough' if sys.version_info.major == 2 else 'fallback'
76defaultFallbackMetadataEncoding = 'cp1252'
77
Luke Diamand0ef67ac2018-06-08 21:32:45 +010078p4_access_checked = False
Anand Kumria21a50752008-08-10 19:26:28 +010079
Joel Holdsworth70c0d552021-12-16 13:46:19 +000080re_ko_keywords = re.compile(br'\$(Id|Header)(:[^$\n]+)?\$')
81re_k_keywords = re.compile(br'\$(Id|Header|Author|Date|DateTime|Change|File|Revision)(:[^$\n]+)?\$')
Joel Holdsworthe665e982021-12-16 13:46:16 +000082
Joel Holdsworthadf159b2022-04-01 15:24:43 +010083
Joel Holdsworthae9b9502021-12-19 15:40:27 +000084def format_size_human_readable(num):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +010085 """Returns a number of units (typically bytes) formatted as a
86 human-readable string.
87 """
Joel Holdsworthae9b9502021-12-19 15:40:27 +000088 if num < 1024:
89 return '{:d} B'.format(num)
90 for unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
91 num /= 1024.0
92 if num < 1024.0:
93 return "{:3.1f} {}B".format(num, unit)
94 return "{:.1f} YiB".format(num)
95
Joel Holdsworthadf159b2022-04-01 15:24:43 +010096
Anand Kumria21a50752008-08-10 19:26:28 +010097def p4_build_cmd(cmd):
98 """Build a suitable p4 command line.
99
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100100 This consolidates building and returning a p4 command line into one
101 location. It means that hooking into the environment, or other
102 configuration can be done more easily.
103 """
Luke Diamand6de040d2011-10-16 10:47:52 -0400104 real_cmd = ["p4"]
Anand Kumriaabcaf072008-08-10 19:26:31 +0100105
106 user = gitConfig("git-p4.user")
107 if len(user) > 0:
Joel Holdsworth12a77f52022-04-01 15:24:53 +0100108 real_cmd += ["-u", user]
Anand Kumriaabcaf072008-08-10 19:26:31 +0100109
110 password = gitConfig("git-p4.password")
111 if len(password) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -0400112 real_cmd += ["-P", password]
Anand Kumriaabcaf072008-08-10 19:26:31 +0100113
114 port = gitConfig("git-p4.port")
115 if len(port) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -0400116 real_cmd += ["-p", port]
Anand Kumriaabcaf072008-08-10 19:26:31 +0100117
118 host = gitConfig("git-p4.host")
119 if len(host) > 0:
Russell Myers41799aa2012-02-22 11:16:05 -0800120 real_cmd += ["-H", host]
Anand Kumriaabcaf072008-08-10 19:26:31 +0100121
122 client = gitConfig("git-p4.client")
123 if len(client) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -0400124 real_cmd += ["-c", client]
Anand Kumriaabcaf072008-08-10 19:26:31 +0100125
Lars Schneider89a6ecc2016-12-04 15:03:11 +0100126 retries = gitConfigInt("git-p4.retries")
127 if retries is None:
128 # Perform 3 retries by default
129 retries = 3
Igor Kushnirbc233522016-12-29 12:22:23 +0200130 if retries > 0:
131 # Provide a way to not pass this option by setting git-p4.retries to 0
132 real_cmd += ["-r", str(retries)]
Luke Diamand6de040d2011-10-16 10:47:52 -0400133
Joel Holdsworth8a470592022-01-06 21:40:34 +0000134 real_cmd += cmd
Luke Diamand0ef67ac2018-06-08 21:32:45 +0100135
136 # now check that we can actually talk to the server
137 global p4_access_checked
138 if not p4_access_checked:
139 p4_access_checked = True # suppress access checks in p4_check_access itself
140 p4_check_access()
141
Anand Kumria21a50752008-08-10 19:26:28 +0100142 return real_cmd
143
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100144
Luke Diamand378f7be2016-12-13 21:51:28 +0000145def git_dir(path):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100146 """Return TRUE if the given path is a git directory (/path/to/dir/.git).
147 This won't automatically add ".git" to a directory.
148 """
Luke Diamand378f7be2016-12-13 21:51:28 +0000149 d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
150 if not d or len(d) == 0:
151 return None
152 else:
153 return d
154
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100155
Miklós Fazekasbbd84862013-03-11 17:45:29 -0400156def chdir(path, is_client_path=False):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100157 """Do chdir to the given path, and set the PWD environment variable for use
158 by P4. It does not look at getcwd() output. Since we're not using the
159 shell, it is necessary to set the PWD environment variable explicitly.
Miklós Fazekasbbd84862013-03-11 17:45:29 -0400160
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100161 Normally, expand the path to force it to be absolute. This addresses
162 the use of relative path names inside P4 settings, e.g.
163 P4CONFIG=.p4config. P4 does not simply open the filename as given; it
164 looks for .p4config using PWD.
Miklós Fazekasbbd84862013-03-11 17:45:29 -0400165
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100166 If is_client_path, the path was handed to us directly by p4, and may be
167 a symbolic link. Do not call os.getcwd() in this case, because it will
168 cause p4 to think that PWD is not inside the client path.
Miklós Fazekasbbd84862013-03-11 17:45:29 -0400169 """
170
171 os.chdir(path)
172 if not is_client_path:
173 path = os.getcwd()
174 os.environ['PWD'] = path
Robert Blum053fd0c2008-08-01 12:50:03 -0700175
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100176
Lars Schneider4d25dc42015-09-26 09:55:02 +0200177def calcDiskFree():
178 """Return free space in bytes on the disk of the given dirname."""
179 if platform.system() == 'Windows':
180 free_bytes = ctypes.c_ulonglong(0)
181 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
182 return free_bytes.value
183 else:
184 st = os.statvfs(os.getcwd())
185 return st.f_bavail * st.f_frsize
186
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100187
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -0300188def die(msg):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100189 """Terminate execution. Make sure that any running child processes have
190 been wait()ed for before calling this.
191 """
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -0300192 if verbose:
193 raise Exception(msg)
194 else:
195 sys.stderr.write(msg + "\n")
196 sys.exit(1)
197
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100198
Ben Keenee2aed5f2019-12-16 14:02:19 +0000199def prompt(prompt_text):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100200 """Prompt the user to choose one of the choices.
Ben Keenee2aed5f2019-12-16 14:02:19 +0000201
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100202 Choices are identified in the prompt_text by square brackets around a
203 single letter option.
204 """
Ben Keenee2aed5f2019-12-16 14:02:19 +0000205 choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
206 while True:
Ben Keene6b602a22020-02-11 18:57:58 +0000207 sys.stderr.flush()
208 sys.stdout.write(prompt_text)
209 sys.stdout.flush()
Joel Holdsworth6febb9f2022-04-01 15:24:58 +0100210 response = sys.stdin.readline().strip().lower()
Ben Keenee2aed5f2019-12-16 14:02:19 +0000211 if not response:
212 continue
213 response = response[0]
214 if response in choices:
215 return response
216
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100217
Yang Zhao6cec21a2019-12-13 15:52:38 -0800218# 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
222 def decode_text_stream(s):
223 return s.decode() if isinstance(s, bytes) else s
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100224
Yang Zhao6cec21a2019-12-13 15:52:38 -0800225 def encode_text_stream(s):
226 return s.encode() if isinstance(s, str) else s
227else:
228 # For python2.7, pass read strings as-is, but also allow writing unicode
229 def decode_text_stream(s):
230 return s
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100231
Yang Zhao6cec21a2019-12-13 15:52:38 -0800232 def encode_text_stream(s):
233 return s.encode('utf_8') if isinstance(s, unicode) else s
234
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100235
Tao Klerksf7b5ff62022-04-30 19:26:52 +0000236class MetadataDecodingException(Exception):
237 def __init__(self, input_string):
238 self.input_string = input_string
239
240 def __str__(self):
241 return """Decoding perforce metadata failed!
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):
254 encodingStrategy = gitConfig('git-p4.metadataDecodingStrategy') or defaultMetadataDecodingStrategy
255 fallbackEncoding = gitConfig('git-p4.metadataFallbackEncoding') or defaultFallbackMetadataEncoding
256 if not isinstance(s, bytes):
257 return s.encode('utf_8')
258 if encodingStrategy == 'passthrough':
259 return s
260 try:
261 s.decode('utf_8')
262 return s
263 except UnicodeDecodeError:
264 if encodingStrategy == 'fallback' and fallbackEncoding:
265 global encoding_fallback_warning_issued
266 global encoding_escape_warning_issued
267 try:
268 if not encoding_fallback_warning_issued:
269 print("\nCould not decode value as utf-8; using configured fallback encoding %s: %s" % (fallbackEncoding, s))
270 print("\n(this warning is only displayed once during an import)")
271 encoding_fallback_warning_issued = True
272 return s.decode(fallbackEncoding).encode('utf_8')
273 except Exception as exc:
274 if not encoding_escape_warning_issued:
275 print("\nCould not decode value with configured fallback encoding %s; escaping bytes over 127: %s" % (fallbackEncoding, s))
276 print("\n(this warning is only displayed once during an import)")
277 encoding_escape_warning_issued = True
278 escaped_bytes = b''
279 # bytes and strings work very differently in python2 vs python3...
280 if str is bytes:
281 for byte in s:
282 byte_number = struct.unpack('>B', byte)[0]
283 if byte_number > 127:
284 escaped_bytes += b'%'
285 escaped_bytes += hex(byte_number)[2:].upper()
286 else:
287 escaped_bytes += byte
288 else:
289 for byte_number in s:
290 if byte_number > 127:
291 escaped_bytes += b'%'
292 escaped_bytes += hex(byte_number).upper().encode()[2:]
293 else:
294 escaped_bytes += bytes([byte_number])
295 return escaped_bytes
296
297 raise MetadataDecodingException(s)
298
Junio C Hamano3af1df02022-05-20 15:27:00 -0700299
Yang Zhaod38208a2019-12-13 15:52:40 -0800300def decode_path(path):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100301 """Decode a given string (bytes or otherwise) using configured path
302 encoding options.
303 """
304
Yang Zhaod38208a2019-12-13 15:52:40 -0800305 encoding = gitConfig('git-p4.pathEncoding') or 'utf_8'
306 if bytes is not str:
307 return path.decode(encoding, errors='replace') if isinstance(path, bytes) else path
308 else:
309 try:
310 path.decode('ascii')
311 except:
312 path = path.decode(encoding, errors='replace')
313 if verbose:
314 print('Path with non-ASCII characters detected. Used {} to decode: {}'.format(encoding, path))
315 return path
316
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100317
Ben Keene9f59ca42020-02-11 18:57:59 +0000318def run_git_hook(cmd, param=[]):
319 """Execute a hook if the hook exists."""
Emily Shaffer0c8ac062021-12-22 04:59:39 +0100320 args = ['git', 'hook', 'run', '--ignore-missing', cmd]
321 if param:
322 args.append("--")
323 for p in param:
324 args.append(p)
325 return subprocess.call(args) == 0
Ben Keene9f59ca42020-02-11 18:57:59 +0000326
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100327
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000328def write_pipe(c, stdin, *k, **kw):
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300329 if verbose:
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000330 sys.stderr.write('Writing pipe: {}\n'.format(' '.join(c)))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300331
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000332 p = subprocess.Popen(c, stdin=subprocess.PIPE, *k, **kw)
Luke Diamand6de040d2011-10-16 10:47:52 -0400333 pipe = p.stdin
334 val = pipe.write(stdin)
335 pipe.close()
336 if p.wait():
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000337 die('Command failed: {}'.format(' '.join(c)))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300338
339 return val
340
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100341
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000342def p4_write_pipe(c, stdin, *k, **kw):
Anand Kumriad9429192008-08-14 23:40:38 +0100343 real_cmd = p4_build_cmd(c)
Yang Zhao6cec21a2019-12-13 15:52:38 -0800344 if bytes is not str and isinstance(stdin, str):
345 stdin = encode_text_stream(stdin)
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000346 return write_pipe(real_cmd, stdin, *k, **kw)
Anand Kumriad9429192008-08-14 23:40:38 +0100347
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100348
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000349def read_pipe_full(c, *k, **kw):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100350 """Read output from command. Returns a tuple of the return status, stdout
351 text and stderr text.
352 """
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300353 if verbose:
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000354 sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -0300355
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000356 p = subprocess.Popen(
357 c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, *k, **kw)
Joel Holdsworth0874bb02022-04-01 15:24:52 +0100358 out, err = p.communicate()
Yang Zhao6cec21a2019-12-13 15:52:38 -0800359 return (p.returncode, out, decode_text_stream(err))
Luke Diamand78871bf2017-04-15 11:36:08 +0100360
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100361
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000362def read_pipe(c, ignore_error=False, raw=False, *k, **kw):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100363 """Read output from command. Returns the output text on success. On
364 failure, terminates execution, unless ignore_error is True, when it
365 returns an empty string.
Yang Zhao86dca242019-12-13 15:52:39 -0800366
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100367 If raw is True, do not attempt to decode output text.
368 """
Joel Holdsworth0874bb02022-04-01 15:24:52 +0100369 retcode, out, err = read_pipe_full(c, *k, **kw)
Luke Diamand78871bf2017-04-15 11:36:08 +0100370 if retcode != 0:
371 if ignore_error:
372 out = ""
373 else:
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000374 die('Command failed: {}\nError: {}'.format(' '.join(c), err))
Yang Zhao86dca242019-12-13 15:52:39 -0800375 if not raw:
376 out = decode_text_stream(out)
Lars Schneider1f5f3902015-09-21 12:01:41 +0200377 return out
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300378
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100379
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000380def read_pipe_text(c, *k, **kw):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100381 """Read output from a command with trailing whitespace stripped. On error,
382 returns None.
383 """
Joel Holdsworth0874bb02022-04-01 15:24:52 +0100384 retcode, out, err = read_pipe_full(c, *k, **kw)
Luke Diamand78871bf2017-04-15 11:36:08 +0100385 if retcode != 0:
386 return None
387 else:
Yang Zhao6cec21a2019-12-13 15:52:38 -0800388 return decode_text_stream(out).rstrip()
Luke Diamand78871bf2017-04-15 11:36:08 +0100389
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100390
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000391def p4_read_pipe(c, ignore_error=False, raw=False, *k, **kw):
Anand Kumriad9429192008-08-14 23:40:38 +0100392 real_cmd = p4_build_cmd(c)
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000393 return read_pipe(real_cmd, ignore_error, raw=raw, *k, **kw)
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300394
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100395
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000396def read_pipe_lines(c, raw=False, *k, **kw):
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300397 if verbose:
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000398 sys.stderr.write('Reading pipe: {}\n'.format(' '.join(c)))
Luke Diamand6de040d2011-10-16 10:47:52 -0400399
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000400 p = subprocess.Popen(c, stdout=subprocess.PIPE, *k, **kw)
Luke Diamand6de040d2011-10-16 10:47:52 -0400401 pipe = p.stdout
Joel Holdsworth9732e222021-12-16 13:46:17 +0000402 lines = pipe.readlines()
403 if not raw:
404 lines = [decode_text_stream(line) for line in lines]
Luke Diamand6de040d2011-10-16 10:47:52 -0400405 if pipe.close() or p.wait():
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000406 die('Command failed: {}'.format(' '.join(c)))
Joel Holdsworth9732e222021-12-16 13:46:17 +0000407 return lines
Simon Hausmanncaace112007-05-15 14:57:57 +0200408
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100409
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000410def p4_read_pipe_lines(c, *k, **kw):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100411 """Specifically invoke p4 on the command supplied."""
Anand Kumria155af832008-08-10 19:26:30 +0100412 real_cmd = p4_build_cmd(c)
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000413 return read_pipe_lines(real_cmd, *k, **kw)
Anand Kumria23181212008-08-10 19:26:24 +0100414
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100415
Gary Gibbons8e9497c2012-07-12 19:29:00 -0400416def p4_has_command(cmd):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100417 """Ask p4 for help on this command. If it returns an error, the command
418 does not exist in this version of p4.
419 """
Gary Gibbons8e9497c2012-07-12 19:29:00 -0400420 real_cmd = p4_build_cmd(["help", cmd])
421 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
422 stderr=subprocess.PIPE)
423 p.communicate()
424 return p.returncode == 0
425
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100426
Pete Wyckoff249da4c2012-11-23 17:35:35 -0500427def p4_has_move_command():
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100428 """See if the move command exists, that it supports -k, and that it has not
429 been administratively disabled. The arguments must be correct, but the
430 filenames do not have to exist. Use ones with wildcards so even if they
431 exist, it will fail.
432 """
Pete Wyckoff249da4c2012-11-23 17:35:35 -0500433
434 if not p4_has_command("move"):
435 return False
436 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
437 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Joel Holdsworth0874bb02022-04-01 15:24:52 +0100438 out, err = p.communicate()
Yang Zhao6cec21a2019-12-13 15:52:38 -0800439 err = decode_text_stream(err)
Pete Wyckoff249da4c2012-11-23 17:35:35 -0500440 # return code will be 1 in either case
441 if err.find("Invalid option") >= 0:
442 return False
443 if err.find("disabled") >= 0:
444 return False
445 # assume it failed because @... was invalid changelist
446 return True
447
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100448
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000449def system(cmd, ignore_error=False, *k, **kw):
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300450 if verbose:
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000451 sys.stderr.write("executing {}\n".format(
452 ' '.join(cmd) if isinstance(cmd, list) else cmd))
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000453 retcode = subprocess.call(cmd, *k, **kw)
Luke Diamandcbff4b22015-11-21 09:54:40 +0000454 if retcode and not ignore_error:
Joel Holdsworth40e7cfd2022-01-06 21:41:56 +0000455 raise subprocess.CalledProcessError(retcode, cmd)
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -0300456
Luke Diamandcbff4b22015-11-21 09:54:40 +0000457 return retcode
458
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100459
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000460def p4_system(cmd, *k, **kw):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100461 """Specifically invoke p4 as the system command."""
Anand Kumria155af832008-08-10 19:26:30 +0100462 real_cmd = p4_build_cmd(cmd)
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000463 retcode = subprocess.call(real_cmd, *k, **kw)
Brandon Caseya235e852013-01-26 11:14:33 -0800464 if retcode:
Joel Holdsworth40e7cfd2022-01-06 21:41:56 +0000465 raise subprocess.CalledProcessError(retcode, real_cmd)
Luke Diamand6de040d2011-10-16 10:47:52 -0400466
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100467
Luke Diamand0ef67ac2018-06-08 21:32:45 +0100468def die_bad_access(s):
469 die("failure accessing depot: {0}".format(s.rstrip()))
470
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100471
Luke Diamand0ef67ac2018-06-08 21:32:45 +0100472def p4_check_access(min_expiration=1):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100473 """Check if we can access Perforce - account still logged in."""
474
Luke Diamand0ef67ac2018-06-08 21:32:45 +0100475 results = p4CmdList(["login", "-s"])
476
477 if len(results) == 0:
478 # should never get here: always get either some results, or a p4ExitCode
479 assert("could not parse response from perforce")
480
481 result = results[0]
482
483 if 'p4ExitCode' in result:
484 # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
485 die_bad_access("could not run p4")
486
487 code = result.get("code")
488 if not code:
489 # we get here if we couldn't connect and there was nothing to unmarshal
490 die_bad_access("could not connect")
491
492 elif code == "stat":
493 expiry = result.get("TicketExpiration")
494 if expiry:
495 expiry = int(expiry)
496 if expiry > min_expiration:
497 # ok to carry on
498 return
499 else:
500 die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
501
502 else:
503 # account without a timeout - all ok
504 return
505
506 elif code == "error":
507 data = result.get("data")
508 if data:
509 die_bad_access("p4 error: {0}".format(data))
510 else:
511 die_bad_access("unknown error")
Peter Osterlundd4990d52019-01-07 21:51:38 +0100512 elif code == "info":
513 return
Luke Diamand0ef67ac2018-06-08 21:32:45 +0100514 else:
515 die_bad_access("unknown error code {0}".format(code))
516
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100517
Pete Wyckoff7f0e5962013-01-26 22:11:13 -0500518_p4_version_string = None
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100519
520
Pete Wyckoff7f0e5962013-01-26 22:11:13 -0500521def p4_version_string():
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100522 """Read the version string, showing just the last line, which hopefully is
523 the interesting version bit.
Pete Wyckoff7f0e5962013-01-26 22:11:13 -0500524
525 $ p4 -V
526 Perforce - The Fast Software Configuration Management System.
527 Copyright 1995-2011 Perforce Software. All rights reserved.
528 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100529 """
Pete Wyckoff7f0e5962013-01-26 22:11:13 -0500530 global _p4_version_string
531 if not _p4_version_string:
532 a = p4_read_pipe_lines(["-V"])
533 _p4_version_string = a[-1].rstrip()
534 return _p4_version_string
535
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100536
Luke Diamand6de040d2011-10-16 10:47:52 -0400537def p4_integrate(src, dest):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400538 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400539
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100540
Pete Wyckoff8d7ec362012-04-29 20:57:14 -0400541def p4_sync(f, *options):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400542 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400543
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100544
Luke Diamand6de040d2011-10-16 10:47:52 -0400545def p4_add(f):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100546 """Forcibly add file names with wildcards."""
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400547 if wildcard_present(f):
548 p4_system(["add", "-f", f])
549 else:
550 p4_system(["add", f])
Luke Diamand6de040d2011-10-16 10:47:52 -0400551
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100552
Luke Diamand6de040d2011-10-16 10:47:52 -0400553def p4_delete(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400554 p4_system(["delete", wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400555
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100556
Romain Picarda02b8bc2016-01-12 13:43:47 +0100557def p4_edit(f, *options):
558 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400559
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100560
Luke Diamand6de040d2011-10-16 10:47:52 -0400561def p4_revert(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400562 p4_system(["revert", wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400563
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100564
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400565def p4_reopen(type, f):
566 p4_system(["reopen", "-t", type, wildcard_encode(f)])
Anand Kumriabf9320f2008-08-10 19:26:26 +0100567
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100568
Luke Diamand46c609e2016-12-02 22:43:19 +0000569def p4_reopen_in_change(changelist, files):
570 cmd = ["reopen", "-c", str(changelist)] + files
571 p4_system(cmd)
572
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100573
Gary Gibbons8e9497c2012-07-12 19:29:00 -0400574def p4_move(src, dest):
575 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
576
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100577
Luke Diamand1051ef02015-06-10 08:30:59 +0100578def p4_last_change():
Miguel Torroja1997e912017-07-13 09:00:35 +0200579 results = p4CmdList(["changes", "-m", "1"], skip_info=True)
Luke Diamand1051ef02015-06-10 08:30:59 +0100580 return int(results[0]['change'])
581
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100582
Luke Diamand123f6312018-05-23 23:20:26 +0100583def p4_describe(change, shelved=False):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100584 """Make sure it returns a valid result by checking for the presence of
585 field "time".
586
587 Return a dict of the results.
588 """
Pete Wyckoff18fa13d2012-11-23 17:35:34 -0500589
Luke Diamand123f6312018-05-23 23:20:26 +0100590 cmd = ["describe", "-s"]
591 if shelved:
592 cmd += ["-S"]
593 cmd += [str(change)]
594
595 ds = p4CmdList(cmd, skip_info=True)
Pete Wyckoff18fa13d2012-11-23 17:35:34 -0500596 if len(ds) != 1:
597 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
598
599 d = ds[0]
600
601 if "p4ExitCode" in d:
602 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
603 str(d)))
604 if "code" in d:
605 if d["code"] == "error":
606 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
607
608 if "time" not in d:
609 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
610
611 return d
612
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100613
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -0400614def split_p4_type(p4type):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100615 """Canonicalize the p4 type and return a tuple of the base type, plus any
616 modifiers. See "p4 help filetypes" for a list and explanation.
617 """
David Brownb9fc6ea2007-09-19 13:12:48 -0700618
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -0400619 p4_filetypes_historical = {
620 "ctempobj": "binary+Sw",
621 "ctext": "text+C",
622 "cxtext": "text+Cx",
623 "ktext": "text+k",
624 "kxtext": "text+kx",
625 "ltext": "text+F",
626 "tempobj": "binary+FSw",
627 "ubinary": "binary+F",
628 "uresource": "resource+F",
629 "uxbinary": "binary+Fx",
630 "xbinary": "binary+x",
631 "xltext": "text+Fx",
632 "xtempobj": "binary+Swx",
633 "xtext": "text+x",
634 "xunicode": "unicode+x",
635 "xutf16": "utf16+x",
636 }
637 if p4type in p4_filetypes_historical:
638 p4type = p4_filetypes_historical[p4type]
639 mods = ""
640 s = p4type.split("+")
641 base = s[0]
642 mods = ""
643 if len(s) > 1:
644 mods = s[1]
645 return (base, mods)
646
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100647
Pete Wyckoff79467e62014-01-21 18:16:45 -0500648def p4_type(f):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100649 """Return the raw p4 type of a file (text, text+ko, etc)."""
650
Pete Wyckoff79467e62014-01-21 18:16:45 -0500651 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
Luke Diamand60df0712012-02-23 07:51:30 +0000652 return results[0]['headType']
653
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100654
Luke Diamand60df0712012-02-23 07:51:30 +0000655def p4_keywords_regexp_for_type(base, type_mods):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100656 """Given a type base and modifier, return a regexp matching the keywords
657 that can be expanded in the file.
658 """
659
Luke Diamand60df0712012-02-23 07:51:30 +0000660 if base in ("text", "unicode", "binary"):
Luke Diamand60df0712012-02-23 07:51:30 +0000661 if "ko" in type_mods:
Joel Holdsworthe665e982021-12-16 13:46:16 +0000662 return re_ko_keywords
Luke Diamand60df0712012-02-23 07:51:30 +0000663 elif "k" in type_mods:
Joel Holdsworthe665e982021-12-16 13:46:16 +0000664 return re_k_keywords
Luke Diamand60df0712012-02-23 07:51:30 +0000665 else:
666 return None
Luke Diamand60df0712012-02-23 07:51:30 +0000667 else:
668 return None
669
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100670
Luke Diamand60df0712012-02-23 07:51:30 +0000671def p4_keywords_regexp_for_file(file):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100672 """Given a file, return a regexp matching the possible RCS keywords that
673 will be expanded, or None for files with kw expansion turned off.
674 """
675
Luke Diamand60df0712012-02-23 07:51:30 +0000676 if not os.path.exists(file):
677 return None
678 else:
Joel Holdsworth0874bb02022-04-01 15:24:52 +0100679 type_base, type_mods = split_p4_type(p4_type(file))
Luke Diamand60df0712012-02-23 07:51:30 +0000680 return p4_keywords_regexp_for_type(type_base, type_mods)
David Brownb9fc6ea2007-09-19 13:12:48 -0700681
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100682
Chris Pettittc65b6702007-11-01 20:43:14 -0700683def setP4ExecBit(file, mode):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100684 """Reopens an already open file and changes the execute bit to match the
685 execute bit setting in the passed in mode.
686 """
Chris Pettittc65b6702007-11-01 20:43:14 -0700687
688 p4Type = "+x"
689
690 if not isModeExec(mode):
691 p4Type = getP4OpenedType(file)
692 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
693 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
694 if p4Type[-1] == "+":
695 p4Type = p4Type[0:-1]
696
Luke Diamand6de040d2011-10-16 10:47:52 -0400697 p4_reopen(p4Type, file)
Chris Pettittc65b6702007-11-01 20:43:14 -0700698
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100699
Chris Pettittc65b6702007-11-01 20:43:14 -0700700def getP4OpenedType(file):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100701 """Returns the perforce file type for the given file."""
Chris Pettittc65b6702007-11-01 20:43:14 -0700702
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400703 result = p4_read_pipe(["opened", wildcard_encode(file)])
Blair Holloway34a0dbf2015-04-04 09:46:03 +0100704 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
Chris Pettittc65b6702007-11-01 20:43:14 -0700705 if match:
706 return match.group(1)
707 else:
Marius Storm-Olsenf3e5ae42008-03-28 15:40:40 +0100708 die("Could not determine file type for %s (result: '%s')" % (file, result))
Chris Pettittc65b6702007-11-01 20:43:14 -0700709
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100710
Luke Diamand06804c72012-04-11 17:21:24 +0200711def getP4Labels(depotPaths):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100712 """Return the set of all p4 labels."""
713
Luke Diamand06804c72012-04-11 17:21:24 +0200714 labels = set()
Ben Keene484d09c2019-12-13 15:52:36 -0800715 if not isinstance(depotPaths, list):
Luke Diamand06804c72012-04-11 17:21:24 +0200716 depotPaths = [depotPaths]
717
718 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
719 label = l['label']
720 labels.add(label)
721
722 return labels
723
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100724
Luke Diamand06804c72012-04-11 17:21:24 +0200725def getGitTags():
Joel Holdsworth522e9142022-04-01 15:24:47 +0100726 """Return the set of all git tags."""
727
Luke Diamand06804c72012-04-11 17:21:24 +0200728 gitTags = set()
729 for line in read_pipe_lines(["git", "tag"]):
730 tag = line.strip()
731 gitTags.add(tag)
732 return gitTags
733
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100734
Yang Zhaoce425eb2019-12-13 15:52:46 -0800735_diff_tree_pattern = None
Chris Pettittb43b0a32007-11-01 20:43:13 -0700736
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100737
Chris Pettittb43b0a32007-11-01 20:43:13 -0700738def parseDiffTreeEntry(entry):
739 """Parses a single diff tree entry into its component elements.
740
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100741 See git-diff-tree(1) manpage for details about the format of the diff
742 output. This method returns a dictionary with the following elements:
Chris Pettittb43b0a32007-11-01 20:43:13 -0700743
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100744 src_mode - The mode of the source file
745 dst_mode - The mode of the destination file
746 src_sha1 - The sha1 for the source file
747 dst_sha1 - The sha1 fr the destination file
748 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
749 status_score - The score for the status (applicable for 'C' and 'R'
750 statuses). This is None if there is no score.
751 src - The path for the source file.
752 dst - The path for the destination file. This is only present for
753 copy or renames. If it is not present, this is None.
Chris Pettittb43b0a32007-11-01 20:43:13 -0700754
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100755 If the pattern is not matched, None is returned.
756 """
Chris Pettittb43b0a32007-11-01 20:43:13 -0700757
Yang Zhaoce425eb2019-12-13 15:52:46 -0800758 global _diff_tree_pattern
759 if not _diff_tree_pattern:
760 _diff_tree_pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
761
762 match = _diff_tree_pattern.match(entry)
Chris Pettittb43b0a32007-11-01 20:43:13 -0700763 if match:
764 return {
765 'src_mode': match.group(1),
766 'dst_mode': match.group(2),
767 'src_sha1': match.group(3),
768 'dst_sha1': match.group(4),
769 'status': match.group(5),
770 'status_score': match.group(6),
771 'src': match.group(7),
772 'dst': match.group(10)
773 }
774 return None
775
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100776
Chris Pettittc65b6702007-11-01 20:43:14 -0700777def isModeExec(mode):
Joel Holdsworth522e9142022-04-01 15:24:47 +0100778 """Returns True if the given git mode represents an executable file,
779 otherwise False.
780 """
Chris Pettittc65b6702007-11-01 20:43:14 -0700781 return mode[-3:] == "755"
782
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100783
Luke Diamand55bb3e32018-06-08 21:32:46 +0100784class P4Exception(Exception):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100785 """Base class for exceptions from the p4 client."""
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100786
Luke Diamand55bb3e32018-06-08 21:32:46 +0100787 def __init__(self, exit_code):
788 self.p4ExitCode = exit_code
789
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100790
Luke Diamand55bb3e32018-06-08 21:32:46 +0100791class P4ServerException(P4Exception):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100792 """Base class for exceptions where we get some kind of marshalled up result
793 from the server.
794 """
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100795
Luke Diamand55bb3e32018-06-08 21:32:46 +0100796 def __init__(self, exit_code, p4_result):
797 super(P4ServerException, self).__init__(exit_code)
798 self.p4_result = p4_result
799 self.code = p4_result[0]['code']
800 self.data = p4_result[0]['data']
801
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100802
Luke Diamand55bb3e32018-06-08 21:32:46 +0100803class P4RequestSizeException(P4ServerException):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100804 """One of the maxresults or maxscanrows errors."""
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100805
Luke Diamand55bb3e32018-06-08 21:32:46 +0100806 def __init__(self, exit_code, p4_result, limit):
807 super(P4RequestSizeException, self).__init__(exit_code, p4_result)
808 self.limit = limit
809
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100810
Luke Diamand5c3d5022020-01-29 11:12:42 +0000811class P4CommandException(P4Exception):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +0100812 """Something went wrong calling p4 which means we have to give up."""
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100813
Luke Diamand5c3d5022020-01-29 11:12:42 +0000814 def __init__(self, msg):
815 self.msg = msg
816
817 def __str__(self):
818 return self.msg
819
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100820
Chris Pettittc65b6702007-11-01 20:43:14 -0700821def isModeExecChanged(src_mode, dst_mode):
822 return isModeExec(src_mode) != isModeExec(dst_mode)
823
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100824
Luke Diamand55bb3e32018-06-08 21:32:46 +0100825def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000826 errors_as_exceptions=False, *k, **kw):
Luke Diamand6de040d2011-10-16 10:47:52 -0400827
Joel Holdsworth8a470592022-01-06 21:40:34 +0000828 cmd = p4_build_cmd(["-G"] + cmd)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -0300829 if verbose:
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000830 sys.stderr.write("Opening pipe: {}\n".format(' '.join(cmd)))
Scott Lamb9f90c732007-07-15 20:58:10 -0700831
832 # Use a temporary file to avoid deadlocks without
833 # subprocess.communicate(), which would put another copy
834 # of stdout into memory.
835 stdin_file = None
836 if stdin is not None:
837 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
Ben Keene484d09c2019-12-13 15:52:36 -0800838 if not isinstance(stdin, list):
Luke Diamand6de040d2011-10-16 10:47:52 -0400839 stdin_file.write(stdin)
840 else:
841 for i in stdin:
Yang Zhao86dca242019-12-13 15:52:39 -0800842 stdin_file.write(encode_text_stream(i))
843 stdin_file.write(b'\n')
Scott Lamb9f90c732007-07-15 20:58:10 -0700844 stdin_file.flush()
845 stdin_file.seek(0)
846
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000847 p4 = subprocess.Popen(
848 cmd, stdin=stdin_file, stdout=subprocess.PIPE, *k, **kw)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100849
850 result = []
851 try:
852 while True:
Scott Lamb9f90c732007-07-15 20:58:10 -0700853 entry = marshal.load(p4.stdout)
Yang Zhao6cec21a2019-12-13 15:52:38 -0800854 if bytes is not str:
855 # Decode unmarshalled dict to use str keys and values, except for:
856 # - `data` which may contain arbitrary binary data
Tao Klerksf7b5ff62022-04-30 19:26:52 +0000857 # - `desc` or `FullName` which may contain non-UTF8 encoded text handled below, eagerly converted to bytes
858 # - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text, handled by decode_path()
Yang Zhao6cec21a2019-12-13 15:52:38 -0800859 decoded_entry = {}
860 for key, value in entry.items():
861 key = key.decode()
Tao Klerksf7b5ff62022-04-30 19:26:52 +0000862 if isinstance(value, bytes) and not (key in ('data', 'desc', 'FullName', 'path', 'clientFile') or key.startswith('depotFile')):
Yang Zhao6cec21a2019-12-13 15:52:38 -0800863 value = value.decode()
864 decoded_entry[key] = value
865 # Parse out data if it's an error response
866 if decoded_entry.get('code') == 'error' and 'data' in decoded_entry:
867 decoded_entry['data'] = decoded_entry['data'].decode()
868 entry = decoded_entry
Miguel Torroja1997e912017-07-13 09:00:35 +0200869 if skip_info:
870 if 'code' in entry and entry['code'] == 'info':
871 continue
Tao Klerksf7b5ff62022-04-30 19:26:52 +0000872 if 'desc' in entry:
873 entry['desc'] = metadata_stream_to_writable_bytes(entry['desc'])
874 if 'FullName' in entry:
875 entry['FullName'] = metadata_stream_to_writable_bytes(entry['FullName'])
Andrew Garberc3f61632011-04-07 02:01:21 -0400876 if cb is not None:
877 cb(entry)
878 else:
879 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100880 except EOFError:
881 pass
Scott Lamb9f90c732007-07-15 20:58:10 -0700882 exitCode = p4.wait()
883 if exitCode != 0:
Luke Diamand55bb3e32018-06-08 21:32:46 +0100884 if errors_as_exceptions:
885 if len(result) > 0:
886 data = result[0].get('data')
887 if data:
888 m = re.search('Too many rows scanned \(over (\d+)\)', data)
889 if not m:
890 m = re.search('Request too large \(over (\d+)\)', data)
891
892 if m:
893 limit = int(m.group(1))
894 raise P4RequestSizeException(exitCode, result, limit)
895
896 raise P4ServerException(exitCode, result)
897 else:
898 raise P4Exception(exitCode)
899 else:
900 entry = {}
901 entry["p4ExitCode"] = exitCode
902 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100903
904 return result
905
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100906
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000907def p4Cmd(cmd, *k, **kw):
908 list = p4CmdList(cmd, *k, **kw)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100909 result = {}
910 for entry in list:
911 result.update(entry)
Joel Holdsworth990547a2022-04-01 15:24:44 +0100912 return result
Simon Hausmann86949ee2007-03-19 20:59:12 +0100913
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100914
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100915def p4Where(depotPath):
916 if not depotPath.endswith("/"):
917 depotPath += "/"
Vitor Antunescd884102015-04-21 23:49:30 +0100918 depotPathLong = depotPath + "..."
919 outputList = p4CmdList(["where", depotPathLong])
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100920 output = None
921 for entry in outputList:
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100922 if "depotFile" in entry:
Vitor Antunescd884102015-04-21 23:49:30 +0100923 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
924 # The base path always ends with "/...".
Yang Zhaod38208a2019-12-13 15:52:40 -0800925 entry_path = decode_path(entry['depotFile'])
926 if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...":
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100927 output = entry
928 break
929 elif "data" in entry:
930 data = entry.get("data")
931 space = data.find(" ")
932 if data[:space] == depotPath:
933 output = entry
934 break
Joel Holdsworthda0134f2022-04-01 15:25:00 +0100935 if output is None:
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100936 return ""
Simon Hausmanndc524032007-05-21 09:34:56 +0200937 if output["code"] == "error":
938 return ""
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100939 clientPath = ""
940 if "path" in output:
Yang Zhaod38208a2019-12-13 15:52:40 -0800941 clientPath = decode_path(output['path'])
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100942 elif "data" in output:
943 data = output.get("data")
Yang Zhaod38208a2019-12-13 15:52:40 -0800944 lastSpace = data.rfind(b" ")
945 clientPath = decode_path(data[lastSpace + 1:])
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100946
947 if clientPath.endswith("..."):
948 clientPath = clientPath[:-3]
949 return clientPath
950
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100951
Simon Hausmann86949ee2007-03-19 20:59:12 +0100952def currentGitBranch():
Luke Diamandeff45112017-04-15 11:36:09 +0100953 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
Simon Hausmann86949ee2007-03-19 20:59:12 +0100954
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100955
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100956def isValidGitDir(path):
Joel Holdsworthda0134f2022-04-01 15:25:00 +0100957 return git_dir(path) is not None
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100958
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100959
Simon Hausmann463e8af2007-05-17 09:13:54 +0200960def parseRevision(ref):
Joel Holdsworth8a470592022-01-06 21:40:34 +0000961 return read_pipe(["git", "rev-parse", ref]).strip()
Simon Hausmann463e8af2007-05-17 09:13:54 +0200962
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100963
Pete Wyckoff28755db2011-12-24 21:07:40 -0500964def branchExists(ref):
965 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
966 ignore_error=True)
967 return len(rev) > 0
968
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100969
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100970def extractLogMessageFromGitCommit(commit):
971 logMessage = ""
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300972
Joel Holdsworthc785e202022-04-01 15:24:57 +0100973 # fixme: title is first line of commit, not 1st paragraph.
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100974 foundTitle = False
Mike Muellerc3f23582019-05-28 11:15:46 -0700975 for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
Joel Holdsworth812ee742022-04-01 15:24:45 +0100976 if not foundTitle:
977 if len(log) == 1:
978 foundTitle = True
979 continue
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100980
Joel Holdsworth812ee742022-04-01 15:24:45 +0100981 logMessage += log
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100982 return logMessage
983
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100984
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -0300985def extractSettingsGitLog(log):
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100986 values = {}
987 for line in log.split("\n"):
988 line = line.strip()
Joel Holdsworth843d8472022-04-01 15:24:54 +0100989 m = re.search(r"^ *\[git-p4: (.*)\]$", line)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300990 if not m:
991 continue
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100992
Joel Holdsworth843d8472022-04-01 15:24:54 +0100993 assignments = m.group(1).split(':')
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300994 for a in assignments:
Joel Holdsworth843d8472022-04-01 15:24:54 +0100995 vals = a.split('=')
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300996 key = vals[0].strip()
Joel Holdsworth843d8472022-04-01 15:24:54 +0100997 val = ('='.join(vals[1:])).strip()
998 if val.endswith('\"') and val.startswith('"'):
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300999 val = val[1:-1]
1000
1001 values[key] = val
1002
Simon Hausmann845b42c2007-06-07 09:19:34 +02001003 paths = values.get("depot-paths")
1004 if not paths:
1005 paths = values.get("depot-path")
Simon Hausmanna3fdd572007-06-07 22:54:32 +02001006 if paths:
1007 values['depot-paths'] = paths.split(',')
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001008 return values
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001009
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001010
Simon Hausmann8136a632007-03-22 21:27:14 +01001011def gitBranchExists(branch):
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001012 proc = subprocess.Popen(["git", "rev-parse", branch],
Joel Holdsworth990547a2022-04-01 15:24:44 +01001013 stderr=subprocess.PIPE, stdout=subprocess.PIPE)
1014 return proc.wait() == 0
Simon Hausmann8136a632007-03-22 21:27:14 +01001015
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001016
Luke Diamand123f6312018-05-23 23:20:26 +01001017def gitUpdateRef(ref, newvalue):
1018 subprocess.check_call(["git", "update-ref", ref, newvalue])
1019
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001020
Luke Diamand123f6312018-05-23 23:20:26 +01001021def gitDeleteRef(ref):
1022 subprocess.check_call(["git", "update-ref", "-d", ref])
1023
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001024
John Chapman36bd8442008-11-08 14:22:49 +11001025_gitConfig = {}
Pete Wyckoffb345d6c2013-01-26 22:11:23 -05001026
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001027
Lars Schneider692e1792015-09-26 09:54:58 +02001028def gitConfig(key, typeSpecifier=None):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001029 if key not in _gitConfig:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001030 cmd = ["git", "config"]
Lars Schneider692e1792015-09-26 09:54:58 +02001031 if typeSpecifier:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001032 cmd += [typeSpecifier]
1033 cmd += [key]
Pete Wyckoffb345d6c2013-01-26 22:11:23 -05001034 s = read_pipe(cmd, ignore_error=True)
1035 _gitConfig[key] = s.strip()
John Chapman36bd8442008-11-08 14:22:49 +11001036 return _gitConfig[key]
Simon Hausmann01265102007-05-25 10:36:10 +02001037
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001038
Pete Wyckoff0d609032013-01-26 22:11:24 -05001039def gitConfigBool(key):
1040 """Return a bool, using git config --bool. It is True only if the
1041 variable is set to true, and False if set to false or not present
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001042 in the config.
1043 """
Pete Wyckoff0d609032013-01-26 22:11:24 -05001044
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001045 if key not in _gitConfig:
Lars Schneider692e1792015-09-26 09:54:58 +02001046 _gitConfig[key] = gitConfig(key, '--bool') == "true"
Simon Hausmann062410b2007-07-18 10:56:31 +02001047 return _gitConfig[key]
1048
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001049
Lars Schneidercb1dafd2015-09-26 09:54:59 +02001050def gitConfigInt(key):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001051 if key not in _gitConfig:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001052 cmd = ["git", "config", "--int", key]
Simon Hausmannb9847332007-03-20 20:54:23 +01001053 s = read_pipe(cmd, ignore_error=True)
Simon Hausmann062410b2007-07-18 10:56:31 +02001054 v = s.strip()
Lars Schneidercb1dafd2015-09-26 09:54:59 +02001055 try:
1056 _gitConfig[key] = int(gitConfig(key, '--int'))
1057 except ValueError:
1058 _gitConfig[key] = None
Simon Hausmann062410b2007-07-18 10:56:31 +02001059 return _gitConfig[key]
1060
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001061
Vitor Antunes7199cf12011-08-19 00:44:05 +01001062def gitConfigList(key):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001063 if key not in _gitConfig:
Pete Wyckoff2abba302013-01-26 22:11:22 -05001064 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
George Vanburghc3c2b052017-01-25 09:17:29 +00001065 _gitConfig[key] = s.strip().splitlines()
Lars Schneider7960e702015-09-26 09:55:00 +02001066 if _gitConfig[key] == ['']:
1067 _gitConfig[key] = []
Vitor Antunes7199cf12011-08-19 00:44:05 +01001068 return _gitConfig[key]
1069
Tao Klerks17f273f2022-04-04 05:10:54 +00001070def fullP4Ref(incomingRef, importIntoRemotes=True):
1071 """Standardize a given provided p4 ref value to a full git ref:
1072 refs/foo/bar/branch -> use it exactly
1073 p4/branch -> prepend refs/remotes/ or refs/heads/
1074 branch -> prepend refs/remotes/p4/ or refs/heads/p4/"""
1075 if incomingRef.startswith("refs/"):
1076 return incomingRef
1077 if importIntoRemotes:
1078 prepend = "refs/remotes/"
1079 else:
1080 prepend = "refs/heads/"
1081 if not incomingRef.startswith("p4/"):
1082 prepend += "p4/"
1083 return prepend + incomingRef
1084
1085def shortP4Ref(incomingRef, importIntoRemotes=True):
1086 """Standardize to a "short ref" if possible:
1087 refs/foo/bar/branch -> ignore
1088 refs/remotes/p4/branch or refs/heads/p4/branch -> shorten
1089 p4/branch -> shorten"""
1090 if importIntoRemotes:
1091 longprefix = "refs/remotes/p4/"
1092 else:
1093 longprefix = "refs/heads/p4/"
1094 if incomingRef.startswith(longprefix):
1095 return incomingRef[len(longprefix):]
1096 if incomingRef.startswith("p4/"):
1097 return incomingRef[3:]
1098 return incomingRef
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001099
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001100def p4BranchesInGit(branchesAreInRemotes=True):
1101 """Find all the branches whose names start with "p4/", looking
1102 in remotes or heads as specified by the argument. Return
1103 a dictionary of { branch: revision } for each one found.
1104 The branch names are the short names, without any
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001105 "p4/" prefix.
1106 """
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001107
Simon Hausmann062410b2007-07-18 10:56:31 +02001108 branches = {}
1109
Joel Holdsworth8a470592022-01-06 21:40:34 +00001110 cmdline = ["git", "rev-parse", "--symbolic"]
Simon Hausmann062410b2007-07-18 10:56:31 +02001111 if branchesAreInRemotes:
Joel Holdsworth8a470592022-01-06 21:40:34 +00001112 cmdline.append("--remotes")
Simon Hausmann062410b2007-07-18 10:56:31 +02001113 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00001114 cmdline.append("--branches")
Simon Hausmann062410b2007-07-18 10:56:31 +02001115
1116 for line in read_pipe_lines(cmdline):
1117 line = line.strip()
1118
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001119 # only import to p4/
1120 if not line.startswith('p4/'):
Simon Hausmann062410b2007-07-18 10:56:31 +02001121 continue
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001122 # special symbolic ref to p4/master
1123 if line == "p4/HEAD":
1124 continue
Simon Hausmann062410b2007-07-18 10:56:31 +02001125
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001126 # strip off p4/ prefix
1127 branch = line[len("p4/"):]
Simon Hausmann062410b2007-07-18 10:56:31 +02001128
1129 branches[branch] = parseRevision(line)
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001130
Simon Hausmann062410b2007-07-18 10:56:31 +02001131 return branches
1132
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001133
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05001134def branch_exists(branch):
1135 """Make sure that the given ref name really exists."""
1136
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001137 cmd = ["git", "rev-parse", "--symbolic", "--verify", branch]
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05001138 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1139 out, _ = p.communicate()
Yang Zhao6cec21a2019-12-13 15:52:38 -08001140 out = decode_text_stream(out)
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05001141 if p.returncode:
1142 return False
1143 # expect exactly one line of output: the branch name
1144 return out.rstrip() == branch
1145
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001146
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01001147def findUpstreamBranchPoint(head="HEAD"):
Simon Hausmann86506fe2007-07-18 12:40:12 +02001148 branches = p4BranchesInGit()
1149 # map from depot-path to branch name
1150 branchByDepotPath = {}
1151 for branch in branches.keys():
1152 tip = branches[branch]
1153 log = extractLogMessageFromGitCommit(tip)
1154 settings = extractSettingsGitLog(log)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001155 if "depot-paths" in settings:
Kirill Frolov944db252022-03-21 12:43:22 +00001156 git_branch = "remotes/p4/" + branch
Simon Hausmann86506fe2007-07-18 12:40:12 +02001157 paths = ",".join(settings["depot-paths"])
Kirill Frolov944db252022-03-21 12:43:22 +00001158 branchByDepotPath[paths] = git_branch
1159 if "change" in settings:
1160 paths = paths + ";" + settings["change"]
1161 branchByDepotPath[paths] = git_branch
Simon Hausmann86506fe2007-07-18 12:40:12 +02001162
Simon Hausmann27d2d812007-06-12 14:31:59 +02001163 settings = None
Simon Hausmann27d2d812007-06-12 14:31:59 +02001164 parent = 0
1165 while parent < 65535:
Simon Hausmann9ceab362007-06-22 00:01:57 +02001166 commit = head + "~%s" % parent
Simon Hausmann27d2d812007-06-12 14:31:59 +02001167 log = extractLogMessageFromGitCommit(commit)
1168 settings = extractSettingsGitLog(log)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001169 if "depot-paths" in settings:
Simon Hausmann86506fe2007-07-18 12:40:12 +02001170 paths = ",".join(settings["depot-paths"])
Kirill Frolov944db252022-03-21 12:43:22 +00001171 if "change" in settings:
1172 expaths = paths + ";" + settings["change"]
1173 if expaths in branchByDepotPath:
1174 return [branchByDepotPath[expaths], settings]
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001175 if paths in branchByDepotPath:
Simon Hausmann86506fe2007-07-18 12:40:12 +02001176 return [branchByDepotPath[paths], settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +02001177
Simon Hausmann86506fe2007-07-18 12:40:12 +02001178 parent = parent + 1
Simon Hausmann27d2d812007-06-12 14:31:59 +02001179
Simon Hausmann86506fe2007-07-18 12:40:12 +02001180 return ["", settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +02001181
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001182
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01001183def createOrUpdateBranchesFromOrigin(localRefPrefix="refs/remotes/p4/", silent=True):
Simon Hausmann5ca44612007-08-24 17:44:16 +02001184 if not silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01001185 print("Creating/updating branch(es) in %s based on origin branch(es)"
Simon Hausmann5ca44612007-08-24 17:44:16 +02001186 % localRefPrefix)
1187
1188 originPrefix = "origin/p4/"
1189
Joel Holdsworth8a470592022-01-06 21:40:34 +00001190 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
Simon Hausmann5ca44612007-08-24 17:44:16 +02001191 line = line.strip()
1192 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
1193 continue
1194
1195 headName = line[len(originPrefix):]
1196 remoteHead = localRefPrefix + headName
1197 originHead = line
1198
1199 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01001200 if 'depot-paths' not in original or 'change' not in original:
Simon Hausmann5ca44612007-08-24 17:44:16 +02001201 continue
1202
1203 update = False
1204 if not gitBranchExists(remoteHead):
1205 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01001206 print("creating %s" % remoteHead)
Simon Hausmann5ca44612007-08-24 17:44:16 +02001207 update = True
1208 else:
1209 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001210 if 'change' in settings:
Simon Hausmann5ca44612007-08-24 17:44:16 +02001211 if settings['depot-paths'] == original['depot-paths']:
1212 originP4Change = int(original['change'])
1213 p4Change = int(settings['change'])
1214 if originP4Change > p4Change:
Luke Diamandf2606b12018-06-19 09:04:10 +01001215 print("%s (%s) is newer than %s (%s). "
Simon Hausmann5ca44612007-08-24 17:44:16 +02001216 "Updating p4 branch from origin."
1217 % (originHead, originP4Change,
1218 remoteHead, p4Change))
1219 update = True
1220 else:
Luke Diamandf2606b12018-06-19 09:04:10 +01001221 print("Ignoring: %s was imported from %s while "
Simon Hausmann5ca44612007-08-24 17:44:16 +02001222 "%s was imported from %s"
1223 % (originHead, ','.join(original['depot-paths']),
1224 remoteHead, ','.join(settings['depot-paths'])))
1225
1226 if update:
Joel Holdsworth8a470592022-01-06 21:40:34 +00001227 system(["git", "update-ref", remoteHead, originHead])
Simon Hausmann5ca44612007-08-24 17:44:16 +02001228
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001229
Simon Hausmann5ca44612007-08-24 17:44:16 +02001230def originP4BranchesExist():
Joel Holdsworth812ee742022-04-01 15:24:45 +01001231 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
Simon Hausmann5ca44612007-08-24 17:44:16 +02001232
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001233
Luke Diamand1051ef02015-06-10 08:30:59 +01001234def p4ParseNumericChangeRange(parts):
1235 changeStart = int(parts[0][1:])
1236 if parts[1] == '#head':
1237 changeEnd = p4_last_change()
1238 else:
1239 changeEnd = int(parts[1])
1240
1241 return (changeStart, changeEnd)
1242
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001243
Luke Diamand1051ef02015-06-10 08:30:59 +01001244def chooseBlockSize(blockSize):
1245 if blockSize:
1246 return blockSize
1247 else:
1248 return defaultBlockSize
1249
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001250
Luke Diamand1051ef02015-06-10 08:30:59 +01001251def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1252 assert depotPaths
1253
1254 # Parse the change range into start and end. Try to find integer
1255 # revision ranges as these can be broken up into blocks to avoid
1256 # hitting server-side limits (maxrows, maxscanresults). But if
1257 # that doesn't work, fall back to using the raw revision specifier
1258 # strings, without using block mode.
1259
Lex Spoon96b2d542015-04-20 11:00:20 -04001260 if changeRange is None or changeRange == '':
Luke Diamand1051ef02015-06-10 08:30:59 +01001261 changeStart = 1
1262 changeEnd = p4_last_change()
1263 block_size = chooseBlockSize(requestedBlockSize)
Lex Spoon96b2d542015-04-20 11:00:20 -04001264 else:
1265 parts = changeRange.split(',')
1266 assert len(parts) == 2
Luke Diamand1051ef02015-06-10 08:30:59 +01001267 try:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001268 changeStart, changeEnd = p4ParseNumericChangeRange(parts)
Luke Diamand1051ef02015-06-10 08:30:59 +01001269 block_size = chooseBlockSize(requestedBlockSize)
Luke Diamand8fa0abf2018-06-08 21:32:47 +01001270 except ValueError:
Luke Diamand1051ef02015-06-10 08:30:59 +01001271 changeStart = parts[0][1:]
1272 changeEnd = parts[1]
1273 if requestedBlockSize:
1274 die("cannot use --changes-block-size with non-numeric revisions")
1275 block_size = None
Lex Spoon96b2d542015-04-20 11:00:20 -04001276
George Vanburgh9943e5b2016-12-17 22:11:23 +00001277 changes = set()
Lex Spoon96b2d542015-04-20 11:00:20 -04001278
Sam Hocevar1f90a642015-12-19 09:39:40 +00001279 # Retrieve changes a block at a time, to prevent running
Luke Diamand3deed5e2018-06-08 21:32:48 +01001280 # into a MaxResults/MaxScanRows error from the server. If
1281 # we _do_ hit one of those errors, turn down the block size
Luke Diamand1051ef02015-06-10 08:30:59 +01001282
Sam Hocevar1f90a642015-12-19 09:39:40 +00001283 while True:
1284 cmd = ['changes']
Luke Diamand1051ef02015-06-10 08:30:59 +01001285
Sam Hocevar1f90a642015-12-19 09:39:40 +00001286 if block_size:
1287 end = min(changeEnd, changeStart + block_size)
1288 revisionRange = "%d,%d" % (changeStart, end)
1289 else:
1290 revisionRange = "%s,%s" % (changeStart, changeEnd)
Luke Diamand1051ef02015-06-10 08:30:59 +01001291
Sam Hocevar1f90a642015-12-19 09:39:40 +00001292 for p in depotPaths:
Luke Diamand1051ef02015-06-10 08:30:59 +01001293 cmd += ["%s...@%s" % (p, revisionRange)]
1294
Luke Diamand3deed5e2018-06-08 21:32:48 +01001295 # fetch the changes
1296 try:
1297 result = p4CmdList(cmd, errors_as_exceptions=True)
1298 except P4RequestSizeException as e:
1299 if not block_size:
1300 block_size = e.limit
1301 elif block_size > e.limit:
1302 block_size = e.limit
1303 else:
1304 block_size = max(2, block_size // 2)
1305
Joel Holdsworthe8f8b3b2022-04-01 15:25:03 +01001306 if verbose:
1307 print("block size error, retrying with block size {0}".format(block_size))
Luke Diamand3deed5e2018-06-08 21:32:48 +01001308 continue
1309 except P4Exception as e:
1310 die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1311
Sam Hocevar1f90a642015-12-19 09:39:40 +00001312 # Insert changes in chronological order
Luke Diamand3deed5e2018-06-08 21:32:48 +01001313 for entry in reversed(result):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001314 if 'change' not in entry:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001315 continue
1316 changes.add(int(entry['change']))
Luke Diamand1051ef02015-06-10 08:30:59 +01001317
Sam Hocevar1f90a642015-12-19 09:39:40 +00001318 if not block_size:
1319 break
Luke Diamand1051ef02015-06-10 08:30:59 +01001320
Sam Hocevar1f90a642015-12-19 09:39:40 +00001321 if end >= changeEnd:
1322 break
Luke Diamand1051ef02015-06-10 08:30:59 +01001323
Sam Hocevar1f90a642015-12-19 09:39:40 +00001324 changeStart = end + 1
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001325
Sam Hocevar1f90a642015-12-19 09:39:40 +00001326 changes = sorted(changes)
1327 return changes
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001328
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001329
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01001330def p4PathStartsWith(path, prefix):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001331 """This method tries to remedy a potential mixed-case issue:
1332
1333 If UserA adds //depot/DirA/file1
1334 and UserB adds //depot/dira/file2
1335
1336 we may or may not have a problem. If you have core.ignorecase=true,
1337 we treat DirA and dira as the same directory.
1338 """
Pete Wyckoff0d609032013-01-26 22:11:24 -05001339 if gitConfigBool("core.ignorecase"):
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01001340 return path.lower().startswith(prefix.lower())
1341 return path.startswith(prefix)
1342
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001343
Pete Wyckoff543987b2012-02-25 20:06:25 -05001344def getClientSpec():
1345 """Look at the p4 client spec, create a View() object that contains
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001346 all the mappings, and return it.
1347 """
Pete Wyckoff543987b2012-02-25 20:06:25 -05001348
Joel Holdsworth8a470592022-01-06 21:40:34 +00001349 specList = p4CmdList(["client", "-o"])
Pete Wyckoff543987b2012-02-25 20:06:25 -05001350 if len(specList) != 1:
1351 die('Output from "client -o" is %d lines, expecting 1' %
1352 len(specList))
1353
1354 # dictionary of all client parameters
1355 entry = specList[0]
1356
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001357 # the //client/ name
1358 client_name = entry["Client"]
1359
Pete Wyckoff543987b2012-02-25 20:06:25 -05001360 # just the keys that start with "View"
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001361 view_keys = [k for k in entry.keys() if k.startswith("View")]
Pete Wyckoff543987b2012-02-25 20:06:25 -05001362
1363 # hold this new View
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001364 view = View(client_name)
Pete Wyckoff543987b2012-02-25 20:06:25 -05001365
1366 # append the lines, in order, to the view
1367 for view_num in range(len(view_keys)):
1368 k = "View%d" % view_num
1369 if k not in view_keys:
1370 die("Expected view key %s missing" % k)
1371 view.append(entry[k])
1372
1373 return view
1374
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001375
Pete Wyckoff543987b2012-02-25 20:06:25 -05001376def getClientRoot():
1377 """Grab the client directory."""
1378
Joel Holdsworth8a470592022-01-06 21:40:34 +00001379 output = p4CmdList(["client", "-o"])
Pete Wyckoff543987b2012-02-25 20:06:25 -05001380 if len(output) != 1:
1381 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1382
1383 entry = output[0]
1384 if "Root" not in entry:
1385 die('Client has no "Root"')
1386
1387 return entry["Root"]
1388
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001389
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001390def wildcard_decode(path):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001391 """Decode P4 wildcards into %xx encoding
1392
1393 P4 wildcards are not allowed in filenames. P4 complains if you simply
1394 add them, but you can force it with "-f", in which case it translates
1395 them into %xx encoding internally.
1396 """
1397
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001398 # Search for and fix just these four characters. Do % last so
1399 # that fixing it does not inadvertently create new %-escapes.
1400 # Cannot have * in a filename in windows; untested as to
1401 # what p4 would do in such a case.
1402 if not platform.system() == "Windows":
1403 path = path.replace("%2A", "*")
1404 path = path.replace("%23", "#") \
1405 .replace("%40", "@") \
1406 .replace("%25", "%")
1407 return path
1408
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001409
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001410def wildcard_encode(path):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001411 """Encode %xx coded wildcards into P4 coding."""
1412
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001413 # do % first to avoid double-encoding the %s introduced here
1414 path = path.replace("%", "%25") \
1415 .replace("*", "%2A") \
1416 .replace("#", "%23") \
1417 .replace("@", "%40")
1418 return path
1419
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001420
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001421def wildcard_present(path):
Brandon Casey598354c2013-01-26 11:14:32 -08001422 m = re.search("[*#@%]", path)
1423 return m is not None
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001424
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001425
Lars Schneidera5db4b12015-09-26 09:55:03 +02001426class LargeFileSystem(object):
1427 """Base class for large file system support."""
1428
1429 def __init__(self, writeToGitStream):
1430 self.largeFiles = set()
1431 self.writeToGitStream = writeToGitStream
1432
1433 def generatePointer(self, cloneDestination, contentFile):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001434 """Return the content of a pointer file that is stored in Git instead
1435 of the actual content.
1436 """
Lars Schneidera5db4b12015-09-26 09:55:03 +02001437 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1438
1439 def pushFile(self, localLargeFile):
1440 """Push the actual content which is not stored in the Git repository to
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001441 a server.
1442 """
Lars Schneidera5db4b12015-09-26 09:55:03 +02001443 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1444
1445 def hasLargeFileExtension(self, relPath):
Yang Zhaoa6b13062019-12-13 15:52:44 -08001446 return functools.reduce(
Lars Schneidera5db4b12015-09-26 09:55:03 +02001447 lambda a, b: a or b,
1448 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1449 False
1450 )
1451
1452 def generateTempFile(self, contents):
1453 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1454 for d in contents:
1455 contentFile.write(d)
1456 contentFile.close()
1457 return contentFile.name
1458
1459 def exceedsLargeFileThreshold(self, relPath, contents):
1460 if gitConfigInt('git-p4.largeFileThreshold'):
1461 contentsSize = sum(len(d) for d in contents)
1462 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1463 return True
1464 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1465 contentsSize = sum(len(d) for d in contents)
1466 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1467 return False
1468 contentTempFile = self.generateTempFile(contents)
Philip.McGrawde5abb52019-08-27 06:43:58 +03001469 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1470 with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1471 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1472 compressedContentsSize = zf.infolist()[0].compress_size
Lars Schneidera5db4b12015-09-26 09:55:03 +02001473 os.remove(contentTempFile)
Lars Schneidera5db4b12015-09-26 09:55:03 +02001474 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1475 return True
1476 return False
1477
1478 def addLargeFile(self, relPath):
1479 self.largeFiles.add(relPath)
1480
1481 def removeLargeFile(self, relPath):
1482 self.largeFiles.remove(relPath)
1483
1484 def isLargeFile(self, relPath):
1485 return relPath in self.largeFiles
1486
1487 def processContent(self, git_mode, relPath, contents):
1488 """Processes the content of git fast import. This method decides if a
1489 file is stored in the large file system and handles all necessary
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001490 steps.
1491 """
Lars Schneidera5db4b12015-09-26 09:55:03 +02001492 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1493 contentTempFile = self.generateTempFile(contents)
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001494 pointer_git_mode, contents, localLargeFile = self.generatePointer(contentTempFile)
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001495 if pointer_git_mode:
1496 git_mode = pointer_git_mode
1497 if localLargeFile:
1498 # Move temp file to final location in large file system
1499 largeFileDir = os.path.dirname(localLargeFile)
1500 if not os.path.isdir(largeFileDir):
1501 os.makedirs(largeFileDir)
1502 shutil.move(contentTempFile, localLargeFile)
1503 self.addLargeFile(relPath)
1504 if gitConfigBool('git-p4.largeFilePush'):
1505 self.pushFile(localLargeFile)
1506 if verbose:
1507 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
Lars Schneidera5db4b12015-09-26 09:55:03 +02001508 return (git_mode, contents)
1509
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001510
Lars Schneidera5db4b12015-09-26 09:55:03 +02001511class MockLFS(LargeFileSystem):
1512 """Mock large file system for testing."""
1513
1514 def generatePointer(self, contentFile):
1515 """The pointer content is the original content prefixed with "pointer-".
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001516 The local filename of the large file storage is derived from the
1517 file content.
Lars Schneidera5db4b12015-09-26 09:55:03 +02001518 """
1519 with open(contentFile, 'r') as f:
1520 content = next(f)
1521 gitMode = '100644'
1522 pointerContents = 'pointer-' + content
1523 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1524 return (gitMode, pointerContents, localLargeFile)
1525
1526 def pushFile(self, localLargeFile):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001527 """The remote filename of the large file storage is the same as the
1528 local one but in a different directory.
Lars Schneidera5db4b12015-09-26 09:55:03 +02001529 """
1530 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1531 if not os.path.exists(remotePath):
1532 os.makedirs(remotePath)
1533 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1534
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001535
Lars Schneiderb47d8072015-09-26 09:55:04 +02001536class GitLFS(LargeFileSystem):
1537 """Git LFS as backend for the git-p4 large file system.
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001538 See https://git-lfs.github.com/ for details.
1539 """
Lars Schneiderb47d8072015-09-26 09:55:04 +02001540
1541 def __init__(self, *args):
1542 LargeFileSystem.__init__(self, *args)
1543 self.baseGitAttributes = []
1544
1545 def generatePointer(self, contentFile):
1546 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1547 mode and content which is stored in the Git repository instead of
1548 the actual content. Return also the new location of the actual
1549 content.
1550 """
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001551 if os.path.getsize(contentFile) == 0:
1552 return (None, '', None)
1553
Lars Schneiderb47d8072015-09-26 09:55:04 +02001554 pointerProcess = subprocess.Popen(
1555 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1556 stdout=subprocess.PIPE
1557 )
Yang Zhao86dca242019-12-13 15:52:39 -08001558 pointerFile = decode_text_stream(pointerProcess.stdout.read())
Lars Schneiderb47d8072015-09-26 09:55:04 +02001559 if pointerProcess.wait():
1560 os.remove(contentFile)
1561 die('git-lfs pointer command failed. Did you install the extension?')
Lars Schneider82f25672016-04-28 08:26:33 +02001562
1563 # Git LFS removed the preamble in the output of the 'pointer' command
1564 # starting from version 1.2.0. Check for the preamble here to support
1565 # earlier versions.
1566 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1567 if pointerFile.startswith('Git LFS pointer for'):
1568 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1569
1570 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
r.burenkovea94b162019-12-11 09:47:23 -08001571 # if someone use external lfs.storage ( not in local repo git )
1572 lfs_path = gitConfig('lfs.storage')
1573 if not lfs_path:
1574 lfs_path = 'lfs'
1575 if not os.path.isabs(lfs_path):
1576 lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001577 localLargeFile = os.path.join(
r.burenkovea94b162019-12-11 09:47:23 -08001578 lfs_path,
1579 'objects', oid[:2], oid[2:4],
Lars Schneiderb47d8072015-09-26 09:55:04 +02001580 oid,
1581 )
1582 # LFS Spec states that pointer files should not have the executable bit set.
1583 gitMode = '100644'
Lars Schneider82f25672016-04-28 08:26:33 +02001584 return (gitMode, pointerFile, localLargeFile)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001585
1586 def pushFile(self, localLargeFile):
1587 uploadProcess = subprocess.Popen(
1588 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1589 )
1590 if uploadProcess.wait():
1591 die('git-lfs push command failed. Did you define a remote?')
1592
1593 def generateGitAttributes(self):
1594 return (
1595 self.baseGitAttributes +
1596 [
1597 '\n',
1598 '#\n',
1599 '# Git LFS (see https://git-lfs.github.com/)\n',
1600 '#\n',
1601 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001602 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001603 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1604 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001605 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001606 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1607 ]
1608 )
1609
1610 def addLargeFile(self, relPath):
1611 LargeFileSystem.addLargeFile(self, relPath)
1612 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1613
1614 def removeLargeFile(self, relPath):
1615 LargeFileSystem.removeLargeFile(self, relPath)
1616 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1617
1618 def processContent(self, git_mode, relPath, contents):
1619 if relPath == '.gitattributes':
1620 self.baseGitAttributes = contents
1621 return (git_mode, self.generateGitAttributes())
1622 else:
1623 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1624
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001625
Simon Hausmannb9847332007-03-20 20:54:23 +01001626class Command:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001627 delete_actions = ("delete", "move/delete", "purge")
1628 add_actions = ("add", "branch", "move/add")
Luke Diamand89143ac2018-10-15 12:14:08 +01001629
Simon Hausmannb9847332007-03-20 20:54:23 +01001630 def __init__(self):
1631 self.usage = "usage: %prog [options]"
Simon Hausmann8910ac02007-03-26 08:18:55 +02001632 self.needsGit = True
Luke Diamand6a10b6a2012-04-24 09:33:23 +01001633 self.verbose = False
Simon Hausmannb9847332007-03-20 20:54:23 +01001634
Mazo, Andreyff8c50e2019-04-01 18:02:26 +00001635 # This is required for the "append" update_shelve action
Luke Diamand8cf422d2017-12-21 11:06:14 +00001636 def ensure_value(self, attr, value):
1637 if not hasattr(self, attr) or getattr(self, attr) is None:
1638 setattr(self, attr, value)
1639 return getattr(self, attr)
1640
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001641
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001642class P4UserMap:
1643 def __init__(self):
1644 self.userMapFromPerforceServer = False
Luke Diamandaffb4742012-01-19 09:52:27 +00001645 self.myP4UserId = None
1646
1647 def p4UserId(self):
1648 if self.myP4UserId:
1649 return self.myP4UserId
1650
Joel Holdsworth8a470592022-01-06 21:40:34 +00001651 results = p4CmdList(["user", "-o"])
Luke Diamandaffb4742012-01-19 09:52:27 +00001652 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001653 if 'User' in r:
Luke Diamandaffb4742012-01-19 09:52:27 +00001654 self.myP4UserId = r['User']
1655 return r['User']
1656 die("Could not find your p4 user id")
1657
1658 def p4UserIsMe(self, p4User):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001659 """Return True if the given p4 user is actually me."""
Luke Diamandaffb4742012-01-19 09:52:27 +00001660 me = self.p4UserId()
1661 if not p4User or p4User != me:
1662 return False
1663 else:
1664 return True
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001665
1666 def getUserCacheFilename(self):
1667 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1668 return home + "/.gitp4-usercache.txt"
1669
1670 def getUserMapFromPerforceServer(self):
1671 if self.userMapFromPerforceServer:
1672 return
1673 self.users = {}
1674 self.emails = {}
1675
Joel Holdsworth8a470592022-01-06 21:40:34 +00001676 for output in p4CmdList(["users"]):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001677 if "User" not in output:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001678 continue
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001679 # "FullName" is bytes. "Email" on the other hand might be bytes
1680 # or unicode string depending on whether we are running under
1681 # python2 or python3. To support
1682 # git-p4.metadataDecodingStrategy=fallback, self.users dict values
1683 # are always bytes, ready to be written to git.
1684 emailbytes = metadata_stream_to_writable_bytes(output["Email"])
1685 self.users[output["User"]] = output["FullName"] + b" <" + emailbytes + b">"
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001686 self.emails[output["Email"]] = output["User"]
1687
Lars Schneider10d08a12016-03-01 11:49:56 +01001688 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1689 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1690 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1691 if mapUser and len(mapUser[0]) == 3:
1692 user = mapUser[0][0]
1693 fullname = mapUser[0][1]
1694 email = mapUser[0][2]
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001695 fulluser = fullname + " <" + email + ">"
1696 self.users[user] = metadata_stream_to_writable_bytes(fulluser)
Lars Schneider10d08a12016-03-01 11:49:56 +01001697 self.emails[email] = user
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001698
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001699 s = b''
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001700 for (key, val) in self.users.items():
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001701 keybytes = metadata_stream_to_writable_bytes(key)
1702 s += b"%s\t%s\n" % (keybytes.expandtabs(1), val.expandtabs(1))
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001703
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001704 open(self.getUserCacheFilename(), 'wb').write(s)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001705 self.userMapFromPerforceServer = True
1706
1707 def loadUserMapFromCache(self):
1708 self.users = {}
1709 self.userMapFromPerforceServer = False
1710 try:
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001711 cache = open(self.getUserCacheFilename(), 'rb')
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001712 lines = cache.readlines()
1713 cache.close()
1714 for line in lines:
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001715 entry = line.strip().split(b"\t")
1716 self.users[entry[0].decode('utf_8')] = entry[1]
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001717 except IOError:
1718 self.getUserMapFromPerforceServer()
1719
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001720
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001721class P4Submit(Command, P4UserMap):
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001722
1723 conflict_behavior_choices = ("ask", "skip", "quit")
1724
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001725 def __init__(self):
Simon Hausmannb9847332007-03-20 20:54:23 +01001726 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001727 P4UserMap.__init__(self)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001728 self.options = [
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001729 optparse.make_option("--origin", dest="origin"),
Vitor Antunesae901092011-02-20 01:18:24 +00001730 optparse.make_option("-M", dest="detectRenames", action="store_true"),
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001731 # preserve the user, requires relevant p4 permissions
1732 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02001733 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
Pete Wyckoffef739f02012-09-09 16:16:11 -04001734 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001735 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001736 optparse.make_option("--conflict", dest="conflict_behavior",
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001737 choices=self.conflict_behavior_choices),
1738 optparse.make_option("--branch", dest="branch"),
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001739 optparse.make_option("--shelve", dest="shelve", action="store_true",
1740 help="Shelve instead of submit. Shelved files are reverted, "
1741 "restoring the workspace to the state before the shelve"),
Luke Diamand8cf422d2017-12-21 11:06:14 +00001742 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
Luke Diamand46c609e2016-12-02 22:43:19 +00001743 metavar="CHANGELIST",
Luke Diamand8cf422d2017-12-21 11:06:14 +00001744 help="update an existing shelved changelist, implies --shelve, "
Romain Merlandf55b87c2018-06-01 09:46:14 +02001745 "repeat in-order for multiple shelved changelists"),
1746 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1747 help="submit only the specified commit(s), one commit or xxx..xxx"),
1748 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1749 help="Disable rebase after submit is completed. Can be useful if you "
Luke Diamandb9d34db2018-06-08 21:32:44 +01001750 "work from a local git branch that is not master"),
1751 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1752 help="Skip Perforce sync of p4/master after submit or shelve"),
Ben Keene4935c452020-02-11 18:58:01 +00001753 optparse.make_option("--no-verify", dest="no_verify", action="store_true",
Ben Keene38ecf752020-02-14 14:44:45 +00001754 help="Bypass p4-pre-submit and p4-changelist hooks"),
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001755 ]
Chen Bin251c8c52018-07-27 21:22:22 +10001756 self.description = """Submit changes from git to the perforce depot.\n
Ben Keene4935c452020-02-11 18:58:01 +00001757 The `p4-pre-submit` hook is executed if it exists and is executable. It
1758 can be bypassed with the `--no-verify` command line option. The hook takes
1759 no parameters and nothing from standard input. Exiting with a non-zero status
1760 from this script prevents `git-p4 submit` from launching.
Chen Bin251c8c52018-07-27 21:22:22 +10001761
Ben Keene4935c452020-02-11 18:58:01 +00001762 One usage scenario is to run unit tests in the hook.
Ben Keene38ecf752020-02-14 14:44:45 +00001763
1764 The `p4-prepare-changelist` hook is executed right after preparing the default
1765 changelist message and before the editor is started. It takes one parameter,
1766 the name of the file that contains the changelist text. Exiting with a non-zero
1767 status from the script will abort the process.
1768
1769 The purpose of the hook is to edit the message file in place, and it is not
1770 supressed by the `--no-verify` option. This hook is called even if
1771 `--prepare-p4-only` is set.
1772
1773 The `p4-changelist` hook is executed after the changelist message has been
1774 edited by the user. It can be bypassed with the `--no-verify` option. It
1775 takes a single parameter, the name of the file that holds the proposed
1776 changelist text. Exiting with a non-zero status causes the command to abort.
1777
1778 The hook is allowed to edit the changelist file and can be used to normalize
1779 the text into some project standard format. It can also be used to refuse the
1780 Submit after inspect the message file.
1781
1782 The `p4-post-changelist` hook is invoked after the submit has successfully
Marlon Rac Cambasisb7e20b42020-11-05 12:48:14 -08001783 occurred in P4. It takes no parameters and is meant primarily for notification
Ben Keene38ecf752020-02-14 14:44:45 +00001784 and cannot affect the outcome of the git p4 submit action.
Ben Keene4935c452020-02-11 18:58:01 +00001785 """
Chen Bin251c8c52018-07-27 21:22:22 +10001786
Simon Hausmannc9b50e62007-03-29 19:15:24 +02001787 self.usage += " [name of git branch to submit into perforce depot]"
Simon Hausmann95124972007-03-23 09:16:07 +01001788 self.origin = ""
Vitor Antunesae901092011-02-20 01:18:24 +00001789 self.detectRenames = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05001790 self.preserveUser = gitConfigBool("git-p4.preserveUser")
Pete Wyckoffef739f02012-09-09 16:16:11 -04001791 self.dry_run = False
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001792 self.shelve = False
Luke Diamand8cf422d2017-12-21 11:06:14 +00001793 self.update_shelve = list()
Romain Merlandf55b87c2018-06-01 09:46:14 +02001794 self.commit = ""
Luke Diamand3b3477e2018-06-08 21:32:43 +01001795 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
Luke Diamandb9d34db2018-06-08 21:32:44 +01001796 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001797 self.prepare_p4_only = False
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001798 self.conflict_behavior = None
Marius Storm-Olsenf7baba82007-06-07 14:07:01 +02001799 self.isWindows = (platform.system() == "Windows")
Luke Diamand06804c72012-04-11 17:21:24 +02001800 self.exportLabels = False
Pete Wyckoff249da4c2012-11-23 17:35:35 -05001801 self.p4HasMoveCommand = p4_has_move_command()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001802 self.branch = None
Ben Keene4935c452020-02-11 18:58:01 +00001803 self.no_verify = False
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001804
Lars Schneidera5db4b12015-09-26 09:55:03 +02001805 if gitConfig('git-p4.largeFileSystem'):
1806 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1807
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001808 def check(self):
Joel Holdsworth8a470592022-01-06 21:40:34 +00001809 if len(p4CmdList(["opened", "..."])) > 0:
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001810 die("You have files opened with perforce! Close them before starting the sync.")
1811
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001812 def separate_jobs_from_description(self, message):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001813 """Extract and return a possible Jobs field in the commit message. It
1814 goes into a separate section in the p4 change specification.
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001815
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001816 A jobs line starts with "Jobs:" and looks like a new field in a
1817 form. Values are white-space separated on the same line or on
1818 following lines that start with a tab.
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001819
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001820 This does not parse and extract the full git commit message like a
1821 p4 form. It just sees the Jobs: line as a marker to pass everything
1822 from then on directly into the p4 form, but outside the description
1823 section.
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001824
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001825 Return a tuple (stripped log message, jobs string).
1826 """
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001827
1828 m = re.search(r'^Jobs:', message, re.MULTILINE)
1829 if m is None:
1830 return (message, None)
1831
1832 jobtext = message[m.start():]
1833 stripped_message = message[:m.start()].rstrip()
1834 return (stripped_message, jobtext)
1835
1836 def prepareLogMessage(self, template, message, jobs):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001837 """Edits the template returned from "p4 change -o" to insert the
1838 message in the Description field, and the jobs text in the Jobs
1839 field.
1840 """
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001841 result = ""
1842
Simon Hausmannedae1e22008-02-19 09:29:06 +01001843 inDescriptionSection = False
1844
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001845 for line in template.split("\n"):
1846 if line.startswith("#"):
1847 result += line + "\n"
1848 continue
1849
Simon Hausmannedae1e22008-02-19 09:29:06 +01001850 if inDescriptionSection:
Michael Horowitzc9dbab02011-02-25 21:31:13 -05001851 if line.startswith("Files:") or line.startswith("Jobs:"):
Simon Hausmannedae1e22008-02-19 09:29:06 +01001852 inDescriptionSection = False
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001853 # insert Jobs section
1854 if jobs:
1855 result += jobs + "\n"
Simon Hausmannedae1e22008-02-19 09:29:06 +01001856 else:
1857 continue
1858 else:
1859 if line.startswith("Description:"):
1860 inDescriptionSection = True
1861 line += "\n"
1862 for messageLine in message.split("\n"):
1863 line += "\t" + messageLine + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001864
Simon Hausmannedae1e22008-02-19 09:29:06 +01001865 result += line + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001866
1867 return result
1868
Joel Holdsworthe665e982021-12-16 13:46:16 +00001869 def patchRCSKeywords(self, file, regexp):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001870 """Attempt to zap the RCS keywords in a p4 controlled file matching the
1871 given regex.
1872 """
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001873 handle, outFileName = tempfile.mkstemp(dir='.')
Luke Diamand60df0712012-02-23 07:51:30 +00001874 try:
Joel Holdsworth70c0d552021-12-16 13:46:19 +00001875 with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile:
Joel Holdsworth8618d322021-12-16 13:46:15 +00001876 for line in inFile.readlines():
Joel Holdsworth70c0d552021-12-16 13:46:19 +00001877 outFile.write(regexp.sub(br'$\1$', line))
Luke Diamand60df0712012-02-23 07:51:30 +00001878 # Forcibly overwrite the original file
1879 os.unlink(file)
1880 shutil.move(outFileName, file)
1881 except:
1882 # cleanup our temporary file
1883 os.unlink(outFileName)
Luke Diamandf2606b12018-06-19 09:04:10 +01001884 print("Failed to strip RCS keywords in %s" % file)
Luke Diamand60df0712012-02-23 07:51:30 +00001885 raise
1886
Luke Diamandf2606b12018-06-19 09:04:10 +01001887 print("Patched up RCS keywords in %s" % file)
Luke Diamand60df0712012-02-23 07:51:30 +00001888
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001889 def p4UserForCommit(self, id):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001890 """Return the tuple (perforce user,git email) for a given git commit
1891 id.
1892 """
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001893 self.getUserMapFromPerforceServer()
Pete Wyckoff9bf28852013-01-26 22:11:20 -05001894 gitEmail = read_pipe(["git", "log", "--max-count=1",
1895 "--format=%ae", id])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001896 gitEmail = gitEmail.strip()
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001897 if gitEmail not in self.emails:
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001898 return (None, gitEmail)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001899 else:
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001900 return (self.emails[gitEmail], gitEmail)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001901
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001902 def checkValidP4Users(self, commits):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001903 """Check if any git authors cannot be mapped to p4 users."""
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001904 for id in commits:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001905 user, email = self.p4UserForCommit(id)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001906 if not user:
1907 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
Pete Wyckoff0d609032013-01-26 22:11:24 -05001908 if gitConfigBool("git-p4.allowMissingP4Users"):
Luke Diamandf2606b12018-06-19 09:04:10 +01001909 print("%s" % msg)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001910 else:
1911 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1912
1913 def lastP4Changelist(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001914 """Get back the last changelist number submitted in this client spec.
1915
1916 This then gets used to patch up the username in the change. If the
1917 same client spec is being used by multiple processes then this might
1918 go wrong.
1919 """
Joel Holdsworth8a470592022-01-06 21:40:34 +00001920 results = p4CmdList(["client", "-o"]) # find the current client
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001921 client = None
1922 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001923 if 'Client' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001924 client = r['Client']
1925 break
1926 if not client:
1927 die("could not get client spec")
Luke Diamand6de040d2011-10-16 10:47:52 -04001928 results = p4CmdList(["changes", "-c", client, "-m", "1"])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001929 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001930 if 'change' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001931 return r['change']
1932 die("Could not get changelist number for last submit - cannot patch up user details")
1933
1934 def modifyChangelistUser(self, changelist, newUser):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001935 """Fixup the user field of a changelist after it has been submitted."""
Joel Holdsworth8a470592022-01-06 21:40:34 +00001936 changes = p4CmdList(["change", "-o", changelist])
Luke Diamandecdba362011-05-07 11:19:43 +01001937 if len(changes) != 1:
1938 die("Bad output from p4 change modifying %s to user %s" %
1939 (changelist, newUser))
1940
1941 c = changes[0]
Joel Holdsworthe8f8b3b2022-04-01 15:25:03 +01001942 if c['User'] == newUser:
1943 # Nothing to do
1944 return
Luke Diamandecdba362011-05-07 11:19:43 +01001945 c['User'] = newUser
Yang Zhao50da1e72019-12-13 15:52:42 -08001946 # p4 does not understand format version 3 and above
1947 input = marshal.dumps(c, 2)
Luke Diamandecdba362011-05-07 11:19:43 +01001948
Joel Holdsworth8a470592022-01-06 21:40:34 +00001949 result = p4CmdList(["change", "-f", "-i"], stdin=input)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001950 for r in result:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001951 if 'code' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001952 if r['code'] == 'error':
1953 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001954 if 'data' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001955 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1956 return
1957 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1958
1959 def canChangeChangelists(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001960 """Check to see if we have p4 admin or super-user permissions, either
1961 of which are required to modify changelists.
1962 """
Luke Diamand52a48802012-01-19 09:52:25 +00001963 results = p4CmdList(["protects", self.depotPath])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001964 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001965 if 'perm' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001966 if r['perm'] == 'admin':
1967 return 1
1968 if r['perm'] == 'super':
1969 return 1
1970 return 0
1971
Luke Diamand46c609e2016-12-02 22:43:19 +00001972 def prepareSubmitTemplate(self, changelist=None):
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001973 """Run "p4 change -o" to grab a change specification template.
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001974
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001975 This does not use "p4 -G", as it is nice to keep the submission
1976 template in original order, since a human might edit it.
1977
1978 Remove lines in the Files section that show changes to files
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001979 outside the depot path we're committing into.
1980 """
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001981
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001982 upstream, settings = findUpstreamBranchPoint()
Sam Hocevarcbc69242015-12-19 09:39:39 +00001983
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001984 template = """\
1985# A Perforce Change Specification.
1986#
1987# Change: The change number. 'new' on a new changelist.
1988# Date: The date this specification was last modified.
1989# Client: The client on which the changelist was created. Read-only.
1990# User: The user who created the changelist.
1991# Status: Either 'pending' or 'submitted'. Read-only.
1992# Type: Either 'public' or 'restricted'. Default is 'public'.
1993# Description: Comments about the changelist. Required.
1994# Jobs: What opened jobs are to be closed by this changelist.
1995# You may delete jobs from this list. (New changelists only.)
1996# Files: What opened files from the default changelist are to be added
1997# to this changelist. You may delete files from this list.
1998# (New changelists only.)
1999"""
2000 files_list = []
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002001 inFilesSection = False
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002002 change_entry = None
Luke Diamand46c609e2016-12-02 22:43:19 +00002003 args = ['change', '-o']
2004 if changelist:
2005 args.append(str(changelist))
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002006 for entry in p4CmdList(args):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002007 if 'code' not in entry:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002008 continue
2009 if entry['code'] == 'stat':
2010 change_entry = entry
2011 break
2012 if not change_entry:
2013 die('Failed to decode output of p4 change -o')
Yang Zhao2e2aa8d2019-12-13 15:52:45 -08002014 for key, value in change_entry.items():
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002015 if key.startswith('File'):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002016 if 'depot-paths' in settings:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002017 if not [p for p in settings['depot-paths']
2018 if p4PathStartsWith(value, p)]:
2019 continue
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002020 else:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002021 if not p4PathStartsWith(value, self.depotPath):
2022 continue
2023 files_list.append(value)
2024 continue
2025 # Output in the order expected by prepareLogMessage
2026 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002027 if key not in change_entry:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002028 continue
2029 template += '\n'
2030 template += key + ':'
2031 if key == 'Description':
2032 template += '\n'
2033 for field_line in change_entry[key].splitlines():
2034 template += '\t'+field_line+'\n'
2035 if len(files_list) > 0:
2036 template += '\n'
2037 template += 'Files:\n'
2038 for path in files_list:
2039 template += '\t'+path+'\n'
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002040 return template
2041
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002042 def edit_template(self, template_file):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002043 """Invoke the editor to let the user change the submission message.
2044
2045 Return true if okay to continue with the submit.
2046 """
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002047
2048 # if configured to skip the editing part, just submit
Pete Wyckoff0d609032013-01-26 22:11:24 -05002049 if gitConfigBool("git-p4.skipSubmitEdit"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002050 return True
2051
2052 # look at the modification time, to check later if the user saved
2053 # the file
2054 mtime = os.stat(template_file).st_mtime
2055
2056 # invoke the editor
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002057 if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002058 editor = os.environ.get("P4EDITOR")
2059 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00002060 editor = read_pipe(["git", "var", "GIT_EDITOR"]).strip()
Luke Diamand2dade7a2015-05-19 23:23:17 +01002061 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002062
2063 # If the file was not saved, prompt to see if this patch should
2064 # be skipped. But skip this verification step if configured so.
Pete Wyckoff0d609032013-01-26 22:11:24 -05002065 if gitConfigBool("git-p4.skipSubmitEditCheck"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002066 return True
2067
Pete Wyckoffd1652042011-12-17 12:39:03 -05002068 # modification time updated means user saved the file
2069 if os.stat(template_file).st_mtime > mtime:
2070 return True
2071
Ben Keenee2aed5f2019-12-16 14:02:19 +00002072 response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
2073 if response == 'y':
2074 return True
2075 if response == 'n':
2076 return False
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002077
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002078 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
Maxime Costeb4073bb2014-05-24 18:40:35 +01002079 # diff
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002080 if "P4DIFF" in os.environ:
Maxime Costeb4073bb2014-05-24 18:40:35 +01002081 del(os.environ["P4DIFF"])
2082 diff = ""
2083 for editedFile in editedFiles:
2084 diff += p4_read_pipe(['diff', '-du',
2085 wildcard_encode(editedFile)])
2086
2087 # new file diff
2088 newdiff = ""
2089 for newFile in filesToAdd:
2090 newdiff += "==== new file ====\n"
2091 newdiff += "--- /dev/null\n"
2092 newdiff += "+++ %s\n" % newFile
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002093
2094 is_link = os.path.islink(newFile)
2095 expect_link = newFile in symlinks
2096
2097 if is_link and expect_link:
2098 newdiff += "+%s\n" % os.readlink(newFile)
2099 else:
2100 f = open(newFile, "r")
dorgon.chang54662d52021-06-21 05:16:13 +00002101 try:
2102 for line in f.readlines():
2103 newdiff += "+" + line
2104 except UnicodeDecodeError:
Joel Holdsworth4768af22022-04-01 15:25:02 +01002105 # Found non-text data and skip, since diff description
2106 # should only include text
2107 pass
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002108 f.close()
Maxime Costeb4073bb2014-05-24 18:40:35 +01002109
Maxime Costee2a892e2014-06-11 14:09:59 +01002110 return (diff + newdiff).replace('\r\n', '\n')
Maxime Costeb4073bb2014-05-24 18:40:35 +01002111
Han-Wen Nienhuys7cb5cbe2007-05-23 16:55:48 -03002112 def applyCommit(self, id):
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002113 """Apply one commit, return True if it succeeded."""
2114
Luke Diamandf2606b12018-06-19 09:04:10 +01002115 print("Applying", read_pipe(["git", "show", "-s",
2116 "--format=format:%h %s", id]))
Vitor Antunesae901092011-02-20 01:18:24 +00002117
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002118 p4User, gitEmail = self.p4UserForCommit(id)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002119
Joel Holdsworth3d8a3032022-01-06 21:40:33 +00002120 diff = read_pipe_lines(
Joel Holdsworth8a470592022-01-06 21:40:34 +00002121 ["git", "diff-tree", "-r"] + self.diffOpts + ["{}^".format(id), id])
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002122 filesToAdd = set()
Romain Picarda02b8bc2016-01-12 13:43:47 +01002123 filesToChangeType = set()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002124 filesToDelete = set()
Simon Hausmannd336c152007-05-16 09:41:26 +02002125 editedFiles = set()
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002126 pureRenameCopy = set()
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002127 symlinks = set()
Chris Pettittc65b6702007-11-01 20:43:14 -07002128 filesToChangeExecBit = {}
Luke Diamand46c609e2016-12-02 22:43:19 +00002129 all_files = list()
Luke Diamand60df0712012-02-23 07:51:30 +00002130
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002131 for line in diff:
Chris Pettittb43b0a32007-11-01 20:43:13 -07002132 diff = parseDiffTreeEntry(line)
2133 modifier = diff['status']
2134 path = diff['src']
Luke Diamand46c609e2016-12-02 22:43:19 +00002135 all_files.append(path)
2136
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002137 if modifier == "M":
Luke Diamand6de040d2011-10-16 10:47:52 -04002138 p4_edit(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07002139 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2140 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmannd336c152007-05-16 09:41:26 +02002141 editedFiles.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002142 elif modifier == "A":
2143 filesToAdd.add(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07002144 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002145 if path in filesToDelete:
2146 filesToDelete.remove(path)
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002147
2148 dst_mode = int(diff['dst_mode'], 8)
Luke Diamanddb2d9972018-06-19 09:04:11 +01002149 if dst_mode == 0o120000:
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002150 symlinks.add(path)
2151
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002152 elif modifier == "D":
2153 filesToDelete.add(path)
2154 if path in filesToAdd:
2155 filesToAdd.remove(path)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002156 elif modifier == "C":
2157 src, dest = diff['src'], diff['dst']
Luke Diamand7a109462019-01-18 09:36:56 +00002158 all_files.append(dest)
Luke Diamand6de040d2011-10-16 10:47:52 -04002159 p4_integrate(src, dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002160 pureRenameCopy.add(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002161 if diff['src_sha1'] != diff['dst_sha1']:
Luke Diamand6de040d2011-10-16 10:47:52 -04002162 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002163 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002164 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Luke Diamand6de040d2011-10-16 10:47:52 -04002165 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002166 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002167 filesToChangeExecBit[dest] = diff['dst_mode']
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05002168 if self.isWindows:
2169 # turn off read-only attribute
2170 os.chmod(dest, stat.S_IWRITE)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002171 os.unlink(dest)
2172 editedFiles.add(dest)
Chris Pettittd9a5f252007-10-15 22:15:06 -07002173 elif modifier == "R":
Chris Pettittb43b0a32007-11-01 20:43:13 -07002174 src, dest = diff['src'], diff['dst']
Luke Diamand7a109462019-01-18 09:36:56 +00002175 all_files.append(dest)
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002176 if self.p4HasMoveCommand:
2177 p4_edit(src) # src must be open before move
2178 p4_move(src, dest) # opens for (move/delete, move/add)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002179 else:
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002180 p4_integrate(src, dest)
2181 if diff['src_sha1'] != diff['dst_sha1']:
2182 p4_edit(dest)
2183 else:
2184 pureRenameCopy.add(dest)
Chris Pettittc65b6702007-11-01 20:43:14 -07002185 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002186 if not self.p4HasMoveCommand:
2187 p4_edit(dest) # with move: already open, writable
Chris Pettittc65b6702007-11-01 20:43:14 -07002188 filesToChangeExecBit[dest] = diff['dst_mode']
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002189 if not self.p4HasMoveCommand:
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05002190 if self.isWindows:
2191 os.chmod(dest, stat.S_IWRITE)
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002192 os.unlink(dest)
2193 filesToDelete.add(src)
Chris Pettittd9a5f252007-10-15 22:15:06 -07002194 editedFiles.add(dest)
Romain Picarda02b8bc2016-01-12 13:43:47 +01002195 elif modifier == "T":
2196 filesToChangeType.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002197 else:
2198 die("unknown modifier %s for %s" % (modifier, path))
2199
Tolga Ceylan749b6682014-05-06 22:48:54 -07002200 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
Simon Hausmann47a130b2007-05-20 16:33:21 +02002201 patchcmd = diffcmd + " | git apply "
Simon Hausmannc1b296b2007-05-20 16:55:05 +02002202 tryPatchCmd = patchcmd + "--check -"
2203 applyPatchCmd = patchcmd + "--check --apply -"
Luke Diamand60df0712012-02-23 07:51:30 +00002204 patch_succeeded = True
Simon Hausmann51a26402007-04-15 09:59:56 +02002205
Ben Keene1ec4a0a2020-02-14 14:44:46 +00002206 if verbose:
2207 print("TryPatch: %s" % tryPatchCmd)
2208
Simon Hausmann47a130b2007-05-20 16:33:21 +02002209 if os.system(tryPatchCmd) != 0:
Luke Diamand60df0712012-02-23 07:51:30 +00002210 fixed_rcs_keywords = False
2211 patch_succeeded = False
Luke Diamandf2606b12018-06-19 09:04:10 +01002212 print("Unfortunately applying the change failed!")
Luke Diamand60df0712012-02-23 07:51:30 +00002213
2214 # Patch failed, maybe it's just RCS keyword woes. Look through
2215 # the patch to see if that's possible.
Pete Wyckoff0d609032013-01-26 22:11:24 -05002216 if gitConfigBool("git-p4.attemptRCSCleanup"):
Luke Diamand60df0712012-02-23 07:51:30 +00002217 file = None
Luke Diamand60df0712012-02-23 07:51:30 +00002218 kwfiles = {}
2219 for file in editedFiles | filesToDelete:
2220 # did this file's delta contain RCS keywords?
Joel Holdsworthe665e982021-12-16 13:46:16 +00002221 regexp = p4_keywords_regexp_for_file(file)
2222 if regexp:
Luke Diamand60df0712012-02-23 07:51:30 +00002223 # this file is a possibility...look for RCS keywords.
Joel Holdsworth70c0d552021-12-16 13:46:19 +00002224 for line in read_pipe_lines(
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01002225 ["git", "diff", "%s^..%s" % (id, id), file],
2226 raw=True):
Luke Diamand60df0712012-02-23 07:51:30 +00002227 if regexp.search(line):
2228 if verbose:
Joel Holdsworthe665e982021-12-16 13:46:16 +00002229 print("got keyword match on %s in %s in %s" % (regex.pattern, line, file))
2230 kwfiles[file] = regexp
Luke Diamand60df0712012-02-23 07:51:30 +00002231 break
2232
Joel Holdsworthe665e982021-12-16 13:46:16 +00002233 for file, regexp in kwfiles.items():
Luke Diamand60df0712012-02-23 07:51:30 +00002234 if verbose:
Joel Holdsworthe665e982021-12-16 13:46:16 +00002235 print("zapping %s with %s" % (line, regexp.pattern))
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05002236 # File is being deleted, so not open in p4. Must
2237 # disable the read-only bit on windows.
2238 if self.isWindows and file not in editedFiles:
2239 os.chmod(file, stat.S_IWRITE)
Luke Diamand60df0712012-02-23 07:51:30 +00002240 self.patchRCSKeywords(file, kwfiles[file])
2241 fixed_rcs_keywords = True
2242
2243 if fixed_rcs_keywords:
Luke Diamandf2606b12018-06-19 09:04:10 +01002244 print("Retrying the patch with RCS keywords cleaned up")
Luke Diamand60df0712012-02-23 07:51:30 +00002245 if os.system(tryPatchCmd) == 0:
2246 patch_succeeded = True
Ben Keene1ec4a0a2020-02-14 14:44:46 +00002247 print("Patch succeesed this time with RCS keywords cleaned")
Luke Diamand60df0712012-02-23 07:51:30 +00002248
2249 if not patch_succeeded:
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002250 for f in editedFiles:
2251 p4_revert(f)
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002252 return False
Simon Hausmann51a26402007-04-15 09:59:56 +02002253
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002254 #
2255 # Apply the patch for real, and do add/delete/+x handling.
2256 #
Joel Holdsworth3d8a3032022-01-06 21:40:33 +00002257 system(applyPatchCmd, shell=True)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002258
Romain Picarda02b8bc2016-01-12 13:43:47 +01002259 for f in filesToChangeType:
2260 p4_edit(f, "-t", "auto")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002261 for f in filesToAdd:
Luke Diamand6de040d2011-10-16 10:47:52 -04002262 p4_add(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002263 for f in filesToDelete:
Luke Diamand6de040d2011-10-16 10:47:52 -04002264 p4_revert(f)
2265 p4_delete(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002266
Chris Pettittc65b6702007-11-01 20:43:14 -07002267 # Set/clear executable bits
2268 for f in filesToChangeExecBit.keys():
2269 mode = filesToChangeExecBit[f]
2270 setP4ExecBit(f, mode)
2271
Luke Diamand8cf422d2017-12-21 11:06:14 +00002272 update_shelve = 0
2273 if len(self.update_shelve) > 0:
2274 update_shelve = self.update_shelve.pop(0)
2275 p4_reopen_in_change(update_shelve, all_files)
Luke Diamand46c609e2016-12-02 22:43:19 +00002276
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002277 #
2278 # Build p4 change description, starting with the contents
2279 # of the git commit message.
2280 #
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01002281 logMessage = extractLogMessageFromGitCommit(id)
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01002282 logMessage = logMessage.strip()
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002283 logMessage, jobs = self.separate_jobs_from_description(logMessage)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002284
Luke Diamand8cf422d2017-12-21 11:06:14 +00002285 template = self.prepareSubmitTemplate(update_shelve)
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04002286 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002287
2288 if self.preserveUser:
Joel Holdsworth812ee742022-04-01 15:24:45 +01002289 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002290
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002291 if self.checkAuthorship and not self.p4UserIsMe(p4User):
2292 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2293 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2294 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2295
2296 separatorLine = "######## everything below this line is just the diff #######\n"
Maxime Costeb4073bb2014-05-24 18:40:35 +01002297 if not self.prepare_p4_only:
2298 submitTemplate += separatorLine
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002299 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002300
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002301 handle, fileName = tempfile.mkstemp()
Maxime Costee2a892e2014-06-11 14:09:59 +01002302 tmpFile = os.fdopen(handle, "w+b")
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002303 if self.isWindows:
2304 submitTemplate = submitTemplate.replace("\n", "\r\n")
Yang Zhao6cec21a2019-12-13 15:52:38 -08002305 tmpFile.write(encode_text_stream(submitTemplate))
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002306 tmpFile.close()
2307
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002308 submitted = False
Luke Diamandecdba362011-05-07 11:19:43 +01002309
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002310 try:
Ben Keene38ecf752020-02-14 14:44:45 +00002311 # Allow the hook to edit the changelist text before presenting it
2312 # to the user.
2313 if not run_git_hook("p4-prepare-changelist", [fileName]):
2314 return False
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002315
2316 if self.prepare_p4_only:
2317 #
2318 # Leave the p4 tree prepared, and the submit template around
2319 # and let the user decide what to do next
2320 #
2321 submitted = True
2322 print("")
2323 print("P4 workspace prepared for submission.")
2324 print("To submit or revert, go to client workspace")
2325 print(" " + self.clientPath)
2326 print("")
2327 print("To submit, use \"p4 submit\" to write a new description,")
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002328 print("or \"p4 submit -i <%s\" to use the one prepared by"
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002329 " \"git p4\"." % fileName)
2330 print("You can delete the file \"%s\" when finished." % fileName)
2331
2332 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002333 print("To preserve change ownership by user %s, you must\n"
2334 "do \"p4 change -f <change>\" after submitting and\n"
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002335 "edit the User field.")
2336 if pureRenameCopy:
2337 print("After submitting, renamed files must be re-synced.")
2338 print("Invoke \"p4 sync -f\" on each of these files:")
2339 for f in pureRenameCopy:
2340 print(" " + f)
2341
2342 print("")
2343 print("To revert the changes, use \"p4 revert ...\", and delete")
2344 print("the submit template file \"%s\"" % fileName)
2345 if filesToAdd:
2346 print("Since the commit adds new files, they must be deleted:")
2347 for f in filesToAdd:
2348 print(" " + f)
2349 print("")
2350 sys.stdout.flush()
2351 return True
2352
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002353 if self.edit_template(fileName):
Ben Keene38ecf752020-02-14 14:44:45 +00002354 if not self.no_verify:
2355 if not run_git_hook("p4-changelist", [fileName]):
2356 print("The p4-changelist hook failed.")
2357 sys.stdout.flush()
2358 return False
2359
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002360 # read the edited message and submit
2361 tmpFile = open(fileName, "rb")
Yang Zhao6cec21a2019-12-13 15:52:38 -08002362 message = decode_text_stream(tmpFile.read())
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002363 tmpFile.close()
2364 if self.isWindows:
2365 message = message.replace("\r\n", "\n")
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002366 if message.find(separatorLine) != -1:
2367 submitTemplate = message[:message.index(separatorLine)]
2368 else:
2369 submitTemplate = message
2370
2371 if len(submitTemplate.strip()) == 0:
2372 print("Changelist is empty, aborting this changelist.")
2373 sys.stdout.flush()
2374 return False
Luke Diamand46c609e2016-12-02 22:43:19 +00002375
Luke Diamand8cf422d2017-12-21 11:06:14 +00002376 if update_shelve:
Luke Diamand46c609e2016-12-02 22:43:19 +00002377 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2378 elif self.shelve:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002379 p4_write_pipe(['shelve', '-i'], submitTemplate)
2380 else:
2381 p4_write_pipe(['submit', '-i'], submitTemplate)
2382 # The rename/copy happened by applying a patch that created a
2383 # new file. This leaves it writable, which confuses p4.
2384 for f in pureRenameCopy:
2385 p4_sync(f, "-f")
Luke Diamandecdba362011-05-07 11:19:43 +01002386
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002387 if self.preserveUser:
2388 if p4User:
2389 # Get last changelist number. Cannot easily get it from
2390 # the submit command output as the output is
2391 # unmarshalled.
2392 changelist = self.lastP4Changelist()
2393 self.modifyChangelistUser(changelist, p4User)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002394
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002395 submitted = True
2396
Ben Keene38ecf752020-02-14 14:44:45 +00002397 run_git_hook("p4-post-changelist")
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002398 finally:
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002399 # Revert changes if we skip this patch
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002400 if not submitted or self.shelve:
2401 if self.shelve:
Joel Holdsworth843d8472022-04-01 15:24:54 +01002402 print("Reverting shelved files.")
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002403 else:
Joel Holdsworth843d8472022-04-01 15:24:54 +01002404 print("Submission cancelled, undoing p4 changes.")
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002405 sys.stdout.flush()
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002406 for f in editedFiles | filesToDelete:
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002407 p4_revert(f)
2408 for f in filesToAdd:
2409 p4_revert(f)
2410 os.remove(f)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002411
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002412 if not self.prepare_p4_only:
2413 os.remove(fileName)
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002414 return submitted
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002415
Luke Diamand06804c72012-04-11 17:21:24 +02002416 def exportGitTags(self, gitTags):
Joel Holdsworth522e9142022-04-01 15:24:47 +01002417 """Export git tags as p4 labels. Create a p4 label and then tag with
2418 that.
2419 """
2420
Luke Diamandc8942a22012-04-11 17:21:24 +02002421 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2422 if len(validLabelRegexp) == 0:
2423 validLabelRegexp = defaultLabelRegexp
2424 m = re.compile(validLabelRegexp)
Luke Diamand06804c72012-04-11 17:21:24 +02002425
2426 for name in gitTags:
2427
2428 if not m.match(name):
2429 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002430 print("tag %s does not match regexp %s" % (name, validLabelRegexp))
Luke Diamand06804c72012-04-11 17:21:24 +02002431 continue
2432
2433 # Get the p4 commit this corresponds to
Luke Diamandc8942a22012-04-11 17:21:24 +02002434 logMessage = extractLogMessageFromGitCommit(name)
2435 values = extractSettingsGitLog(logMessage)
Luke Diamand06804c72012-04-11 17:21:24 +02002436
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002437 if 'change' not in values:
Luke Diamand06804c72012-04-11 17:21:24 +02002438 # a tag pointing to something not sent to p4; ignore
2439 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002440 print("git tag %s does not give a p4 commit" % name)
Luke Diamand06804c72012-04-11 17:21:24 +02002441 continue
Luke Diamandc8942a22012-04-11 17:21:24 +02002442 else:
2443 changelist = values['change']
Luke Diamand06804c72012-04-11 17:21:24 +02002444
2445 # Get the tag details.
2446 inHeader = True
2447 isAnnotated = False
2448 body = []
2449 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2450 l = l.strip()
2451 if inHeader:
2452 if re.match(r'tag\s+', l):
2453 isAnnotated = True
2454 elif re.match(r'\s*$', l):
2455 inHeader = False
2456 continue
2457 else:
2458 body.append(l)
2459
2460 if not isAnnotated:
2461 body = ["lightweight tag imported by git p4\n"]
2462
2463 # Create the label - use the same view as the client spec we are using
2464 clientSpec = getClientSpec()
2465
Joel Holdsworth6febb9f2022-04-01 15:24:58 +01002466 labelTemplate = "Label: %s\n" % name
Luke Diamand06804c72012-04-11 17:21:24 +02002467 labelTemplate += "Description:\n"
2468 for b in body:
2469 labelTemplate += "\t" + b + "\n"
2470 labelTemplate += "View:\n"
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002471 for depot_side in clientSpec.mappings:
2472 labelTemplate += "\t%s\n" % depot_side
Luke Diamand06804c72012-04-11 17:21:24 +02002473
Pete Wyckoffef739f02012-09-09 16:16:11 -04002474 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002475 print("Would create p4 label %s for tag" % name)
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002476 elif self.prepare_p4_only:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002477 print("Not creating p4 label %s for tag due to option"
Luke Diamandf2606b12018-06-19 09:04:10 +01002478 " --prepare-p4-only" % name)
Pete Wyckoffef739f02012-09-09 16:16:11 -04002479 else:
2480 p4_write_pipe(["label", "-i"], labelTemplate)
Luke Diamand06804c72012-04-11 17:21:24 +02002481
Pete Wyckoffef739f02012-09-09 16:16:11 -04002482 # Use the label
2483 p4_system(["tag", "-l", name] +
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002484 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
Luke Diamand06804c72012-04-11 17:21:24 +02002485
Pete Wyckoffef739f02012-09-09 16:16:11 -04002486 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002487 print("created p4 label for tag %s" % name)
Luke Diamand06804c72012-04-11 17:21:24 +02002488
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002489 def run(self, args):
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002490 if len(args) == 0:
2491 self.master = currentGitBranch()
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002492 elif len(args) == 1:
2493 self.master = args[0]
Pete Wyckoff28755db2011-12-24 21:07:40 -05002494 if not branchExists(self.master):
2495 die("Branch %s does not exist" % self.master)
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002496 else:
2497 return False
2498
Luke Diamand8cf422d2017-12-21 11:06:14 +00002499 for i in self.update_shelve:
2500 if i <= 0:
2501 sys.exit("invalid changelist %d" % i)
2502
Luke Diamand00ad6e32015-11-21 09:54:41 +00002503 if self.master:
2504 allowSubmit = gitConfig("git-p4.allowSubmit")
2505 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2506 die("%s is not in git-p4.allowSubmit" % self.master)
Jing Xue4c2d5d72008-06-22 14:12:39 -04002507
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002508 upstream, settings = findUpstreamBranchPoint()
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002509 self.depotPath = settings['depot-paths'][0]
Simon Hausmann27d2d812007-06-12 14:31:59 +02002510 if len(self.origin) == 0:
2511 self.origin = upstream
Simon Hausmanna3fdd572007-06-07 22:54:32 +02002512
Luke Diamand8cf422d2017-12-21 11:06:14 +00002513 if len(self.update_shelve) > 0:
Luke Diamand46c609e2016-12-02 22:43:19 +00002514 self.shelve = True
2515
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002516 if self.preserveUser:
2517 if not self.canChangeChangelists():
2518 die("Cannot preserve user names without p4 super-user or admin permissions")
2519
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04002520 # if not set from the command line, try the config file
2521 if self.conflict_behavior is None:
2522 val = gitConfig("git-p4.conflict")
2523 if val:
2524 if val not in self.conflict_behavior_choices:
2525 die("Invalid value '%s' for config git-p4.conflict" % val)
2526 else:
2527 val = "ask"
2528 self.conflict_behavior = val
2529
Simon Hausmanna3fdd572007-06-07 22:54:32 +02002530 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002531 print("Origin branch is " + self.origin)
Simon Hausmann95124972007-03-23 09:16:07 +01002532
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002533 if len(self.depotPath) == 0:
Luke Diamandf2606b12018-06-19 09:04:10 +01002534 print("Internal error: cannot locate perforce depot path from existing branches")
Simon Hausmann95124972007-03-23 09:16:07 +01002535 sys.exit(128)
2536
Pete Wyckoff543987b2012-02-25 20:06:25 -05002537 self.useClientSpec = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05002538 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff543987b2012-02-25 20:06:25 -05002539 self.useClientSpec = True
2540 if self.useClientSpec:
2541 self.clientSpecDirs = getClientSpec()
Simon Hausmann95124972007-03-23 09:16:07 +01002542
Ville Skyttä2e3a16b2016-08-09 11:53:38 +03002543 # Check for the existence of P4 branches
Vitor Antunescd884102015-04-21 23:49:30 +01002544 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2545
2546 if self.useClientSpec and not branchesDetected:
Pete Wyckoff543987b2012-02-25 20:06:25 -05002547 # all files are relative to the client spec
2548 self.clientPath = getClientRoot()
2549 else:
2550 self.clientPath = p4Where(self.depotPath)
2551
2552 if self.clientPath == "":
2553 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
Simon Hausmann95124972007-03-23 09:16:07 +01002554
Luke Diamandf2606b12018-06-19 09:04:10 +01002555 print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
Simon Hausmann7944f142007-05-21 11:04:26 +02002556 self.oldWorkingDirectory = os.getcwd()
Simon Hausmannc1b296b2007-05-20 16:55:05 +02002557
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002558 # ensure the clientPath exists
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002559 new_client_dir = False
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002560 if not os.path.exists(self.clientPath):
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002561 new_client_dir = True
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002562 os.makedirs(self.clientPath)
2563
Miklós Fazekasbbd84862013-03-11 17:45:29 -04002564 chdir(self.clientPath, is_client_path=True)
Pete Wyckoffef739f02012-09-09 16:16:11 -04002565 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002566 print("Would synchronize p4 checkout in %s" % self.clientPath)
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002567 else:
Luke Diamandf2606b12018-06-19 09:04:10 +01002568 print("Synchronizing p4 checkout...")
Pete Wyckoffef739f02012-09-09 16:16:11 -04002569 if new_client_dir:
2570 # old one was destroyed, and maybe nobody told p4
2571 p4_sync("...", "-f")
2572 else:
2573 p4_sync("...")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002574 self.check()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002575
Simon Hausmann4c750c02008-02-19 09:37:16 +01002576 commits = []
Luke Diamand00ad6e32015-11-21 09:54:41 +00002577 if self.master:
Ævar Arnfjörð Bjarmason89f32a92018-05-10 12:43:00 +00002578 committish = self.master
Luke Diamand00ad6e32015-11-21 09:54:41 +00002579 else:
Ævar Arnfjörð Bjarmason89f32a92018-05-10 12:43:00 +00002580 committish = 'HEAD'
Luke Diamand00ad6e32015-11-21 09:54:41 +00002581
Romain Merlandf55b87c2018-06-01 09:46:14 +02002582 if self.commit != "":
2583 if self.commit.find("..") != -1:
2584 limits_ish = self.commit.split("..")
2585 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2586 commits.append(line.strip())
2587 commits.reverse()
2588 else:
2589 commits.append(self.commit)
2590 else:
Junio C Hamanoe6388992018-06-18 10:18:41 -07002591 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
Romain Merlandf55b87c2018-06-01 09:46:14 +02002592 commits.append(line.strip())
2593 commits.reverse()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002594
Pete Wyckoff0d609032013-01-26 22:11:24 -05002595 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
Luke Diamand848de9c2011-05-13 20:46:00 +01002596 self.checkAuthorship = False
2597 else:
2598 self.checkAuthorship = True
2599
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002600 if self.preserveUser:
2601 self.checkValidP4Users(commits)
2602
Gary Gibbons84cb0002012-07-04 09:40:19 -04002603 #
2604 # Build up a set of options to be passed to diff when
2605 # submitting each commit to p4.
2606 #
2607 if self.detectRenames:
2608 # command-line -M arg
Joel Holdsworth8a470592022-01-06 21:40:34 +00002609 self.diffOpts = ["-M"]
Gary Gibbons84cb0002012-07-04 09:40:19 -04002610 else:
2611 # If not explicitly set check the config variable
2612 detectRenames = gitConfig("git-p4.detectRenames")
2613
2614 if detectRenames.lower() == "false" or detectRenames == "":
Joel Holdsworth8a470592022-01-06 21:40:34 +00002615 self.diffOpts = []
Gary Gibbons84cb0002012-07-04 09:40:19 -04002616 elif detectRenames.lower() == "true":
Joel Holdsworth8a470592022-01-06 21:40:34 +00002617 self.diffOpts = ["-M"]
Gary Gibbons84cb0002012-07-04 09:40:19 -04002618 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00002619 self.diffOpts = ["-M{}".format(detectRenames)]
Gary Gibbons84cb0002012-07-04 09:40:19 -04002620
2621 # no command-line arg for -C or --find-copies-harder, just
2622 # config variables
2623 detectCopies = gitConfig("git-p4.detectCopies")
2624 if detectCopies.lower() == "false" or detectCopies == "":
2625 pass
2626 elif detectCopies.lower() == "true":
Joel Holdsworth8a470592022-01-06 21:40:34 +00002627 self.diffOpts.append("-C")
Gary Gibbons84cb0002012-07-04 09:40:19 -04002628 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00002629 self.diffOpts.append("-C{}".format(detectCopies))
Gary Gibbons84cb0002012-07-04 09:40:19 -04002630
Pete Wyckoff0d609032013-01-26 22:11:24 -05002631 if gitConfigBool("git-p4.detectCopiesHarder"):
Joel Holdsworth8a470592022-01-06 21:40:34 +00002632 self.diffOpts.append("--find-copies-harder")
Gary Gibbons84cb0002012-07-04 09:40:19 -04002633
Luke Diamand8cf422d2017-12-21 11:06:14 +00002634 num_shelves = len(self.update_shelve)
2635 if num_shelves > 0 and num_shelves != len(commits):
2636 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2637 (len(commits), num_shelves))
2638
Ben Keene4935c452020-02-11 18:58:01 +00002639 if not self.no_verify:
2640 try:
2641 if not run_git_hook("p4-pre-submit"):
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002642 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip "
2643 "this pre-submission check by adding\nthe command line option '--no-verify', "
Ben Keene4935c452020-02-11 18:58:01 +00002644 "however,\nthis will also skip the p4-changelist hook as well.")
2645 sys.exit(1)
2646 except Exception as e:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002647 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "
Joel Holdsworth84af8b82022-04-01 15:24:50 +01002648 "with the error '{0}'".format(e.message))
Ben Keeneaa8b7662020-02-11 18:58:00 +00002649 sys.exit(1)
Chen Bin251c8c52018-07-27 21:22:22 +10002650
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002651 #
2652 # Apply the commits, one at a time. On failure, ask if should
2653 # continue to try the rest of the patches, or quit.
2654 #
Pete Wyckoffef739f02012-09-09 16:16:11 -04002655 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002656 print("Would apply")
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002657 applied = []
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002658 last = len(commits) - 1
2659 for i, commit in enumerate(commits):
Pete Wyckoffef739f02012-09-09 16:16:11 -04002660 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002661 print(" ", read_pipe(["git", "show", "-s",
2662 "--format=format:%h %s", commit]))
Pete Wyckoffef739f02012-09-09 16:16:11 -04002663 ok = True
2664 else:
2665 ok = self.applyCommit(commit)
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002666 if ok:
2667 applied.append(commit)
Ben Keene2dfdd702020-05-12 13:15:59 +00002668 if self.prepare_p4_only:
2669 if i < last:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002670 print("Processing only the first commit due to option"
Ben Keene2dfdd702020-05-12 13:15:59 +00002671 " --prepare-p4-only")
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002672 break
Ben Keene2dfdd702020-05-12 13:15:59 +00002673 else:
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002674 if i < last:
Ben Keenee2aed5f2019-12-16 14:02:19 +00002675 # prompt for what to do, or use the option/variable
2676 if self.conflict_behavior == "ask":
2677 print("What do you want to do?")
2678 response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2679 elif self.conflict_behavior == "skip":
2680 response = "s"
2681 elif self.conflict_behavior == "quit":
2682 response = "q"
2683 else:
2684 die("Unknown conflict_behavior '%s'" %
2685 self.conflict_behavior)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002686
Ben Keenee2aed5f2019-12-16 14:02:19 +00002687 if response == "s":
2688 print("Skipping this commit, but applying the rest")
2689 if response == "q":
2690 print("Quitting")
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002691 break
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002692
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002693 chdir(self.oldWorkingDirectory)
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002694 shelved_applied = "shelved" if self.shelve else "applied"
Pete Wyckoffef739f02012-09-09 16:16:11 -04002695 if self.dry_run:
2696 pass
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002697 elif self.prepare_p4_only:
2698 pass
Pete Wyckoffef739f02012-09-09 16:16:11 -04002699 elif len(commits) == len(applied):
Luke Diamandf2606b12018-06-19 09:04:10 +01002700 print("All commits {0}!".format(shelved_applied))
Simon Hausmann14594f42007-08-22 09:07:15 +02002701
Simon Hausmann4c750c02008-02-19 09:37:16 +01002702 sync = P4Sync()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05002703 if self.branch:
2704 sync.branch = self.branch
Luke Diamandb9d34db2018-06-08 21:32:44 +01002705 if self.disable_p4sync:
2706 sync.sync_origin_only()
2707 else:
2708 sync.run([])
Simon Hausmann14594f42007-08-22 09:07:15 +02002709
Luke Diamandb9d34db2018-06-08 21:32:44 +01002710 if not self.disable_rebase:
2711 rebase = P4Rebase()
2712 rebase.rebase()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002713
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002714 else:
2715 if len(applied) == 0:
Luke Diamandf2606b12018-06-19 09:04:10 +01002716 print("No commits {0}.".format(shelved_applied))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002717 else:
Luke Diamandf2606b12018-06-19 09:04:10 +01002718 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002719 for c in commits:
2720 if c in applied:
2721 star = "*"
2722 else:
2723 star = " "
Luke Diamandf2606b12018-06-19 09:04:10 +01002724 print(star, read_pipe(["git", "show", "-s",
2725 "--format=format:%h %s", c]))
2726 print("You will have to do 'git p4 sync' and rebase.")
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002727
Pete Wyckoff0d609032013-01-26 22:11:24 -05002728 if gitConfigBool("git-p4.exportLabels"):
Luke Diamand06dcd152012-05-11 07:25:18 +01002729 self.exportLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02002730
2731 if self.exportLabels:
2732 p4Labels = getP4Labels(self.depotPath)
2733 gitTags = getGitTags()
2734
2735 missingGitTags = gitTags - p4Labels
2736 self.exportGitTags(missingGitTags)
2737
Ondřej Bílka98e023d2013-07-29 10:18:21 +02002738 # exit with error unless everything applied perfectly
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002739 if len(commits) != len(applied):
Joel Holdsworth812ee742022-04-01 15:24:45 +01002740 sys.exit(1)
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002741
Simon Hausmannb9847332007-03-20 20:54:23 +01002742 return True
2743
Joel Holdsworthadf159b2022-04-01 15:24:43 +01002744
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002745class View(object):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002746 """Represent a p4 view ("p4 help views"), and map files in a repo according
2747 to the view.
2748 """
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002749
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002750 def __init__(self, client_name):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002751 self.mappings = []
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002752 self.client_prefix = "//%s/" % client_name
2753 # cache results of "p4 where" to lookup client file locations
2754 self.client_spec_path_cache = {}
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002755
2756 def append(self, view_line):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002757 """Parse a view line, splitting it into depot and client sides. Append
2758 to self.mappings, preserving order. This is only needed for tag
2759 creation.
2760 """
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002761
2762 # Split the view line into exactly two words. P4 enforces
2763 # structure on these lines that simplifies this quite a bit.
2764 #
2765 # Either or both words may be double-quoted.
2766 # Single quotes do not matter.
2767 # Double-quote marks cannot occur inside the words.
2768 # A + or - prefix is also inside the quotes.
2769 # There are no quotes unless they contain a space.
2770 # The line is already white-space stripped.
2771 # The two words are separated by a single space.
2772 #
2773 if view_line[0] == '"':
2774 # First word is double quoted. Find its end.
2775 close_quote_index = view_line.find('"', 1)
2776 if close_quote_index <= 0:
2777 die("No first-word closing quote found: %s" % view_line)
2778 depot_side = view_line[1:close_quote_index]
2779 # skip closing quote and space
2780 rhs_index = close_quote_index + 1 + 1
2781 else:
2782 space_index = view_line.find(" ")
2783 if space_index <= 0:
2784 die("No word-splitting space found: %s" % view_line)
2785 depot_side = view_line[0:space_index]
2786 rhs_index = space_index + 1
2787
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002788 # prefix + means overlay on previous mapping
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002789 if depot_side.startswith("+"):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002790 depot_side = depot_side[1:]
2791
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002792 # prefix - means exclude this path, leave out of mappings
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002793 exclude = False
2794 if depot_side.startswith("-"):
2795 exclude = True
2796 depot_side = depot_side[1:]
2797
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002798 if not exclude:
2799 self.mappings.append(depot_side)
2800
2801 def convert_client_path(self, clientFile):
2802 # chop off //client/ part to make it relative
Yang Zhaod38208a2019-12-13 15:52:40 -08002803 if not decode_path(clientFile).startswith(self.client_prefix):
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002804 die("No prefix '%s' on clientFile '%s'" %
2805 (self.client_prefix, clientFile))
2806 return clientFile[len(self.client_prefix):]
2807
2808 def update_client_spec_path_cache(self, files):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002809 """Caching file paths by "p4 where" batch query."""
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002810
2811 # List depot file paths exclude that already cached
Yang Zhaod38208a2019-12-13 15:52:40 -08002812 fileArgs = [f['path'] for f in files if decode_path(f['path']) not in self.client_spec_path_cache]
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002813
2814 if len(fileArgs) == 0:
2815 return # All files in cache
2816
2817 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2818 for res in where_result:
2819 if "code" in res and res["code"] == "error":
2820 # assume error is "... file(s) not in client view"
2821 continue
2822 if "clientFile" not in res:
Pete Wyckoff20005442014-01-21 18:16:46 -05002823 die("No clientFile in 'p4 where' output")
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002824 if "unmap" in res:
2825 # it will list all of them, but only one not unmap-ped
2826 continue
Yang Zhaod38208a2019-12-13 15:52:40 -08002827 depot_path = decode_path(res['depotFile'])
Lars Schneidera0a50d82015-08-28 14:00:34 +02002828 if gitConfigBool("core.ignorecase"):
Yang Zhaod38208a2019-12-13 15:52:40 -08002829 depot_path = depot_path.lower()
2830 self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002831
2832 # not found files or unmap files set to ""
2833 for depotFile in fileArgs:
Yang Zhaod38208a2019-12-13 15:52:40 -08002834 depotFile = decode_path(depotFile)
Lars Schneidera0a50d82015-08-28 14:00:34 +02002835 if gitConfigBool("core.ignorecase"):
2836 depotFile = depotFile.lower()
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002837 if depotFile not in self.client_spec_path_cache:
Yang Zhaod38208a2019-12-13 15:52:40 -08002838 self.client_spec_path_cache[depotFile] = b''
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002839
2840 def map_in_client(self, depot_path):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002841 """Return the relative location in the client where this depot file
2842 should live.
2843
2844 Returns "" if the file should not be mapped in the client.
2845 """
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002846
Lars Schneidera0a50d82015-08-28 14:00:34 +02002847 if gitConfigBool("core.ignorecase"):
2848 depot_path = depot_path.lower()
2849
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002850 if depot_path in self.client_spec_path_cache:
2851 return self.client_spec_path_cache[depot_path]
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002852
Joel Holdsworth84af8b82022-04-01 15:24:50 +01002853 die("Error: %s is not found in client spec path" % depot_path)
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002854 return ""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002855
Joel Holdsworthadf159b2022-04-01 15:24:43 +01002856
Mazo, Andreyff8c50e2019-04-01 18:02:26 +00002857def cloneExcludeCallback(option, opt_str, value, parser):
2858 # prepend "/" because the first "/" was consumed as part of the option itself.
2859 # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2860 parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2861
Joel Holdsworthadf159b2022-04-01 15:24:43 +01002862
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002863class P4Sync(Command, P4UserMap):
Pete Wyckoff56c09342011-02-19 08:17:57 -05002864
Simon Hausmannb9847332007-03-20 20:54:23 +01002865 def __init__(self):
2866 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002867 P4UserMap.__init__(self)
Simon Hausmannb9847332007-03-20 20:54:23 +01002868 self.options = [
2869 optparse.make_option("--branch", dest="branch"),
2870 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2871 optparse.make_option("--changesfile", dest="changesFile"),
2872 optparse.make_option("--silent", dest="silent", action="store_true"),
Simon Hausmannef48f902007-05-17 22:17:49 +02002873 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02002874 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Han-Wen Nienhuysd2c6dd32007-05-23 18:49:35 -03002875 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2876 help="Import into refs/heads/ , not refs/remotes"),
Lex Spoon96b2d542015-04-20 11:00:20 -04002877 optparse.make_option("--max-changes", dest="maxChanges",
2878 help="Maximum number of changes to import"),
2879 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2880 help="Internal block size to use when iteratively calling p4 changes"),
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03002881 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002882 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2883 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
Luke Diamand51334bb2015-01-17 20:56:38 +00002884 help="Only sync files that are included in the Perforce Client Spec"),
2885 optparse.make_option("-/", dest="cloneExclude",
Mazo, Andreyff8c50e2019-04-01 18:02:26 +00002886 action="callback", callback=cloneExcludeCallback, type="string",
Luke Diamand51334bb2015-01-17 20:56:38 +00002887 help="exclude depot path"),
Simon Hausmannb9847332007-03-20 20:54:23 +01002888 ]
2889 self.description = """Imports from Perforce into a git repository.\n
2890 example:
2891 //depot/my/project/ -- to import the current head
2892 //depot/my/project/@all -- to import everything
2893 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2894
2895 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2896
2897 self.usage += " //depot/path[@revRange]"
Simon Hausmannb9847332007-03-20 20:54:23 +01002898 self.silent = False
Reilly Grant1d7367d2009-09-10 00:02:38 -07002899 self.createdBranches = set()
2900 self.committedChanges = set()
Simon Hausmann569d1bd2007-03-22 21:34:16 +01002901 self.branch = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01002902 self.detectBranches = False
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02002903 self.detectLabels = False
Luke Diamand06804c72012-04-11 17:21:24 +02002904 self.importLabels = False
Simon Hausmannb9847332007-03-20 20:54:23 +01002905 self.changesFile = ""
Simon Hausmann01265102007-05-25 10:36:10 +02002906 self.syncWithOrigin = True
Simon Hausmanna028a982007-05-23 00:03:08 +02002907 self.importIntoRemotes = True
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02002908 self.maxChanges = ""
Luke Diamand1051ef02015-06-10 08:30:59 +01002909 self.changes_block_size = None
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -03002910 self.keepRepoPath = False
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002911 self.depotPaths = None
Simon Hausmann3c699642007-06-16 13:09:21 +02002912 self.p4BranchesInGit = []
Tommy Thorn354081d2008-02-03 10:38:51 -08002913 self.cloneExclude = []
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002914 self.useClientSpec = False
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05002915 self.useClientSpec_from_options = False
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002916 self.clientSpecDirs = None
Vitor Antunesfed23692012-01-25 23:48:22 +00002917 self.tempBranches = []
Lars Schneiderd6041762016-06-29 09:35:27 +02002918 self.tempBranchLocation = "refs/git-p4-tmp"
Lars Schneidera5db4b12015-09-26 09:55:03 +02002919 self.largeFileSystem = None
Luke Diamand123f6312018-05-23 23:20:26 +01002920 self.suppress_meta_comment = False
Lars Schneidera5db4b12015-09-26 09:55:03 +02002921
2922 if gitConfig('git-p4.largeFileSystem'):
2923 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2924 self.largeFileSystem = largeFileSystemConstructor(
2925 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2926 )
Simon Hausmannb9847332007-03-20 20:54:23 +01002927
Simon Hausmann01265102007-05-25 10:36:10 +02002928 if gitConfig("git-p4.syncFromOrigin") == "false":
2929 self.syncWithOrigin = False
2930
Luke Diamand123f6312018-05-23 23:20:26 +01002931 self.depotPaths = []
2932 self.changeRange = ""
2933 self.previousDepotPaths = []
2934 self.hasOrigin = False
2935
2936 # map from branch depot path to parent branch
2937 self.knownBranches = {}
2938 self.initialParents = {}
2939
2940 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2941 self.labels = {}
2942
Vitor Antunesfed23692012-01-25 23:48:22 +00002943 def checkpoint(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01002944 """Force a checkpoint in fast-import and wait for it to finish."""
Vitor Antunesfed23692012-01-25 23:48:22 +00002945 self.gitStream.write("checkpoint\n\n")
2946 self.gitStream.write("progress checkpoint\n\n")
Yang Zhao4294d742019-12-13 15:52:43 -08002947 self.gitStream.flush()
Vitor Antunesfed23692012-01-25 23:48:22 +00002948 out = self.gitOutput.readline()
2949 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002950 print("checkpoint finished: " + out)
Vitor Antunesfed23692012-01-25 23:48:22 +00002951
Mazo, Andreya2bee102019-04-01 18:02:32 +00002952 def isPathWanted(self, path):
2953 for p in self.cloneExclude:
2954 if p.endswith("/"):
2955 if p4PathStartsWith(path, p):
2956 return False
2957 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2958 elif path.lower() == p.lower():
2959 return False
2960 for p in self.depotPaths:
Yang Zhaod38208a2019-12-13 15:52:40 -08002961 if p4PathStartsWith(path, decode_path(p)):
Mazo, Andreya2bee102019-04-01 18:02:32 +00002962 return True
2963 return False
2964
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01002965 def extractFilesFromCommit(self, commit, shelved=False, shelved_cl=0):
Simon Hausmannb9847332007-03-20 20:54:23 +01002966 files = []
2967 fnum = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002968 while "depotFile%s" % fnum in commit:
Joel Holdsworth6febb9f2022-04-01 15:24:58 +01002969 path = commit["depotFile%s" % fnum]
Yang Zhaod38208a2019-12-13 15:52:40 -08002970 found = self.isPathWanted(decode_path(path))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002971 if not found:
Simon Hausmannb9847332007-03-20 20:54:23 +01002972 fnum = fnum + 1
2973 continue
2974
2975 file = {}
2976 file["path"] = path
2977 file["rev"] = commit["rev%s" % fnum]
2978 file["action"] = commit["action%s" % fnum]
2979 file["type"] = commit["type%s" % fnum]
Luke Diamand123f6312018-05-23 23:20:26 +01002980 if shelved:
2981 file["shelved_cl"] = int(shelved_cl)
Simon Hausmannb9847332007-03-20 20:54:23 +01002982 files.append(file)
2983 fnum = fnum + 1
2984 return files
2985
Jan Durovec26e6a272016-04-19 19:49:41 +00002986 def extractJobsFromCommit(self, commit):
2987 jobs = []
2988 jnum = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002989 while "job%s" % jnum in commit:
Jan Durovec26e6a272016-04-19 19:49:41 +00002990 job = commit["job%s" % jnum]
2991 jobs.append(job)
2992 jnum = jnum + 1
2993 return jobs
2994
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002995 def stripRepoPath(self, path, prefixes):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002996 """When streaming files, this is called to map a p4 depot path to where
2997 it should go in git. The prefixes are either self.depotPaths, or
2998 self.branchPrefixes in the case of branch detection.
2999 """
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003000
Ian Wienand39527102011-02-11 16:33:48 -08003001 if self.useClientSpec:
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003002 # branch detection moves files up a level (the branch name)
3003 # from what client spec interpretation gives
Yang Zhaod38208a2019-12-13 15:52:40 -08003004 path = decode_path(self.clientSpecDirs.map_in_client(path))
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003005 if self.detectBranches:
3006 for b in self.knownBranches:
Mazo, Andreyf2768cb2019-04-01 18:02:24 +00003007 if p4PathStartsWith(path, b + "/"):
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003008 path = path[len(b)+1:]
3009
3010 elif self.keepRepoPath:
3011 # Preserve everything in relative path name except leading
3012 # //depot/; just look at first prefix as they all should
3013 # be in the same depot.
3014 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
3015 if p4PathStartsWith(path, depot):
3016 path = path[len(depot):]
Ian Wienand39527102011-02-11 16:33:48 -08003017
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04003018 else:
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04003019 for p in prefixes:
3020 if p4PathStartsWith(path, p):
3021 path = path[len(p):]
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003022 break
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003023
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04003024 path = wildcard_decode(path)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003025 return path
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -03003026
Simon Hausmann71b112d2007-05-19 11:54:11 +02003027 def splitFilesIntoBranches(self, commit):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01003028 """Look at each depotFile in the commit to figure out to what branch it
3029 belongs.
3030 """
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003031
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09003032 if self.clientSpecDirs:
3033 files = self.extractFilesFromCommit(commit)
3034 self.clientSpecDirs.update_client_spec_path_cache(files)
3035
Simon Hausmannd5904672007-05-19 11:07:32 +02003036 branches = {}
Simon Hausmann71b112d2007-05-19 11:54:11 +02003037 fnum = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003038 while "depotFile%s" % fnum in commit:
Yang Zhaod38208a2019-12-13 15:52:40 -08003039 raw_path = commit["depotFile%s" % fnum]
3040 path = decode_path(raw_path)
Mazo, Andreyd15068a2019-04-01 18:02:38 +00003041 found = self.isPathWanted(path)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003042 if not found:
Simon Hausmann71b112d2007-05-19 11:54:11 +02003043 fnum = fnum + 1
3044 continue
3045
3046 file = {}
Yang Zhaod38208a2019-12-13 15:52:40 -08003047 file["path"] = raw_path
Simon Hausmann71b112d2007-05-19 11:54:11 +02003048 file["rev"] = commit["rev%s" % fnum]
3049 file["action"] = commit["action%s" % fnum]
3050 file["type"] = commit["type%s" % fnum]
3051 fnum = fnum + 1
3052
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003053 # start with the full relative path where this file would
3054 # go in a p4 client
3055 if self.useClientSpec:
Yang Zhaod38208a2019-12-13 15:52:40 -08003056 relPath = decode_path(self.clientSpecDirs.map_in_client(path))
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003057 else:
3058 relPath = self.stripRepoPath(path, self.depotPaths)
Simon Hausmannb9847332007-03-20 20:54:23 +01003059
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003060 for branch in self.knownBranches.keys():
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003061 # add a trailing slash so that a commit into qt/4.2foo
3062 # doesn't end up in qt/4.2, e.g.
Mazo, Andreyf2768cb2019-04-01 18:02:24 +00003063 if p4PathStartsWith(relPath, branch + "/"):
Simon Hausmannd5904672007-05-19 11:07:32 +02003064 if branch not in branches:
3065 branches[branch] = []
Simon Hausmann71b112d2007-05-19 11:54:11 +02003066 branches[branch].append(file)
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003067 break
Simon Hausmannb9847332007-03-20 20:54:23 +01003068
3069 return branches
3070
Lars Schneidera5db4b12015-09-26 09:55:03 +02003071 def writeToGitStream(self, gitMode, relPath, contents):
Yang Zhao6cec21a2019-12-13 15:52:38 -08003072 self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
Lars Schneidera5db4b12015-09-26 09:55:03 +02003073 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
3074 for d in contents:
3075 self.gitStream.write(d)
3076 self.gitStream.write('\n')
3077
Lars Schneidera8b05162017-02-09 16:06:56 +01003078 def encodeWithUTF8(self, path):
3079 try:
3080 path.decode('ascii')
3081 except:
3082 encoding = 'utf8'
3083 if gitConfig('git-p4.pathEncoding'):
3084 encoding = gitConfig('git-p4.pathEncoding')
3085 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
3086 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003087 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
Lars Schneidera8b05162017-02-09 16:06:56 +01003088 return path
3089
Luke Diamandb9327052009-07-30 00:13:46 +01003090 def streamOneP4File(self, file, contents):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003091 """Output one file from the P4 stream.
3092
3093 This is a helper for streamP4Files().
3094 """
3095
Yang Zhaod38208a2019-12-13 15:52:40 -08003096 file_path = file['depotFile']
3097 relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
3098
Luke Diamandb9327052009-07-30 00:13:46 +01003099 if verbose:
Luke Diamand0742b7c2018-10-12 06:28:31 +01003100 if 'fileSize' in self.stream_file:
3101 size = int(self.stream_file['fileSize'])
3102 else:
Joel Holdsworth4768af22022-04-01 15:25:02 +01003103 # Deleted files don't get a fileSize apparently
3104 size = 0
Joel Holdsworthae9b9502021-12-19 15:40:27 +00003105 sys.stdout.write('\r%s --> %s (%s)\n' % (
3106 file_path, relPath, format_size_human_readable(size)))
Lars Schneiderd2176a52015-09-26 09:55:01 +02003107 sys.stdout.flush()
Luke Diamandb9327052009-07-30 00:13:46 +01003108
Joel Holdsworth0874bb02022-04-01 15:24:52 +01003109 type_base, type_mods = split_p4_type(file["type"])
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04003110
3111 git_mode = "100644"
3112 if "x" in type_mods:
3113 git_mode = "100755"
3114 if type_base == "symlink":
3115 git_mode = "120000"
Alexandru Juncu1292df12013-08-08 16:17:38 +03003116 # p4 print on a symlink sometimes contains "target\n";
3117 # if it does, remove the newline
Yang Zhao6cec21a2019-12-13 15:52:38 -08003118 data = ''.join(decode_text_stream(c) for c in contents)
Pete Wyckoff40f846c2014-01-21 18:16:40 -05003119 if not data:
3120 # Some version of p4 allowed creating a symlink that pointed
3121 # to nothing. This causes p4 errors when checking out such
3122 # a change, and errors here too. Work around it by ignoring
3123 # the bad symlink; hopefully a future change fixes it.
Yang Zhaod38208a2019-12-13 15:52:40 -08003124 print("\nIgnoring empty symlink in %s" % file_path)
Pete Wyckoff40f846c2014-01-21 18:16:40 -05003125 return
3126 elif data[-1] == '\n':
Alexandru Juncu1292df12013-08-08 16:17:38 +03003127 contents = [data[:-1]]
3128 else:
3129 contents = [data]
Luke Diamandb9327052009-07-30 00:13:46 +01003130
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04003131 if type_base == "utf16":
Pete Wyckoff55aa5712011-09-17 19:16:14 -04003132 # p4 delivers different text in the python output to -G
3133 # than it does when using "print -o", or normal p4 client
3134 # operations. utf16 is converted to ascii or utf8, perhaps.
3135 # But ascii text saved as -t utf16 is completely mangled.
3136 # Invoke print -o to get the real contents.
Pete Wyckoff7f0e5962013-01-26 22:11:13 -05003137 #
3138 # On windows, the newlines will always be mangled by print, so put
3139 # them back too. This is not needed to the cygwin windows version,
3140 # just the native "NT" type.
3141 #
Lars Schneider1f5f3902015-09-21 12:01:41 +02003142 try:
Yang Zhaod38208a2019-12-13 15:52:40 -08003143 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
Lars Schneider1f5f3902015-09-21 12:01:41 +02003144 except Exception as e:
3145 if 'Translation of file content failed' in str(e):
3146 type_base = 'binary'
3147 else:
3148 raise e
3149 else:
3150 if p4_version_string().find('/NT') >= 0:
Yang Zhaod38208a2019-12-13 15:52:40 -08003151 text = text.replace(b'\r\n', b'\n')
Joel Holdsworth84af8b82022-04-01 15:24:50 +01003152 contents = [text]
Pete Wyckoff55aa5712011-09-17 19:16:14 -04003153
Pete Wyckoff9f7ef0e2011-11-05 13:36:07 -04003154 if type_base == "apple":
3155 # Apple filetype files will be streamed as a concatenation of
3156 # its appledouble header and the contents. This is useless
3157 # on both macs and non-macs. If using "print -q -o xx", it
3158 # will create "xx" with the data, and "%xx" with the header.
3159 # This is also not very useful.
3160 #
3161 # Ideally, someday, this script can learn how to generate
3162 # appledouble files directly and import those to git, but
3163 # non-mac machines can never find a use for apple filetype.
Luke Diamandf2606b12018-06-19 09:04:10 +01003164 print("\nIgnoring apple filetype file %s" % file['depotFile'])
Pete Wyckoff9f7ef0e2011-11-05 13:36:07 -04003165 return
3166
Tao Klerksfbe5f6b2022-04-04 05:50:36 +00003167 if type_base == "utf8":
3168 # The type utf8 explicitly means utf8 *with BOM*. These are
3169 # streamed just like regular text files, however, without
3170 # the BOM in the stream.
3171 # Therefore, to accurately import these files into git, we
3172 # need to explicitly re-add the BOM before writing.
3173 # 'contents' is a set of bytes in this case, so create the
3174 # BOM prefix as a b'' literal.
3175 contents = [b'\xef\xbb\xbf' + contents[0]] + contents[1:]
3176
Pete Wyckoff55aa5712011-09-17 19:16:14 -04003177 # Note that we do not try to de-mangle keywords on utf16 files,
3178 # even though in theory somebody may want that.
Joel Holdsworthe665e982021-12-16 13:46:16 +00003179 regexp = p4_keywords_regexp_for_type(type_base, type_mods)
3180 if regexp:
Joel Holdsworth70c0d552021-12-16 13:46:19 +00003181 contents = [regexp.sub(br'$\1$', c) for c in contents]
Luke Diamandb9327052009-07-30 00:13:46 +01003182
Lars Schneidera5db4b12015-09-26 09:55:03 +02003183 if self.largeFileSystem:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01003184 git_mode, contents = self.largeFileSystem.processContent(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01003185
Lars Schneidera5db4b12015-09-26 09:55:03 +02003186 self.writeToGitStream(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01003187
3188 def streamOneP4Deletion(self, file):
Yang Zhaod38208a2019-12-13 15:52:40 -08003189 relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
Luke Diamandb9327052009-07-30 00:13:46 +01003190 if verbose:
Lars Schneiderd2176a52015-09-26 09:55:01 +02003191 sys.stdout.write("delete %s\n" % relPath)
3192 sys.stdout.flush()
Yang Zhao6cec21a2019-12-13 15:52:38 -08003193 self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
Luke Diamandb9327052009-07-30 00:13:46 +01003194
Lars Schneidera5db4b12015-09-26 09:55:03 +02003195 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
3196 self.largeFileSystem.removeLargeFile(relPath)
3197
Luke Diamandb9327052009-07-30 00:13:46 +01003198 def streamP4FilesCb(self, marshalled):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003199 """Handle another chunk of streaming data."""
Luke Diamandb9327052009-07-30 00:13:46 +01003200
Pete Wyckoff78189be2012-11-23 17:35:36 -05003201 # catch p4 errors and complain
3202 err = None
3203 if "code" in marshalled:
3204 if marshalled["code"] == "error":
3205 if "data" in marshalled:
3206 err = marshalled["data"].rstrip()
Lars Schneider4d25dc42015-09-26 09:55:02 +02003207
3208 if not err and 'fileSize' in self.stream_file:
3209 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3210 if required_bytes > 0:
Joel Holdsworthae9b9502021-12-19 15:40:27 +00003211 err = 'Not enough space left on %s! Free at least %s.' % (
3212 os.getcwd(), format_size_human_readable(required_bytes))
Lars Schneider4d25dc42015-09-26 09:55:02 +02003213
Pete Wyckoff78189be2012-11-23 17:35:36 -05003214 if err:
3215 f = None
3216 if self.stream_have_file_info:
3217 if "depotFile" in self.stream_file:
3218 f = self.stream_file["depotFile"]
3219 # force a failure in fast-import, else an empty
3220 # commit will be made
3221 self.gitStream.write("\n")
3222 self.gitStream.write("die-now\n")
3223 self.gitStream.close()
3224 # ignore errors, but make sure it exits first
3225 self.importProcess.wait()
3226 if f:
3227 die("Error from p4 print for %s: %s" % (f, err))
3228 else:
3229 die("Error from p4 print: %s" % err)
3230
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003231 if 'depotFile' in marshalled and self.stream_have_file_info:
Andrew Garberc3f61632011-04-07 02:01:21 -04003232 # start of a new file - output the old one first
3233 self.streamOneP4File(self.stream_file, self.stream_contents)
3234 self.stream_file = {}
3235 self.stream_contents = []
3236 self.stream_have_file_info = False
Luke Diamandb9327052009-07-30 00:13:46 +01003237
Andrew Garberc3f61632011-04-07 02:01:21 -04003238 # pick up the new file information... for the
3239 # 'data' field we need to append to our array
3240 for k in marshalled.keys():
3241 if k == 'data':
Lars Schneiderd2176a52015-09-26 09:55:01 +02003242 if 'streamContentSize' not in self.stream_file:
3243 self.stream_file['streamContentSize'] = 0
3244 self.stream_file['streamContentSize'] += len(marshalled['data'])
Andrew Garberc3f61632011-04-07 02:01:21 -04003245 self.stream_contents.append(marshalled['data'])
3246 else:
3247 self.stream_file[k] = marshalled[k]
Luke Diamandb9327052009-07-30 00:13:46 +01003248
Lars Schneiderd2176a52015-09-26 09:55:01 +02003249 if (verbose and
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01003250 'streamContentSize' in self.stream_file and
3251 'fileSize' in self.stream_file and
3252 'depotFile' in self.stream_file):
Lars Schneiderd2176a52015-09-26 09:55:01 +02003253 size = int(self.stream_file["fileSize"])
3254 if size > 0:
3255 progress = 100*self.stream_file['streamContentSize']/size
Joel Holdsworthae9b9502021-12-19 15:40:27 +00003256 sys.stdout.write('\r%s %d%% (%s)' % (
3257 self.stream_file['depotFile'], progress,
3258 format_size_human_readable(size)))
Lars Schneiderd2176a52015-09-26 09:55:01 +02003259 sys.stdout.flush()
3260
Andrew Garberc3f61632011-04-07 02:01:21 -04003261 self.stream_have_file_info = True
Luke Diamandb9327052009-07-30 00:13:46 +01003262
Luke Diamandb9327052009-07-30 00:13:46 +01003263 def streamP4Files(self, files):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003264 """Stream directly from "p4 files" into "git fast-import."""
3265
Simon Hausmann30b59402008-03-03 11:55:48 +01003266 filesForCommit = []
3267 filesToRead = []
Luke Diamandb9327052009-07-30 00:13:46 +01003268 filesToDelete = []
Simon Hausmann30b59402008-03-03 11:55:48 +01003269
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01003270 for f in files:
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05003271 filesForCommit.append(f)
3272 if f['action'] in self.delete_actions:
3273 filesToDelete.append(f)
3274 else:
3275 filesToRead.append(f)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003276
Luke Diamandb9327052009-07-30 00:13:46 +01003277 # deleted files...
3278 for f in filesToDelete:
3279 self.streamOneP4Deletion(f)
3280
Simon Hausmann30b59402008-03-03 11:55:48 +01003281 if len(filesToRead) > 0:
Luke Diamandb9327052009-07-30 00:13:46 +01003282 self.stream_file = {}
3283 self.stream_contents = []
3284 self.stream_have_file_info = False
3285
Andrew Garberc3f61632011-04-07 02:01:21 -04003286 # curry self argument
3287 def streamP4FilesCbSelf(entry):
3288 self.streamP4FilesCb(entry)
Luke Diamandb9327052009-07-30 00:13:46 +01003289
Luke Diamand123f6312018-05-23 23:20:26 +01003290 fileArgs = []
3291 for f in filesToRead:
3292 if 'shelved_cl' in f:
3293 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3294 # the contents
Yang Zhao6cec21a2019-12-13 15:52:38 -08003295 fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
Luke Diamand123f6312018-05-23 23:20:26 +01003296 else:
Yang Zhao6cec21a2019-12-13 15:52:38 -08003297 fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
Luke Diamand123f6312018-05-23 23:20:26 +01003298
3299 fileArgs.append(fileArg)
Luke Diamand6de040d2011-10-16 10:47:52 -04003300
3301 p4CmdList(["-x", "-", "print"],
3302 stdin=fileArgs,
3303 cb=streamP4FilesCbSelf)
Han-Wen Nienhuysf2eda792007-05-23 18:49:35 -03003304
Luke Diamandb9327052009-07-30 00:13:46 +01003305 # do the last chunk
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003306 if 'depotFile' in self.stream_file:
Luke Diamandb9327052009-07-30 00:13:46 +01003307 self.streamOneP4File(self.stream_file, self.stream_contents)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003308
Luke Diamandaffb4742012-01-19 09:52:27 +00003309 def make_email(self, userid):
3310 if userid in self.users:
3311 return self.users[userid]
3312 else:
Tao Klerksf7b5ff62022-04-30 19:26:52 +00003313 userid_bytes = metadata_stream_to_writable_bytes(userid)
3314 return b"%s <a@b>" % userid_bytes
Luke Diamandaffb4742012-01-19 09:52:27 +00003315
Luke Diamand06804c72012-04-11 17:21:24 +02003316 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01003317 """Stream a p4 tag.
3318
3319 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3320 """
Luke Diamandb43702a2015-08-27 08:18:58 +01003321
Luke Diamand06804c72012-04-11 17:21:24 +02003322 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003323 print("writing tag %s for commit %s" % (labelName, commit))
Luke Diamand06804c72012-04-11 17:21:24 +02003324 gitStream.write("tag %s\n" % labelName)
3325 gitStream.write("from %s\n" % commit)
3326
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003327 if 'Owner' in labelDetails:
Luke Diamand06804c72012-04-11 17:21:24 +02003328 owner = labelDetails["Owner"]
3329 else:
3330 owner = None
3331
3332 # Try to use the owner of the p4 label, or failing that,
3333 # the current p4 user id.
3334 if owner:
3335 email = self.make_email(owner)
3336 else:
3337 email = self.make_email(self.p4UserId())
Luke Diamand06804c72012-04-11 17:21:24 +02003338
Tao Klerksf7b5ff62022-04-30 19:26:52 +00003339 gitStream.write("tagger ")
3340 gitStream.write(email)
3341 gitStream.write(" %s %s\n" % (epoch, self.tz))
Luke Diamand06804c72012-04-11 17:21:24 +02003342
Joel Holdsworth12a77f52022-04-01 15:24:53 +01003343 print("labelDetails=", labelDetails)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003344 if 'Description' in labelDetails:
Luke Diamand06804c72012-04-11 17:21:24 +02003345 description = labelDetails['Description']
3346 else:
3347 description = 'Label from git p4'
3348
3349 gitStream.write("data %d\n" % len(description))
3350 gitStream.write(description)
3351 gitStream.write("\n")
3352
Lars Schneider4ae048e2015-12-08 10:36:22 +01003353 def inClientSpec(self, path):
3354 if not self.clientSpecDirs:
3355 return True
3356 inClientSpec = self.clientSpecDirs.map_in_client(path)
3357 if not inClientSpec and self.verbose:
3358 print('Ignoring file outside of client spec: {0}'.format(path))
3359 return inClientSpec
3360
3361 def hasBranchPrefix(self, path):
3362 if not self.branchPrefixes:
3363 return True
3364 hasPrefix = [p for p in self.branchPrefixes
3365 if p4PathStartsWith(path, p)]
Andrew Oakley09667d02016-06-22 10:26:11 +01003366 if not hasPrefix and self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01003367 print('Ignoring file outside of prefix: {0}'.format(path))
3368 return hasPrefix
3369
Andrew Oakley82e46d62020-05-10 11:16:50 +01003370 def findShadowedFiles(self, files, change):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003371 """Perforce allows you commit files and directories with the same name,
3372 so you could have files //depot/foo and //depot/foo/bar both checked
3373 in. A p4 sync of a repository in this state fails. Deleting one of
3374 the files recovers the repository.
3375
3376 Git will not allow the broken state to exist and only the most
3377 recent of the conflicting names is left in the repository. When one
3378 of the conflicting files is deleted we need to re-add the other one
3379 to make sure the git repository recovers in the same way as
3380 perforce.
3381 """
3382
Andrew Oakley82e46d62020-05-10 11:16:50 +01003383 deleted = [f for f in files if f['action'] in self.delete_actions]
3384 to_check = set()
3385 for f in deleted:
3386 path = decode_path(f['path'])
3387 to_check.add(path + '/...')
3388 while True:
3389 path = path.rsplit("/", 1)[0]
3390 if path == "/" or path in to_check:
3391 break
3392 to_check.add(path)
3393 to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
3394 if self.hasBranchPrefix(p)]
3395 if to_check:
3396 stat_result = p4CmdList(["-x", "-", "fstat", "-T",
3397 "depotFile,headAction,headRev,headType"], stdin=to_check)
3398 for record in stat_result:
3399 if record['code'] != 'stat':
3400 continue
3401 if record['headAction'] in self.delete_actions:
3402 continue
3403 files.append({
3404 'action': 'add',
3405 'path': record['depotFile'],
3406 'rev': record['headRev'],
3407 'type': record['headType']})
3408
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01003409 def commit(self, details, files, branch, parent="", allow_empty=False):
Simon Hausmannb9847332007-03-20 20:54:23 +01003410 epoch = details["time"]
3411 author = details["user"]
Jan Durovec26e6a272016-04-19 19:49:41 +00003412 jobs = self.extractJobsFromCommit(details)
Simon Hausmannb9847332007-03-20 20:54:23 +01003413
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003414 if self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01003415 print('commit into {0}'.format(branch))
Han-Wen Nienhuys96e07dd2007-05-23 18:49:35 -03003416
Andrew Oakley82e46d62020-05-10 11:16:50 +01003417 files = [f for f in files
3418 if self.hasBranchPrefix(decode_path(f['path']))]
3419 self.findShadowedFiles(files, details['change'])
3420
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09003421 if self.clientSpecDirs:
3422 self.clientSpecDirs.update_client_spec_path_cache(files)
3423
Andrew Oakley82e46d62020-05-10 11:16:50 +01003424 files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
Lars Schneider4ae048e2015-12-08 10:36:22 +01003425
Luke Diamand89143ac2018-10-15 12:14:08 +01003426 if gitConfigBool('git-p4.keepEmptyCommits'):
3427 allow_empty = True
3428
3429 if not files and not allow_empty:
Lars Schneider4ae048e2015-12-08 10:36:22 +01003430 print('Ignoring revision {0} as it would produce an empty commit.'
3431 .format(details['change']))
3432 return
3433
Simon Hausmannb9847332007-03-20 20:54:23 +01003434 self.gitStream.write("commit %s\n" % branch)
Luke Diamandb43702a2015-08-27 08:18:58 +01003435 self.gitStream.write("mark :%s\n" % details["change"])
Simon Hausmannb9847332007-03-20 20:54:23 +01003436 self.committedChanges.add(int(details["change"]))
Simon Hausmannb607e712007-05-20 10:55:54 +02003437 if author not in self.users:
3438 self.getUserMapFromPerforceServer()
Simon Hausmannb9847332007-03-20 20:54:23 +01003439
Tao Klerksf7b5ff62022-04-30 19:26:52 +00003440 self.gitStream.write("committer ")
3441 self.gitStream.write(self.make_email(author))
3442 self.gitStream.write(" %s %s\n" % (epoch, self.tz))
Simon Hausmannb9847332007-03-20 20:54:23 +01003443
3444 self.gitStream.write("data <<EOT\n")
3445 self.gitStream.write(details["desc"])
Jan Durovec26e6a272016-04-19 19:49:41 +00003446 if len(jobs) > 0:
3447 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
Luke Diamand123f6312018-05-23 23:20:26 +01003448
3449 if not self.suppress_meta_comment:
3450 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3451 (','.join(self.branchPrefixes), details["change"]))
3452 if len(details['options']) > 0:
3453 self.gitStream.write(": options = %s" % details['options'])
3454 self.gitStream.write("]\n")
3455
3456 self.gitStream.write("EOT\n\n")
Simon Hausmannb9847332007-03-20 20:54:23 +01003457
3458 if len(parent) > 0:
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003459 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003460 print("parent %s" % parent)
Simon Hausmannb9847332007-03-20 20:54:23 +01003461 self.gitStream.write("from %s\n" % parent)
3462
Lars Schneider4ae048e2015-12-08 10:36:22 +01003463 self.streamP4Files(files)
Simon Hausmannb9847332007-03-20 20:54:23 +01003464 self.gitStream.write("\n")
3465
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003466 change = int(details["change"])
3467
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003468 if change in self.labels:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003469 label = self.labels[change]
3470 labelDetails = label[0]
3471 labelRevisions = label[1]
Simon Hausmann71b112d2007-05-19 11:54:11 +02003472 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003473 print("Change %s is labelled %s" % (change, labelDetails))
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003474
Luke Diamand6de040d2011-10-16 10:47:52 -04003475 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003476 for p in self.branchPrefixes])
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003477
3478 if len(files) == len(labelRevisions):
3479
3480 cleanedFiles = {}
3481 for info in files:
Pete Wyckoff56c09342011-02-19 08:17:57 -05003482 if info["action"] in self.delete_actions:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003483 continue
3484 cleanedFiles[info["depotFile"]] = info["rev"]
3485
3486 if cleanedFiles == labelRevisions:
Luke Diamand06804c72012-04-11 17:21:24 +02003487 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003488
3489 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02003490 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003491 print("Tag %s does not match with change %s: files do not match."
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003492 % (labelDetails["label"], change))
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003493
3494 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02003495 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003496 print("Tag %s does not match with change %s: file count is different."
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003497 % (labelDetails["label"], change))
Simon Hausmannb9847332007-03-20 20:54:23 +01003498
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003499 def getLabels(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003500 """Build a dictionary of changelists and labels, for "detect-labels"
3501 option.
3502 """
3503
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003504 self.labels = {}
3505
Luke Diamand52a48802012-01-19 09:52:25 +00003506 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
Simon Hausmann10c32112007-04-08 10:15:47 +02003507 if len(l) > 0 and not self.silent:
Luke Diamand4d885192018-06-19 09:04:08 +01003508 print("Finding files belonging to labels in %s" % self.depotPaths)
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003509
3510 for output in l:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003511 label = output["label"]
3512 revisions = {}
3513 newestChange = 0
Simon Hausmann71b112d2007-05-19 11:54:11 +02003514 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003515 print("Querying files for label %s" % label)
Luke Diamand6de040d2011-10-16 10:47:52 -04003516 for file in p4CmdList(["files"] +
3517 ["%s...@%s" % (p, label)
3518 for p in self.depotPaths]):
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003519 revisions[file["depotFile"]] = file["rev"]
3520 change = int(file["change"])
3521 if change > newestChange:
3522 newestChange = change
3523
Simon Hausmann9bda3a82007-05-19 12:05:40 +02003524 self.labels[newestChange] = [output, revisions]
3525
3526 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003527 print("Label changes: %s" % self.labels.keys())
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003528
Luke Diamand06804c72012-04-11 17:21:24 +02003529 def importP4Labels(self, stream, p4Labels):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003530 """Import p4 labels as git tags. A direct mapping does not exist, so
3531 assume that if all the files are at the same revision then we can
3532 use that, or it's something more complicated we should just ignore.
3533 """
3534
Luke Diamand06804c72012-04-11 17:21:24 +02003535 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003536 print("import p4 labels: " + ' '.join(p4Labels))
Luke Diamand06804c72012-04-11 17:21:24 +02003537
3538 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
Luke Diamandc8942a22012-04-11 17:21:24 +02003539 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
Luke Diamand06804c72012-04-11 17:21:24 +02003540 if len(validLabelRegexp) == 0:
3541 validLabelRegexp = defaultLabelRegexp
3542 m = re.compile(validLabelRegexp)
3543
3544 for name in p4Labels:
3545 commitFound = False
3546
3547 if not m.match(name):
3548 if verbose:
Joel Holdsworth12a77f52022-04-01 15:24:53 +01003549 print("label %s does not match regexp %s" % (name, validLabelRegexp))
Luke Diamand06804c72012-04-11 17:21:24 +02003550 continue
3551
3552 if name in ignoredP4Labels:
3553 continue
3554
3555 labelDetails = p4CmdList(['label', "-o", name])[0]
3556
3557 # get the most recent changelist for each file in this label
3558 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3559 for p in self.depotPaths])
3560
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003561 if 'change' in change:
Luke Diamand06804c72012-04-11 17:21:24 +02003562 # find the corresponding git commit; take the oldest commit
3563 changelist = int(change['change'])
Luke Diamandb43702a2015-08-27 08:18:58 +01003564 if changelist in self.committedChanges:
3565 gitCommit = ":%d" % changelist # use a fast-import mark
Luke Diamand06804c72012-04-11 17:21:24 +02003566 commitFound = True
Luke Diamandb43702a2015-08-27 08:18:58 +01003567 else:
3568 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3569 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3570 if len(gitCommit) == 0:
Luke Diamandf2606b12018-06-19 09:04:10 +01003571 print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
Luke Diamandb43702a2015-08-27 08:18:58 +01003572 else:
3573 commitFound = True
3574 gitCommit = gitCommit.strip()
3575
3576 if commitFound:
Luke Diamand06804c72012-04-11 17:21:24 +02003577 # Convert from p4 time format
3578 try:
3579 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3580 except ValueError:
Luke Diamandf2606b12018-06-19 09:04:10 +01003581 print("Could not convert label time %s" % labelDetails['Update'])
Luke Diamand06804c72012-04-11 17:21:24 +02003582 tmwhen = 1
3583
3584 when = int(time.mktime(tmwhen))
3585 self.streamTag(stream, name, labelDetails, gitCommit, when)
3586 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003587 print("p4 label %s mapped to git commit %s" % (name, gitCommit))
Luke Diamand06804c72012-04-11 17:21:24 +02003588 else:
3589 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003590 print("Label %s has no changelists - possibly deleted?" % name)
Luke Diamand06804c72012-04-11 17:21:24 +02003591
3592 if not commitFound:
3593 # We can't import this label; don't try again as it will get very
3594 # expensive repeatedly fetching all the files for labels that will
3595 # never be imported. If the label is moved in the future, the
3596 # ignore will need to be removed manually.
3597 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3598
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003599 def guessProjectName(self):
3600 for p in self.depotPaths:
Simon Hausmann6e5295c2007-06-11 08:50:57 +02003601 if p.endswith("/"):
3602 p = p[:-1]
3603 p = p[p.strip().rfind("/") + 1:]
3604 if not p.endswith("/"):
Joel Holdsworth812ee742022-04-01 15:24:45 +01003605 p += "/"
Simon Hausmann6e5295c2007-06-11 08:50:57 +02003606 return p
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003607
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003608 def getBranchMapping(self):
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003609 lostAndFoundBranches = set()
3610
Vitor Antunes8ace74c2011-08-19 00:44:04 +01003611 user = gitConfig("git-p4.branchUser")
Vitor Antunes8ace74c2011-08-19 00:44:04 +01003612
Joel Holdsworth8a470592022-01-06 21:40:34 +00003613 for info in p4CmdList(
3614 ["branches"] + (["-u", user] if len(user) > 0 else [])):
Luke Diamand52a48802012-01-19 09:52:25 +00003615 details = p4Cmd(["branch", "-o", info["branch"]])
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003616 viewIdx = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003617 while "View%s" % viewIdx in details:
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003618 paths = details["View%s" % viewIdx].split(" ")
3619 viewIdx = viewIdx + 1
3620 # require standard //depot/foo/... //depot/bar/... mapping
3621 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3622 continue
3623 source = paths[0]
3624 destination = paths[1]
Joel Holdsworthc785e202022-04-01 15:24:57 +01003625 # HACK
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01003626 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
Simon Hausmann6509e192007-06-07 09:41:53 +02003627 source = source[len(self.depotPaths[0]):-4]
3628 destination = destination[len(self.depotPaths[0]):-4]
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003629
Simon Hausmann1a2edf42007-06-17 15:10:24 +02003630 if destination in self.knownBranches:
3631 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003632 print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3633 print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
Simon Hausmann1a2edf42007-06-17 15:10:24 +02003634 continue
3635
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003636 self.knownBranches[destination] = source
3637
3638 lostAndFoundBranches.discard(destination)
3639
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003640 if source not in self.knownBranches:
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003641 lostAndFoundBranches.add(source)
3642
Vitor Antunes7199cf12011-08-19 00:44:05 +01003643 # Perforce does not strictly require branches to be defined, so we also
3644 # check git config for a branch list.
3645 #
3646 # Example of branch definition in git config file:
3647 # [git-p4]
3648 # branchList=main:branchA
3649 # branchList=main:branchB
3650 # branchList=branchA:branchC
3651 configBranches = gitConfigList("git-p4.branchList")
3652 for branch in configBranches:
3653 if branch:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01003654 source, destination = branch.split(":")
Vitor Antunes7199cf12011-08-19 00:44:05 +01003655 self.knownBranches[destination] = source
3656
3657 lostAndFoundBranches.discard(destination)
3658
3659 if source not in self.knownBranches:
3660 lostAndFoundBranches.add(source)
3661
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003662 for branch in lostAndFoundBranches:
3663 self.knownBranches[branch] = branch
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003664
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01003665 def getBranchMappingFromGitBranches(self):
3666 branches = p4BranchesInGit(self.importIntoRemotes)
3667 for branch in branches.keys():
3668 if branch == "master":
3669 branch = "main"
3670 else:
3671 branch = branch[len(self.projectName):]
3672 self.knownBranches[branch] = branch
3673
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003674 def updateOptionDict(self, d):
3675 option_keys = {}
3676 if self.keepRepoPath:
3677 option_keys['keepRepoPath'] = 1
3678
3679 d["options"] = ' '.join(sorted(option_keys.keys()))
3680
3681 def readOptions(self, d):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003682 self.keepRepoPath = ('options' in d
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003683 and ('keepRepoPath' in d['options']))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003684
Simon Hausmann8134f692007-08-26 16:44:55 +02003685 def gitRefForBranch(self, branch):
3686 if branch == "main":
3687 return self.refPrefix + "master"
3688
3689 if len(branch) <= 0:
3690 return branch
3691
3692 return self.refPrefix + self.projectName + branch
3693
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003694 def gitCommitByP4Change(self, ref, change):
3695 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003696 print("looking in ref " + ref + " for change %s using bisect..." % change)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003697
3698 earliestCommit = ""
3699 latestCommit = parseRevision(ref)
3700
3701 while True:
3702 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003703 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
Joel Holdsworth8a470592022-01-06 21:40:34 +00003704 next = read_pipe(["git", "rev-list", "--bisect",
3705 latestCommit, earliestCommit]).strip()
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003706 if len(next) == 0:
3707 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003708 print("argh")
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003709 return ""
3710 log = extractLogMessageFromGitCommit(next)
3711 settings = extractSettingsGitLog(log)
3712 currentChange = int(settings['change'])
3713 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003714 print("current change %s" % currentChange)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003715
3716 if currentChange == change:
3717 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003718 print("found %s" % next)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003719 return next
3720
3721 if currentChange < change:
3722 earliestCommit = "^%s" % next
3723 else:
Mazo, Andrey2dda7412019-04-01 18:02:17 +00003724 if next == latestCommit:
3725 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3726 latestCommit = "%s^@" % next
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003727
3728 return ""
3729
3730 def importNewBranch(self, branch, maxChange):
3731 # make fast-import flush all changes to disk and update the refs using the checkpoint
3732 # command so that we can try to find the branch parent in the git history
Joel Holdsworth990547a2022-04-01 15:24:44 +01003733 self.gitStream.write("checkpoint\n\n")
3734 self.gitStream.flush()
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003735 branchPrefix = self.depotPaths[0] + branch + "/"
3736 range = "@1,%s" % maxChange
Lex Spoon96b2d542015-04-20 11:00:20 -04003737 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003738 if len(changes) <= 0:
3739 return False
3740 firstChange = changes[0]
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003741 sourceBranch = self.knownBranches[branch]
3742 sourceDepotPath = self.depotPaths[0] + sourceBranch
3743 sourceRef = self.gitRefForBranch(sourceBranch)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003744
Luke Diamand52a48802012-01-19 09:52:25 +00003745 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003746 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3747 if len(gitParent) > 0:
3748 self.initialParents[self.gitRefForBranch(branch)] = gitParent
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003749
3750 self.importChanges(changes)
3751 return True
3752
Vitor Antunesfed23692012-01-25 23:48:22 +00003753 def searchParent(self, parent, branch, target):
Joachim Kuebart6b798182021-05-05 11:56:26 +00003754 targetTree = read_pipe(["git", "rev-parse",
3755 "{}^{{tree}}".format(target)]).strip()
3756 for line in read_pipe_lines(["git", "rev-list", "--format=%H %T",
Pete Wyckoffc7d34882013-01-26 22:11:21 -05003757 "--no-merges", parent]):
Joachim Kuebart6b798182021-05-05 11:56:26 +00003758 if line.startswith("commit "):
3759 continue
3760 commit, tree = line.strip().split(" ")
3761 if tree == targetTree:
Vitor Antunesfed23692012-01-25 23:48:22 +00003762 if self.verbose:
Joachim Kuebart6b798182021-05-05 11:56:26 +00003763 print("Found parent of %s in commit %s" % (branch, commit))
3764 return commit
3765 return None
Vitor Antunesfed23692012-01-25 23:48:22 +00003766
Luke Diamand89143ac2018-10-15 12:14:08 +01003767 def importChanges(self, changes, origin_revision=0):
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003768 cnt = 1
3769 for change in changes:
Luke Diamand89143ac2018-10-15 12:14:08 +01003770 description = p4_describe(change)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003771 self.updateOptionDict(description)
3772
3773 if not self.silent:
Joel Holdsworth0f829622021-12-19 15:40:28 +00003774 sys.stdout.write("\rImporting revision %s (%d%%)" % (
3775 change, (cnt * 100) // len(changes)))
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003776 sys.stdout.flush()
3777 cnt = cnt + 1
3778
3779 try:
3780 if self.detectBranches:
3781 branches = self.splitFilesIntoBranches(description)
3782 for branch in branches.keys():
Joel Holdsworthc785e202022-04-01 15:24:57 +01003783 # HACK --hwn
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003784 branchPrefix = self.depotPaths[0] + branch + "/"
Joel Holdsworth84af8b82022-04-01 15:24:50 +01003785 self.branchPrefixes = [branchPrefix]
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003786
3787 parent = ""
3788
3789 filesForCommit = branches[branch]
3790
3791 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003792 print("branch is %s" % branch)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003793
3794 self.updatedBranches.add(branch)
3795
3796 if branch not in self.createdBranches:
3797 self.createdBranches.add(branch)
3798 parent = self.knownBranches[branch]
3799 if parent == branch:
3800 parent = ""
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003801 else:
3802 fullBranch = self.projectName + branch
3803 if fullBranch not in self.p4BranchesInGit:
3804 if not self.silent:
Joel Holdsworth990547a2022-04-01 15:24:44 +01003805 print("\n Importing new branch %s" % fullBranch)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003806 if self.importNewBranch(branch, change - 1):
3807 parent = ""
3808 self.p4BranchesInGit.append(fullBranch)
3809 if not self.silent:
Joel Holdsworth990547a2022-04-01 15:24:44 +01003810 print("\n Resuming with change %s" % change)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003811
3812 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003813 print("parent determined through known branches: %s" % parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003814
Simon Hausmann8134f692007-08-26 16:44:55 +02003815 branch = self.gitRefForBranch(branch)
3816 parent = self.gitRefForBranch(parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003817
3818 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003819 print("looking for initial parent for %s; current parent is %s" % (branch, parent))
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003820
3821 if len(parent) == 0 and branch in self.initialParents:
3822 parent = self.initialParents[branch]
3823 del self.initialParents[branch]
3824
Vitor Antunesfed23692012-01-25 23:48:22 +00003825 blob = None
3826 if len(parent) > 0:
Pete Wyckoff4f9273d2013-01-26 22:11:04 -05003827 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
Vitor Antunesfed23692012-01-25 23:48:22 +00003828 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003829 print("Creating temporary branch: " + tempBranch)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003830 self.commit(description, filesForCommit, tempBranch)
Vitor Antunesfed23692012-01-25 23:48:22 +00003831 self.tempBranches.append(tempBranch)
3832 self.checkpoint()
3833 blob = self.searchParent(parent, branch, tempBranch)
3834 if blob:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003835 self.commit(description, filesForCommit, branch, blob)
Vitor Antunesfed23692012-01-25 23:48:22 +00003836 else:
3837 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003838 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003839 self.commit(description, filesForCommit, branch, parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003840 else:
Luke Diamand89143ac2018-10-15 12:14:08 +01003841 files = self.extractFilesFromCommit(description)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003842 self.commit(description, files, self.branch,
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003843 self.initialParent)
Pete Wyckoff47497842013-01-14 19:47:04 -05003844 # only needed once, to connect to the previous commit
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003845 self.initialParent = ""
3846 except IOError:
Luke Diamandf2606b12018-06-19 09:04:10 +01003847 print(self.gitError.read())
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003848 sys.exit(1)
3849
Luke Diamandb9d34db2018-06-08 21:32:44 +01003850 def sync_origin_only(self):
3851 if self.syncWithOrigin:
3852 self.hasOrigin = originP4BranchesExist()
3853 if self.hasOrigin:
3854 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003855 print('Syncing with origin first, using "git fetch origin"')
Joel Holdsworth8a470592022-01-06 21:40:34 +00003856 system(["git", "fetch", "origin"])
Luke Diamandb9d34db2018-06-08 21:32:44 +01003857
Simon Hausmannc208a242007-08-26 16:07:18 +02003858 def importHeadRevision(self, revision):
Luke Diamandf2606b12018-06-19 09:04:10 +01003859 print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
Simon Hausmannc208a242007-08-26 16:07:18 +02003860
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003861 details = {}
3862 details["user"] = "git perforce import user"
Pete Wyckoff1494fcb2011-02-19 08:17:56 -05003863 details["desc"] = ("Initial import of %s from the state at revision %s\n"
Simon Hausmannc208a242007-08-26 16:07:18 +02003864 % (' '.join(self.depotPaths), revision))
3865 details["change"] = revision
3866 newestRevision = 0
3867
3868 fileCnt = 0
Joel Holdsworth12a77f52022-04-01 15:24:53 +01003869 fileArgs = ["%s...%s" % (p, revision) for p in self.depotPaths]
Luke Diamand6de040d2011-10-16 10:47:52 -04003870
3871 for info in p4CmdList(["files"] + fileArgs):
Simon Hausmannc208a242007-08-26 16:07:18 +02003872
Pete Wyckoff68b28592011-02-19 08:17:55 -05003873 if 'code' in info and info['code'] == 'error':
Simon Hausmannc208a242007-08-26 16:07:18 +02003874 sys.stderr.write("p4 returned an error: %s\n"
3875 % info['data'])
Pete Wyckoffd88e7072011-02-19 08:17:58 -05003876 if info['data'].find("must refer to client") >= 0:
3877 sys.stderr.write("This particular p4 error is misleading.\n")
Joel Holdsworth990547a2022-04-01 15:24:44 +01003878 sys.stderr.write("Perhaps the depot path was misspelled.\n")
Pete Wyckoffd88e7072011-02-19 08:17:58 -05003879 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
Simon Hausmannc208a242007-08-26 16:07:18 +02003880 sys.exit(1)
Pete Wyckoff68b28592011-02-19 08:17:55 -05003881 if 'p4ExitCode' in info:
3882 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
Simon Hausmannc208a242007-08-26 16:07:18 +02003883 sys.exit(1)
3884
Simon Hausmannc208a242007-08-26 16:07:18 +02003885 change = int(info["change"])
3886 if change > newestRevision:
3887 newestRevision = change
3888
Pete Wyckoff56c09342011-02-19 08:17:57 -05003889 if info["action"] in self.delete_actions:
Simon Hausmannc208a242007-08-26 16:07:18 +02003890 continue
3891
Joel Holdsworth84af8b82022-04-01 15:24:50 +01003892 for prop in ["depotFile", "rev", "action", "type"]:
Simon Hausmannc208a242007-08-26 16:07:18 +02003893 details["%s%s" % (prop, fileCnt)] = info[prop]
3894
3895 fileCnt = fileCnt + 1
3896
3897 details["change"] = newestRevision
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003898
Pete Wyckoff9dcb9f22012-04-08 20:18:01 -04003899 # Use time from top-most change so that all git p4 clones of
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003900 # the same p4 repo have the same commit SHA1s.
Pete Wyckoff18fa13d2012-11-23 17:35:34 -05003901 res = p4_describe(newestRevision)
3902 details["time"] = res["time"]
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003903
Simon Hausmannc208a242007-08-26 16:07:18 +02003904 self.updateOptionDict(details)
3905 try:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003906 self.commit(details, self.extractFilesFromCommit(details), self.branch)
Philip.McGrawde5abb52019-08-27 06:43:58 +03003907 except IOError as err:
Luke Diamandf2606b12018-06-19 09:04:10 +01003908 print("IO error with git fast-import. Is your git version recent enough?")
Philip.McGrawde5abb52019-08-27 06:43:58 +03003909 print("IO error details: {}".format(err))
Luke Diamandf2606b12018-06-19 09:04:10 +01003910 print(self.gitError.read())
Simon Hausmannc208a242007-08-26 16:07:18 +02003911
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003912 def importRevisions(self, args, branch_arg_given):
3913 changes = []
3914
3915 if len(self.changesFile) > 0:
Luke Diamand43f33e42020-01-30 11:50:34 +00003916 with open(self.changesFile) as f:
3917 output = f.readlines()
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003918 changeSet = set()
3919 for line in output:
3920 changeSet.add(int(line))
3921
3922 for change in changeSet:
3923 changes.append(change)
3924
3925 changes.sort()
3926 else:
3927 # catch "git p4 sync" with no new branches, in a repo that
3928 # does not have any existing p4 branches
3929 if len(args) == 0:
3930 if not self.p4BranchesInGit:
Luke Diamand6026aff2020-01-29 11:12:45 +00003931 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003932
3933 # The default branch is master, unless --branch is used to
3934 # specify something else. Make sure it exists, or complain
3935 # nicely about how to use --branch.
3936 if not self.detectBranches:
3937 if not branch_exists(self.branch):
3938 if branch_arg_given:
Luke Diamand6026aff2020-01-29 11:12:45 +00003939 raise P4CommandException("Error: branch %s does not exist." % self.branch)
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003940 else:
Luke Diamand6026aff2020-01-29 11:12:45 +00003941 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003942 self.branch)
3943
3944 if self.verbose:
3945 print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3946 self.changeRange))
3947 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3948
3949 if len(self.maxChanges) > 0:
3950 changes = changes[:min(int(self.maxChanges), len(changes))]
3951
3952 if len(changes) == 0:
3953 if not self.silent:
3954 print("No changes to import!")
3955 else:
3956 if not self.silent and not self.detectBranches:
3957 print("Import destination: %s" % self.branch)
3958
3959 self.updatedBranches = set()
3960
3961 if not self.detectBranches:
3962 if args:
3963 # start a new branch
3964 self.initialParent = ""
3965 else:
3966 # build on a previous revision
3967 self.initialParent = parseRevision(self.branch)
3968
3969 self.importChanges(changes)
3970
3971 if not self.silent:
3972 print("")
3973 if len(self.updatedBranches) > 0:
3974 sys.stdout.write("Updated branches: ")
3975 for b in self.updatedBranches:
3976 sys.stdout.write("%s " % b)
3977 sys.stdout.write("\n")
3978
Luke Diamand123f6312018-05-23 23:20:26 +01003979 def openStreams(self):
3980 self.importProcess = subprocess.Popen(["git", "fast-import"],
3981 stdin=subprocess.PIPE,
3982 stdout=subprocess.PIPE,
Joel Holdsworth990547a2022-04-01 15:24:44 +01003983 stderr=subprocess.PIPE)
Luke Diamand123f6312018-05-23 23:20:26 +01003984 self.gitOutput = self.importProcess.stdout
3985 self.gitStream = self.importProcess.stdin
3986 self.gitError = self.importProcess.stderr
3987
Yang Zhao86dca242019-12-13 15:52:39 -08003988 if bytes is not str:
3989 # Wrap gitStream.write() so that it can be called using `str` arguments
3990 def make_encoded_write(write):
3991 def encoded_write(s):
3992 return write(s.encode() if isinstance(s, str) else s)
3993 return encoded_write
3994
3995 self.gitStream.write = make_encoded_write(self.gitStream.write)
3996
Luke Diamand123f6312018-05-23 23:20:26 +01003997 def closeStreams(self):
Luke Diamand837b3a62020-01-29 11:12:41 +00003998 if self.gitStream is None:
3999 return
Luke Diamand123f6312018-05-23 23:20:26 +01004000 self.gitStream.close()
4001 if self.importProcess.wait() != 0:
4002 die("fast-import failed: %s" % self.gitError.read())
4003 self.gitOutput.close()
4004 self.gitError.close()
Luke Diamand837b3a62020-01-29 11:12:41 +00004005 self.gitStream = None
Simon Hausmannc208a242007-08-26 16:07:18 +02004006
Simon Hausmannb9847332007-03-20 20:54:23 +01004007 def run(self, args):
Simon Hausmanna028a982007-05-23 00:03:08 +02004008 if self.importIntoRemotes:
4009 self.refPrefix = "refs/remotes/p4/"
4010 else:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02004011 self.refPrefix = "refs/heads/p4/"
Simon Hausmanna028a982007-05-23 00:03:08 +02004012
Luke Diamandb9d34db2018-06-08 21:32:44 +01004013 self.sync_origin_only()
Simon Hausmann10f880f2007-05-24 22:28:28 +02004014
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05004015 branch_arg_given = bool(self.branch)
Simon Hausmann569d1bd2007-03-22 21:34:16 +01004016 if len(self.branch) == 0:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02004017 self.branch = self.refPrefix + "master"
Simon Hausmanna028a982007-05-23 00:03:08 +02004018 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
Joel Holdsworth8a470592022-01-06 21:40:34 +00004019 system(["git", "update-ref", self.branch, "refs/heads/p4"])
4020 system(["git", "branch", "-D", "p4"])
Simon Hausmann179caeb2007-03-22 22:17:42 +01004021
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05004022 # accept either the command-line option, or the configuration variable
4023 if self.useClientSpec:
4024 # will use this after clone to set the variable
4025 self.useClientSpec_from_options = True
4026 else:
Pete Wyckoff0d609032013-01-26 22:11:24 -05004027 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff09fca772011-12-24 21:07:39 -05004028 self.useClientSpec = True
4029 if self.useClientSpec:
Pete Wyckoff543987b2012-02-25 20:06:25 -05004030 self.clientSpecDirs = getClientSpec()
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01004031
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004032 # TODO: should always look at previous commits,
4033 # merge with previous imports, if possible.
4034 if args == []:
Simon Hausmannd414c742007-05-25 11:36:42 +02004035 if self.hasOrigin:
Simon Hausmann5ca44612007-08-24 17:44:16 +02004036 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
Pete Wyckoff3b650fc2013-01-14 19:46:58 -05004037
4038 # branches holds mapping from branch name to sha1
4039 branches = p4BranchesInGit(self.importIntoRemotes)
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004040
4041 # restrict to just this one, disabling detect-branches
4042 if branch_arg_given:
Tao Klerks17f273f2022-04-04 05:10:54 +00004043 short = shortP4Ref(self.branch, self.importIntoRemotes)
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004044 if short in branches:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004045 self.p4BranchesInGit = [short]
Tao Klerks17f273f2022-04-04 05:10:54 +00004046 elif self.branch.startswith('refs/') and \
4047 branchExists(self.branch) and \
4048 '[git-p4:' in extractLogMessageFromGitCommit(self.branch):
Junio C Hamanoaf3a3202022-05-20 15:26:55 -07004049 self.p4BranchesInGit = [self.branch]
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004050 else:
4051 self.p4BranchesInGit = branches.keys()
Simon Hausmannabcd7902007-05-24 22:25:36 +02004052
4053 if len(self.p4BranchesInGit) > 1:
4054 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01004055 print("Importing from/into multiple branches")
Simon Hausmannabcd7902007-05-24 22:25:36 +02004056 self.detectBranches = True
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004057 for branch in branches.keys():
4058 self.initialParents[self.refPrefix + branch] = \
4059 branches[branch]
Simon Hausmann967f72e2007-03-23 09:30:41 +01004060
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004061 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01004062 print("branches: %s" % self.p4BranchesInGit)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004063
4064 p4Change = 0
4065 for branch in self.p4BranchesInGit:
Tao Klerks17f273f2022-04-04 05:10:54 +00004066 logMsg = extractLogMessageFromGitCommit(fullP4Ref(branch,
4067 self.importIntoRemotes))
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004068
4069 settings = extractSettingsGitLog(logMsg)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004070
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004071 self.readOptions(settings)
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01004072 if 'depot-paths' in settings and 'change' in settings:
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004073 change = int(settings['change']) + 1
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004074 p4Change = max(p4Change, change)
4075
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004076 depotPaths = sorted(settings['depot-paths'])
4077 if self.previousDepotPaths == []:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004078 self.previousDepotPaths = depotPaths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004079 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004080 paths = []
4081 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
Vitor Antunes04d277b2011-08-19 00:44:03 +01004082 prev_list = prev.split("/")
4083 cur_list = cur.split("/")
4084 for i in range(0, min(len(cur_list), len(prev_list))):
Luke Diamandfc35c9d2018-06-19 09:04:06 +01004085 if cur_list[i] != prev_list[i]:
Simon Hausmann583e1702007-06-07 09:37:13 +02004086 i = i - 1
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004087 break
4088
Joel Holdsworth843d8472022-04-01 15:24:54 +01004089 paths.append("/".join(cur_list[:i + 1]))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004090
4091 self.previousDepotPaths = paths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004092
4093 if p4Change > 0:
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004094 self.depotPaths = sorted(self.previousDepotPaths)
Simon Hausmannd5904672007-05-19 11:07:32 +02004095 self.changeRange = "@%s,#head" % p4Change
Simon Hausmann341dc1c2007-05-21 00:39:16 +02004096 if not self.silent and not self.detectBranches:
Luke Diamandf2606b12018-06-19 09:04:10 +01004097 print("Performing incremental import into %s git branch" % self.branch)
Simon Hausmann569d1bd2007-03-22 21:34:16 +01004098
Tao Klerks17f273f2022-04-04 05:10:54 +00004099 self.branch = fullP4Ref(self.branch, self.importIntoRemotes)
Simon Hausmann179caeb2007-03-22 22:17:42 +01004100
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004101 if len(args) == 0 and self.depotPaths:
Simon Hausmannb9847332007-03-20 20:54:23 +01004102 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01004103 print("Depot paths: %s" % ' '.join(self.depotPaths))
Simon Hausmannb9847332007-03-20 20:54:23 +01004104 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004105 if self.depotPaths and self.depotPaths != args:
Luke Diamandf2606b12018-06-19 09:04:10 +01004106 print("previous import used depot path %s and now %s was specified. "
Joel Holdsworth843d8472022-04-01 15:24:54 +01004107 "This doesn't work!" % (' '.join(self.depotPaths),
4108 ' '.join(args)))
Simon Hausmannb9847332007-03-20 20:54:23 +01004109 sys.exit(1)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004110
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004111 self.depotPaths = sorted(args)
Simon Hausmannb9847332007-03-20 20:54:23 +01004112
Simon Hausmann1c49fc12007-08-26 16:04:34 +02004113 revision = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01004114 self.users = {}
Simon Hausmannb9847332007-03-20 20:54:23 +01004115
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05004116 # Make sure no revision specifiers are used when --changesfile
4117 # is specified.
4118 bad_changesfile = False
4119 if len(self.changesFile) > 0:
4120 for p in self.depotPaths:
4121 if p.find("@") >= 0 or p.find("#") >= 0:
4122 bad_changesfile = True
4123 break
4124 if bad_changesfile:
4125 die("Option --changesfile is incompatible with revision specifiers")
4126
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004127 newPaths = []
4128 for p in self.depotPaths:
4129 if p.find("@") != -1:
4130 atIdx = p.index("@")
4131 self.changeRange = p[atIdx:]
4132 if self.changeRange == "@all":
4133 self.changeRange = ""
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004134 elif ',' not in self.changeRange:
Simon Hausmann1c49fc12007-08-26 16:04:34 +02004135 revision = self.changeRange
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004136 self.changeRange = ""
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07004137 p = p[:atIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004138 elif p.find("#") != -1:
4139 hashIdx = p.index("#")
Simon Hausmann1c49fc12007-08-26 16:04:34 +02004140 revision = p[hashIdx:]
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07004141 p = p[:hashIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004142 elif self.previousDepotPaths == []:
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05004143 # pay attention to changesfile, if given, else import
4144 # the entire p4 tree at the head revision
4145 if len(self.changesFile) == 0:
4146 revision = "#head"
Simon Hausmannb9847332007-03-20 20:54:23 +01004147
Joel Holdsworth843d8472022-04-01 15:24:54 +01004148 p = re.sub("\.\.\.$", "", p)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004149 if not p.endswith("/"):
4150 p += "/"
4151
4152 newPaths.append(p)
4153
4154 self.depotPaths = newPaths
4155
Pete Wyckoffe63231e2012-08-11 12:55:02 -04004156 # --detect-branches may change this for each branch
4157 self.branchPrefixes = self.depotPaths
4158
Simon Hausmannb607e712007-05-20 10:55:54 +02004159 self.loadUserMapFromCache()
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02004160 self.labels = {}
4161 if self.detectLabels:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004162 self.getLabels()
Simon Hausmannb9847332007-03-20 20:54:23 +01004163
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02004164 if self.detectBranches:
Joel Holdsworthc785e202022-04-01 15:24:57 +01004165 # FIXME - what's a P4 projectName ?
Simon Hausmanndf450922007-06-08 08:49:22 +02004166 self.projectName = self.guessProjectName()
4167
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01004168 if self.hasOrigin:
4169 self.getBranchMappingFromGitBranches()
4170 else:
4171 self.getBranchMapping()
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004172 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01004173 print("p4-git branches: %s" % self.p4BranchesInGit)
4174 print("initial parents: %s" % self.initialParents)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004175 for b in self.p4BranchesInGit:
4176 if b != "master":
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004177
Joel Holdsworthc785e202022-04-01 15:24:57 +01004178 # FIXME
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004179 b = b[len(self.projectName):]
4180 self.createdBranches.add(b)
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02004181
Luke Diamand19fa5ac2020-01-29 11:12:46 +00004182 p4_check_access()
4183
Luke Diamand123f6312018-05-23 23:20:26 +01004184 self.openStreams()
Simon Hausmannb9847332007-03-20 20:54:23 +01004185
Luke Diamand6026aff2020-01-29 11:12:45 +00004186 err = None
Simon Hausmannb9847332007-03-20 20:54:23 +01004187
Luke Diamand6026aff2020-01-29 11:12:45 +00004188 try:
4189 if revision:
4190 self.importHeadRevision(revision)
4191 else:
4192 self.importRevisions(args, branch_arg_given)
Luke Diamand06804c72012-04-11 17:21:24 +02004193
Luke Diamand6026aff2020-01-29 11:12:45 +00004194 if gitConfigBool("git-p4.importLabels"):
4195 self.importLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02004196
Luke Diamand6026aff2020-01-29 11:12:45 +00004197 if self.importLabels:
4198 p4Labels = getP4Labels(self.depotPaths)
4199 gitTags = getGitTags()
Simon Hausmannb9847332007-03-20 20:54:23 +01004200
Luke Diamand6026aff2020-01-29 11:12:45 +00004201 missingP4Labels = p4Labels - gitTags
4202 self.importP4Labels(self.gitStream, missingP4Labels)
4203
4204 except P4CommandException as e:
4205 err = e
4206
4207 finally:
4208 self.closeStreams()
4209
4210 if err:
4211 die(str(err))
Simon Hausmannb9847332007-03-20 20:54:23 +01004212
Vitor Antunesfed23692012-01-25 23:48:22 +00004213 # Cleanup temporary branches created during import
4214 if self.tempBranches != []:
4215 for branch in self.tempBranches:
Joel Holdsworth8a470592022-01-06 21:40:34 +00004216 read_pipe(["git", "update-ref", "-d", branch])
Vitor Antunesfed23692012-01-25 23:48:22 +00004217 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
4218
Pete Wyckoff55d12432013-01-14 19:46:59 -05004219 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4220 # a convenient shortcut refname "p4".
4221 if self.importIntoRemotes:
4222 head_ref = self.refPrefix + "HEAD"
4223 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
4224 system(["git", "symbolic-ref", head_ref, self.branch])
4225
Simon Hausmannb9847332007-03-20 20:54:23 +01004226 return True
4227
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004228
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004229class P4Rebase(Command):
4230 def __init__(self):
4231 Command.__init__(self)
Luke Diamand06804c72012-04-11 17:21:24 +02004232 self.options = [
4233 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02004234 ]
Luke Diamand06804c72012-04-11 17:21:24 +02004235 self.importLabels = False
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03004236 self.description = ("Fetches the latest revision from perforce and "
4237 + "rebases the current work (branch) against it")
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004238
4239 def run(self, args):
4240 sync = P4Sync()
Luke Diamand06804c72012-04-11 17:21:24 +02004241 sync.importLabels = self.importLabels
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004242 sync.run([])
Simon Hausmannd7e38682007-06-12 14:34:46 +02004243
Simon Hausmann14594f42007-08-22 09:07:15 +02004244 return self.rebase()
4245
4246 def rebase(self):
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01004247 if os.system("git update-index --refresh") != 0:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004248 die("Some files in your working directory are modified and different than what is in your index. You can use git update-index <filename> to bring the index up to date or stash away all your changes with git stash.")
Joel Holdsworth8a470592022-01-06 21:40:34 +00004249 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004250 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01004251
Joel Holdsworth0874bb02022-04-01 15:24:52 +01004252 upstream, settings = findUpstreamBranchPoint()
Simon Hausmannd7e38682007-06-12 14:34:46 +02004253 if len(upstream) == 0:
4254 die("Cannot find upstream branchpoint for rebase")
4255
4256 # the branchpoint may be p4/foo~3, so strip off the parent
4257 upstream = re.sub("~[0-9]+$", "", upstream)
4258
Luke Diamandf2606b12018-06-19 09:04:10 +01004259 print("Rebasing the current branch onto %s" % upstream)
Joel Holdsworth8a470592022-01-06 21:40:34 +00004260 oldHead = read_pipe(["git", "rev-parse", "HEAD"]).strip()
4261 system(["git", "rebase", upstream])
4262 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead,
4263 "HEAD", "--"])
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004264 return True
4265
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004266
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004267class P4Clone(P4Sync):
4268 def __init__(self):
4269 P4Sync.__init__(self)
4270 self.description = "Creates a new git repository and imports from Perforce into it"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004271 self.usage = "usage: %prog [options] //depot/path[@revRange]"
Tommy Thorn354081d2008-02-03 10:38:51 -08004272 self.options += [
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004273 optparse.make_option("--destination", dest="cloneDestination",
4274 action='store', default=None,
Tommy Thorn354081d2008-02-03 10:38:51 -08004275 help="where to leave result of the clone"),
Pete Wyckoff38200072011-02-19 08:18:01 -05004276 optparse.make_option("--bare", dest="cloneBare",
4277 action="store_true", default=False),
Tommy Thorn354081d2008-02-03 10:38:51 -08004278 ]
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004279 self.cloneDestination = None
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004280 self.needsGit = False
Pete Wyckoff38200072011-02-19 08:18:01 -05004281 self.cloneBare = False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004282
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004283 def defaultDestination(self, args):
Joel Holdsworthc785e202022-04-01 15:24:57 +01004284 # TODO: use common prefix of args?
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004285 depotPath = args[0]
4286 depotDir = re.sub("(@[^@]*)$", "", depotPath)
4287 depotDir = re.sub("(#[^#]*)$", "", depotDir)
Toby Allsopp053d9e42008-02-05 09:41:43 +13004288 depotDir = re.sub(r"\.\.\.$", "", depotDir)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004289 depotDir = re.sub(r"/$", "", depotDir)
4290 return os.path.split(depotDir)[1]
4291
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004292 def run(self, args):
4293 if len(args) < 1:
4294 return False
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004295
4296 if self.keepRepoPath and not self.cloneDestination:
4297 sys.stderr.write("Must specify destination for --keep-path\n")
4298 sys.exit(1)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004299
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004300 depotPaths = args
Simon Hausmann5e100b52007-06-07 21:12:25 +02004301
4302 if not self.cloneDestination and len(depotPaths) > 1:
4303 self.cloneDestination = depotPaths[-1]
4304 depotPaths = depotPaths[:-1]
4305
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004306 for p in depotPaths:
4307 if not p.startswith("//"):
Pete Wyckoff0f487d32013-01-26 22:11:06 -05004308 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004309 return False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004310
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004311 if not self.cloneDestination:
Marius Storm-Olsen98ad4fa2007-06-07 15:08:33 +02004312 self.cloneDestination = self.defaultDestination(args)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004313
Luke Diamandf2606b12018-06-19 09:04:10 +01004314 print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
Pete Wyckoff38200072011-02-19 08:18:01 -05004315
Kevin Greenc3bf3f12007-06-11 16:48:07 -04004316 if not os.path.exists(self.cloneDestination):
4317 os.makedirs(self.cloneDestination)
Robert Blum053fd0c2008-08-01 12:50:03 -07004318 chdir(self.cloneDestination)
Pete Wyckoff38200072011-02-19 08:18:01 -05004319
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004320 init_cmd = ["git", "init"]
Pete Wyckoff38200072011-02-19 08:18:01 -05004321 if self.cloneBare:
4322 init_cmd.append("--bare")
Brandon Caseya235e852013-01-26 11:14:33 -08004323 retcode = subprocess.call(init_cmd)
4324 if retcode:
Joel Holdsworth40e7cfd2022-01-06 21:41:56 +00004325 raise subprocess.CalledProcessError(retcode, init_cmd)
Pete Wyckoff38200072011-02-19 08:18:01 -05004326
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004327 if not P4Sync.run(self, depotPaths):
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004328 return False
Pete Wyckoffc5959562013-01-14 19:47:01 -05004329
4330 # create a master branch and check out a work tree
4331 if gitBranchExists(self.branch):
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004332 system(["git", "branch", currentGitBranch(), self.branch])
Pete Wyckoffc5959562013-01-14 19:47:01 -05004333 if not self.cloneBare:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004334 system(["git", "checkout", "-f"])
Pete Wyckoffc5959562013-01-14 19:47:01 -05004335 else:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01004336 print('Not checking out any branch, use '
Luke Diamandf2606b12018-06-19 09:04:10 +01004337 '"git checkout -q -b master <branch>"')
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03004338
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05004339 # auto-set this variable if invoked with --use-client-spec
4340 if self.useClientSpec_from_options:
Joel Holdsworth8a470592022-01-06 21:40:34 +00004341 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05004342
Tao Klerksf7b5ff62022-04-30 19:26:52 +00004343 # persist any git-p4 encoding-handling config options passed in for clone:
4344 if gitConfig('git-p4.metadataDecodingStrategy'):
4345 system(["git", "config", "git-p4.metadataDecodingStrategy", gitConfig('git-p4.metadataDecodingStrategy')])
4346 if gitConfig('git-p4.metadataFallbackEncoding'):
4347 system(["git", "config", "git-p4.metadataFallbackEncoding", gitConfig('git-p4.metadataFallbackEncoding')])
4348 if gitConfig('git-p4.pathEncoding'):
4349 system(["git", "config", "git-p4.pathEncoding", gitConfig('git-p4.pathEncoding')])
4350
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004351 return True
4352
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004353
Luke Diamand123f6312018-05-23 23:20:26 +01004354class P4Unshelve(Command):
4355 def __init__(self):
4356 Command.__init__(self)
4357 self.options = []
4358 self.origin = "HEAD"
4359 self.description = "Unshelve a P4 changelist into a git commit"
4360 self.usage = "usage: %prog [options] changelist"
4361 self.options += [
4362 optparse.make_option("--origin", dest="origin",
4363 help="Use this base revision instead of the default (%s)" % self.origin),
4364 ]
4365 self.verbose = False
4366 self.noCommit = False
Luke Diamand08813122018-10-15 12:14:07 +01004367 self.destbranch = "refs/remotes/p4-unshelved"
Luke Diamand123f6312018-05-23 23:20:26 +01004368
4369 def renameBranch(self, branch_name):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01004370 """Rename the existing branch to branch_name.N ."""
Luke Diamand123f6312018-05-23 23:20:26 +01004371
4372 found = True
Joel Holdsworth12a77f52022-04-01 15:24:53 +01004373 for i in range(0, 1000):
Luke Diamand123f6312018-05-23 23:20:26 +01004374 backup_branch_name = "{0}.{1}".format(branch_name, i)
4375 if not gitBranchExists(backup_branch_name):
Joel Holdsworth4768af22022-04-01 15:25:02 +01004376 # Copy ref to backup
4377 gitUpdateRef(backup_branch_name, branch_name)
Luke Diamand123f6312018-05-23 23:20:26 +01004378 gitDeleteRef(branch_name)
4379 found = True
4380 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4381 break
4382
4383 if not found:
4384 sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
4385
4386 def findLastP4Revision(self, starting_point):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01004387 """Look back from starting_point for the first commit created by git-p4
4388 to find the P4 commit we are based on, and the depot-paths.
4389 """
Luke Diamand123f6312018-05-23 23:20:26 +01004390
4391 for parent in (range(65535)):
Luke Diamand0acbf592020-09-19 09:54:41 +01004392 log = extractLogMessageFromGitCommit("{0}~{1}".format(starting_point, parent))
Luke Diamand123f6312018-05-23 23:20:26 +01004393 settings = extractSettingsGitLog(log)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01004394 if 'change' in settings:
Luke Diamand123f6312018-05-23 23:20:26 +01004395 return settings
4396
4397 sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4398
Luke Diamand89143ac2018-10-15 12:14:08 +01004399 def createShelveParent(self, change, branch_name, sync, origin):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01004400 """Create a commit matching the parent of the shelved changelist
4401 'change'.
4402 """
Luke Diamand89143ac2018-10-15 12:14:08 +01004403 parent_description = p4_describe(change, shelved=True)
4404 parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4405 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4406
4407 parent_files = []
4408 for f in files:
4409 # if it was added in the shelved changelist, it won't exist in the parent
4410 if f['action'] in self.add_actions:
4411 continue
4412
4413 # if it was deleted in the shelved changelist it must not be deleted
4414 # in the parent - we might even need to create it if the origin branch
4415 # does not have it
4416 if f['action'] in self.delete_actions:
4417 f['action'] = 'add'
4418
4419 parent_files.append(f)
4420
4421 sync.commit(parent_description, parent_files, branch_name,
4422 parent=origin, allow_empty=True)
4423 print("created parent commit for {0} based on {1} in {2}".format(
4424 change, self.origin, branch_name))
4425
Luke Diamand123f6312018-05-23 23:20:26 +01004426 def run(self, args):
4427 if len(args) != 1:
4428 return False
4429
4430 if not gitBranchExists(self.origin):
4431 sys.exit("origin branch {0} does not exist".format(self.origin))
4432
4433 sync = P4Sync()
4434 changes = args
Luke Diamand123f6312018-05-23 23:20:26 +01004435
Luke Diamand89143ac2018-10-15 12:14:08 +01004436 # only one change at a time
Luke Diamand123f6312018-05-23 23:20:26 +01004437 change = changes[0]
4438
4439 # if the target branch already exists, rename it
4440 branch_name = "{0}/{1}".format(self.destbranch, change)
4441 if gitBranchExists(branch_name):
4442 self.renameBranch(branch_name)
4443 sync.branch = branch_name
4444
4445 sync.verbose = self.verbose
4446 sync.suppress_meta_comment = True
4447
4448 settings = self.findLastP4Revision(self.origin)
Luke Diamand123f6312018-05-23 23:20:26 +01004449 sync.depotPaths = settings['depot-paths']
4450 sync.branchPrefixes = sync.depotPaths
4451
4452 sync.openStreams()
4453 sync.loadUserMapFromCache()
4454 sync.silent = True
Luke Diamand89143ac2018-10-15 12:14:08 +01004455
4456 # create a commit for the parent of the shelved changelist
4457 self.createShelveParent(change, branch_name, sync, self.origin)
4458
4459 # create the commit for the shelved changelist itself
4460 description = p4_describe(change, True)
4461 files = sync.extractFilesFromCommit(description, True, change)
4462
4463 sync.commit(description, files, branch_name, "")
Luke Diamand123f6312018-05-23 23:20:26 +01004464 sync.closeStreams()
4465
4466 print("unshelved changelist {0} into {1}".format(change, branch_name))
4467
4468 return True
4469
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004470
Simon Hausmann09d89de2007-06-20 23:10:28 +02004471class P4Branches(Command):
4472 def __init__(self):
4473 Command.__init__(self)
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004474 self.options = []
Simon Hausmann09d89de2007-06-20 23:10:28 +02004475 self.description = ("Shows the git branches that hold imports and their "
4476 + "corresponding perforce depot paths")
4477 self.verbose = False
4478
4479 def run(self, args):
Simon Hausmann5ca44612007-08-24 17:44:16 +02004480 if originP4BranchesExist():
4481 createOrUpdateBranchesFromOrigin()
4482
Joel Holdsworth8a470592022-01-06 21:40:34 +00004483 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
Simon Hausmann09d89de2007-06-20 23:10:28 +02004484 line = line.strip()
4485
4486 if not line.startswith('p4/') or line == "p4/HEAD":
4487 continue
4488 branch = line
4489
4490 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4491 settings = extractSettingsGitLog(log)
4492
Luke Diamandf2606b12018-06-19 09:04:10 +01004493 print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
Simon Hausmann09d89de2007-06-20 23:10:28 +02004494 return True
4495
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004496
Simon Hausmannb9847332007-03-20 20:54:23 +01004497class HelpFormatter(optparse.IndentedHelpFormatter):
4498 def __init__(self):
4499 optparse.IndentedHelpFormatter.__init__(self)
4500
4501 def format_description(self, description):
4502 if description:
4503 return description + "\n"
4504 else:
4505 return ""
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004506
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004507
Simon Hausmann86949ee2007-03-19 20:59:12 +01004508def printUsage(commands):
Luke Diamandf2606b12018-06-19 09:04:10 +01004509 print("usage: %s <command> [options]" % sys.argv[0])
4510 print("")
4511 print("valid commands: %s" % ", ".join(commands))
4512 print("")
4513 print("Try %s <command> --help for command specific help." % sys.argv[0])
4514 print("")
Simon Hausmann86949ee2007-03-19 20:59:12 +01004515
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004516
Simon Hausmann86949ee2007-03-19 20:59:12 +01004517commands = {
Joel Holdsworth2bcf6112022-04-01 15:24:56 +01004518 "submit": P4Submit,
4519 "commit": P4Submit,
4520 "sync": P4Sync,
4521 "rebase": P4Rebase,
4522 "clone": P4Clone,
4523 "branches": P4Branches,
4524 "unshelve": P4Unshelve,
Simon Hausmann86949ee2007-03-19 20:59:12 +01004525}
4526
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004527
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004528def main():
4529 if len(sys.argv[1:]) == 0:
4530 printUsage(commands.keys())
4531 sys.exit(2)
Simon Hausmann86949ee2007-03-19 20:59:12 +01004532
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004533 cmdName = sys.argv[1]
4534 try:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004535 klass = commands[cmdName]
4536 cmd = klass()
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004537 except KeyError:
Luke Diamandf2606b12018-06-19 09:04:10 +01004538 print("unknown command %s" % cmdName)
4539 print("")
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004540 printUsage(commands.keys())
4541 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004542
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004543 options = cmd.options
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004544 cmd.gitdir = os.environ.get("GIT_DIR", None)
Simon Hausmann86949ee2007-03-19 20:59:12 +01004545
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004546 args = sys.argv[2:]
Simon Hausmanne20a9e52007-03-26 00:13:51 +02004547
Pete Wyckoffb0ccc802012-09-09 16:16:10 -04004548 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
Luke Diamand6a10b6a2012-04-24 09:33:23 +01004549 if cmd.needsGit:
4550 options.append(optparse.make_option("--git-dir", dest="gitdir"))
Simon Hausmanne20a9e52007-03-26 00:13:51 +02004551
Luke Diamand6a10b6a2012-04-24 09:33:23 +01004552 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4553 options,
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01004554 description=cmd.description,
4555 formatter=HelpFormatter())
Simon Hausmann86949ee2007-03-19 20:59:12 +01004556
Ben Keene608e3802019-12-16 14:02:20 +00004557 try:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01004558 cmd, args = parser.parse_args(sys.argv[2:], cmd)
Ben Keene608e3802019-12-16 14:02:20 +00004559 except:
4560 parser.print_help()
4561 raise
4562
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004563 global verbose
4564 verbose = cmd.verbose
4565 if cmd.needsGit:
Joel Holdsworthda0134f2022-04-01 15:25:00 +01004566 if cmd.gitdir is None:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004567 cmd.gitdir = os.path.abspath(".git")
4568 if not isValidGitDir(cmd.gitdir):
Luke Diamand378f7be2016-12-13 21:51:28 +00004569 # "rev-parse --git-dir" without arguments will try $PWD/.git
Joel Holdsworth8a470592022-01-06 21:40:34 +00004570 cmd.gitdir = read_pipe(["git", "rev-parse", "--git-dir"]).strip()
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004571 if os.path.exists(cmd.gitdir):
Joel Holdsworth8a470592022-01-06 21:40:34 +00004572 cdup = read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004573 if len(cdup) > 0:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004574 chdir(cdup)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004575
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004576 if not isValidGitDir(cmd.gitdir):
4577 if isValidGitDir(cmd.gitdir + "/.git"):
4578 cmd.gitdir += "/.git"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004579 else:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004580 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
Simon Hausmann8910ac02007-03-26 08:18:55 +02004581
Luke Diamand378f7be2016-12-13 21:51:28 +00004582 # so git commands invoked from the P4 workspace will succeed
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004583 os.environ["GIT_DIR"] = cmd.gitdir
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004584
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004585 if not cmd.run(args):
4586 parser.print_help()
Pete Wyckoff09fca772011-12-24 21:07:39 -05004587 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004588
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004589
4590if __name__ == '__main__':
4591 main()