blob: 0eb3bb4c47dcf12a9f1d98e1b76a3681197ac52e [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
Kilian Kilgerd2054832022-07-21 09:07:20 +0000825def p4KeysContainingNonUtf8Chars():
826 """Returns all keys which may contain non UTF-8 encoded strings
827 for which a fallback strategy has to be applied.
828 """
829 return ['desc', 'client', 'FullName']
830
831
832def p4KeysContainingBinaryData():
833 """Returns all keys which may contain arbitrary binary data
834 """
835 return ['data']
836
837
838def p4KeyContainsFilePaths(key):
839 """Returns True if the key contains file paths. These are handled by decode_path().
840 Otherwise False.
841 """
842 return key.startswith('depotFile') or key in ['path', 'clientFile']
843
844
845def p4KeyWhichCanBeDirectlyDecoded(key):
846 """Returns True if the key can be directly decoded as UTF-8 string
847 Otherwise False.
848
849 Keys which can not be encoded directly:
850 - `data` which may contain arbitrary binary data
851 - `desc` or `client` or `FullName` which may contain non-UTF8 encoded text
852 - `depotFile[0-9]*`, `path`, or `clientFile` which may contain non-UTF8 encoded text, handled by decode_path()
853 """
854 if key in p4KeysContainingNonUtf8Chars() or \
855 key in p4KeysContainingBinaryData() or \
856 p4KeyContainsFilePaths(key):
857 return False
858 return True
859
860
Luke Diamand55bb3e32018-06-08 21:32:46 +0100861def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000862 errors_as_exceptions=False, *k, **kw):
Luke Diamand6de040d2011-10-16 10:47:52 -0400863
Joel Holdsworth8a470592022-01-06 21:40:34 +0000864 cmd = p4_build_cmd(["-G"] + cmd)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -0300865 if verbose:
Joel Holdsworth727e6ea2022-01-06 21:40:35 +0000866 sys.stderr.write("Opening pipe: {}\n".format(' '.join(cmd)))
Scott Lamb9f90c732007-07-15 20:58:10 -0700867
868 # Use a temporary file to avoid deadlocks without
869 # subprocess.communicate(), which would put another copy
870 # of stdout into memory.
871 stdin_file = None
872 if stdin is not None:
873 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
Ben Keene484d09c2019-12-13 15:52:36 -0800874 if not isinstance(stdin, list):
Luke Diamand6de040d2011-10-16 10:47:52 -0400875 stdin_file.write(stdin)
876 else:
877 for i in stdin:
Yang Zhao86dca242019-12-13 15:52:39 -0800878 stdin_file.write(encode_text_stream(i))
879 stdin_file.write(b'\n')
Scott Lamb9f90c732007-07-15 20:58:10 -0700880 stdin_file.flush()
881 stdin_file.seek(0)
882
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000883 p4 = subprocess.Popen(
884 cmd, stdin=stdin_file, stdout=subprocess.PIPE, *k, **kw)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100885
886 result = []
887 try:
888 while True:
Scott Lamb9f90c732007-07-15 20:58:10 -0700889 entry = marshal.load(p4.stdout)
Kilian Kilgerd2054832022-07-21 09:07:20 +0000890
Yang Zhao6cec21a2019-12-13 15:52:38 -0800891 if bytes is not str:
Kilian Kilgerd2054832022-07-21 09:07:20 +0000892 # Decode unmarshalled dict to use str keys and values. Special cases are handled below.
Yang Zhao6cec21a2019-12-13 15:52:38 -0800893 decoded_entry = {}
894 for key, value in entry.items():
895 key = key.decode()
Kilian Kilgerd2054832022-07-21 09:07:20 +0000896 if isinstance(value, bytes) and p4KeyWhichCanBeDirectlyDecoded(key):
Yang Zhao6cec21a2019-12-13 15:52:38 -0800897 value = value.decode()
898 decoded_entry[key] = value
899 # Parse out data if it's an error response
900 if decoded_entry.get('code') == 'error' and 'data' in decoded_entry:
901 decoded_entry['data'] = decoded_entry['data'].decode()
902 entry = decoded_entry
Miguel Torroja1997e912017-07-13 09:00:35 +0200903 if skip_info:
904 if 'code' in entry and entry['code'] == 'info':
905 continue
Kilian Kilgerd2054832022-07-21 09:07:20 +0000906 for key in p4KeysContainingNonUtf8Chars():
907 if key in entry:
908 entry[key] = metadata_stream_to_writable_bytes(entry[key])
Andrew Garberc3f61632011-04-07 02:01:21 -0400909 if cb is not None:
910 cb(entry)
911 else:
912 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100913 except EOFError:
914 pass
Scott Lamb9f90c732007-07-15 20:58:10 -0700915 exitCode = p4.wait()
916 if exitCode != 0:
Luke Diamand55bb3e32018-06-08 21:32:46 +0100917 if errors_as_exceptions:
918 if len(result) > 0:
919 data = result[0].get('data')
920 if data:
921 m = re.search('Too many rows scanned \(over (\d+)\)', data)
922 if not m:
923 m = re.search('Request too large \(over (\d+)\)', data)
924
925 if m:
926 limit = int(m.group(1))
927 raise P4RequestSizeException(exitCode, result, limit)
928
929 raise P4ServerException(exitCode, result)
930 else:
931 raise P4Exception(exitCode)
932 else:
933 entry = {}
934 entry["p4ExitCode"] = exitCode
935 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100936
937 return result
938
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100939
Joel Holdsworth3d8a3032022-01-06 21:40:33 +0000940def p4Cmd(cmd, *k, **kw):
941 list = p4CmdList(cmd, *k, **kw)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100942 result = {}
943 for entry in list:
944 result.update(entry)
Joel Holdsworth990547a2022-04-01 15:24:44 +0100945 return result
Simon Hausmann86949ee2007-03-19 20:59:12 +0100946
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100947
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100948def p4Where(depotPath):
949 if not depotPath.endswith("/"):
950 depotPath += "/"
Vitor Antunescd884102015-04-21 23:49:30 +0100951 depotPathLong = depotPath + "..."
952 outputList = p4CmdList(["where", depotPathLong])
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100953 output = None
954 for entry in outputList:
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100955 if "depotFile" in entry:
Vitor Antunescd884102015-04-21 23:49:30 +0100956 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
957 # The base path always ends with "/...".
Yang Zhaod38208a2019-12-13 15:52:40 -0800958 entry_path = decode_path(entry['depotFile'])
959 if entry_path.find(depotPath) == 0 and entry_path[-4:] == "/...":
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100960 output = entry
961 break
962 elif "data" in entry:
963 data = entry.get("data")
964 space = data.find(" ")
965 if data[:space] == depotPath:
966 output = entry
967 break
Joel Holdsworthda0134f2022-04-01 15:25:00 +0100968 if output is None:
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100969 return ""
Simon Hausmanndc524032007-05-21 09:34:56 +0200970 if output["code"] == "error":
971 return ""
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100972 clientPath = ""
973 if "path" in output:
Yang Zhaod38208a2019-12-13 15:52:40 -0800974 clientPath = decode_path(output['path'])
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100975 elif "data" in output:
976 data = output.get("data")
Yang Zhaod38208a2019-12-13 15:52:40 -0800977 lastSpace = data.rfind(b" ")
978 clientPath = decode_path(data[lastSpace + 1:])
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100979
980 if clientPath.endswith("..."):
981 clientPath = clientPath[:-3]
982 return clientPath
983
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100984
Simon Hausmann86949ee2007-03-19 20:59:12 +0100985def currentGitBranch():
Luke Diamandeff45112017-04-15 11:36:09 +0100986 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
Simon Hausmann86949ee2007-03-19 20:59:12 +0100987
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100988
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100989def isValidGitDir(path):
Joel Holdsworthda0134f2022-04-01 15:25:00 +0100990 return git_dir(path) is not None
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100991
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100992
Simon Hausmann463e8af2007-05-17 09:13:54 +0200993def parseRevision(ref):
Joel Holdsworth8a470592022-01-06 21:40:34 +0000994 return read_pipe(["git", "rev-parse", ref]).strip()
Simon Hausmann463e8af2007-05-17 09:13:54 +0200995
Joel Holdsworthadf159b2022-04-01 15:24:43 +0100996
Pete Wyckoff28755db2011-12-24 21:07:40 -0500997def branchExists(ref):
998 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
999 ignore_error=True)
1000 return len(rev) > 0
1001
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001002
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001003def extractLogMessageFromGitCommit(commit):
1004 logMessage = ""
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -03001005
Joel Holdsworthc785e202022-04-01 15:24:57 +01001006 # fixme: title is first line of commit, not 1st paragraph.
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001007 foundTitle = False
Mike Muellerc3f23582019-05-28 11:15:46 -07001008 for log in read_pipe_lines(["git", "cat-file", "commit", commit]):
Joel Holdsworth812ee742022-04-01 15:24:45 +01001009 if not foundTitle:
1010 if len(log) == 1:
1011 foundTitle = True
1012 continue
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001013
Joel Holdsworth812ee742022-04-01 15:24:45 +01001014 logMessage += log
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001015 return logMessage
1016
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001017
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001018def extractSettingsGitLog(log):
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001019 values = {}
1020 for line in log.split("\n"):
1021 line = line.strip()
Joel Holdsworth843d8472022-04-01 15:24:54 +01001022 m = re.search(r"^ *\[git-p4: (.*)\]$", line)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03001023 if not m:
1024 continue
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001025
Joel Holdsworth843d8472022-04-01 15:24:54 +01001026 assignments = m.group(1).split(':')
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03001027 for a in assignments:
Joel Holdsworth843d8472022-04-01 15:24:54 +01001028 vals = a.split('=')
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03001029 key = vals[0].strip()
Joel Holdsworth843d8472022-04-01 15:24:54 +01001030 val = ('='.join(vals[1:])).strip()
1031 if val.endswith('\"') and val.startswith('"'):
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03001032 val = val[1:-1]
1033
1034 values[key] = val
1035
Simon Hausmann845b42c2007-06-07 09:19:34 +02001036 paths = values.get("depot-paths")
1037 if not paths:
1038 paths = values.get("depot-path")
Simon Hausmanna3fdd572007-06-07 22:54:32 +02001039 if paths:
1040 values['depot-paths'] = paths.split(',')
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001041 return values
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001042
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001043
Simon Hausmann8136a632007-03-22 21:27:14 +01001044def gitBranchExists(branch):
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001045 proc = subprocess.Popen(["git", "rev-parse", branch],
Joel Holdsworth990547a2022-04-01 15:24:44 +01001046 stderr=subprocess.PIPE, stdout=subprocess.PIPE)
1047 return proc.wait() == 0
Simon Hausmann8136a632007-03-22 21:27:14 +01001048
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001049
Luke Diamand123f6312018-05-23 23:20:26 +01001050def gitUpdateRef(ref, newvalue):
1051 subprocess.check_call(["git", "update-ref", ref, newvalue])
1052
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001053
Luke Diamand123f6312018-05-23 23:20:26 +01001054def gitDeleteRef(ref):
1055 subprocess.check_call(["git", "update-ref", "-d", ref])
1056
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001057
John Chapman36bd8442008-11-08 14:22:49 +11001058_gitConfig = {}
Pete Wyckoffb345d6c2013-01-26 22:11:23 -05001059
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001060
Lars Schneider692e1792015-09-26 09:54:58 +02001061def gitConfig(key, typeSpecifier=None):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001062 if key not in _gitConfig:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001063 cmd = ["git", "config"]
Lars Schneider692e1792015-09-26 09:54:58 +02001064 if typeSpecifier:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001065 cmd += [typeSpecifier]
1066 cmd += [key]
Pete Wyckoffb345d6c2013-01-26 22:11:23 -05001067 s = read_pipe(cmd, ignore_error=True)
1068 _gitConfig[key] = s.strip()
John Chapman36bd8442008-11-08 14:22:49 +11001069 return _gitConfig[key]
Simon Hausmann01265102007-05-25 10:36:10 +02001070
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001071
Pete Wyckoff0d609032013-01-26 22:11:24 -05001072def gitConfigBool(key):
1073 """Return a bool, using git config --bool. It is True only if the
1074 variable is set to true, and False if set to false or not present
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001075 in the config.
1076 """
Pete Wyckoff0d609032013-01-26 22:11:24 -05001077
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001078 if key not in _gitConfig:
Lars Schneider692e1792015-09-26 09:54:58 +02001079 _gitConfig[key] = gitConfig(key, '--bool') == "true"
Simon Hausmann062410b2007-07-18 10:56:31 +02001080 return _gitConfig[key]
1081
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001082
Lars Schneidercb1dafd2015-09-26 09:54:59 +02001083def gitConfigInt(key):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001084 if key not in _gitConfig:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001085 cmd = ["git", "config", "--int", key]
Simon Hausmannb9847332007-03-20 20:54:23 +01001086 s = read_pipe(cmd, ignore_error=True)
Simon Hausmann062410b2007-07-18 10:56:31 +02001087 v = s.strip()
Lars Schneidercb1dafd2015-09-26 09:54:59 +02001088 try:
1089 _gitConfig[key] = int(gitConfig(key, '--int'))
1090 except ValueError:
1091 _gitConfig[key] = None
Simon Hausmann062410b2007-07-18 10:56:31 +02001092 return _gitConfig[key]
1093
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001094
Vitor Antunes7199cf12011-08-19 00:44:05 +01001095def gitConfigList(key):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001096 if key not in _gitConfig:
Pete Wyckoff2abba302013-01-26 22:11:22 -05001097 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
George Vanburghc3c2b052017-01-25 09:17:29 +00001098 _gitConfig[key] = s.strip().splitlines()
Lars Schneider7960e702015-09-26 09:55:00 +02001099 if _gitConfig[key] == ['']:
1100 _gitConfig[key] = []
Vitor Antunes7199cf12011-08-19 00:44:05 +01001101 return _gitConfig[key]
1102
Tao Klerks17f273f2022-04-04 05:10:54 +00001103def fullP4Ref(incomingRef, importIntoRemotes=True):
1104 """Standardize a given provided p4 ref value to a full git ref:
1105 refs/foo/bar/branch -> use it exactly
1106 p4/branch -> prepend refs/remotes/ or refs/heads/
1107 branch -> prepend refs/remotes/p4/ or refs/heads/p4/"""
1108 if incomingRef.startswith("refs/"):
1109 return incomingRef
1110 if importIntoRemotes:
1111 prepend = "refs/remotes/"
1112 else:
1113 prepend = "refs/heads/"
1114 if not incomingRef.startswith("p4/"):
1115 prepend += "p4/"
1116 return prepend + incomingRef
1117
1118def shortP4Ref(incomingRef, importIntoRemotes=True):
1119 """Standardize to a "short ref" if possible:
1120 refs/foo/bar/branch -> ignore
1121 refs/remotes/p4/branch or refs/heads/p4/branch -> shorten
1122 p4/branch -> shorten"""
1123 if importIntoRemotes:
1124 longprefix = "refs/remotes/p4/"
1125 else:
1126 longprefix = "refs/heads/p4/"
1127 if incomingRef.startswith(longprefix):
1128 return incomingRef[len(longprefix):]
1129 if incomingRef.startswith("p4/"):
1130 return incomingRef[3:]
1131 return incomingRef
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001132
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001133def p4BranchesInGit(branchesAreInRemotes=True):
1134 """Find all the branches whose names start with "p4/", looking
1135 in remotes or heads as specified by the argument. Return
1136 a dictionary of { branch: revision } for each one found.
1137 The branch names are the short names, without any
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001138 "p4/" prefix.
1139 """
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001140
Simon Hausmann062410b2007-07-18 10:56:31 +02001141 branches = {}
1142
Joel Holdsworth8a470592022-01-06 21:40:34 +00001143 cmdline = ["git", "rev-parse", "--symbolic"]
Simon Hausmann062410b2007-07-18 10:56:31 +02001144 if branchesAreInRemotes:
Joel Holdsworth8a470592022-01-06 21:40:34 +00001145 cmdline.append("--remotes")
Simon Hausmann062410b2007-07-18 10:56:31 +02001146 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00001147 cmdline.append("--branches")
Simon Hausmann062410b2007-07-18 10:56:31 +02001148
1149 for line in read_pipe_lines(cmdline):
1150 line = line.strip()
1151
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001152 # only import to p4/
1153 if not line.startswith('p4/'):
Simon Hausmann062410b2007-07-18 10:56:31 +02001154 continue
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001155 # special symbolic ref to p4/master
1156 if line == "p4/HEAD":
1157 continue
Simon Hausmann062410b2007-07-18 10:56:31 +02001158
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001159 # strip off p4/ prefix
1160 branch = line[len("p4/"):]
Simon Hausmann062410b2007-07-18 10:56:31 +02001161
1162 branches[branch] = parseRevision(line)
Pete Wyckoff2c8037e2013-01-14 19:46:57 -05001163
Simon Hausmann062410b2007-07-18 10:56:31 +02001164 return branches
1165
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001166
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05001167def branch_exists(branch):
1168 """Make sure that the given ref name really exists."""
1169
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001170 cmd = ["git", "rev-parse", "--symbolic", "--verify", branch]
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05001171 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1172 out, _ = p.communicate()
Yang Zhao6cec21a2019-12-13 15:52:38 -08001173 out = decode_text_stream(out)
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05001174 if p.returncode:
1175 return False
1176 # expect exactly one line of output: the branch name
1177 return out.rstrip() == branch
1178
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001179
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01001180def findUpstreamBranchPoint(head="HEAD"):
Simon Hausmann86506fe2007-07-18 12:40:12 +02001181 branches = p4BranchesInGit()
1182 # map from depot-path to branch name
1183 branchByDepotPath = {}
1184 for branch in branches.keys():
1185 tip = branches[branch]
1186 log = extractLogMessageFromGitCommit(tip)
1187 settings = extractSettingsGitLog(log)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001188 if "depot-paths" in settings:
Kirill Frolov944db252022-03-21 12:43:22 +00001189 git_branch = "remotes/p4/" + branch
Simon Hausmann86506fe2007-07-18 12:40:12 +02001190 paths = ",".join(settings["depot-paths"])
Kirill Frolov944db252022-03-21 12:43:22 +00001191 branchByDepotPath[paths] = git_branch
1192 if "change" in settings:
1193 paths = paths + ";" + settings["change"]
1194 branchByDepotPath[paths] = git_branch
Simon Hausmann86506fe2007-07-18 12:40:12 +02001195
Simon Hausmann27d2d812007-06-12 14:31:59 +02001196 settings = None
Simon Hausmann27d2d812007-06-12 14:31:59 +02001197 parent = 0
1198 while parent < 65535:
Simon Hausmann9ceab362007-06-22 00:01:57 +02001199 commit = head + "~%s" % parent
Simon Hausmann27d2d812007-06-12 14:31:59 +02001200 log = extractLogMessageFromGitCommit(commit)
1201 settings = extractSettingsGitLog(log)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001202 if "depot-paths" in settings:
Simon Hausmann86506fe2007-07-18 12:40:12 +02001203 paths = ",".join(settings["depot-paths"])
Kirill Frolov944db252022-03-21 12:43:22 +00001204 if "change" in settings:
1205 expaths = paths + ";" + settings["change"]
1206 if expaths in branchByDepotPath:
1207 return [branchByDepotPath[expaths], settings]
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001208 if paths in branchByDepotPath:
Simon Hausmann86506fe2007-07-18 12:40:12 +02001209 return [branchByDepotPath[paths], settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +02001210
Simon Hausmann86506fe2007-07-18 12:40:12 +02001211 parent = parent + 1
Simon Hausmann27d2d812007-06-12 14:31:59 +02001212
Simon Hausmann86506fe2007-07-18 12:40:12 +02001213 return ["", settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +02001214
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001215
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01001216def createOrUpdateBranchesFromOrigin(localRefPrefix="refs/remotes/p4/", silent=True):
Simon Hausmann5ca44612007-08-24 17:44:16 +02001217 if not silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01001218 print("Creating/updating branch(es) in %s based on origin branch(es)"
Simon Hausmann5ca44612007-08-24 17:44:16 +02001219 % localRefPrefix)
1220
1221 originPrefix = "origin/p4/"
1222
Joel Holdsworth8a470592022-01-06 21:40:34 +00001223 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
Simon Hausmann5ca44612007-08-24 17:44:16 +02001224 line = line.strip()
1225 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
1226 continue
1227
1228 headName = line[len(originPrefix):]
1229 remoteHead = localRefPrefix + headName
1230 originHead = line
1231
1232 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01001233 if 'depot-paths' not in original or 'change' not in original:
Simon Hausmann5ca44612007-08-24 17:44:16 +02001234 continue
1235
1236 update = False
1237 if not gitBranchExists(remoteHead):
1238 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01001239 print("creating %s" % remoteHead)
Simon Hausmann5ca44612007-08-24 17:44:16 +02001240 update = True
1241 else:
1242 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001243 if 'change' in settings:
Simon Hausmann5ca44612007-08-24 17:44:16 +02001244 if settings['depot-paths'] == original['depot-paths']:
1245 originP4Change = int(original['change'])
1246 p4Change = int(settings['change'])
1247 if originP4Change > p4Change:
Luke Diamandf2606b12018-06-19 09:04:10 +01001248 print("%s (%s) is newer than %s (%s). "
Simon Hausmann5ca44612007-08-24 17:44:16 +02001249 "Updating p4 branch from origin."
1250 % (originHead, originP4Change,
1251 remoteHead, p4Change))
1252 update = True
1253 else:
Luke Diamandf2606b12018-06-19 09:04:10 +01001254 print("Ignoring: %s was imported from %s while "
Simon Hausmann5ca44612007-08-24 17:44:16 +02001255 "%s was imported from %s"
1256 % (originHead, ','.join(original['depot-paths']),
1257 remoteHead, ','.join(settings['depot-paths'])))
1258
1259 if update:
Joel Holdsworth8a470592022-01-06 21:40:34 +00001260 system(["git", "update-ref", remoteHead, originHead])
Simon Hausmann5ca44612007-08-24 17:44:16 +02001261
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001262
Simon Hausmann5ca44612007-08-24 17:44:16 +02001263def originP4BranchesExist():
Joel Holdsworth812ee742022-04-01 15:24:45 +01001264 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
Simon Hausmann5ca44612007-08-24 17:44:16 +02001265
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001266
Luke Diamand1051ef02015-06-10 08:30:59 +01001267def p4ParseNumericChangeRange(parts):
1268 changeStart = int(parts[0][1:])
1269 if parts[1] == '#head':
1270 changeEnd = p4_last_change()
1271 else:
1272 changeEnd = int(parts[1])
1273
1274 return (changeStart, changeEnd)
1275
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001276
Luke Diamand1051ef02015-06-10 08:30:59 +01001277def chooseBlockSize(blockSize):
1278 if blockSize:
1279 return blockSize
1280 else:
1281 return defaultBlockSize
1282
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001283
Luke Diamand1051ef02015-06-10 08:30:59 +01001284def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
1285 assert depotPaths
1286
1287 # Parse the change range into start and end. Try to find integer
1288 # revision ranges as these can be broken up into blocks to avoid
1289 # hitting server-side limits (maxrows, maxscanresults). But if
1290 # that doesn't work, fall back to using the raw revision specifier
1291 # strings, without using block mode.
1292
Lex Spoon96b2d542015-04-20 11:00:20 -04001293 if changeRange is None or changeRange == '':
Luke Diamand1051ef02015-06-10 08:30:59 +01001294 changeStart = 1
1295 changeEnd = p4_last_change()
1296 block_size = chooseBlockSize(requestedBlockSize)
Lex Spoon96b2d542015-04-20 11:00:20 -04001297 else:
1298 parts = changeRange.split(',')
1299 assert len(parts) == 2
Luke Diamand1051ef02015-06-10 08:30:59 +01001300 try:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001301 changeStart, changeEnd = p4ParseNumericChangeRange(parts)
Luke Diamand1051ef02015-06-10 08:30:59 +01001302 block_size = chooseBlockSize(requestedBlockSize)
Luke Diamand8fa0abf2018-06-08 21:32:47 +01001303 except ValueError:
Luke Diamand1051ef02015-06-10 08:30:59 +01001304 changeStart = parts[0][1:]
1305 changeEnd = parts[1]
1306 if requestedBlockSize:
1307 die("cannot use --changes-block-size with non-numeric revisions")
1308 block_size = None
Lex Spoon96b2d542015-04-20 11:00:20 -04001309
George Vanburgh9943e5b2016-12-17 22:11:23 +00001310 changes = set()
Lex Spoon96b2d542015-04-20 11:00:20 -04001311
Sam Hocevar1f90a642015-12-19 09:39:40 +00001312 # Retrieve changes a block at a time, to prevent running
Luke Diamand3deed5e2018-06-08 21:32:48 +01001313 # into a MaxResults/MaxScanRows error from the server. If
1314 # we _do_ hit one of those errors, turn down the block size
Luke Diamand1051ef02015-06-10 08:30:59 +01001315
Sam Hocevar1f90a642015-12-19 09:39:40 +00001316 while True:
1317 cmd = ['changes']
Luke Diamand1051ef02015-06-10 08:30:59 +01001318
Sam Hocevar1f90a642015-12-19 09:39:40 +00001319 if block_size:
1320 end = min(changeEnd, changeStart + block_size)
1321 revisionRange = "%d,%d" % (changeStart, end)
1322 else:
1323 revisionRange = "%s,%s" % (changeStart, changeEnd)
Luke Diamand1051ef02015-06-10 08:30:59 +01001324
Sam Hocevar1f90a642015-12-19 09:39:40 +00001325 for p in depotPaths:
Luke Diamand1051ef02015-06-10 08:30:59 +01001326 cmd += ["%s...@%s" % (p, revisionRange)]
1327
Luke Diamand3deed5e2018-06-08 21:32:48 +01001328 # fetch the changes
1329 try:
1330 result = p4CmdList(cmd, errors_as_exceptions=True)
1331 except P4RequestSizeException as e:
1332 if not block_size:
1333 block_size = e.limit
1334 elif block_size > e.limit:
1335 block_size = e.limit
1336 else:
1337 block_size = max(2, block_size // 2)
1338
Joel Holdsworthe8f8b3b2022-04-01 15:25:03 +01001339 if verbose:
1340 print("block size error, retrying with block size {0}".format(block_size))
Luke Diamand3deed5e2018-06-08 21:32:48 +01001341 continue
1342 except P4Exception as e:
1343 die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1344
Sam Hocevar1f90a642015-12-19 09:39:40 +00001345 # Insert changes in chronological order
Luke Diamand3deed5e2018-06-08 21:32:48 +01001346 for entry in reversed(result):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001347 if 'change' not in entry:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001348 continue
1349 changes.add(int(entry['change']))
Luke Diamand1051ef02015-06-10 08:30:59 +01001350
Sam Hocevar1f90a642015-12-19 09:39:40 +00001351 if not block_size:
1352 break
Luke Diamand1051ef02015-06-10 08:30:59 +01001353
Sam Hocevar1f90a642015-12-19 09:39:40 +00001354 if end >= changeEnd:
1355 break
Luke Diamand1051ef02015-06-10 08:30:59 +01001356
Sam Hocevar1f90a642015-12-19 09:39:40 +00001357 changeStart = end + 1
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001358
Sam Hocevar1f90a642015-12-19 09:39:40 +00001359 changes = sorted(changes)
1360 return changes
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001361
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001362
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01001363def p4PathStartsWith(path, prefix):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001364 """This method tries to remedy a potential mixed-case issue:
1365
1366 If UserA adds //depot/DirA/file1
1367 and UserB adds //depot/dira/file2
1368
1369 we may or may not have a problem. If you have core.ignorecase=true,
1370 we treat DirA and dira as the same directory.
1371 """
Pete Wyckoff0d609032013-01-26 22:11:24 -05001372 if gitConfigBool("core.ignorecase"):
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01001373 return path.lower().startswith(prefix.lower())
1374 return path.startswith(prefix)
1375
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001376
Pete Wyckoff543987b2012-02-25 20:06:25 -05001377def getClientSpec():
1378 """Look at the p4 client spec, create a View() object that contains
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001379 all the mappings, and return it.
1380 """
Pete Wyckoff543987b2012-02-25 20:06:25 -05001381
Joel Holdsworth8a470592022-01-06 21:40:34 +00001382 specList = p4CmdList(["client", "-o"])
Pete Wyckoff543987b2012-02-25 20:06:25 -05001383 if len(specList) != 1:
1384 die('Output from "client -o" is %d lines, expecting 1' %
1385 len(specList))
1386
1387 # dictionary of all client parameters
1388 entry = specList[0]
1389
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001390 # the //client/ name
1391 client_name = entry["Client"]
1392
Pete Wyckoff543987b2012-02-25 20:06:25 -05001393 # just the keys that start with "View"
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001394 view_keys = [k for k in entry.keys() if k.startswith("View")]
Pete Wyckoff543987b2012-02-25 20:06:25 -05001395
1396 # hold this new View
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001397 view = View(client_name)
Pete Wyckoff543987b2012-02-25 20:06:25 -05001398
1399 # append the lines, in order, to the view
1400 for view_num in range(len(view_keys)):
1401 k = "View%d" % view_num
1402 if k not in view_keys:
1403 die("Expected view key %s missing" % k)
1404 view.append(entry[k])
1405
1406 return view
1407
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001408
Pete Wyckoff543987b2012-02-25 20:06:25 -05001409def getClientRoot():
1410 """Grab the client directory."""
1411
Joel Holdsworth8a470592022-01-06 21:40:34 +00001412 output = p4CmdList(["client", "-o"])
Pete Wyckoff543987b2012-02-25 20:06:25 -05001413 if len(output) != 1:
1414 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1415
1416 entry = output[0]
1417 if "Root" not in entry:
1418 die('Client has no "Root"')
1419
1420 return entry["Root"]
1421
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001422
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001423def wildcard_decode(path):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001424 """Decode P4 wildcards into %xx encoding
1425
1426 P4 wildcards are not allowed in filenames. P4 complains if you simply
1427 add them, but you can force it with "-f", in which case it translates
1428 them into %xx encoding internally.
1429 """
1430
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001431 # Search for and fix just these four characters. Do % last so
1432 # that fixing it does not inadvertently create new %-escapes.
1433 # Cannot have * in a filename in windows; untested as to
1434 # what p4 would do in such a case.
1435 if not platform.system() == "Windows":
1436 path = path.replace("%2A", "*")
1437 path = path.replace("%23", "#") \
1438 .replace("%40", "@") \
1439 .replace("%25", "%")
1440 return path
1441
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001442
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001443def wildcard_encode(path):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001444 """Encode %xx coded wildcards into P4 coding."""
1445
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001446 # do % first to avoid double-encoding the %s introduced here
1447 path = path.replace("%", "%25") \
1448 .replace("*", "%2A") \
1449 .replace("#", "%23") \
1450 .replace("@", "%40")
1451 return path
1452
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001453
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001454def wildcard_present(path):
Brandon Casey598354c2013-01-26 11:14:32 -08001455 m = re.search("[*#@%]", path)
1456 return m is not None
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001457
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001458
Lars Schneidera5db4b12015-09-26 09:55:03 +02001459class LargeFileSystem(object):
1460 """Base class for large file system support."""
1461
1462 def __init__(self, writeToGitStream):
1463 self.largeFiles = set()
1464 self.writeToGitStream = writeToGitStream
1465
1466 def generatePointer(self, cloneDestination, contentFile):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001467 """Return the content of a pointer file that is stored in Git instead
1468 of the actual content.
1469 """
Lars Schneidera5db4b12015-09-26 09:55:03 +02001470 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1471
1472 def pushFile(self, localLargeFile):
1473 """Push the actual content which is not stored in the Git repository to
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001474 a server.
1475 """
Lars Schneidera5db4b12015-09-26 09:55:03 +02001476 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1477
1478 def hasLargeFileExtension(self, relPath):
Yang Zhaoa6b13062019-12-13 15:52:44 -08001479 return functools.reduce(
Lars Schneidera5db4b12015-09-26 09:55:03 +02001480 lambda a, b: a or b,
1481 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1482 False
1483 )
1484
1485 def generateTempFile(self, contents):
1486 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1487 for d in contents:
1488 contentFile.write(d)
1489 contentFile.close()
1490 return contentFile.name
1491
1492 def exceedsLargeFileThreshold(self, relPath, contents):
1493 if gitConfigInt('git-p4.largeFileThreshold'):
1494 contentsSize = sum(len(d) for d in contents)
1495 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1496 return True
1497 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1498 contentsSize = sum(len(d) for d in contents)
1499 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1500 return False
1501 contentTempFile = self.generateTempFile(contents)
Philip.McGrawde5abb52019-08-27 06:43:58 +03001502 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=True)
1503 with zipfile.ZipFile(compressedContentFile, mode='w') as zf:
1504 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1505 compressedContentsSize = zf.infolist()[0].compress_size
Lars Schneidera5db4b12015-09-26 09:55:03 +02001506 os.remove(contentTempFile)
Lars Schneidera5db4b12015-09-26 09:55:03 +02001507 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1508 return True
1509 return False
1510
1511 def addLargeFile(self, relPath):
1512 self.largeFiles.add(relPath)
1513
1514 def removeLargeFile(self, relPath):
1515 self.largeFiles.remove(relPath)
1516
1517 def isLargeFile(self, relPath):
1518 return relPath in self.largeFiles
1519
1520 def processContent(self, git_mode, relPath, contents):
1521 """Processes the content of git fast import. This method decides if a
1522 file is stored in the large file system and handles all necessary
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001523 steps.
1524 """
Matthew McClain10c89a02023-10-18 19:25:58 -05001525 # symlinks aren't processed by smudge/clean filters
1526 if git_mode == "120000":
1527 return (git_mode, contents)
1528
Lars Schneidera5db4b12015-09-26 09:55:03 +02001529 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1530 contentTempFile = self.generateTempFile(contents)
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001531 pointer_git_mode, contents, localLargeFile = self.generatePointer(contentTempFile)
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001532 if pointer_git_mode:
1533 git_mode = pointer_git_mode
1534 if localLargeFile:
1535 # Move temp file to final location in large file system
1536 largeFileDir = os.path.dirname(localLargeFile)
1537 if not os.path.isdir(largeFileDir):
1538 os.makedirs(largeFileDir)
1539 shutil.move(contentTempFile, localLargeFile)
1540 self.addLargeFile(relPath)
1541 if gitConfigBool('git-p4.largeFilePush'):
1542 self.pushFile(localLargeFile)
1543 if verbose:
1544 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
Lars Schneidera5db4b12015-09-26 09:55:03 +02001545 return (git_mode, contents)
1546
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001547
Lars Schneidera5db4b12015-09-26 09:55:03 +02001548class MockLFS(LargeFileSystem):
1549 """Mock large file system for testing."""
1550
1551 def generatePointer(self, contentFile):
1552 """The pointer content is the original content prefixed with "pointer-".
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001553 The local filename of the large file storage is derived from the
1554 file content.
Lars Schneidera5db4b12015-09-26 09:55:03 +02001555 """
1556 with open(contentFile, 'r') as f:
1557 content = next(f)
1558 gitMode = '100644'
1559 pointerContents = 'pointer-' + content
1560 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1561 return (gitMode, pointerContents, localLargeFile)
1562
1563 def pushFile(self, localLargeFile):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001564 """The remote filename of the large file storage is the same as the
1565 local one but in a different directory.
Lars Schneidera5db4b12015-09-26 09:55:03 +02001566 """
1567 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1568 if not os.path.exists(remotePath):
1569 os.makedirs(remotePath)
1570 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1571
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001572
Lars Schneiderb47d8072015-09-26 09:55:04 +02001573class GitLFS(LargeFileSystem):
1574 """Git LFS as backend for the git-p4 large file system.
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001575 See https://git-lfs.github.com/ for details.
1576 """
Lars Schneiderb47d8072015-09-26 09:55:04 +02001577
1578 def __init__(self, *args):
1579 LargeFileSystem.__init__(self, *args)
1580 self.baseGitAttributes = []
1581
1582 def generatePointer(self, contentFile):
1583 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1584 mode and content which is stored in the Git repository instead of
1585 the actual content. Return also the new location of the actual
1586 content.
1587 """
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001588 if os.path.getsize(contentFile) == 0:
1589 return (None, '', None)
1590
Lars Schneiderb47d8072015-09-26 09:55:04 +02001591 pointerProcess = subprocess.Popen(
1592 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1593 stdout=subprocess.PIPE
1594 )
Yang Zhao86dca242019-12-13 15:52:39 -08001595 pointerFile = decode_text_stream(pointerProcess.stdout.read())
Lars Schneiderb47d8072015-09-26 09:55:04 +02001596 if pointerProcess.wait():
1597 os.remove(contentFile)
1598 die('git-lfs pointer command failed. Did you install the extension?')
Lars Schneider82f25672016-04-28 08:26:33 +02001599
1600 # Git LFS removed the preamble in the output of the 'pointer' command
1601 # starting from version 1.2.0. Check for the preamble here to support
1602 # earlier versions.
1603 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1604 if pointerFile.startswith('Git LFS pointer for'):
1605 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1606
1607 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
r.burenkovea94b162019-12-11 09:47:23 -08001608 # if someone use external lfs.storage ( not in local repo git )
1609 lfs_path = gitConfig('lfs.storage')
1610 if not lfs_path:
1611 lfs_path = 'lfs'
1612 if not os.path.isabs(lfs_path):
1613 lfs_path = os.path.join(os.getcwd(), '.git', lfs_path)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001614 localLargeFile = os.path.join(
r.burenkovea94b162019-12-11 09:47:23 -08001615 lfs_path,
1616 'objects', oid[:2], oid[2:4],
Lars Schneiderb47d8072015-09-26 09:55:04 +02001617 oid,
1618 )
1619 # LFS Spec states that pointer files should not have the executable bit set.
1620 gitMode = '100644'
Lars Schneider82f25672016-04-28 08:26:33 +02001621 return (gitMode, pointerFile, localLargeFile)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001622
1623 def pushFile(self, localLargeFile):
1624 uploadProcess = subprocess.Popen(
1625 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1626 )
1627 if uploadProcess.wait():
1628 die('git-lfs push command failed. Did you define a remote?')
1629
1630 def generateGitAttributes(self):
1631 return (
1632 self.baseGitAttributes +
1633 [
1634 '\n',
1635 '#\n',
1636 '# Git LFS (see https://git-lfs.github.com/)\n',
1637 '#\n',
1638 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001639 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001640 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1641 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001642 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001643 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1644 ]
1645 )
1646
1647 def addLargeFile(self, relPath):
1648 LargeFileSystem.addLargeFile(self, relPath)
1649 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1650
1651 def removeLargeFile(self, relPath):
1652 LargeFileSystem.removeLargeFile(self, relPath)
1653 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1654
1655 def processContent(self, git_mode, relPath, contents):
1656 if relPath == '.gitattributes':
1657 self.baseGitAttributes = contents
1658 return (git_mode, self.generateGitAttributes())
1659 else:
1660 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1661
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001662
Simon Hausmannb9847332007-03-20 20:54:23 +01001663class Command:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01001664 delete_actions = ("delete", "move/delete", "purge")
1665 add_actions = ("add", "branch", "move/add")
Luke Diamand89143ac2018-10-15 12:14:08 +01001666
Simon Hausmannb9847332007-03-20 20:54:23 +01001667 def __init__(self):
1668 self.usage = "usage: %prog [options]"
Simon Hausmann8910ac02007-03-26 08:18:55 +02001669 self.needsGit = True
Luke Diamand6a10b6a2012-04-24 09:33:23 +01001670 self.verbose = False
Simon Hausmannb9847332007-03-20 20:54:23 +01001671
Mazo, Andreyff8c50e2019-04-01 18:02:26 +00001672 # This is required for the "append" update_shelve action
Luke Diamand8cf422d2017-12-21 11:06:14 +00001673 def ensure_value(self, attr, value):
1674 if not hasattr(self, attr) or getattr(self, attr) is None:
1675 setattr(self, attr, value)
1676 return getattr(self, attr)
1677
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001678
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001679class P4UserMap:
1680 def __init__(self):
1681 self.userMapFromPerforceServer = False
Luke Diamandaffb4742012-01-19 09:52:27 +00001682 self.myP4UserId = None
1683
1684 def p4UserId(self):
1685 if self.myP4UserId:
1686 return self.myP4UserId
1687
Joel Holdsworth8a470592022-01-06 21:40:34 +00001688 results = p4CmdList(["user", "-o"])
Luke Diamandaffb4742012-01-19 09:52:27 +00001689 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001690 if 'User' in r:
Luke Diamandaffb4742012-01-19 09:52:27 +00001691 self.myP4UserId = r['User']
1692 return r['User']
1693 die("Could not find your p4 user id")
1694
1695 def p4UserIsMe(self, p4User):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001696 """Return True if the given p4 user is actually me."""
Luke Diamandaffb4742012-01-19 09:52:27 +00001697 me = self.p4UserId()
1698 if not p4User or p4User != me:
1699 return False
1700 else:
1701 return True
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001702
1703 def getUserCacheFilename(self):
1704 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1705 return home + "/.gitp4-usercache.txt"
1706
1707 def getUserMapFromPerforceServer(self):
1708 if self.userMapFromPerforceServer:
1709 return
1710 self.users = {}
1711 self.emails = {}
1712
Joel Holdsworth8a470592022-01-06 21:40:34 +00001713 for output in p4CmdList(["users"]):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001714 if "User" not in output:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001715 continue
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001716 # "FullName" is bytes. "Email" on the other hand might be bytes
1717 # or unicode string depending on whether we are running under
1718 # python2 or python3. To support
1719 # git-p4.metadataDecodingStrategy=fallback, self.users dict values
1720 # are always bytes, ready to be written to git.
1721 emailbytes = metadata_stream_to_writable_bytes(output["Email"])
1722 self.users[output["User"]] = output["FullName"] + b" <" + emailbytes + b">"
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001723 self.emails[output["Email"]] = output["User"]
1724
Lars Schneider10d08a12016-03-01 11:49:56 +01001725 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1726 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1727 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1728 if mapUser and len(mapUser[0]) == 3:
1729 user = mapUser[0][0]
1730 fullname = mapUser[0][1]
1731 email = mapUser[0][2]
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001732 fulluser = fullname + " <" + email + ">"
1733 self.users[user] = metadata_stream_to_writable_bytes(fulluser)
Lars Schneider10d08a12016-03-01 11:49:56 +01001734 self.emails[email] = user
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001735
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001736 s = b''
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001737 for (key, val) in self.users.items():
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001738 keybytes = metadata_stream_to_writable_bytes(key)
1739 s += b"%s\t%s\n" % (keybytes.expandtabs(1), val.expandtabs(1))
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001740
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001741 open(self.getUserCacheFilename(), 'wb').write(s)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001742 self.userMapFromPerforceServer = True
1743
1744 def loadUserMapFromCache(self):
1745 self.users = {}
1746 self.userMapFromPerforceServer = False
1747 try:
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001748 cache = open(self.getUserCacheFilename(), 'rb')
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001749 lines = cache.readlines()
1750 cache.close()
1751 for line in lines:
Tao Klerksf7b5ff62022-04-30 19:26:52 +00001752 entry = line.strip().split(b"\t")
1753 self.users[entry[0].decode('utf_8')] = entry[1]
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001754 except IOError:
1755 self.getUserMapFromPerforceServer()
1756
Joel Holdsworthadf159b2022-04-01 15:24:43 +01001757
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001758class P4Submit(Command, P4UserMap):
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001759
1760 conflict_behavior_choices = ("ask", "skip", "quit")
1761
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001762 def __init__(self):
Simon Hausmannb9847332007-03-20 20:54:23 +01001763 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001764 P4UserMap.__init__(self)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001765 self.options = [
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001766 optparse.make_option("--origin", dest="origin"),
Vitor Antunesae901092011-02-20 01:18:24 +00001767 optparse.make_option("-M", dest="detectRenames", action="store_true"),
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001768 # preserve the user, requires relevant p4 permissions
1769 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02001770 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
Pete Wyckoffef739f02012-09-09 16:16:11 -04001771 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001772 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001773 optparse.make_option("--conflict", dest="conflict_behavior",
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001774 choices=self.conflict_behavior_choices),
1775 optparse.make_option("--branch", dest="branch"),
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001776 optparse.make_option("--shelve", dest="shelve", action="store_true",
1777 help="Shelve instead of submit. Shelved files are reverted, "
1778 "restoring the workspace to the state before the shelve"),
Luke Diamand8cf422d2017-12-21 11:06:14 +00001779 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
Luke Diamand46c609e2016-12-02 22:43:19 +00001780 metavar="CHANGELIST",
Luke Diamand8cf422d2017-12-21 11:06:14 +00001781 help="update an existing shelved changelist, implies --shelve, "
Romain Merlandf55b87c2018-06-01 09:46:14 +02001782 "repeat in-order for multiple shelved changelists"),
1783 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1784 help="submit only the specified commit(s), one commit or xxx..xxx"),
1785 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1786 help="Disable rebase after submit is completed. Can be useful if you "
Luke Diamandb9d34db2018-06-08 21:32:44 +01001787 "work from a local git branch that is not master"),
1788 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1789 help="Skip Perforce sync of p4/master after submit or shelve"),
Ben Keene4935c452020-02-11 18:58:01 +00001790 optparse.make_option("--no-verify", dest="no_verify", action="store_true",
Ben Keene38ecf752020-02-14 14:44:45 +00001791 help="Bypass p4-pre-submit and p4-changelist hooks"),
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001792 ]
Chen Bin251c8c52018-07-27 21:22:22 +10001793 self.description = """Submit changes from git to the perforce depot.\n
Ben Keene4935c452020-02-11 18:58:01 +00001794 The `p4-pre-submit` hook is executed if it exists and is executable. It
1795 can be bypassed with the `--no-verify` command line option. The hook takes
1796 no parameters and nothing from standard input. Exiting with a non-zero status
1797 from this script prevents `git-p4 submit` from launching.
Chen Bin251c8c52018-07-27 21:22:22 +10001798
Ben Keene4935c452020-02-11 18:58:01 +00001799 One usage scenario is to run unit tests in the hook.
Ben Keene38ecf752020-02-14 14:44:45 +00001800
1801 The `p4-prepare-changelist` hook is executed right after preparing the default
1802 changelist message and before the editor is started. It takes one parameter,
1803 the name of the file that contains the changelist text. Exiting with a non-zero
1804 status from the script will abort the process.
1805
1806 The purpose of the hook is to edit the message file in place, and it is not
1807 supressed by the `--no-verify` option. This hook is called even if
1808 `--prepare-p4-only` is set.
1809
1810 The `p4-changelist` hook is executed after the changelist message has been
1811 edited by the user. It can be bypassed with the `--no-verify` option. It
1812 takes a single parameter, the name of the file that holds the proposed
1813 changelist text. Exiting with a non-zero status causes the command to abort.
1814
1815 The hook is allowed to edit the changelist file and can be used to normalize
1816 the text into some project standard format. It can also be used to refuse the
1817 Submit after inspect the message file.
1818
1819 The `p4-post-changelist` hook is invoked after the submit has successfully
Marlon Rac Cambasisb7e20b42020-11-05 12:48:14 -08001820 occurred in P4. It takes no parameters and is meant primarily for notification
Ben Keene38ecf752020-02-14 14:44:45 +00001821 and cannot affect the outcome of the git p4 submit action.
Ben Keene4935c452020-02-11 18:58:01 +00001822 """
Chen Bin251c8c52018-07-27 21:22:22 +10001823
Simon Hausmannc9b50e62007-03-29 19:15:24 +02001824 self.usage += " [name of git branch to submit into perforce depot]"
Simon Hausmann95124972007-03-23 09:16:07 +01001825 self.origin = ""
Vitor Antunesae901092011-02-20 01:18:24 +00001826 self.detectRenames = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05001827 self.preserveUser = gitConfigBool("git-p4.preserveUser")
Pete Wyckoffef739f02012-09-09 16:16:11 -04001828 self.dry_run = False
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001829 self.shelve = False
Luke Diamand8cf422d2017-12-21 11:06:14 +00001830 self.update_shelve = list()
Romain Merlandf55b87c2018-06-01 09:46:14 +02001831 self.commit = ""
Luke Diamand3b3477e2018-06-08 21:32:43 +01001832 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
Luke Diamandb9d34db2018-06-08 21:32:44 +01001833 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001834 self.prepare_p4_only = False
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001835 self.conflict_behavior = None
Marius Storm-Olsenf7baba82007-06-07 14:07:01 +02001836 self.isWindows = (platform.system() == "Windows")
Luke Diamand06804c72012-04-11 17:21:24 +02001837 self.exportLabels = False
Pete Wyckoff249da4c2012-11-23 17:35:35 -05001838 self.p4HasMoveCommand = p4_has_move_command()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001839 self.branch = None
Ben Keene4935c452020-02-11 18:58:01 +00001840 self.no_verify = False
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001841
Lars Schneidera5db4b12015-09-26 09:55:03 +02001842 if gitConfig('git-p4.largeFileSystem'):
1843 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1844
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001845 def check(self):
Joel Holdsworth8a470592022-01-06 21:40:34 +00001846 if len(p4CmdList(["opened", "..."])) > 0:
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001847 die("You have files opened with perforce! Close them before starting the sync.")
1848
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001849 def separate_jobs_from_description(self, message):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001850 """Extract and return a possible Jobs field in the commit message. It
1851 goes into a separate section in the p4 change specification.
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001852
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001853 A jobs line starts with "Jobs:" and looks like a new field in a
1854 form. Values are white-space separated on the same line or on
1855 following lines that start with a tab.
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001856
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001857 This does not parse and extract the full git commit message like a
1858 p4 form. It just sees the Jobs: line as a marker to pass everything
1859 from then on directly into the p4 form, but outside the description
1860 section.
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001861
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001862 Return a tuple (stripped log message, jobs string).
1863 """
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001864
1865 m = re.search(r'^Jobs:', message, re.MULTILINE)
1866 if m is None:
1867 return (message, None)
1868
1869 jobtext = message[m.start():]
1870 stripped_message = message[:m.start()].rstrip()
1871 return (stripped_message, jobtext)
1872
1873 def prepareLogMessage(self, template, message, jobs):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01001874 """Edits the template returned from "p4 change -o" to insert the
1875 message in the Description field, and the jobs text in the Jobs
1876 field.
1877 """
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001878 result = ""
1879
Simon Hausmannedae1e22008-02-19 09:29:06 +01001880 inDescriptionSection = False
1881
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001882 for line in template.split("\n"):
1883 if line.startswith("#"):
1884 result += line + "\n"
1885 continue
1886
Simon Hausmannedae1e22008-02-19 09:29:06 +01001887 if inDescriptionSection:
Michael Horowitzc9dbab02011-02-25 21:31:13 -05001888 if line.startswith("Files:") or line.startswith("Jobs:"):
Simon Hausmannedae1e22008-02-19 09:29:06 +01001889 inDescriptionSection = False
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001890 # insert Jobs section
1891 if jobs:
1892 result += jobs + "\n"
Simon Hausmannedae1e22008-02-19 09:29:06 +01001893 else:
1894 continue
1895 else:
1896 if line.startswith("Description:"):
1897 inDescriptionSection = True
1898 line += "\n"
1899 for messageLine in message.split("\n"):
1900 line += "\t" + messageLine + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001901
Simon Hausmannedae1e22008-02-19 09:29:06 +01001902 result += line + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001903
1904 return result
1905
Joel Holdsworthe665e982021-12-16 13:46:16 +00001906 def patchRCSKeywords(self, file, regexp):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001907 """Attempt to zap the RCS keywords in a p4 controlled file matching the
1908 given regex.
1909 """
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001910 handle, outFileName = tempfile.mkstemp(dir='.')
Luke Diamand60df0712012-02-23 07:51:30 +00001911 try:
Joel Holdsworth70c0d552021-12-16 13:46:19 +00001912 with os.fdopen(handle, "wb") as outFile, open(file, "rb") as inFile:
Joel Holdsworth8618d322021-12-16 13:46:15 +00001913 for line in inFile.readlines():
Joel Holdsworth70c0d552021-12-16 13:46:19 +00001914 outFile.write(regexp.sub(br'$\1$', line))
Luke Diamand60df0712012-02-23 07:51:30 +00001915 # Forcibly overwrite the original file
1916 os.unlink(file)
1917 shutil.move(outFileName, file)
1918 except:
1919 # cleanup our temporary file
1920 os.unlink(outFileName)
Luke Diamandf2606b12018-06-19 09:04:10 +01001921 print("Failed to strip RCS keywords in %s" % file)
Luke Diamand60df0712012-02-23 07:51:30 +00001922 raise
1923
Luke Diamandf2606b12018-06-19 09:04:10 +01001924 print("Patched up RCS keywords in %s" % file)
Luke Diamand60df0712012-02-23 07:51:30 +00001925
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001926 def p4UserForCommit(self, id):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001927 """Return the tuple (perforce user,git email) for a given git commit
1928 id.
1929 """
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001930 self.getUserMapFromPerforceServer()
Pete Wyckoff9bf28852013-01-26 22:11:20 -05001931 gitEmail = read_pipe(["git", "log", "--max-count=1",
1932 "--format=%ae", id])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001933 gitEmail = gitEmail.strip()
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001934 if gitEmail not in self.emails:
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001935 return (None, gitEmail)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001936 else:
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001937 return (self.emails[gitEmail], gitEmail)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001938
Joel Holdsworth12a77f52022-04-01 15:24:53 +01001939 def checkValidP4Users(self, commits):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001940 """Check if any git authors cannot be mapped to p4 users."""
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001941 for id in commits:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01001942 user, email = self.p4UserForCommit(id)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001943 if not user:
1944 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
Pete Wyckoff0d609032013-01-26 22:11:24 -05001945 if gitConfigBool("git-p4.allowMissingP4Users"):
Luke Diamandf2606b12018-06-19 09:04:10 +01001946 print("%s" % msg)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001947 else:
1948 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1949
1950 def lastP4Changelist(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001951 """Get back the last changelist number submitted in this client spec.
1952
1953 This then gets used to patch up the username in the change. If the
1954 same client spec is being used by multiple processes then this might
1955 go wrong.
1956 """
Joel Holdsworth8a470592022-01-06 21:40:34 +00001957 results = p4CmdList(["client", "-o"]) # find the current client
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001958 client = None
1959 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001960 if 'Client' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001961 client = r['Client']
1962 break
1963 if not client:
1964 die("could not get client spec")
Luke Diamand6de040d2011-10-16 10:47:52 -04001965 results = p4CmdList(["changes", "-c", client, "-m", "1"])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001966 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001967 if 'change' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001968 return r['change']
1969 die("Could not get changelist number for last submit - cannot patch up user details")
1970
1971 def modifyChangelistUser(self, changelist, newUser):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001972 """Fixup the user field of a changelist after it has been submitted."""
Joel Holdsworth8a470592022-01-06 21:40:34 +00001973 changes = p4CmdList(["change", "-o", changelist])
Luke Diamandecdba362011-05-07 11:19:43 +01001974 if len(changes) != 1:
1975 die("Bad output from p4 change modifying %s to user %s" %
1976 (changelist, newUser))
1977
1978 c = changes[0]
Joel Holdsworthe8f8b3b2022-04-01 15:25:03 +01001979 if c['User'] == newUser:
1980 # Nothing to do
1981 return
Luke Diamandecdba362011-05-07 11:19:43 +01001982 c['User'] = newUser
Yang Zhao50da1e72019-12-13 15:52:42 -08001983 # p4 does not understand format version 3 and above
1984 input = marshal.dumps(c, 2)
Luke Diamandecdba362011-05-07 11:19:43 +01001985
Joel Holdsworth8a470592022-01-06 21:40:34 +00001986 result = p4CmdList(["change", "-f", "-i"], stdin=input)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001987 for r in result:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001988 if 'code' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001989 if r['code'] == 'error':
1990 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
Luke Diamanddba1c9d2018-06-19 09:04:07 +01001991 if 'data' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001992 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1993 return
1994 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1995
1996 def canChangeChangelists(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01001997 """Check to see if we have p4 admin or super-user permissions, either
1998 of which are required to modify changelists.
1999 """
Luke Diamand52a48802012-01-19 09:52:25 +00002000 results = p4CmdList(["protects", self.depotPath])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002001 for r in results:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002002 if 'perm' in r:
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002003 if r['perm'] == 'admin':
2004 return 1
2005 if r['perm'] == 'super':
2006 return 1
2007 return 0
2008
Luke Diamand46c609e2016-12-02 22:43:19 +00002009 def prepareSubmitTemplate(self, changelist=None):
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04002010 """Run "p4 change -o" to grab a change specification template.
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002011
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04002012 This does not use "p4 -G", as it is nice to keep the submission
2013 template in original order, since a human might edit it.
2014
2015 Remove lines in the Files section that show changes to files
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002016 outside the depot path we're committing into.
2017 """
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04002018
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002019 upstream, settings = findUpstreamBranchPoint()
Sam Hocevarcbc69242015-12-19 09:39:39 +00002020
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002021 template = """\
2022# A Perforce Change Specification.
2023#
2024# Change: The change number. 'new' on a new changelist.
2025# Date: The date this specification was last modified.
2026# Client: The client on which the changelist was created. Read-only.
2027# User: The user who created the changelist.
2028# Status: Either 'pending' or 'submitted'. Read-only.
2029# Type: Either 'public' or 'restricted'. Default is 'public'.
2030# Description: Comments about the changelist. Required.
2031# Jobs: What opened jobs are to be closed by this changelist.
2032# You may delete jobs from this list. (New changelists only.)
2033# Files: What opened files from the default changelist are to be added
2034# to this changelist. You may delete files from this list.
2035# (New changelists only.)
2036"""
2037 files_list = []
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002038 inFilesSection = False
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002039 change_entry = None
Luke Diamand46c609e2016-12-02 22:43:19 +00002040 args = ['change', '-o']
2041 if changelist:
2042 args.append(str(changelist))
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002043 for entry in p4CmdList(args):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002044 if 'code' not in entry:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002045 continue
2046 if entry['code'] == 'stat':
2047 change_entry = entry
2048 break
2049 if not change_entry:
2050 die('Failed to decode output of p4 change -o')
Yang Zhao2e2aa8d2019-12-13 15:52:45 -08002051 for key, value in change_entry.items():
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002052 if key.startswith('File'):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002053 if 'depot-paths' in settings:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002054 if not [p for p in settings['depot-paths']
2055 if p4PathStartsWith(value, p)]:
2056 continue
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002057 else:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002058 if not p4PathStartsWith(value, self.depotPath):
2059 continue
2060 files_list.append(value)
2061 continue
2062 # Output in the order expected by prepareLogMessage
2063 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002064 if key not in change_entry:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02002065 continue
2066 template += '\n'
2067 template += key + ':'
2068 if key == 'Description':
2069 template += '\n'
2070 for field_line in change_entry[key].splitlines():
2071 template += '\t'+field_line+'\n'
2072 if len(files_list) > 0:
2073 template += '\n'
2074 template += 'Files:\n'
2075 for path in files_list:
2076 template += '\t'+path+'\n'
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002077 return template
2078
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002079 def edit_template(self, template_file):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002080 """Invoke the editor to let the user change the submission message.
2081
2082 Return true if okay to continue with the submit.
2083 """
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002084
2085 # if configured to skip the editing part, just submit
Pete Wyckoff0d609032013-01-26 22:11:24 -05002086 if gitConfigBool("git-p4.skipSubmitEdit"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002087 return True
2088
2089 # look at the modification time, to check later if the user saved
2090 # the file
2091 mtime = os.stat(template_file).st_mtime
2092
2093 # invoke the editor
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002094 if "P4EDITOR" in os.environ and (os.environ.get("P4EDITOR") != ""):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002095 editor = os.environ.get("P4EDITOR")
2096 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00002097 editor = read_pipe(["git", "var", "GIT_EDITOR"]).strip()
Luke Diamand2dade7a2015-05-19 23:23:17 +01002098 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002099
2100 # If the file was not saved, prompt to see if this patch should
2101 # be skipped. But skip this verification step if configured so.
Pete Wyckoff0d609032013-01-26 22:11:24 -05002102 if gitConfigBool("git-p4.skipSubmitEditCheck"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002103 return True
2104
Pete Wyckoffd1652042011-12-17 12:39:03 -05002105 # modification time updated means user saved the file
2106 if os.stat(template_file).st_mtime > mtime:
2107 return True
2108
Ben Keenee2aed5f2019-12-16 14:02:19 +00002109 response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
2110 if response == 'y':
2111 return True
2112 if response == 'n':
2113 return False
Pete Wyckoff7c766e52011-12-04 19:22:45 -05002114
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002115 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
Maxime Costeb4073bb2014-05-24 18:40:35 +01002116 # diff
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002117 if "P4DIFF" in os.environ:
Maxime Costeb4073bb2014-05-24 18:40:35 +01002118 del(os.environ["P4DIFF"])
2119 diff = ""
2120 for editedFile in editedFiles:
2121 diff += p4_read_pipe(['diff', '-du',
2122 wildcard_encode(editedFile)])
2123
2124 # new file diff
2125 newdiff = ""
2126 for newFile in filesToAdd:
2127 newdiff += "==== new file ====\n"
2128 newdiff += "--- /dev/null\n"
2129 newdiff += "+++ %s\n" % newFile
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002130
2131 is_link = os.path.islink(newFile)
2132 expect_link = newFile in symlinks
2133
2134 if is_link and expect_link:
2135 newdiff += "+%s\n" % os.readlink(newFile)
2136 else:
2137 f = open(newFile, "r")
dorgon.chang54662d52021-06-21 05:16:13 +00002138 try:
2139 for line in f.readlines():
2140 newdiff += "+" + line
2141 except UnicodeDecodeError:
Joel Holdsworth4768af22022-04-01 15:25:02 +01002142 # Found non-text data and skip, since diff description
2143 # should only include text
2144 pass
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002145 f.close()
Maxime Costeb4073bb2014-05-24 18:40:35 +01002146
Maxime Costee2a892e2014-06-11 14:09:59 +01002147 return (diff + newdiff).replace('\r\n', '\n')
Maxime Costeb4073bb2014-05-24 18:40:35 +01002148
Han-Wen Nienhuys7cb5cbe2007-05-23 16:55:48 -03002149 def applyCommit(self, id):
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002150 """Apply one commit, return True if it succeeded."""
2151
Luke Diamandf2606b12018-06-19 09:04:10 +01002152 print("Applying", read_pipe(["git", "show", "-s",
2153 "--format=format:%h %s", id]))
Vitor Antunesae901092011-02-20 01:18:24 +00002154
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002155 p4User, gitEmail = self.p4UserForCommit(id)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002156
Joel Holdsworth3d8a3032022-01-06 21:40:33 +00002157 diff = read_pipe_lines(
Joel Holdsworth8a470592022-01-06 21:40:34 +00002158 ["git", "diff-tree", "-r"] + self.diffOpts + ["{}^".format(id), id])
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002159 filesToAdd = set()
Romain Picarda02b8bc2016-01-12 13:43:47 +01002160 filesToChangeType = set()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002161 filesToDelete = set()
Simon Hausmannd336c152007-05-16 09:41:26 +02002162 editedFiles = set()
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002163 pureRenameCopy = set()
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002164 symlinks = set()
Chris Pettittc65b6702007-11-01 20:43:14 -07002165 filesToChangeExecBit = {}
Luke Diamand46c609e2016-12-02 22:43:19 +00002166 all_files = list()
Luke Diamand60df0712012-02-23 07:51:30 +00002167
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002168 for line in diff:
Chris Pettittb43b0a32007-11-01 20:43:13 -07002169 diff = parseDiffTreeEntry(line)
2170 modifier = diff['status']
2171 path = diff['src']
Luke Diamand46c609e2016-12-02 22:43:19 +00002172 all_files.append(path)
2173
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002174 if modifier == "M":
Luke Diamand6de040d2011-10-16 10:47:52 -04002175 p4_edit(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07002176 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
2177 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmannd336c152007-05-16 09:41:26 +02002178 editedFiles.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002179 elif modifier == "A":
2180 filesToAdd.add(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07002181 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002182 if path in filesToDelete:
2183 filesToDelete.remove(path)
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002184
2185 dst_mode = int(diff['dst_mode'], 8)
Luke Diamanddb2d9972018-06-19 09:04:11 +01002186 if dst_mode == 0o120000:
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002187 symlinks.add(path)
2188
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002189 elif modifier == "D":
2190 filesToDelete.add(path)
2191 if path in filesToAdd:
2192 filesToAdd.remove(path)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002193 elif modifier == "C":
2194 src, dest = diff['src'], diff['dst']
Luke Diamand7a109462019-01-18 09:36:56 +00002195 all_files.append(dest)
Luke Diamand6de040d2011-10-16 10:47:52 -04002196 p4_integrate(src, dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002197 pureRenameCopy.add(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002198 if diff['src_sha1'] != diff['dst_sha1']:
Luke Diamand6de040d2011-10-16 10:47:52 -04002199 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002200 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002201 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Luke Diamand6de040d2011-10-16 10:47:52 -04002202 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002203 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002204 filesToChangeExecBit[dest] = diff['dst_mode']
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05002205 if self.isWindows:
2206 # turn off read-only attribute
2207 os.chmod(dest, stat.S_IWRITE)
Vitor Antunes4fddb412011-02-20 01:18:25 +00002208 os.unlink(dest)
2209 editedFiles.add(dest)
Chris Pettittd9a5f252007-10-15 22:15:06 -07002210 elif modifier == "R":
Chris Pettittb43b0a32007-11-01 20:43:13 -07002211 src, dest = diff['src'], diff['dst']
Luke Diamand7a109462019-01-18 09:36:56 +00002212 all_files.append(dest)
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002213 if self.p4HasMoveCommand:
2214 p4_edit(src) # src must be open before move
2215 p4_move(src, dest) # opens for (move/delete, move/add)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04002216 else:
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002217 p4_integrate(src, dest)
2218 if diff['src_sha1'] != diff['dst_sha1']:
2219 p4_edit(dest)
2220 else:
2221 pureRenameCopy.add(dest)
Chris Pettittc65b6702007-11-01 20:43:14 -07002222 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002223 if not self.p4HasMoveCommand:
2224 p4_edit(dest) # with move: already open, writable
Chris Pettittc65b6702007-11-01 20:43:14 -07002225 filesToChangeExecBit[dest] = diff['dst_mode']
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002226 if not self.p4HasMoveCommand:
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05002227 if self.isWindows:
2228 os.chmod(dest, stat.S_IWRITE)
Gary Gibbons8e9497c2012-07-12 19:29:00 -04002229 os.unlink(dest)
2230 filesToDelete.add(src)
Chris Pettittd9a5f252007-10-15 22:15:06 -07002231 editedFiles.add(dest)
Romain Picarda02b8bc2016-01-12 13:43:47 +01002232 elif modifier == "T":
2233 filesToChangeType.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002234 else:
2235 die("unknown modifier %s for %s" % (modifier, path))
2236
Tolga Ceylan749b6682014-05-06 22:48:54 -07002237 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
Simon Hausmann47a130b2007-05-20 16:33:21 +02002238 patchcmd = diffcmd + " | git apply "
Simon Hausmannc1b296b2007-05-20 16:55:05 +02002239 tryPatchCmd = patchcmd + "--check -"
2240 applyPatchCmd = patchcmd + "--check --apply -"
Luke Diamand60df0712012-02-23 07:51:30 +00002241 patch_succeeded = True
Simon Hausmann51a26402007-04-15 09:59:56 +02002242
Ben Keene1ec4a0a2020-02-14 14:44:46 +00002243 if verbose:
2244 print("TryPatch: %s" % tryPatchCmd)
2245
Simon Hausmann47a130b2007-05-20 16:33:21 +02002246 if os.system(tryPatchCmd) != 0:
Luke Diamand60df0712012-02-23 07:51:30 +00002247 fixed_rcs_keywords = False
2248 patch_succeeded = False
Luke Diamandf2606b12018-06-19 09:04:10 +01002249 print("Unfortunately applying the change failed!")
Luke Diamand60df0712012-02-23 07:51:30 +00002250
2251 # Patch failed, maybe it's just RCS keyword woes. Look through
2252 # the patch to see if that's possible.
Pete Wyckoff0d609032013-01-26 22:11:24 -05002253 if gitConfigBool("git-p4.attemptRCSCleanup"):
Luke Diamand60df0712012-02-23 07:51:30 +00002254 file = None
Luke Diamand60df0712012-02-23 07:51:30 +00002255 kwfiles = {}
2256 for file in editedFiles | filesToDelete:
2257 # did this file's delta contain RCS keywords?
Joel Holdsworthe665e982021-12-16 13:46:16 +00002258 regexp = p4_keywords_regexp_for_file(file)
2259 if regexp:
Luke Diamand60df0712012-02-23 07:51:30 +00002260 # this file is a possibility...look for RCS keywords.
Joel Holdsworth70c0d552021-12-16 13:46:19 +00002261 for line in read_pipe_lines(
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01002262 ["git", "diff", "%s^..%s" % (id, id), file],
2263 raw=True):
Luke Diamand60df0712012-02-23 07:51:30 +00002264 if regexp.search(line):
2265 if verbose:
Moritz Baumannc0d2b072022-07-20 18:55:29 +00002266 print("got keyword match on %s in %s in %s" % (regexp.pattern, line, file))
Joel Holdsworthe665e982021-12-16 13:46:16 +00002267 kwfiles[file] = regexp
Luke Diamand60df0712012-02-23 07:51:30 +00002268 break
2269
Joel Holdsworthe665e982021-12-16 13:46:16 +00002270 for file, regexp in kwfiles.items():
Luke Diamand60df0712012-02-23 07:51:30 +00002271 if verbose:
Joel Holdsworthe665e982021-12-16 13:46:16 +00002272 print("zapping %s with %s" % (line, regexp.pattern))
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05002273 # File is being deleted, so not open in p4. Must
2274 # disable the read-only bit on windows.
2275 if self.isWindows and file not in editedFiles:
2276 os.chmod(file, stat.S_IWRITE)
Luke Diamand60df0712012-02-23 07:51:30 +00002277 self.patchRCSKeywords(file, kwfiles[file])
2278 fixed_rcs_keywords = True
2279
2280 if fixed_rcs_keywords:
Luke Diamandf2606b12018-06-19 09:04:10 +01002281 print("Retrying the patch with RCS keywords cleaned up")
Luke Diamand60df0712012-02-23 07:51:30 +00002282 if os.system(tryPatchCmd) == 0:
2283 patch_succeeded = True
Ben Keene1ec4a0a2020-02-14 14:44:46 +00002284 print("Patch succeesed this time with RCS keywords cleaned")
Luke Diamand60df0712012-02-23 07:51:30 +00002285
2286 if not patch_succeeded:
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002287 for f in editedFiles:
2288 p4_revert(f)
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002289 return False
Simon Hausmann51a26402007-04-15 09:59:56 +02002290
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002291 #
2292 # Apply the patch for real, and do add/delete/+x handling.
2293 #
Joel Holdsworth3d8a3032022-01-06 21:40:33 +00002294 system(applyPatchCmd, shell=True)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002295
Romain Picarda02b8bc2016-01-12 13:43:47 +01002296 for f in filesToChangeType:
2297 p4_edit(f, "-t", "auto")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002298 for f in filesToAdd:
Luke Diamand6de040d2011-10-16 10:47:52 -04002299 p4_add(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002300 for f in filesToDelete:
Luke Diamand6de040d2011-10-16 10:47:52 -04002301 p4_revert(f)
2302 p4_delete(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002303
Chris Pettittc65b6702007-11-01 20:43:14 -07002304 # Set/clear executable bits
2305 for f in filesToChangeExecBit.keys():
2306 mode = filesToChangeExecBit[f]
2307 setP4ExecBit(f, mode)
2308
Luke Diamand8cf422d2017-12-21 11:06:14 +00002309 update_shelve = 0
2310 if len(self.update_shelve) > 0:
2311 update_shelve = self.update_shelve.pop(0)
2312 p4_reopen_in_change(update_shelve, all_files)
Luke Diamand46c609e2016-12-02 22:43:19 +00002313
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002314 #
2315 # Build p4 change description, starting with the contents
2316 # of the git commit message.
2317 #
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01002318 logMessage = extractLogMessageFromGitCommit(id)
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01002319 logMessage = logMessage.strip()
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002320 logMessage, jobs = self.separate_jobs_from_description(logMessage)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002321
Luke Diamand8cf422d2017-12-21 11:06:14 +00002322 template = self.prepareSubmitTemplate(update_shelve)
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04002323 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002324
2325 if self.preserveUser:
Joel Holdsworth812ee742022-04-01 15:24:45 +01002326 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002327
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002328 if self.checkAuthorship and not self.p4UserIsMe(p4User):
2329 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
2330 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
2331 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
2332
2333 separatorLine = "######## everything below this line is just the diff #######\n"
Maxime Costeb4073bb2014-05-24 18:40:35 +01002334 if not self.prepare_p4_only:
2335 submitTemplate += separatorLine
Luke Diamanddf8a9e82016-12-17 01:00:40 +00002336 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002337
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002338 handle, fileName = tempfile.mkstemp()
Maxime Costee2a892e2014-06-11 14:09:59 +01002339 tmpFile = os.fdopen(handle, "w+b")
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002340 if self.isWindows:
2341 submitTemplate = submitTemplate.replace("\n", "\r\n")
Yang Zhao6cec21a2019-12-13 15:52:38 -08002342 tmpFile.write(encode_text_stream(submitTemplate))
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002343 tmpFile.close()
2344
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002345 submitted = False
Luke Diamandecdba362011-05-07 11:19:43 +01002346
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002347 try:
Ben Keene38ecf752020-02-14 14:44:45 +00002348 # Allow the hook to edit the changelist text before presenting it
2349 # to the user.
2350 if not run_git_hook("p4-prepare-changelist", [fileName]):
2351 return False
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002352
2353 if self.prepare_p4_only:
2354 #
2355 # Leave the p4 tree prepared, and the submit template around
2356 # and let the user decide what to do next
2357 #
2358 submitted = True
2359 print("")
2360 print("P4 workspace prepared for submission.")
2361 print("To submit or revert, go to client workspace")
2362 print(" " + self.clientPath)
2363 print("")
2364 print("To submit, use \"p4 submit\" to write a new description,")
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002365 print("or \"p4 submit -i <%s\" to use the one prepared by"
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002366 " \"git p4\"." % fileName)
2367 print("You can delete the file \"%s\" when finished." % fileName)
2368
2369 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002370 print("To preserve change ownership by user %s, you must\n"
2371 "do \"p4 change -f <change>\" after submitting and\n"
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002372 "edit the User field.")
2373 if pureRenameCopy:
2374 print("After submitting, renamed files must be re-synced.")
2375 print("Invoke \"p4 sync -f\" on each of these files:")
2376 for f in pureRenameCopy:
2377 print(" " + f)
2378
2379 print("")
2380 print("To revert the changes, use \"p4 revert ...\", and delete")
2381 print("the submit template file \"%s\"" % fileName)
2382 if filesToAdd:
2383 print("Since the commit adds new files, they must be deleted:")
2384 for f in filesToAdd:
2385 print(" " + f)
2386 print("")
2387 sys.stdout.flush()
2388 return True
2389
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002390 if self.edit_template(fileName):
Ben Keene38ecf752020-02-14 14:44:45 +00002391 if not self.no_verify:
2392 if not run_git_hook("p4-changelist", [fileName]):
2393 print("The p4-changelist hook failed.")
2394 sys.stdout.flush()
2395 return False
2396
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002397 # read the edited message and submit
2398 tmpFile = open(fileName, "rb")
Yang Zhao6cec21a2019-12-13 15:52:38 -08002399 message = decode_text_stream(tmpFile.read())
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002400 tmpFile.close()
2401 if self.isWindows:
2402 message = message.replace("\r\n", "\n")
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002403 if message.find(separatorLine) != -1:
2404 submitTemplate = message[:message.index(separatorLine)]
2405 else:
2406 submitTemplate = message
2407
2408 if len(submitTemplate.strip()) == 0:
2409 print("Changelist is empty, aborting this changelist.")
2410 sys.stdout.flush()
2411 return False
Luke Diamand46c609e2016-12-02 22:43:19 +00002412
Luke Diamand8cf422d2017-12-21 11:06:14 +00002413 if update_shelve:
Luke Diamand46c609e2016-12-02 22:43:19 +00002414 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2415 elif self.shelve:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002416 p4_write_pipe(['shelve', '-i'], submitTemplate)
2417 else:
2418 p4_write_pipe(['submit', '-i'], submitTemplate)
2419 # The rename/copy happened by applying a patch that created a
2420 # new file. This leaves it writable, which confuses p4.
2421 for f in pureRenameCopy:
2422 p4_sync(f, "-f")
Luke Diamandecdba362011-05-07 11:19:43 +01002423
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002424 if self.preserveUser:
2425 if p4User:
2426 # Get last changelist number. Cannot easily get it from
2427 # the submit command output as the output is
2428 # unmarshalled.
2429 changelist = self.lastP4Changelist()
2430 self.modifyChangelistUser(changelist, p4User)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002431
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002432 submitted = True
2433
Ben Keene38ecf752020-02-14 14:44:45 +00002434 run_git_hook("p4-post-changelist")
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002435 finally:
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002436 # Revert changes if we skip this patch
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002437 if not submitted or self.shelve:
2438 if self.shelve:
Joel Holdsworth843d8472022-04-01 15:24:54 +01002439 print("Reverting shelved files.")
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002440 else:
Joel Holdsworth843d8472022-04-01 15:24:54 +01002441 print("Submission cancelled, undoing p4 changes.")
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002442 sys.stdout.flush()
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002443 for f in editedFiles | filesToDelete:
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002444 p4_revert(f)
2445 for f in filesToAdd:
2446 p4_revert(f)
2447 os.remove(f)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002448
Ben Keenecd1e0dc2020-02-14 14:44:44 +00002449 if not self.prepare_p4_only:
2450 os.remove(fileName)
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002451 return submitted
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002452
Luke Diamand06804c72012-04-11 17:21:24 +02002453 def exportGitTags(self, gitTags):
Joel Holdsworth522e9142022-04-01 15:24:47 +01002454 """Export git tags as p4 labels. Create a p4 label and then tag with
2455 that.
2456 """
2457
Luke Diamandc8942a22012-04-11 17:21:24 +02002458 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2459 if len(validLabelRegexp) == 0:
2460 validLabelRegexp = defaultLabelRegexp
2461 m = re.compile(validLabelRegexp)
Luke Diamand06804c72012-04-11 17:21:24 +02002462
2463 for name in gitTags:
2464
2465 if not m.match(name):
2466 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002467 print("tag %s does not match regexp %s" % (name, validLabelRegexp))
Luke Diamand06804c72012-04-11 17:21:24 +02002468 continue
2469
2470 # Get the p4 commit this corresponds to
Luke Diamandc8942a22012-04-11 17:21:24 +02002471 logMessage = extractLogMessageFromGitCommit(name)
2472 values = extractSettingsGitLog(logMessage)
Luke Diamand06804c72012-04-11 17:21:24 +02002473
Luke Diamanddba1c9d2018-06-19 09:04:07 +01002474 if 'change' not in values:
Luke Diamand06804c72012-04-11 17:21:24 +02002475 # a tag pointing to something not sent to p4; ignore
2476 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002477 print("git tag %s does not give a p4 commit" % name)
Luke Diamand06804c72012-04-11 17:21:24 +02002478 continue
Luke Diamandc8942a22012-04-11 17:21:24 +02002479 else:
2480 changelist = values['change']
Luke Diamand06804c72012-04-11 17:21:24 +02002481
2482 # Get the tag details.
2483 inHeader = True
2484 isAnnotated = False
2485 body = []
2486 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2487 l = l.strip()
2488 if inHeader:
2489 if re.match(r'tag\s+', l):
2490 isAnnotated = True
2491 elif re.match(r'\s*$', l):
2492 inHeader = False
2493 continue
2494 else:
2495 body.append(l)
2496
2497 if not isAnnotated:
2498 body = ["lightweight tag imported by git p4\n"]
2499
2500 # Create the label - use the same view as the client spec we are using
2501 clientSpec = getClientSpec()
2502
Joel Holdsworth6febb9f2022-04-01 15:24:58 +01002503 labelTemplate = "Label: %s\n" % name
Luke Diamand06804c72012-04-11 17:21:24 +02002504 labelTemplate += "Description:\n"
2505 for b in body:
2506 labelTemplate += "\t" + b + "\n"
2507 labelTemplate += "View:\n"
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002508 for depot_side in clientSpec.mappings:
2509 labelTemplate += "\t%s\n" % depot_side
Luke Diamand06804c72012-04-11 17:21:24 +02002510
Pete Wyckoffef739f02012-09-09 16:16:11 -04002511 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002512 print("Would create p4 label %s for tag" % name)
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002513 elif self.prepare_p4_only:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002514 print("Not creating p4 label %s for tag due to option"
Luke Diamandf2606b12018-06-19 09:04:10 +01002515 " --prepare-p4-only" % name)
Pete Wyckoffef739f02012-09-09 16:16:11 -04002516 else:
2517 p4_write_pipe(["label", "-i"], labelTemplate)
Luke Diamand06804c72012-04-11 17:21:24 +02002518
Pete Wyckoffef739f02012-09-09 16:16:11 -04002519 # Use the label
2520 p4_system(["tag", "-l", name] +
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002521 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
Luke Diamand06804c72012-04-11 17:21:24 +02002522
Pete Wyckoffef739f02012-09-09 16:16:11 -04002523 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002524 print("created p4 label for tag %s" % name)
Luke Diamand06804c72012-04-11 17:21:24 +02002525
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002526 def run(self, args):
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002527 if len(args) == 0:
2528 self.master = currentGitBranch()
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002529 elif len(args) == 1:
2530 self.master = args[0]
Pete Wyckoff28755db2011-12-24 21:07:40 -05002531 if not branchExists(self.master):
2532 die("Branch %s does not exist" % self.master)
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002533 else:
2534 return False
2535
Luke Diamand8cf422d2017-12-21 11:06:14 +00002536 for i in self.update_shelve:
2537 if i <= 0:
2538 sys.exit("invalid changelist %d" % i)
2539
Luke Diamand00ad6e32015-11-21 09:54:41 +00002540 if self.master:
2541 allowSubmit = gitConfig("git-p4.allowSubmit")
2542 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2543 die("%s is not in git-p4.allowSubmit" % self.master)
Jing Xue4c2d5d72008-06-22 14:12:39 -04002544
Joel Holdsworth0874bb02022-04-01 15:24:52 +01002545 upstream, settings = findUpstreamBranchPoint()
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002546 self.depotPath = settings['depot-paths'][0]
Simon Hausmann27d2d812007-06-12 14:31:59 +02002547 if len(self.origin) == 0:
2548 self.origin = upstream
Simon Hausmanna3fdd572007-06-07 22:54:32 +02002549
Luke Diamand8cf422d2017-12-21 11:06:14 +00002550 if len(self.update_shelve) > 0:
Luke Diamand46c609e2016-12-02 22:43:19 +00002551 self.shelve = True
2552
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002553 if self.preserveUser:
2554 if not self.canChangeChangelists():
2555 die("Cannot preserve user names without p4 super-user or admin permissions")
2556
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04002557 # if not set from the command line, try the config file
2558 if self.conflict_behavior is None:
2559 val = gitConfig("git-p4.conflict")
2560 if val:
2561 if val not in self.conflict_behavior_choices:
2562 die("Invalid value '%s' for config git-p4.conflict" % val)
2563 else:
2564 val = "ask"
2565 self.conflict_behavior = val
2566
Simon Hausmanna3fdd572007-06-07 22:54:32 +02002567 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002568 print("Origin branch is " + self.origin)
Simon Hausmann95124972007-03-23 09:16:07 +01002569
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002570 if len(self.depotPath) == 0:
Luke Diamandf2606b12018-06-19 09:04:10 +01002571 print("Internal error: cannot locate perforce depot path from existing branches")
Simon Hausmann95124972007-03-23 09:16:07 +01002572 sys.exit(128)
2573
Pete Wyckoff543987b2012-02-25 20:06:25 -05002574 self.useClientSpec = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05002575 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff543987b2012-02-25 20:06:25 -05002576 self.useClientSpec = True
2577 if self.useClientSpec:
2578 self.clientSpecDirs = getClientSpec()
Simon Hausmann95124972007-03-23 09:16:07 +01002579
Ville Skyttä2e3a16b2016-08-09 11:53:38 +03002580 # Check for the existence of P4 branches
Vitor Antunescd884102015-04-21 23:49:30 +01002581 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2582
2583 if self.useClientSpec and not branchesDetected:
Pete Wyckoff543987b2012-02-25 20:06:25 -05002584 # all files are relative to the client spec
2585 self.clientPath = getClientRoot()
2586 else:
2587 self.clientPath = p4Where(self.depotPath)
2588
2589 if self.clientPath == "":
2590 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
Simon Hausmann95124972007-03-23 09:16:07 +01002591
Luke Diamandf2606b12018-06-19 09:04:10 +01002592 print("Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath))
Simon Hausmann7944f142007-05-21 11:04:26 +02002593 self.oldWorkingDirectory = os.getcwd()
Simon Hausmannc1b296b2007-05-20 16:55:05 +02002594
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002595 # ensure the clientPath exists
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002596 new_client_dir = False
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002597 if not os.path.exists(self.clientPath):
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002598 new_client_dir = True
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002599 os.makedirs(self.clientPath)
2600
Miklós Fazekasbbd84862013-03-11 17:45:29 -04002601 chdir(self.clientPath, is_client_path=True)
Pete Wyckoffef739f02012-09-09 16:16:11 -04002602 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002603 print("Would synchronize p4 checkout in %s" % self.clientPath)
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002604 else:
Luke Diamandf2606b12018-06-19 09:04:10 +01002605 print("Synchronizing p4 checkout...")
Pete Wyckoffef739f02012-09-09 16:16:11 -04002606 if new_client_dir:
2607 # old one was destroyed, and maybe nobody told p4
2608 p4_sync("...", "-f")
2609 else:
2610 p4_sync("...")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002611 self.check()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002612
Simon Hausmann4c750c02008-02-19 09:37:16 +01002613 commits = []
Luke Diamand00ad6e32015-11-21 09:54:41 +00002614 if self.master:
Ævar Arnfjörð Bjarmason89f32a92018-05-10 12:43:00 +00002615 committish = self.master
Luke Diamand00ad6e32015-11-21 09:54:41 +00002616 else:
Ævar Arnfjörð Bjarmason89f32a92018-05-10 12:43:00 +00002617 committish = 'HEAD'
Luke Diamand00ad6e32015-11-21 09:54:41 +00002618
Romain Merlandf55b87c2018-06-01 09:46:14 +02002619 if self.commit != "":
2620 if self.commit.find("..") != -1:
2621 limits_ish = self.commit.split("..")
2622 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2623 commits.append(line.strip())
2624 commits.reverse()
2625 else:
2626 commits.append(self.commit)
2627 else:
Junio C Hamanoe6388992018-06-18 10:18:41 -07002628 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
Romain Merlandf55b87c2018-06-01 09:46:14 +02002629 commits.append(line.strip())
2630 commits.reverse()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002631
Pete Wyckoff0d609032013-01-26 22:11:24 -05002632 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
Luke Diamand848de9c2011-05-13 20:46:00 +01002633 self.checkAuthorship = False
2634 else:
2635 self.checkAuthorship = True
2636
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002637 if self.preserveUser:
2638 self.checkValidP4Users(commits)
2639
Gary Gibbons84cb0002012-07-04 09:40:19 -04002640 #
2641 # Build up a set of options to be passed to diff when
2642 # submitting each commit to p4.
2643 #
2644 if self.detectRenames:
2645 # command-line -M arg
Joel Holdsworth8a470592022-01-06 21:40:34 +00002646 self.diffOpts = ["-M"]
Gary Gibbons84cb0002012-07-04 09:40:19 -04002647 else:
2648 # If not explicitly set check the config variable
2649 detectRenames = gitConfig("git-p4.detectRenames")
2650
2651 if detectRenames.lower() == "false" or detectRenames == "":
Joel Holdsworth8a470592022-01-06 21:40:34 +00002652 self.diffOpts = []
Gary Gibbons84cb0002012-07-04 09:40:19 -04002653 elif detectRenames.lower() == "true":
Joel Holdsworth8a470592022-01-06 21:40:34 +00002654 self.diffOpts = ["-M"]
Gary Gibbons84cb0002012-07-04 09:40:19 -04002655 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00002656 self.diffOpts = ["-M{}".format(detectRenames)]
Gary Gibbons84cb0002012-07-04 09:40:19 -04002657
2658 # no command-line arg for -C or --find-copies-harder, just
2659 # config variables
2660 detectCopies = gitConfig("git-p4.detectCopies")
2661 if detectCopies.lower() == "false" or detectCopies == "":
2662 pass
2663 elif detectCopies.lower() == "true":
Joel Holdsworth8a470592022-01-06 21:40:34 +00002664 self.diffOpts.append("-C")
Gary Gibbons84cb0002012-07-04 09:40:19 -04002665 else:
Joel Holdsworth8a470592022-01-06 21:40:34 +00002666 self.diffOpts.append("-C{}".format(detectCopies))
Gary Gibbons84cb0002012-07-04 09:40:19 -04002667
Pete Wyckoff0d609032013-01-26 22:11:24 -05002668 if gitConfigBool("git-p4.detectCopiesHarder"):
Joel Holdsworth8a470592022-01-06 21:40:34 +00002669 self.diffOpts.append("--find-copies-harder")
Gary Gibbons84cb0002012-07-04 09:40:19 -04002670
Luke Diamand8cf422d2017-12-21 11:06:14 +00002671 num_shelves = len(self.update_shelve)
2672 if num_shelves > 0 and num_shelves != len(commits):
2673 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2674 (len(commits), num_shelves))
2675
Ben Keene4935c452020-02-11 18:58:01 +00002676 if not self.no_verify:
2677 try:
2678 if not run_git_hook("p4-pre-submit"):
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002679 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nYou can skip "
2680 "this pre-submission check by adding\nthe command line option '--no-verify', "
Ben Keene4935c452020-02-11 18:58:01 +00002681 "however,\nthis will also skip the p4-changelist hook as well.")
2682 sys.exit(1)
2683 except Exception as e:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002684 print("\nThe p4-pre-submit hook failed, aborting the submit.\n\nThe hook failed "
Joel Holdsworth84af8b82022-04-01 15:24:50 +01002685 "with the error '{0}'".format(e.message))
Ben Keeneaa8b7662020-02-11 18:58:00 +00002686 sys.exit(1)
Chen Bin251c8c52018-07-27 21:22:22 +10002687
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002688 #
2689 # Apply the commits, one at a time. On failure, ask if should
2690 # continue to try the rest of the patches, or quit.
2691 #
Pete Wyckoffef739f02012-09-09 16:16:11 -04002692 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002693 print("Would apply")
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002694 applied = []
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002695 last = len(commits) - 1
2696 for i, commit in enumerate(commits):
Pete Wyckoffef739f02012-09-09 16:16:11 -04002697 if self.dry_run:
Luke Diamandf2606b12018-06-19 09:04:10 +01002698 print(" ", read_pipe(["git", "show", "-s",
2699 "--format=format:%h %s", commit]))
Pete Wyckoffef739f02012-09-09 16:16:11 -04002700 ok = True
2701 else:
2702 ok = self.applyCommit(commit)
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002703 if ok:
2704 applied.append(commit)
Ben Keene2dfdd702020-05-12 13:15:59 +00002705 if self.prepare_p4_only:
2706 if i < last:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01002707 print("Processing only the first commit due to option"
Ben Keene2dfdd702020-05-12 13:15:59 +00002708 " --prepare-p4-only")
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002709 break
Ben Keene2dfdd702020-05-12 13:15:59 +00002710 else:
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002711 if i < last:
Ben Keenee2aed5f2019-12-16 14:02:19 +00002712 # prompt for what to do, or use the option/variable
2713 if self.conflict_behavior == "ask":
2714 print("What do you want to do?")
2715 response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
2716 elif self.conflict_behavior == "skip":
2717 response = "s"
2718 elif self.conflict_behavior == "quit":
2719 response = "q"
2720 else:
2721 die("Unknown conflict_behavior '%s'" %
2722 self.conflict_behavior)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002723
Ben Keenee2aed5f2019-12-16 14:02:19 +00002724 if response == "s":
2725 print("Skipping this commit, but applying the rest")
2726 if response == "q":
2727 print("Quitting")
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002728 break
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002729
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002730 chdir(self.oldWorkingDirectory)
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002731 shelved_applied = "shelved" if self.shelve else "applied"
Pete Wyckoffef739f02012-09-09 16:16:11 -04002732 if self.dry_run:
2733 pass
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002734 elif self.prepare_p4_only:
2735 pass
Pete Wyckoffef739f02012-09-09 16:16:11 -04002736 elif len(commits) == len(applied):
Luke Diamandf2606b12018-06-19 09:04:10 +01002737 print("All commits {0}!".format(shelved_applied))
Simon Hausmann14594f42007-08-22 09:07:15 +02002738
Simon Hausmann4c750c02008-02-19 09:37:16 +01002739 sync = P4Sync()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05002740 if self.branch:
2741 sync.branch = self.branch
Luke Diamandb9d34db2018-06-08 21:32:44 +01002742 if self.disable_p4sync:
2743 sync.sync_origin_only()
2744 else:
2745 sync.run([])
Simon Hausmann14594f42007-08-22 09:07:15 +02002746
Luke Diamandb9d34db2018-06-08 21:32:44 +01002747 if not self.disable_rebase:
2748 rebase = P4Rebase()
2749 rebase.rebase()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002750
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002751 else:
2752 if len(applied) == 0:
Luke Diamandf2606b12018-06-19 09:04:10 +01002753 print("No commits {0}.".format(shelved_applied))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002754 else:
Luke Diamandf2606b12018-06-19 09:04:10 +01002755 print("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002756 for c in commits:
2757 if c in applied:
2758 star = "*"
2759 else:
2760 star = " "
Luke Diamandf2606b12018-06-19 09:04:10 +01002761 print(star, read_pipe(["git", "show", "-s",
2762 "--format=format:%h %s", c]))
2763 print("You will have to do 'git p4 sync' and rebase.")
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002764
Pete Wyckoff0d609032013-01-26 22:11:24 -05002765 if gitConfigBool("git-p4.exportLabels"):
Luke Diamand06dcd152012-05-11 07:25:18 +01002766 self.exportLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02002767
2768 if self.exportLabels:
2769 p4Labels = getP4Labels(self.depotPath)
2770 gitTags = getGitTags()
2771
2772 missingGitTags = gitTags - p4Labels
2773 self.exportGitTags(missingGitTags)
2774
Ondřej Bílka98e023d2013-07-29 10:18:21 +02002775 # exit with error unless everything applied perfectly
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002776 if len(commits) != len(applied):
Joel Holdsworth812ee742022-04-01 15:24:45 +01002777 sys.exit(1)
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002778
Simon Hausmannb9847332007-03-20 20:54:23 +01002779 return True
2780
Joel Holdsworthadf159b2022-04-01 15:24:43 +01002781
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002782class View(object):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002783 """Represent a p4 view ("p4 help views"), and map files in a repo according
2784 to the view.
2785 """
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002786
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002787 def __init__(self, client_name):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002788 self.mappings = []
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002789 self.client_prefix = "//%s/" % client_name
2790 # cache results of "p4 where" to lookup client file locations
2791 self.client_spec_path_cache = {}
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002792
2793 def append(self, view_line):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002794 """Parse a view line, splitting it into depot and client sides. Append
2795 to self.mappings, preserving order. This is only needed for tag
2796 creation.
2797 """
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002798
2799 # Split the view line into exactly two words. P4 enforces
2800 # structure on these lines that simplifies this quite a bit.
2801 #
2802 # Either or both words may be double-quoted.
2803 # Single quotes do not matter.
2804 # Double-quote marks cannot occur inside the words.
2805 # A + or - prefix is also inside the quotes.
2806 # There are no quotes unless they contain a space.
2807 # The line is already white-space stripped.
2808 # The two words are separated by a single space.
2809 #
2810 if view_line[0] == '"':
2811 # First word is double quoted. Find its end.
2812 close_quote_index = view_line.find('"', 1)
2813 if close_quote_index <= 0:
2814 die("No first-word closing quote found: %s" % view_line)
2815 depot_side = view_line[1:close_quote_index]
2816 # skip closing quote and space
2817 rhs_index = close_quote_index + 1 + 1
2818 else:
2819 space_index = view_line.find(" ")
2820 if space_index <= 0:
2821 die("No word-splitting space found: %s" % view_line)
2822 depot_side = view_line[0:space_index]
2823 rhs_index = space_index + 1
2824
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002825 # prefix + means overlay on previous mapping
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002826 if depot_side.startswith("+"):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002827 depot_side = depot_side[1:]
2828
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002829 # prefix - means exclude this path, leave out of mappings
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002830 exclude = False
2831 if depot_side.startswith("-"):
2832 exclude = True
2833 depot_side = depot_side[1:]
2834
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002835 if not exclude:
2836 self.mappings.append(depot_side)
2837
2838 def convert_client_path(self, clientFile):
2839 # chop off //client/ part to make it relative
Yang Zhaod38208a2019-12-13 15:52:40 -08002840 if not decode_path(clientFile).startswith(self.client_prefix):
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002841 die("No prefix '%s' on clientFile '%s'" %
2842 (self.client_prefix, clientFile))
2843 return clientFile[len(self.client_prefix):]
2844
2845 def update_client_spec_path_cache(self, files):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002846 """Caching file paths by "p4 where" batch query."""
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002847
2848 # List depot file paths exclude that already cached
Yang Zhaod38208a2019-12-13 15:52:40 -08002849 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 +09002850
2851 if len(fileArgs) == 0:
2852 return # All files in cache
2853
2854 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2855 for res in where_result:
2856 if "code" in res and res["code"] == "error":
2857 # assume error is "... file(s) not in client view"
2858 continue
2859 if "clientFile" not in res:
Pete Wyckoff20005442014-01-21 18:16:46 -05002860 die("No clientFile in 'p4 where' output")
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002861 if "unmap" in res:
2862 # it will list all of them, but only one not unmap-ped
2863 continue
Yang Zhaod38208a2019-12-13 15:52:40 -08002864 depot_path = decode_path(res['depotFile'])
Lars Schneidera0a50d82015-08-28 14:00:34 +02002865 if gitConfigBool("core.ignorecase"):
Yang Zhaod38208a2019-12-13 15:52:40 -08002866 depot_path = depot_path.lower()
2867 self.client_spec_path_cache[depot_path] = self.convert_client_path(res["clientFile"])
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002868
2869 # not found files or unmap files set to ""
2870 for depotFile in fileArgs:
Yang Zhaod38208a2019-12-13 15:52:40 -08002871 depotFile = decode_path(depotFile)
Lars Schneidera0a50d82015-08-28 14:00:34 +02002872 if gitConfigBool("core.ignorecase"):
2873 depotFile = depotFile.lower()
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002874 if depotFile not in self.client_spec_path_cache:
Yang Zhaod38208a2019-12-13 15:52:40 -08002875 self.client_spec_path_cache[depotFile] = b''
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002876
2877 def map_in_client(self, depot_path):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01002878 """Return the relative location in the client where this depot file
2879 should live.
2880
2881 Returns "" if the file should not be mapped in the client.
2882 """
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002883
Lars Schneidera0a50d82015-08-28 14:00:34 +02002884 if gitConfigBool("core.ignorecase"):
2885 depot_path = depot_path.lower()
2886
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002887 if depot_path in self.client_spec_path_cache:
2888 return self.client_spec_path_cache[depot_path]
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002889
Joel Holdsworth84af8b82022-04-01 15:24:50 +01002890 die("Error: %s is not found in client spec path" % depot_path)
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002891 return ""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002892
Joel Holdsworthadf159b2022-04-01 15:24:43 +01002893
Mazo, Andreyff8c50e2019-04-01 18:02:26 +00002894def cloneExcludeCallback(option, opt_str, value, parser):
2895 # prepend "/" because the first "/" was consumed as part of the option itself.
2896 # ("-//depot/A/..." becomes "/depot/A/..." after option parsing)
2897 parser.values.cloneExclude += ["/" + re.sub(r"\.\.\.$", "", value)]
2898
Joel Holdsworthadf159b2022-04-01 15:24:43 +01002899
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002900class P4Sync(Command, P4UserMap):
Pete Wyckoff56c09342011-02-19 08:17:57 -05002901
Simon Hausmannb9847332007-03-20 20:54:23 +01002902 def __init__(self):
2903 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002904 P4UserMap.__init__(self)
Simon Hausmannb9847332007-03-20 20:54:23 +01002905 self.options = [
2906 optparse.make_option("--branch", dest="branch"),
2907 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2908 optparse.make_option("--changesfile", dest="changesFile"),
2909 optparse.make_option("--silent", dest="silent", action="store_true"),
Simon Hausmannef48f902007-05-17 22:17:49 +02002910 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02002911 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Han-Wen Nienhuysd2c6dd32007-05-23 18:49:35 -03002912 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2913 help="Import into refs/heads/ , not refs/remotes"),
Lex Spoon96b2d542015-04-20 11:00:20 -04002914 optparse.make_option("--max-changes", dest="maxChanges",
2915 help="Maximum number of changes to import"),
2916 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2917 help="Internal block size to use when iteratively calling p4 changes"),
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03002918 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002919 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2920 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
Luke Diamand51334bb2015-01-17 20:56:38 +00002921 help="Only sync files that are included in the Perforce Client Spec"),
2922 optparse.make_option("-/", dest="cloneExclude",
Mazo, Andreyff8c50e2019-04-01 18:02:26 +00002923 action="callback", callback=cloneExcludeCallback, type="string",
Luke Diamand51334bb2015-01-17 20:56:38 +00002924 help="exclude depot path"),
Simon Hausmannb9847332007-03-20 20:54:23 +01002925 ]
2926 self.description = """Imports from Perforce into a git repository.\n
2927 example:
2928 //depot/my/project/ -- to import the current head
2929 //depot/my/project/@all -- to import everything
2930 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2931
2932 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2933
2934 self.usage += " //depot/path[@revRange]"
Simon Hausmannb9847332007-03-20 20:54:23 +01002935 self.silent = False
Reilly Grant1d7367d2009-09-10 00:02:38 -07002936 self.createdBranches = set()
2937 self.committedChanges = set()
Simon Hausmann569d1bd2007-03-22 21:34:16 +01002938 self.branch = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01002939 self.detectBranches = False
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02002940 self.detectLabels = False
Luke Diamand06804c72012-04-11 17:21:24 +02002941 self.importLabels = False
Simon Hausmannb9847332007-03-20 20:54:23 +01002942 self.changesFile = ""
Simon Hausmann01265102007-05-25 10:36:10 +02002943 self.syncWithOrigin = True
Simon Hausmanna028a982007-05-23 00:03:08 +02002944 self.importIntoRemotes = True
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02002945 self.maxChanges = ""
Luke Diamand1051ef02015-06-10 08:30:59 +01002946 self.changes_block_size = None
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -03002947 self.keepRepoPath = False
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002948 self.depotPaths = None
Simon Hausmann3c699642007-06-16 13:09:21 +02002949 self.p4BranchesInGit = []
Tommy Thorn354081d2008-02-03 10:38:51 -08002950 self.cloneExclude = []
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002951 self.useClientSpec = False
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05002952 self.useClientSpec_from_options = False
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002953 self.clientSpecDirs = None
Vitor Antunesfed23692012-01-25 23:48:22 +00002954 self.tempBranches = []
Lars Schneiderd6041762016-06-29 09:35:27 +02002955 self.tempBranchLocation = "refs/git-p4-tmp"
Lars Schneidera5db4b12015-09-26 09:55:03 +02002956 self.largeFileSystem = None
Luke Diamand123f6312018-05-23 23:20:26 +01002957 self.suppress_meta_comment = False
Lars Schneidera5db4b12015-09-26 09:55:03 +02002958
2959 if gitConfig('git-p4.largeFileSystem'):
2960 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2961 self.largeFileSystem = largeFileSystemConstructor(
2962 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2963 )
Simon Hausmannb9847332007-03-20 20:54:23 +01002964
Simon Hausmann01265102007-05-25 10:36:10 +02002965 if gitConfig("git-p4.syncFromOrigin") == "false":
2966 self.syncWithOrigin = False
2967
Luke Diamand123f6312018-05-23 23:20:26 +01002968 self.depotPaths = []
2969 self.changeRange = ""
2970 self.previousDepotPaths = []
2971 self.hasOrigin = False
2972
2973 # map from branch depot path to parent branch
2974 self.knownBranches = {}
2975 self.initialParents = {}
2976
2977 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2978 self.labels = {}
2979
Vitor Antunesfed23692012-01-25 23:48:22 +00002980 def checkpoint(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01002981 """Force a checkpoint in fast-import and wait for it to finish."""
Vitor Antunesfed23692012-01-25 23:48:22 +00002982 self.gitStream.write("checkpoint\n\n")
2983 self.gitStream.write("progress checkpoint\n\n")
Yang Zhao4294d742019-12-13 15:52:43 -08002984 self.gitStream.flush()
Vitor Antunesfed23692012-01-25 23:48:22 +00002985 out = self.gitOutput.readline()
2986 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01002987 print("checkpoint finished: " + out)
Vitor Antunesfed23692012-01-25 23:48:22 +00002988
Mazo, Andreya2bee102019-04-01 18:02:32 +00002989 def isPathWanted(self, path):
2990 for p in self.cloneExclude:
2991 if p.endswith("/"):
2992 if p4PathStartsWith(path, p):
2993 return False
2994 # "-//depot/file1" without a trailing "/" should only exclude "file1", but not "file111" or "file1_dir/file2"
2995 elif path.lower() == p.lower():
2996 return False
2997 for p in self.depotPaths:
Yang Zhaod38208a2019-12-13 15:52:40 -08002998 if p4PathStartsWith(path, decode_path(p)):
Mazo, Andreya2bee102019-04-01 18:02:32 +00002999 return True
3000 return False
3001
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01003002 def extractFilesFromCommit(self, commit, shelved=False, shelved_cl=0):
Simon Hausmannb9847332007-03-20 20:54:23 +01003003 files = []
3004 fnum = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003005 while "depotFile%s" % fnum in commit:
Joel Holdsworth6febb9f2022-04-01 15:24:58 +01003006 path = commit["depotFile%s" % fnum]
Yang Zhaod38208a2019-12-13 15:52:40 -08003007 found = self.isPathWanted(decode_path(path))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003008 if not found:
Simon Hausmannb9847332007-03-20 20:54:23 +01003009 fnum = fnum + 1
3010 continue
3011
3012 file = {}
3013 file["path"] = path
3014 file["rev"] = commit["rev%s" % fnum]
3015 file["action"] = commit["action%s" % fnum]
3016 file["type"] = commit["type%s" % fnum]
Luke Diamand123f6312018-05-23 23:20:26 +01003017 if shelved:
3018 file["shelved_cl"] = int(shelved_cl)
Simon Hausmannb9847332007-03-20 20:54:23 +01003019 files.append(file)
3020 fnum = fnum + 1
3021 return files
3022
Jan Durovec26e6a272016-04-19 19:49:41 +00003023 def extractJobsFromCommit(self, commit):
3024 jobs = []
3025 jnum = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003026 while "job%s" % jnum in commit:
Jan Durovec26e6a272016-04-19 19:49:41 +00003027 job = commit["job%s" % jnum]
3028 jobs.append(job)
3029 jnum = jnum + 1
3030 return jobs
3031
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003032 def stripRepoPath(self, path, prefixes):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01003033 """When streaming files, this is called to map a p4 depot path to where
3034 it should go in git. The prefixes are either self.depotPaths, or
3035 self.branchPrefixes in the case of branch detection.
3036 """
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003037
Ian Wienand39527102011-02-11 16:33:48 -08003038 if self.useClientSpec:
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003039 # branch detection moves files up a level (the branch name)
3040 # from what client spec interpretation gives
Yang Zhaod38208a2019-12-13 15:52:40 -08003041 path = decode_path(self.clientSpecDirs.map_in_client(path))
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003042 if self.detectBranches:
3043 for b in self.knownBranches:
Mazo, Andreyf2768cb2019-04-01 18:02:24 +00003044 if p4PathStartsWith(path, b + "/"):
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003045 path = path[len(b)+1:]
3046
3047 elif self.keepRepoPath:
3048 # Preserve everything in relative path name except leading
3049 # //depot/; just look at first prefix as they all should
3050 # be in the same depot.
3051 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
3052 if p4PathStartsWith(path, depot):
3053 path = path[len(depot):]
Ian Wienand39527102011-02-11 16:33:48 -08003054
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04003055 else:
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04003056 for p in prefixes:
3057 if p4PathStartsWith(path, p):
3058 path = path[len(p):]
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003059 break
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003060
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04003061 path = wildcard_decode(path)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003062 return path
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -03003063
Simon Hausmann71b112d2007-05-19 11:54:11 +02003064 def splitFilesIntoBranches(self, commit):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01003065 """Look at each depotFile in the commit to figure out to what branch it
3066 belongs.
3067 """
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003068
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09003069 if self.clientSpecDirs:
3070 files = self.extractFilesFromCommit(commit)
3071 self.clientSpecDirs.update_client_spec_path_cache(files)
3072
Simon Hausmannd5904672007-05-19 11:07:32 +02003073 branches = {}
Simon Hausmann71b112d2007-05-19 11:54:11 +02003074 fnum = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003075 while "depotFile%s" % fnum in commit:
Yang Zhaod38208a2019-12-13 15:52:40 -08003076 raw_path = commit["depotFile%s" % fnum]
3077 path = decode_path(raw_path)
Mazo, Andreyd15068a2019-04-01 18:02:38 +00003078 found = self.isPathWanted(path)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003079 if not found:
Simon Hausmann71b112d2007-05-19 11:54:11 +02003080 fnum = fnum + 1
3081 continue
3082
3083 file = {}
Yang Zhaod38208a2019-12-13 15:52:40 -08003084 file["path"] = raw_path
Simon Hausmann71b112d2007-05-19 11:54:11 +02003085 file["rev"] = commit["rev%s" % fnum]
3086 file["action"] = commit["action%s" % fnum]
3087 file["type"] = commit["type%s" % fnum]
3088 fnum = fnum + 1
3089
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003090 # start with the full relative path where this file would
3091 # go in a p4 client
3092 if self.useClientSpec:
Yang Zhaod38208a2019-12-13 15:52:40 -08003093 relPath = decode_path(self.clientSpecDirs.map_in_client(path))
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003094 else:
3095 relPath = self.stripRepoPath(path, self.depotPaths)
Simon Hausmannb9847332007-03-20 20:54:23 +01003096
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003097 for branch in self.knownBranches.keys():
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04003098 # add a trailing slash so that a commit into qt/4.2foo
3099 # doesn't end up in qt/4.2, e.g.
Mazo, Andreyf2768cb2019-04-01 18:02:24 +00003100 if p4PathStartsWith(relPath, branch + "/"):
Simon Hausmannd5904672007-05-19 11:07:32 +02003101 if branch not in branches:
3102 branches[branch] = []
Simon Hausmann71b112d2007-05-19 11:54:11 +02003103 branches[branch].append(file)
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003104 break
Simon Hausmannb9847332007-03-20 20:54:23 +01003105
3106 return branches
3107
Lars Schneidera5db4b12015-09-26 09:55:03 +02003108 def writeToGitStream(self, gitMode, relPath, contents):
Yang Zhao6cec21a2019-12-13 15:52:38 -08003109 self.gitStream.write(encode_text_stream(u'M {} inline {}\n'.format(gitMode, relPath)))
Lars Schneidera5db4b12015-09-26 09:55:03 +02003110 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
3111 for d in contents:
3112 self.gitStream.write(d)
3113 self.gitStream.write('\n')
3114
Lars Schneidera8b05162017-02-09 16:06:56 +01003115 def encodeWithUTF8(self, path):
3116 try:
3117 path.decode('ascii')
3118 except:
3119 encoding = 'utf8'
3120 if gitConfig('git-p4.pathEncoding'):
3121 encoding = gitConfig('git-p4.pathEncoding')
3122 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
3123 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003124 print('Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path))
Lars Schneidera8b05162017-02-09 16:06:56 +01003125 return path
3126
Luke Diamandb9327052009-07-30 00:13:46 +01003127 def streamOneP4File(self, file, contents):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003128 """Output one file from the P4 stream.
3129
3130 This is a helper for streamP4Files().
3131 """
3132
Yang Zhaod38208a2019-12-13 15:52:40 -08003133 file_path = file['depotFile']
3134 relPath = self.stripRepoPath(decode_path(file_path), self.branchPrefixes)
3135
Luke Diamandb9327052009-07-30 00:13:46 +01003136 if verbose:
Luke Diamand0742b7c2018-10-12 06:28:31 +01003137 if 'fileSize' in self.stream_file:
3138 size = int(self.stream_file['fileSize'])
3139 else:
Joel Holdsworth4768af22022-04-01 15:25:02 +01003140 # Deleted files don't get a fileSize apparently
3141 size = 0
Joel Holdsworthae9b9502021-12-19 15:40:27 +00003142 sys.stdout.write('\r%s --> %s (%s)\n' % (
3143 file_path, relPath, format_size_human_readable(size)))
Lars Schneiderd2176a52015-09-26 09:55:01 +02003144 sys.stdout.flush()
Luke Diamandb9327052009-07-30 00:13:46 +01003145
Joel Holdsworth0874bb02022-04-01 15:24:52 +01003146 type_base, type_mods = split_p4_type(file["type"])
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04003147
3148 git_mode = "100644"
3149 if "x" in type_mods:
3150 git_mode = "100755"
3151 if type_base == "symlink":
3152 git_mode = "120000"
Alexandru Juncu1292df12013-08-08 16:17:38 +03003153 # p4 print on a symlink sometimes contains "target\n";
3154 # if it does, remove the newline
Yang Zhao6cec21a2019-12-13 15:52:38 -08003155 data = ''.join(decode_text_stream(c) for c in contents)
Pete Wyckoff40f846c2014-01-21 18:16:40 -05003156 if not data:
3157 # Some version of p4 allowed creating a symlink that pointed
3158 # to nothing. This causes p4 errors when checking out such
3159 # a change, and errors here too. Work around it by ignoring
3160 # the bad symlink; hopefully a future change fixes it.
Yang Zhaod38208a2019-12-13 15:52:40 -08003161 print("\nIgnoring empty symlink in %s" % file_path)
Pete Wyckoff40f846c2014-01-21 18:16:40 -05003162 return
3163 elif data[-1] == '\n':
Alexandru Juncu1292df12013-08-08 16:17:38 +03003164 contents = [data[:-1]]
3165 else:
3166 contents = [data]
Luke Diamandb9327052009-07-30 00:13:46 +01003167
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04003168 if type_base == "utf16":
Pete Wyckoff55aa5712011-09-17 19:16:14 -04003169 # p4 delivers different text in the python output to -G
3170 # than it does when using "print -o", or normal p4 client
3171 # operations. utf16 is converted to ascii or utf8, perhaps.
3172 # But ascii text saved as -t utf16 is completely mangled.
3173 # Invoke print -o to get the real contents.
Pete Wyckoff7f0e5962013-01-26 22:11:13 -05003174 #
3175 # On windows, the newlines will always be mangled by print, so put
3176 # them back too. This is not needed to the cygwin windows version,
3177 # just the native "NT" type.
3178 #
Lars Schneider1f5f3902015-09-21 12:01:41 +02003179 try:
Yang Zhaod38208a2019-12-13 15:52:40 -08003180 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (decode_path(file['depotFile']), file['change'])], raw=True)
Lars Schneider1f5f3902015-09-21 12:01:41 +02003181 except Exception as e:
3182 if 'Translation of file content failed' in str(e):
3183 type_base = 'binary'
3184 else:
3185 raise e
3186 else:
3187 if p4_version_string().find('/NT') >= 0:
Moritz Baumann4d35f742022-07-20 18:17:45 +00003188 text = text.replace(b'\x0d\x00\x0a\x00', b'\x0a\x00')
Joel Holdsworth84af8b82022-04-01 15:24:50 +01003189 contents = [text]
Pete Wyckoff55aa5712011-09-17 19:16:14 -04003190
Pete Wyckoff9f7ef0e2011-11-05 13:36:07 -04003191 if type_base == "apple":
3192 # Apple filetype files will be streamed as a concatenation of
3193 # its appledouble header and the contents. This is useless
3194 # on both macs and non-macs. If using "print -q -o xx", it
3195 # will create "xx" with the data, and "%xx" with the header.
3196 # This is also not very useful.
3197 #
3198 # Ideally, someday, this script can learn how to generate
3199 # appledouble files directly and import those to git, but
3200 # non-mac machines can never find a use for apple filetype.
Luke Diamandf2606b12018-06-19 09:04:10 +01003201 print("\nIgnoring apple filetype file %s" % file['depotFile'])
Pete Wyckoff9f7ef0e2011-11-05 13:36:07 -04003202 return
3203
Tao Klerksfbe5f6b2022-04-04 05:50:36 +00003204 if type_base == "utf8":
3205 # The type utf8 explicitly means utf8 *with BOM*. These are
3206 # streamed just like regular text files, however, without
3207 # the BOM in the stream.
3208 # Therefore, to accurately import these files into git, we
3209 # need to explicitly re-add the BOM before writing.
3210 # 'contents' is a set of bytes in this case, so create the
3211 # BOM prefix as a b'' literal.
3212 contents = [b'\xef\xbb\xbf' + contents[0]] + contents[1:]
3213
Pete Wyckoff55aa5712011-09-17 19:16:14 -04003214 # Note that we do not try to de-mangle keywords on utf16 files,
3215 # even though in theory somebody may want that.
Joel Holdsworthe665e982021-12-16 13:46:16 +00003216 regexp = p4_keywords_regexp_for_type(type_base, type_mods)
3217 if regexp:
Joel Holdsworth70c0d552021-12-16 13:46:19 +00003218 contents = [regexp.sub(br'$\1$', c) for c in contents]
Luke Diamandb9327052009-07-30 00:13:46 +01003219
Lars Schneidera5db4b12015-09-26 09:55:03 +02003220 if self.largeFileSystem:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01003221 git_mode, contents = self.largeFileSystem.processContent(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01003222
Lars Schneidera5db4b12015-09-26 09:55:03 +02003223 self.writeToGitStream(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01003224
3225 def streamOneP4Deletion(self, file):
Yang Zhaod38208a2019-12-13 15:52:40 -08003226 relPath = self.stripRepoPath(decode_path(file['path']), self.branchPrefixes)
Luke Diamandb9327052009-07-30 00:13:46 +01003227 if verbose:
Lars Schneiderd2176a52015-09-26 09:55:01 +02003228 sys.stdout.write("delete %s\n" % relPath)
3229 sys.stdout.flush()
Yang Zhao6cec21a2019-12-13 15:52:38 -08003230 self.gitStream.write(encode_text_stream(u'D {}\n'.format(relPath)))
Luke Diamandb9327052009-07-30 00:13:46 +01003231
Lars Schneidera5db4b12015-09-26 09:55:03 +02003232 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
3233 self.largeFileSystem.removeLargeFile(relPath)
3234
Luke Diamandb9327052009-07-30 00:13:46 +01003235 def streamP4FilesCb(self, marshalled):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003236 """Handle another chunk of streaming data."""
Luke Diamandb9327052009-07-30 00:13:46 +01003237
Pete Wyckoff78189be2012-11-23 17:35:36 -05003238 # catch p4 errors and complain
3239 err = None
3240 if "code" in marshalled:
3241 if marshalled["code"] == "error":
3242 if "data" in marshalled:
3243 err = marshalled["data"].rstrip()
Lars Schneider4d25dc42015-09-26 09:55:02 +02003244
3245 if not err and 'fileSize' in self.stream_file:
3246 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
3247 if required_bytes > 0:
Joel Holdsworthae9b9502021-12-19 15:40:27 +00003248 err = 'Not enough space left on %s! Free at least %s.' % (
3249 os.getcwd(), format_size_human_readable(required_bytes))
Lars Schneider4d25dc42015-09-26 09:55:02 +02003250
Pete Wyckoff78189be2012-11-23 17:35:36 -05003251 if err:
3252 f = None
3253 if self.stream_have_file_info:
3254 if "depotFile" in self.stream_file:
3255 f = self.stream_file["depotFile"]
3256 # force a failure in fast-import, else an empty
3257 # commit will be made
3258 self.gitStream.write("\n")
3259 self.gitStream.write("die-now\n")
3260 self.gitStream.close()
3261 # ignore errors, but make sure it exits first
3262 self.importProcess.wait()
3263 if f:
3264 die("Error from p4 print for %s: %s" % (f, err))
3265 else:
3266 die("Error from p4 print: %s" % err)
3267
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003268 if 'depotFile' in marshalled and self.stream_have_file_info:
Andrew Garberc3f61632011-04-07 02:01:21 -04003269 # start of a new file - output the old one first
3270 self.streamOneP4File(self.stream_file, self.stream_contents)
3271 self.stream_file = {}
3272 self.stream_contents = []
3273 self.stream_have_file_info = False
Luke Diamandb9327052009-07-30 00:13:46 +01003274
Andrew Garberc3f61632011-04-07 02:01:21 -04003275 # pick up the new file information... for the
3276 # 'data' field we need to append to our array
3277 for k in marshalled.keys():
3278 if k == 'data':
Lars Schneiderd2176a52015-09-26 09:55:01 +02003279 if 'streamContentSize' not in self.stream_file:
3280 self.stream_file['streamContentSize'] = 0
3281 self.stream_file['streamContentSize'] += len(marshalled['data'])
Andrew Garberc3f61632011-04-07 02:01:21 -04003282 self.stream_contents.append(marshalled['data'])
3283 else:
3284 self.stream_file[k] = marshalled[k]
Luke Diamandb9327052009-07-30 00:13:46 +01003285
Lars Schneiderd2176a52015-09-26 09:55:01 +02003286 if (verbose and
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01003287 'streamContentSize' in self.stream_file and
3288 'fileSize' in self.stream_file and
3289 'depotFile' in self.stream_file):
Lars Schneiderd2176a52015-09-26 09:55:01 +02003290 size = int(self.stream_file["fileSize"])
3291 if size > 0:
3292 progress = 100*self.stream_file['streamContentSize']/size
Joel Holdsworthae9b9502021-12-19 15:40:27 +00003293 sys.stdout.write('\r%s %d%% (%s)' % (
3294 self.stream_file['depotFile'], progress,
3295 format_size_human_readable(size)))
Lars Schneiderd2176a52015-09-26 09:55:01 +02003296 sys.stdout.flush()
3297
Andrew Garberc3f61632011-04-07 02:01:21 -04003298 self.stream_have_file_info = True
Luke Diamandb9327052009-07-30 00:13:46 +01003299
Luke Diamandb9327052009-07-30 00:13:46 +01003300 def streamP4Files(self, files):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003301 """Stream directly from "p4 files" into "git fast-import."""
3302
Simon Hausmann30b59402008-03-03 11:55:48 +01003303 filesForCommit = []
3304 filesToRead = []
Luke Diamandb9327052009-07-30 00:13:46 +01003305 filesToDelete = []
Simon Hausmann30b59402008-03-03 11:55:48 +01003306
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01003307 for f in files:
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05003308 filesForCommit.append(f)
3309 if f['action'] in self.delete_actions:
3310 filesToDelete.append(f)
3311 else:
3312 filesToRead.append(f)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003313
Luke Diamandb9327052009-07-30 00:13:46 +01003314 # deleted files...
3315 for f in filesToDelete:
3316 self.streamOneP4Deletion(f)
3317
Simon Hausmann30b59402008-03-03 11:55:48 +01003318 if len(filesToRead) > 0:
Luke Diamandb9327052009-07-30 00:13:46 +01003319 self.stream_file = {}
3320 self.stream_contents = []
3321 self.stream_have_file_info = False
3322
Andrew Garberc3f61632011-04-07 02:01:21 -04003323 # curry self argument
3324 def streamP4FilesCbSelf(entry):
3325 self.streamP4FilesCb(entry)
Luke Diamandb9327052009-07-30 00:13:46 +01003326
Luke Diamand123f6312018-05-23 23:20:26 +01003327 fileArgs = []
3328 for f in filesToRead:
3329 if 'shelved_cl' in f:
3330 # Handle shelved CLs using the "p4 print file@=N" syntax to print
3331 # the contents
Yang Zhao6cec21a2019-12-13 15:52:38 -08003332 fileArg = f['path'] + encode_text_stream('@={}'.format(f['shelved_cl']))
Luke Diamand123f6312018-05-23 23:20:26 +01003333 else:
Yang Zhao6cec21a2019-12-13 15:52:38 -08003334 fileArg = f['path'] + encode_text_stream('#{}'.format(f['rev']))
Luke Diamand123f6312018-05-23 23:20:26 +01003335
3336 fileArgs.append(fileArg)
Luke Diamand6de040d2011-10-16 10:47:52 -04003337
3338 p4CmdList(["-x", "-", "print"],
3339 stdin=fileArgs,
3340 cb=streamP4FilesCbSelf)
Han-Wen Nienhuysf2eda792007-05-23 18:49:35 -03003341
Luke Diamandb9327052009-07-30 00:13:46 +01003342 # do the last chunk
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003343 if 'depotFile' in self.stream_file:
Luke Diamandb9327052009-07-30 00:13:46 +01003344 self.streamOneP4File(self.stream_file, self.stream_contents)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003345
Luke Diamandaffb4742012-01-19 09:52:27 +00003346 def make_email(self, userid):
3347 if userid in self.users:
3348 return self.users[userid]
3349 else:
Tao Klerksf7b5ff62022-04-30 19:26:52 +00003350 userid_bytes = metadata_stream_to_writable_bytes(userid)
3351 return b"%s <a@b>" % userid_bytes
Luke Diamandaffb4742012-01-19 09:52:27 +00003352
Luke Diamand06804c72012-04-11 17:21:24 +02003353 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01003354 """Stream a p4 tag.
3355
3356 Commit is either a git commit, or a fast-import mark, ":<p4commit>".
3357 """
Luke Diamandb43702a2015-08-27 08:18:58 +01003358
Luke Diamand06804c72012-04-11 17:21:24 +02003359 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003360 print("writing tag %s for commit %s" % (labelName, commit))
Luke Diamand06804c72012-04-11 17:21:24 +02003361 gitStream.write("tag %s\n" % labelName)
3362 gitStream.write("from %s\n" % commit)
3363
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003364 if 'Owner' in labelDetails:
Luke Diamand06804c72012-04-11 17:21:24 +02003365 owner = labelDetails["Owner"]
3366 else:
3367 owner = None
3368
3369 # Try to use the owner of the p4 label, or failing that,
3370 # the current p4 user id.
3371 if owner:
3372 email = self.make_email(owner)
3373 else:
3374 email = self.make_email(self.p4UserId())
Luke Diamand06804c72012-04-11 17:21:24 +02003375
Tao Klerksf7b5ff62022-04-30 19:26:52 +00003376 gitStream.write("tagger ")
3377 gitStream.write(email)
3378 gitStream.write(" %s %s\n" % (epoch, self.tz))
Luke Diamand06804c72012-04-11 17:21:24 +02003379
Joel Holdsworth12a77f52022-04-01 15:24:53 +01003380 print("labelDetails=", labelDetails)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003381 if 'Description' in labelDetails:
Luke Diamand06804c72012-04-11 17:21:24 +02003382 description = labelDetails['Description']
3383 else:
3384 description = 'Label from git p4'
3385
3386 gitStream.write("data %d\n" % len(description))
3387 gitStream.write(description)
3388 gitStream.write("\n")
3389
Lars Schneider4ae048e2015-12-08 10:36:22 +01003390 def inClientSpec(self, path):
3391 if not self.clientSpecDirs:
3392 return True
3393 inClientSpec = self.clientSpecDirs.map_in_client(path)
3394 if not inClientSpec and self.verbose:
3395 print('Ignoring file outside of client spec: {0}'.format(path))
3396 return inClientSpec
3397
3398 def hasBranchPrefix(self, path):
3399 if not self.branchPrefixes:
3400 return True
3401 hasPrefix = [p for p in self.branchPrefixes
3402 if p4PathStartsWith(path, p)]
Andrew Oakley09667d02016-06-22 10:26:11 +01003403 if not hasPrefix and self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01003404 print('Ignoring file outside of prefix: {0}'.format(path))
3405 return hasPrefix
3406
Andrew Oakley82e46d62020-05-10 11:16:50 +01003407 def findShadowedFiles(self, files, change):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003408 """Perforce allows you commit files and directories with the same name,
3409 so you could have files //depot/foo and //depot/foo/bar both checked
3410 in. A p4 sync of a repository in this state fails. Deleting one of
3411 the files recovers the repository.
3412
3413 Git will not allow the broken state to exist and only the most
3414 recent of the conflicting names is left in the repository. When one
3415 of the conflicting files is deleted we need to re-add the other one
3416 to make sure the git repository recovers in the same way as
3417 perforce.
3418 """
3419
Andrew Oakley82e46d62020-05-10 11:16:50 +01003420 deleted = [f for f in files if f['action'] in self.delete_actions]
3421 to_check = set()
3422 for f in deleted:
3423 path = decode_path(f['path'])
3424 to_check.add(path + '/...')
3425 while True:
3426 path = path.rsplit("/", 1)[0]
3427 if path == "/" or path in to_check:
3428 break
3429 to_check.add(path)
3430 to_check = ['%s@%s' % (wildcard_encode(p), change) for p in to_check
3431 if self.hasBranchPrefix(p)]
3432 if to_check:
3433 stat_result = p4CmdList(["-x", "-", "fstat", "-T",
3434 "depotFile,headAction,headRev,headType"], stdin=to_check)
3435 for record in stat_result:
3436 if record['code'] != 'stat':
3437 continue
3438 if record['headAction'] in self.delete_actions:
3439 continue
3440 files.append({
3441 'action': 'add',
3442 'path': record['depotFile'],
3443 'rev': record['headRev'],
3444 'type': record['headType']})
3445
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01003446 def commit(self, details, files, branch, parent="", allow_empty=False):
Simon Hausmannb9847332007-03-20 20:54:23 +01003447 epoch = details["time"]
3448 author = details["user"]
Jan Durovec26e6a272016-04-19 19:49:41 +00003449 jobs = self.extractJobsFromCommit(details)
Simon Hausmannb9847332007-03-20 20:54:23 +01003450
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003451 if self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01003452 print('commit into {0}'.format(branch))
Han-Wen Nienhuys96e07dd2007-05-23 18:49:35 -03003453
Andrew Oakley82e46d62020-05-10 11:16:50 +01003454 files = [f for f in files
3455 if self.hasBranchPrefix(decode_path(f['path']))]
3456 self.findShadowedFiles(files, details['change'])
3457
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09003458 if self.clientSpecDirs:
3459 self.clientSpecDirs.update_client_spec_path_cache(files)
3460
Andrew Oakley82e46d62020-05-10 11:16:50 +01003461 files = [f for f in files if self.inClientSpec(decode_path(f['path']))]
Lars Schneider4ae048e2015-12-08 10:36:22 +01003462
Luke Diamand89143ac2018-10-15 12:14:08 +01003463 if gitConfigBool('git-p4.keepEmptyCommits'):
3464 allow_empty = True
3465
3466 if not files and not allow_empty:
Lars Schneider4ae048e2015-12-08 10:36:22 +01003467 print('Ignoring revision {0} as it would produce an empty commit.'
3468 .format(details['change']))
3469 return
3470
Simon Hausmannb9847332007-03-20 20:54:23 +01003471 self.gitStream.write("commit %s\n" % branch)
Luke Diamandb43702a2015-08-27 08:18:58 +01003472 self.gitStream.write("mark :%s\n" % details["change"])
Simon Hausmannb9847332007-03-20 20:54:23 +01003473 self.committedChanges.add(int(details["change"]))
Simon Hausmannb607e712007-05-20 10:55:54 +02003474 if author not in self.users:
3475 self.getUserMapFromPerforceServer()
Simon Hausmannb9847332007-03-20 20:54:23 +01003476
Tao Klerksf7b5ff62022-04-30 19:26:52 +00003477 self.gitStream.write("committer ")
3478 self.gitStream.write(self.make_email(author))
3479 self.gitStream.write(" %s %s\n" % (epoch, self.tz))
Simon Hausmannb9847332007-03-20 20:54:23 +01003480
3481 self.gitStream.write("data <<EOT\n")
3482 self.gitStream.write(details["desc"])
Jan Durovec26e6a272016-04-19 19:49:41 +00003483 if len(jobs) > 0:
3484 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
Luke Diamand123f6312018-05-23 23:20:26 +01003485
3486 if not self.suppress_meta_comment:
3487 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3488 (','.join(self.branchPrefixes), details["change"]))
3489 if len(details['options']) > 0:
3490 self.gitStream.write(": options = %s" % details['options'])
3491 self.gitStream.write("]\n")
3492
3493 self.gitStream.write("EOT\n\n")
Simon Hausmannb9847332007-03-20 20:54:23 +01003494
3495 if len(parent) > 0:
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003496 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003497 print("parent %s" % parent)
Simon Hausmannb9847332007-03-20 20:54:23 +01003498 self.gitStream.write("from %s\n" % parent)
3499
Lars Schneider4ae048e2015-12-08 10:36:22 +01003500 self.streamP4Files(files)
Simon Hausmannb9847332007-03-20 20:54:23 +01003501 self.gitStream.write("\n")
3502
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003503 change = int(details["change"])
3504
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003505 if change in self.labels:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003506 label = self.labels[change]
3507 labelDetails = label[0]
3508 labelRevisions = label[1]
Simon Hausmann71b112d2007-05-19 11:54:11 +02003509 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003510 print("Change %s is labelled %s" % (change, labelDetails))
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003511
Luke Diamand6de040d2011-10-16 10:47:52 -04003512 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003513 for p in self.branchPrefixes])
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003514
3515 if len(files) == len(labelRevisions):
3516
3517 cleanedFiles = {}
3518 for info in files:
Pete Wyckoff56c09342011-02-19 08:17:57 -05003519 if info["action"] in self.delete_actions:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003520 continue
3521 cleanedFiles[info["depotFile"]] = info["rev"]
3522
3523 if cleanedFiles == labelRevisions:
Luke Diamand06804c72012-04-11 17:21:24 +02003524 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003525
3526 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02003527 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003528 print("Tag %s does not match with change %s: files do not match."
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003529 % (labelDetails["label"], change))
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003530
3531 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02003532 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003533 print("Tag %s does not match with change %s: file count is different."
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003534 % (labelDetails["label"], change))
Simon Hausmannb9847332007-03-20 20:54:23 +01003535
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003536 def getLabels(self):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003537 """Build a dictionary of changelists and labels, for "detect-labels"
3538 option.
3539 """
3540
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003541 self.labels = {}
3542
Luke Diamand52a48802012-01-19 09:52:25 +00003543 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
Simon Hausmann10c32112007-04-08 10:15:47 +02003544 if len(l) > 0 and not self.silent:
Luke Diamand4d885192018-06-19 09:04:08 +01003545 print("Finding files belonging to labels in %s" % self.depotPaths)
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003546
3547 for output in l:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003548 label = output["label"]
3549 revisions = {}
3550 newestChange = 0
Simon Hausmann71b112d2007-05-19 11:54:11 +02003551 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003552 print("Querying files for label %s" % label)
Luke Diamand6de040d2011-10-16 10:47:52 -04003553 for file in p4CmdList(["files"] +
3554 ["%s...@%s" % (p, label)
3555 for p in self.depotPaths]):
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003556 revisions[file["depotFile"]] = file["rev"]
3557 change = int(file["change"])
3558 if change > newestChange:
3559 newestChange = change
3560
Simon Hausmann9bda3a82007-05-19 12:05:40 +02003561 self.labels[newestChange] = [output, revisions]
3562
3563 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003564 print("Label changes: %s" % self.labels.keys())
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003565
Luke Diamand06804c72012-04-11 17:21:24 +02003566 def importP4Labels(self, stream, p4Labels):
Joel Holdsworth522e9142022-04-01 15:24:47 +01003567 """Import p4 labels as git tags. A direct mapping does not exist, so
3568 assume that if all the files are at the same revision then we can
3569 use that, or it's something more complicated we should just ignore.
3570 """
3571
Luke Diamand06804c72012-04-11 17:21:24 +02003572 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003573 print("import p4 labels: " + ' '.join(p4Labels))
Luke Diamand06804c72012-04-11 17:21:24 +02003574
3575 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
Luke Diamandc8942a22012-04-11 17:21:24 +02003576 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
Luke Diamand06804c72012-04-11 17:21:24 +02003577 if len(validLabelRegexp) == 0:
3578 validLabelRegexp = defaultLabelRegexp
3579 m = re.compile(validLabelRegexp)
3580
3581 for name in p4Labels:
3582 commitFound = False
3583
3584 if not m.match(name):
3585 if verbose:
Joel Holdsworth12a77f52022-04-01 15:24:53 +01003586 print("label %s does not match regexp %s" % (name, validLabelRegexp))
Luke Diamand06804c72012-04-11 17:21:24 +02003587 continue
3588
3589 if name in ignoredP4Labels:
3590 continue
3591
3592 labelDetails = p4CmdList(['label', "-o", name])[0]
3593
3594 # get the most recent changelist for each file in this label
3595 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3596 for p in self.depotPaths])
3597
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003598 if 'change' in change:
Luke Diamand06804c72012-04-11 17:21:24 +02003599 # find the corresponding git commit; take the oldest commit
3600 changelist = int(change['change'])
Luke Diamandb43702a2015-08-27 08:18:58 +01003601 if changelist in self.committedChanges:
3602 gitCommit = ":%d" % changelist # use a fast-import mark
Luke Diamand06804c72012-04-11 17:21:24 +02003603 commitFound = True
Luke Diamandb43702a2015-08-27 08:18:58 +01003604 else:
3605 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3606 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3607 if len(gitCommit) == 0:
Luke Diamandf2606b12018-06-19 09:04:10 +01003608 print("importing label %s: could not find git commit for changelist %d" % (name, changelist))
Luke Diamandb43702a2015-08-27 08:18:58 +01003609 else:
3610 commitFound = True
3611 gitCommit = gitCommit.strip()
3612
3613 if commitFound:
Luke Diamand06804c72012-04-11 17:21:24 +02003614 # Convert from p4 time format
3615 try:
3616 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3617 except ValueError:
Luke Diamandf2606b12018-06-19 09:04:10 +01003618 print("Could not convert label time %s" % labelDetails['Update'])
Luke Diamand06804c72012-04-11 17:21:24 +02003619 tmwhen = 1
3620
3621 when = int(time.mktime(tmwhen))
3622 self.streamTag(stream, name, labelDetails, gitCommit, when)
3623 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003624 print("p4 label %s mapped to git commit %s" % (name, gitCommit))
Luke Diamand06804c72012-04-11 17:21:24 +02003625 else:
3626 if verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003627 print("Label %s has no changelists - possibly deleted?" % name)
Luke Diamand06804c72012-04-11 17:21:24 +02003628
3629 if not commitFound:
3630 # We can't import this label; don't try again as it will get very
3631 # expensive repeatedly fetching all the files for labels that will
3632 # never be imported. If the label is moved in the future, the
3633 # ignore will need to be removed manually.
3634 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3635
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003636 def guessProjectName(self):
3637 for p in self.depotPaths:
Simon Hausmann6e5295c2007-06-11 08:50:57 +02003638 if p.endswith("/"):
3639 p = p[:-1]
3640 p = p[p.strip().rfind("/") + 1:]
3641 if not p.endswith("/"):
Joel Holdsworth812ee742022-04-01 15:24:45 +01003642 p += "/"
Simon Hausmann6e5295c2007-06-11 08:50:57 +02003643 return p
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003644
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003645 def getBranchMapping(self):
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003646 lostAndFoundBranches = set()
3647
Vitor Antunes8ace74c2011-08-19 00:44:04 +01003648 user = gitConfig("git-p4.branchUser")
Vitor Antunes8ace74c2011-08-19 00:44:04 +01003649
Joel Holdsworth8a470592022-01-06 21:40:34 +00003650 for info in p4CmdList(
3651 ["branches"] + (["-u", user] if len(user) > 0 else [])):
Luke Diamand52a48802012-01-19 09:52:25 +00003652 details = p4Cmd(["branch", "-o", info["branch"]])
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003653 viewIdx = 0
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003654 while "View%s" % viewIdx in details:
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003655 paths = details["View%s" % viewIdx].split(" ")
3656 viewIdx = viewIdx + 1
3657 # require standard //depot/foo/... //depot/bar/... mapping
3658 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3659 continue
3660 source = paths[0]
3661 destination = paths[1]
Joel Holdsworthc785e202022-04-01 15:24:57 +01003662 # HACK
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01003663 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
Simon Hausmann6509e192007-06-07 09:41:53 +02003664 source = source[len(self.depotPaths[0]):-4]
3665 destination = destination[len(self.depotPaths[0]):-4]
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003666
Simon Hausmann1a2edf42007-06-17 15:10:24 +02003667 if destination in self.knownBranches:
3668 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003669 print("p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination))
3670 print("but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination))
Simon Hausmann1a2edf42007-06-17 15:10:24 +02003671 continue
3672
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003673 self.knownBranches[destination] = source
3674
3675 lostAndFoundBranches.discard(destination)
3676
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003677 if source not in self.knownBranches:
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003678 lostAndFoundBranches.add(source)
3679
Vitor Antunes7199cf12011-08-19 00:44:05 +01003680 # Perforce does not strictly require branches to be defined, so we also
3681 # check git config for a branch list.
3682 #
3683 # Example of branch definition in git config file:
3684 # [git-p4]
3685 # branchList=main:branchA
3686 # branchList=main:branchB
3687 # branchList=branchA:branchC
3688 configBranches = gitConfigList("git-p4.branchList")
3689 for branch in configBranches:
3690 if branch:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01003691 source, destination = branch.split(":")
Vitor Antunes7199cf12011-08-19 00:44:05 +01003692 self.knownBranches[destination] = source
3693
3694 lostAndFoundBranches.discard(destination)
3695
3696 if source not in self.knownBranches:
3697 lostAndFoundBranches.add(source)
3698
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003699 for branch in lostAndFoundBranches:
3700 self.knownBranches[branch] = branch
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003701
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01003702 def getBranchMappingFromGitBranches(self):
3703 branches = p4BranchesInGit(self.importIntoRemotes)
3704 for branch in branches.keys():
3705 if branch == "master":
3706 branch = "main"
3707 else:
3708 branch = branch[len(self.projectName):]
3709 self.knownBranches[branch] = branch
3710
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003711 def updateOptionDict(self, d):
3712 option_keys = {}
3713 if self.keepRepoPath:
3714 option_keys['keepRepoPath'] = 1
3715
3716 d["options"] = ' '.join(sorted(option_keys.keys()))
3717
3718 def readOptions(self, d):
Luke Diamanddba1c9d2018-06-19 09:04:07 +01003719 self.keepRepoPath = ('options' in d
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003720 and ('keepRepoPath' in d['options']))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003721
Simon Hausmann8134f692007-08-26 16:44:55 +02003722 def gitRefForBranch(self, branch):
3723 if branch == "main":
3724 return self.refPrefix + "master"
3725
3726 if len(branch) <= 0:
3727 return branch
3728
3729 return self.refPrefix + self.projectName + branch
3730
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003731 def gitCommitByP4Change(self, ref, change):
3732 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003733 print("looking in ref " + ref + " for change %s using bisect..." % change)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003734
3735 earliestCommit = ""
3736 latestCommit = parseRevision(ref)
3737
3738 while True:
3739 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003740 print("trying: earliest %s latest %s" % (earliestCommit, latestCommit))
Joel Holdsworth8a470592022-01-06 21:40:34 +00003741 next = read_pipe(["git", "rev-list", "--bisect",
3742 latestCommit, earliestCommit]).strip()
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003743 if len(next) == 0:
3744 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003745 print("argh")
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003746 return ""
3747 log = extractLogMessageFromGitCommit(next)
3748 settings = extractSettingsGitLog(log)
3749 currentChange = int(settings['change'])
3750 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003751 print("current change %s" % currentChange)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003752
3753 if currentChange == change:
3754 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003755 print("found %s" % next)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003756 return next
3757
3758 if currentChange < change:
3759 earliestCommit = "^%s" % next
3760 else:
Mazo, Andrey2dda7412019-04-01 18:02:17 +00003761 if next == latestCommit:
3762 die("Infinite loop while looking in ref %s for change %s. Check your branch mappings" % (ref, change))
3763 latestCommit = "%s^@" % next
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003764
3765 return ""
3766
3767 def importNewBranch(self, branch, maxChange):
3768 # make fast-import flush all changes to disk and update the refs using the checkpoint
3769 # command so that we can try to find the branch parent in the git history
Joel Holdsworth990547a2022-04-01 15:24:44 +01003770 self.gitStream.write("checkpoint\n\n")
3771 self.gitStream.flush()
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003772 branchPrefix = self.depotPaths[0] + branch + "/"
3773 range = "@1,%s" % maxChange
Lex Spoon96b2d542015-04-20 11:00:20 -04003774 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003775 if len(changes) <= 0:
3776 return False
3777 firstChange = changes[0]
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003778 sourceBranch = self.knownBranches[branch]
3779 sourceDepotPath = self.depotPaths[0] + sourceBranch
3780 sourceRef = self.gitRefForBranch(sourceBranch)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003781
Luke Diamand52a48802012-01-19 09:52:25 +00003782 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003783 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3784 if len(gitParent) > 0:
3785 self.initialParents[self.gitRefForBranch(branch)] = gitParent
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003786
3787 self.importChanges(changes)
3788 return True
3789
Vitor Antunesfed23692012-01-25 23:48:22 +00003790 def searchParent(self, parent, branch, target):
Joachim Kuebart6b798182021-05-05 11:56:26 +00003791 targetTree = read_pipe(["git", "rev-parse",
3792 "{}^{{tree}}".format(target)]).strip()
3793 for line in read_pipe_lines(["git", "rev-list", "--format=%H %T",
Pete Wyckoffc7d34882013-01-26 22:11:21 -05003794 "--no-merges", parent]):
Joachim Kuebart6b798182021-05-05 11:56:26 +00003795 if line.startswith("commit "):
3796 continue
3797 commit, tree = line.strip().split(" ")
3798 if tree == targetTree:
Vitor Antunesfed23692012-01-25 23:48:22 +00003799 if self.verbose:
Joachim Kuebart6b798182021-05-05 11:56:26 +00003800 print("Found parent of %s in commit %s" % (branch, commit))
3801 return commit
3802 return None
Vitor Antunesfed23692012-01-25 23:48:22 +00003803
Luke Diamand89143ac2018-10-15 12:14:08 +01003804 def importChanges(self, changes, origin_revision=0):
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003805 cnt = 1
3806 for change in changes:
Luke Diamand89143ac2018-10-15 12:14:08 +01003807 description = p4_describe(change)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003808 self.updateOptionDict(description)
3809
3810 if not self.silent:
Joel Holdsworth0f829622021-12-19 15:40:28 +00003811 sys.stdout.write("\rImporting revision %s (%d%%)" % (
3812 change, (cnt * 100) // len(changes)))
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003813 sys.stdout.flush()
3814 cnt = cnt + 1
3815
3816 try:
3817 if self.detectBranches:
3818 branches = self.splitFilesIntoBranches(description)
3819 for branch in branches.keys():
Joel Holdsworthc785e202022-04-01 15:24:57 +01003820 # HACK --hwn
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003821 branchPrefix = self.depotPaths[0] + branch + "/"
Joel Holdsworth84af8b82022-04-01 15:24:50 +01003822 self.branchPrefixes = [branchPrefix]
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003823
3824 parent = ""
3825
3826 filesForCommit = branches[branch]
3827
3828 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003829 print("branch is %s" % branch)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003830
3831 self.updatedBranches.add(branch)
3832
3833 if branch not in self.createdBranches:
3834 self.createdBranches.add(branch)
3835 parent = self.knownBranches[branch]
3836 if parent == branch:
3837 parent = ""
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003838 else:
3839 fullBranch = self.projectName + branch
3840 if fullBranch not in self.p4BranchesInGit:
3841 if not self.silent:
Joel Holdsworth990547a2022-04-01 15:24:44 +01003842 print("\n Importing new branch %s" % fullBranch)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003843 if self.importNewBranch(branch, change - 1):
3844 parent = ""
3845 self.p4BranchesInGit.append(fullBranch)
3846 if not self.silent:
Joel Holdsworth990547a2022-04-01 15:24:44 +01003847 print("\n Resuming with change %s" % change)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003848
3849 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003850 print("parent determined through known branches: %s" % parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003851
Simon Hausmann8134f692007-08-26 16:44:55 +02003852 branch = self.gitRefForBranch(branch)
3853 parent = self.gitRefForBranch(parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003854
3855 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003856 print("looking for initial parent for %s; current parent is %s" % (branch, parent))
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003857
3858 if len(parent) == 0 and branch in self.initialParents:
3859 parent = self.initialParents[branch]
3860 del self.initialParents[branch]
3861
Vitor Antunesfed23692012-01-25 23:48:22 +00003862 blob = None
3863 if len(parent) > 0:
Pete Wyckoff4f9273d2013-01-26 22:11:04 -05003864 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
Vitor Antunesfed23692012-01-25 23:48:22 +00003865 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003866 print("Creating temporary branch: " + tempBranch)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003867 self.commit(description, filesForCommit, tempBranch)
Vitor Antunesfed23692012-01-25 23:48:22 +00003868 self.tempBranches.append(tempBranch)
3869 self.checkpoint()
3870 blob = self.searchParent(parent, branch, tempBranch)
3871 if blob:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003872 self.commit(description, filesForCommit, branch, blob)
Vitor Antunesfed23692012-01-25 23:48:22 +00003873 else:
3874 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01003875 print("Parent of %s not found. Committing into head of %s" % (branch, parent))
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003876 self.commit(description, filesForCommit, branch, parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003877 else:
Luke Diamand89143ac2018-10-15 12:14:08 +01003878 files = self.extractFilesFromCommit(description)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003879 self.commit(description, files, self.branch,
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003880 self.initialParent)
Pete Wyckoff47497842013-01-14 19:47:04 -05003881 # only needed once, to connect to the previous commit
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003882 self.initialParent = ""
3883 except IOError:
Luke Diamandf2606b12018-06-19 09:04:10 +01003884 print(self.gitError.read())
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003885 sys.exit(1)
3886
Luke Diamandb9d34db2018-06-08 21:32:44 +01003887 def sync_origin_only(self):
3888 if self.syncWithOrigin:
3889 self.hasOrigin = originP4BranchesExist()
3890 if self.hasOrigin:
3891 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01003892 print('Syncing with origin first, using "git fetch origin"')
Joel Holdsworth8a470592022-01-06 21:40:34 +00003893 system(["git", "fetch", "origin"])
Luke Diamandb9d34db2018-06-08 21:32:44 +01003894
Simon Hausmannc208a242007-08-26 16:07:18 +02003895 def importHeadRevision(self, revision):
Luke Diamandf2606b12018-06-19 09:04:10 +01003896 print("Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch))
Simon Hausmannc208a242007-08-26 16:07:18 +02003897
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003898 details = {}
3899 details["user"] = "git perforce import user"
Pete Wyckoff1494fcb2011-02-19 08:17:56 -05003900 details["desc"] = ("Initial import of %s from the state at revision %s\n"
Simon Hausmannc208a242007-08-26 16:07:18 +02003901 % (' '.join(self.depotPaths), revision))
3902 details["change"] = revision
3903 newestRevision = 0
3904
3905 fileCnt = 0
Joel Holdsworth12a77f52022-04-01 15:24:53 +01003906 fileArgs = ["%s...%s" % (p, revision) for p in self.depotPaths]
Luke Diamand6de040d2011-10-16 10:47:52 -04003907
3908 for info in p4CmdList(["files"] + fileArgs):
Simon Hausmannc208a242007-08-26 16:07:18 +02003909
Pete Wyckoff68b28592011-02-19 08:17:55 -05003910 if 'code' in info and info['code'] == 'error':
Simon Hausmannc208a242007-08-26 16:07:18 +02003911 sys.stderr.write("p4 returned an error: %s\n"
3912 % info['data'])
Pete Wyckoffd88e7072011-02-19 08:17:58 -05003913 if info['data'].find("must refer to client") >= 0:
3914 sys.stderr.write("This particular p4 error is misleading.\n")
Joel Holdsworth990547a2022-04-01 15:24:44 +01003915 sys.stderr.write("Perhaps the depot path was misspelled.\n")
Pete Wyckoffd88e7072011-02-19 08:17:58 -05003916 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
Simon Hausmannc208a242007-08-26 16:07:18 +02003917 sys.exit(1)
Pete Wyckoff68b28592011-02-19 08:17:55 -05003918 if 'p4ExitCode' in info:
3919 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
Simon Hausmannc208a242007-08-26 16:07:18 +02003920 sys.exit(1)
3921
Simon Hausmannc208a242007-08-26 16:07:18 +02003922 change = int(info["change"])
3923 if change > newestRevision:
3924 newestRevision = change
3925
Pete Wyckoff56c09342011-02-19 08:17:57 -05003926 if info["action"] in self.delete_actions:
Simon Hausmannc208a242007-08-26 16:07:18 +02003927 continue
3928
Joel Holdsworth84af8b82022-04-01 15:24:50 +01003929 for prop in ["depotFile", "rev", "action", "type"]:
Simon Hausmannc208a242007-08-26 16:07:18 +02003930 details["%s%s" % (prop, fileCnt)] = info[prop]
3931
3932 fileCnt = fileCnt + 1
3933
3934 details["change"] = newestRevision
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003935
Pete Wyckoff9dcb9f22012-04-08 20:18:01 -04003936 # Use time from top-most change so that all git p4 clones of
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003937 # the same p4 repo have the same commit SHA1s.
Pete Wyckoff18fa13d2012-11-23 17:35:34 -05003938 res = p4_describe(newestRevision)
3939 details["time"] = res["time"]
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003940
Simon Hausmannc208a242007-08-26 16:07:18 +02003941 self.updateOptionDict(details)
3942 try:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003943 self.commit(details, self.extractFilesFromCommit(details), self.branch)
Philip.McGrawde5abb52019-08-27 06:43:58 +03003944 except IOError as err:
Luke Diamandf2606b12018-06-19 09:04:10 +01003945 print("IO error with git fast-import. Is your git version recent enough?")
Philip.McGrawde5abb52019-08-27 06:43:58 +03003946 print("IO error details: {}".format(err))
Luke Diamandf2606b12018-06-19 09:04:10 +01003947 print(self.gitError.read())
Simon Hausmannc208a242007-08-26 16:07:18 +02003948
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003949 def importRevisions(self, args, branch_arg_given):
3950 changes = []
3951
3952 if len(self.changesFile) > 0:
Luke Diamand43f33e42020-01-30 11:50:34 +00003953 with open(self.changesFile) as f:
3954 output = f.readlines()
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003955 changeSet = set()
3956 for line in output:
3957 changeSet.add(int(line))
3958
3959 for change in changeSet:
3960 changes.append(change)
3961
3962 changes.sort()
3963 else:
3964 # catch "git p4 sync" with no new branches, in a repo that
3965 # does not have any existing p4 branches
3966 if len(args) == 0:
3967 if not self.p4BranchesInGit:
Luke Diamand6026aff2020-01-29 11:12:45 +00003968 raise P4CommandException("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003969
3970 # The default branch is master, unless --branch is used to
3971 # specify something else. Make sure it exists, or complain
3972 # nicely about how to use --branch.
3973 if not self.detectBranches:
3974 if not branch_exists(self.branch):
3975 if branch_arg_given:
Luke Diamand6026aff2020-01-29 11:12:45 +00003976 raise P4CommandException("Error: branch %s does not exist." % self.branch)
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003977 else:
Luke Diamand6026aff2020-01-29 11:12:45 +00003978 raise P4CommandException("Error: no branch %s; perhaps specify one with --branch." %
Luke Diamandca5b5cc2020-01-29 11:12:44 +00003979 self.branch)
3980
3981 if self.verbose:
3982 print("Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
3983 self.changeRange))
3984 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
3985
3986 if len(self.maxChanges) > 0:
3987 changes = changes[:min(int(self.maxChanges), len(changes))]
3988
3989 if len(changes) == 0:
3990 if not self.silent:
3991 print("No changes to import!")
3992 else:
3993 if not self.silent and not self.detectBranches:
3994 print("Import destination: %s" % self.branch)
3995
3996 self.updatedBranches = set()
3997
3998 if not self.detectBranches:
3999 if args:
4000 # start a new branch
4001 self.initialParent = ""
4002 else:
4003 # build on a previous revision
4004 self.initialParent = parseRevision(self.branch)
4005
4006 self.importChanges(changes)
4007
4008 if not self.silent:
4009 print("")
4010 if len(self.updatedBranches) > 0:
4011 sys.stdout.write("Updated branches: ")
4012 for b in self.updatedBranches:
4013 sys.stdout.write("%s " % b)
4014 sys.stdout.write("\n")
4015
Luke Diamand123f6312018-05-23 23:20:26 +01004016 def openStreams(self):
4017 self.importProcess = subprocess.Popen(["git", "fast-import"],
4018 stdin=subprocess.PIPE,
4019 stdout=subprocess.PIPE,
Joel Holdsworth990547a2022-04-01 15:24:44 +01004020 stderr=subprocess.PIPE)
Luke Diamand123f6312018-05-23 23:20:26 +01004021 self.gitOutput = self.importProcess.stdout
4022 self.gitStream = self.importProcess.stdin
4023 self.gitError = self.importProcess.stderr
4024
Yang Zhao86dca242019-12-13 15:52:39 -08004025 if bytes is not str:
4026 # Wrap gitStream.write() so that it can be called using `str` arguments
4027 def make_encoded_write(write):
4028 def encoded_write(s):
4029 return write(s.encode() if isinstance(s, str) else s)
4030 return encoded_write
4031
4032 self.gitStream.write = make_encoded_write(self.gitStream.write)
4033
Luke Diamand123f6312018-05-23 23:20:26 +01004034 def closeStreams(self):
Luke Diamand837b3a62020-01-29 11:12:41 +00004035 if self.gitStream is None:
4036 return
Luke Diamand123f6312018-05-23 23:20:26 +01004037 self.gitStream.close()
4038 if self.importProcess.wait() != 0:
4039 die("fast-import failed: %s" % self.gitError.read())
4040 self.gitOutput.close()
4041 self.gitError.close()
Luke Diamand837b3a62020-01-29 11:12:41 +00004042 self.gitStream = None
Simon Hausmannc208a242007-08-26 16:07:18 +02004043
Simon Hausmannb9847332007-03-20 20:54:23 +01004044 def run(self, args):
Simon Hausmanna028a982007-05-23 00:03:08 +02004045 if self.importIntoRemotes:
4046 self.refPrefix = "refs/remotes/p4/"
4047 else:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02004048 self.refPrefix = "refs/heads/p4/"
Simon Hausmanna028a982007-05-23 00:03:08 +02004049
Luke Diamandb9d34db2018-06-08 21:32:44 +01004050 self.sync_origin_only()
Simon Hausmann10f880f2007-05-24 22:28:28 +02004051
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05004052 branch_arg_given = bool(self.branch)
Simon Hausmann569d1bd2007-03-22 21:34:16 +01004053 if len(self.branch) == 0:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02004054 self.branch = self.refPrefix + "master"
Simon Hausmanna028a982007-05-23 00:03:08 +02004055 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
Joel Holdsworth8a470592022-01-06 21:40:34 +00004056 system(["git", "update-ref", self.branch, "refs/heads/p4"])
4057 system(["git", "branch", "-D", "p4"])
Simon Hausmann179caeb2007-03-22 22:17:42 +01004058
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05004059 # accept either the command-line option, or the configuration variable
4060 if self.useClientSpec:
4061 # will use this after clone to set the variable
4062 self.useClientSpec_from_options = True
4063 else:
Pete Wyckoff0d609032013-01-26 22:11:24 -05004064 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff09fca772011-12-24 21:07:39 -05004065 self.useClientSpec = True
4066 if self.useClientSpec:
Pete Wyckoff543987b2012-02-25 20:06:25 -05004067 self.clientSpecDirs = getClientSpec()
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01004068
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004069 # TODO: should always look at previous commits,
4070 # merge with previous imports, if possible.
4071 if args == []:
Simon Hausmannd414c742007-05-25 11:36:42 +02004072 if self.hasOrigin:
Simon Hausmann5ca44612007-08-24 17:44:16 +02004073 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
Pete Wyckoff3b650fc2013-01-14 19:46:58 -05004074
4075 # branches holds mapping from branch name to sha1
4076 branches = p4BranchesInGit(self.importIntoRemotes)
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004077
4078 # restrict to just this one, disabling detect-branches
4079 if branch_arg_given:
Tao Klerks17f273f2022-04-04 05:10:54 +00004080 short = shortP4Ref(self.branch, self.importIntoRemotes)
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004081 if short in branches:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004082 self.p4BranchesInGit = [short]
Tao Klerks17f273f2022-04-04 05:10:54 +00004083 elif self.branch.startswith('refs/') and \
4084 branchExists(self.branch) and \
4085 '[git-p4:' in extractLogMessageFromGitCommit(self.branch):
Junio C Hamanoaf3a3202022-05-20 15:26:55 -07004086 self.p4BranchesInGit = [self.branch]
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004087 else:
4088 self.p4BranchesInGit = branches.keys()
Simon Hausmannabcd7902007-05-24 22:25:36 +02004089
4090 if len(self.p4BranchesInGit) > 1:
4091 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01004092 print("Importing from/into multiple branches")
Simon Hausmannabcd7902007-05-24 22:25:36 +02004093 self.detectBranches = True
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05004094 for branch in branches.keys():
4095 self.initialParents[self.refPrefix + branch] = \
4096 branches[branch]
Simon Hausmann967f72e2007-03-23 09:30:41 +01004097
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004098 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01004099 print("branches: %s" % self.p4BranchesInGit)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004100
4101 p4Change = 0
4102 for branch in self.p4BranchesInGit:
Tao Klerks17f273f2022-04-04 05:10:54 +00004103 logMsg = extractLogMessageFromGitCommit(fullP4Ref(branch,
4104 self.importIntoRemotes))
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004105
4106 settings = extractSettingsGitLog(logMsg)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004107
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004108 self.readOptions(settings)
Joel Holdsworth7a3e83d2022-04-01 15:24:59 +01004109 if 'depot-paths' in settings and 'change' in settings:
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004110 change = int(settings['change']) + 1
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004111 p4Change = max(p4Change, change)
4112
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004113 depotPaths = sorted(settings['depot-paths'])
4114 if self.previousDepotPaths == []:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004115 self.previousDepotPaths = depotPaths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004116 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004117 paths = []
4118 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
Vitor Antunes04d277b2011-08-19 00:44:03 +01004119 prev_list = prev.split("/")
4120 cur_list = cur.split("/")
4121 for i in range(0, min(len(cur_list), len(prev_list))):
Luke Diamandfc35c9d2018-06-19 09:04:06 +01004122 if cur_list[i] != prev_list[i]:
Simon Hausmann583e1702007-06-07 09:37:13 +02004123 i = i - 1
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004124 break
4125
Joel Holdsworth843d8472022-04-01 15:24:54 +01004126 paths.append("/".join(cur_list[:i + 1]))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004127
4128 self.previousDepotPaths = paths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004129
4130 if p4Change > 0:
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004131 self.depotPaths = sorted(self.previousDepotPaths)
Simon Hausmannd5904672007-05-19 11:07:32 +02004132 self.changeRange = "@%s,#head" % p4Change
Simon Hausmann341dc1c2007-05-21 00:39:16 +02004133 if not self.silent and not self.detectBranches:
Luke Diamandf2606b12018-06-19 09:04:10 +01004134 print("Performing incremental import into %s git branch" % self.branch)
Simon Hausmann569d1bd2007-03-22 21:34:16 +01004135
Tao Klerks17f273f2022-04-04 05:10:54 +00004136 self.branch = fullP4Ref(self.branch, self.importIntoRemotes)
Simon Hausmann179caeb2007-03-22 22:17:42 +01004137
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004138 if len(args) == 0 and self.depotPaths:
Simon Hausmannb9847332007-03-20 20:54:23 +01004139 if not self.silent:
Luke Diamandf2606b12018-06-19 09:04:10 +01004140 print("Depot paths: %s" % ' '.join(self.depotPaths))
Simon Hausmannb9847332007-03-20 20:54:23 +01004141 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004142 if self.depotPaths and self.depotPaths != args:
Luke Diamandf2606b12018-06-19 09:04:10 +01004143 print("previous import used depot path %s and now %s was specified. "
Joel Holdsworth843d8472022-04-01 15:24:54 +01004144 "This doesn't work!" % (' '.join(self.depotPaths),
4145 ' '.join(args)))
Simon Hausmannb9847332007-03-20 20:54:23 +01004146 sys.exit(1)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004147
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004148 self.depotPaths = sorted(args)
Simon Hausmannb9847332007-03-20 20:54:23 +01004149
Simon Hausmann1c49fc12007-08-26 16:04:34 +02004150 revision = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01004151 self.users = {}
Simon Hausmannb9847332007-03-20 20:54:23 +01004152
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05004153 # Make sure no revision specifiers are used when --changesfile
4154 # is specified.
4155 bad_changesfile = False
4156 if len(self.changesFile) > 0:
4157 for p in self.depotPaths:
4158 if p.find("@") >= 0 or p.find("#") >= 0:
4159 bad_changesfile = True
4160 break
4161 if bad_changesfile:
4162 die("Option --changesfile is incompatible with revision specifiers")
4163
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004164 newPaths = []
4165 for p in self.depotPaths:
4166 if p.find("@") != -1:
4167 atIdx = p.index("@")
4168 self.changeRange = p[atIdx:]
4169 if self.changeRange == "@all":
4170 self.changeRange = ""
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004171 elif ',' not in self.changeRange:
Simon Hausmann1c49fc12007-08-26 16:04:34 +02004172 revision = self.changeRange
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004173 self.changeRange = ""
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07004174 p = p[:atIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004175 elif p.find("#") != -1:
4176 hashIdx = p.index("#")
Simon Hausmann1c49fc12007-08-26 16:04:34 +02004177 revision = p[hashIdx:]
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07004178 p = p[:hashIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004179 elif self.previousDepotPaths == []:
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05004180 # pay attention to changesfile, if given, else import
4181 # the entire p4 tree at the head revision
4182 if len(self.changesFile) == 0:
4183 revision = "#head"
Simon Hausmannb9847332007-03-20 20:54:23 +01004184
Joel Holdsworth843d8472022-04-01 15:24:54 +01004185 p = re.sub("\.\.\.$", "", p)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004186 if not p.endswith("/"):
4187 p += "/"
4188
4189 newPaths.append(p)
4190
4191 self.depotPaths = newPaths
4192
Pete Wyckoffe63231e2012-08-11 12:55:02 -04004193 # --detect-branches may change this for each branch
4194 self.branchPrefixes = self.depotPaths
4195
Simon Hausmannb607e712007-05-20 10:55:54 +02004196 self.loadUserMapFromCache()
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02004197 self.labels = {}
4198 if self.detectLabels:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004199 self.getLabels()
Simon Hausmannb9847332007-03-20 20:54:23 +01004200
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02004201 if self.detectBranches:
Joel Holdsworthc785e202022-04-01 15:24:57 +01004202 # FIXME - what's a P4 projectName ?
Simon Hausmanndf450922007-06-08 08:49:22 +02004203 self.projectName = self.guessProjectName()
4204
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01004205 if self.hasOrigin:
4206 self.getBranchMappingFromGitBranches()
4207 else:
4208 self.getBranchMapping()
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004209 if self.verbose:
Luke Diamandf2606b12018-06-19 09:04:10 +01004210 print("p4-git branches: %s" % self.p4BranchesInGit)
4211 print("initial parents: %s" % self.initialParents)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004212 for b in self.p4BranchesInGit:
4213 if b != "master":
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004214
Joel Holdsworthc785e202022-04-01 15:24:57 +01004215 # FIXME
Simon Hausmann29bdbac2007-05-19 10:23:12 +02004216 b = b[len(self.projectName):]
4217 self.createdBranches.add(b)
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02004218
Luke Diamand19fa5ac2020-01-29 11:12:46 +00004219 p4_check_access()
4220
Luke Diamand123f6312018-05-23 23:20:26 +01004221 self.openStreams()
Simon Hausmannb9847332007-03-20 20:54:23 +01004222
Luke Diamand6026aff2020-01-29 11:12:45 +00004223 err = None
Simon Hausmannb9847332007-03-20 20:54:23 +01004224
Luke Diamand6026aff2020-01-29 11:12:45 +00004225 try:
4226 if revision:
4227 self.importHeadRevision(revision)
4228 else:
4229 self.importRevisions(args, branch_arg_given)
Luke Diamand06804c72012-04-11 17:21:24 +02004230
Luke Diamand6026aff2020-01-29 11:12:45 +00004231 if gitConfigBool("git-p4.importLabels"):
4232 self.importLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02004233
Luke Diamand6026aff2020-01-29 11:12:45 +00004234 if self.importLabels:
4235 p4Labels = getP4Labels(self.depotPaths)
4236 gitTags = getGitTags()
Simon Hausmannb9847332007-03-20 20:54:23 +01004237
Luke Diamand6026aff2020-01-29 11:12:45 +00004238 missingP4Labels = p4Labels - gitTags
4239 self.importP4Labels(self.gitStream, missingP4Labels)
4240
4241 except P4CommandException as e:
4242 err = e
4243
4244 finally:
4245 self.closeStreams()
4246
4247 if err:
4248 die(str(err))
Simon Hausmannb9847332007-03-20 20:54:23 +01004249
Vitor Antunesfed23692012-01-25 23:48:22 +00004250 # Cleanup temporary branches created during import
4251 if self.tempBranches != []:
4252 for branch in self.tempBranches:
Joel Holdsworth8a470592022-01-06 21:40:34 +00004253 read_pipe(["git", "update-ref", "-d", branch])
Vitor Antunesfed23692012-01-25 23:48:22 +00004254 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
4255
Pete Wyckoff55d12432013-01-14 19:46:59 -05004256 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
4257 # a convenient shortcut refname "p4".
4258 if self.importIntoRemotes:
4259 head_ref = self.refPrefix + "HEAD"
4260 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
4261 system(["git", "symbolic-ref", head_ref, self.branch])
4262
Simon Hausmannb9847332007-03-20 20:54:23 +01004263 return True
4264
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004265
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004266class P4Rebase(Command):
4267 def __init__(self):
4268 Command.__init__(self)
Luke Diamand06804c72012-04-11 17:21:24 +02004269 self.options = [
4270 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02004271 ]
Luke Diamand06804c72012-04-11 17:21:24 +02004272 self.importLabels = False
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03004273 self.description = ("Fetches the latest revision from perforce and "
4274 + "rebases the current work (branch) against it")
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004275
4276 def run(self, args):
4277 sync = P4Sync()
Luke Diamand06804c72012-04-11 17:21:24 +02004278 sync.importLabels = self.importLabels
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004279 sync.run([])
Simon Hausmannd7e38682007-06-12 14:34:46 +02004280
Simon Hausmann14594f42007-08-22 09:07:15 +02004281 return self.rebase()
4282
4283 def rebase(self):
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01004284 if os.system("git update-index --refresh") != 0:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004285 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 +00004286 if len(read_pipe(["git", "diff-index", "HEAD", "--"])) > 0:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004287 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.")
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01004288
Joel Holdsworth0874bb02022-04-01 15:24:52 +01004289 upstream, settings = findUpstreamBranchPoint()
Simon Hausmannd7e38682007-06-12 14:34:46 +02004290 if len(upstream) == 0:
4291 die("Cannot find upstream branchpoint for rebase")
4292
4293 # the branchpoint may be p4/foo~3, so strip off the parent
4294 upstream = re.sub("~[0-9]+$", "", upstream)
4295
Luke Diamandf2606b12018-06-19 09:04:10 +01004296 print("Rebasing the current branch onto %s" % upstream)
Joel Holdsworth8a470592022-01-06 21:40:34 +00004297 oldHead = read_pipe(["git", "rev-parse", "HEAD"]).strip()
4298 system(["git", "rebase", upstream])
4299 system(["git", "diff-tree", "--stat", "--summary", "-M", oldHead,
4300 "HEAD", "--"])
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02004301 return True
4302
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004303
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004304class P4Clone(P4Sync):
4305 def __init__(self):
4306 P4Sync.__init__(self)
4307 self.description = "Creates a new git repository and imports from Perforce into it"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004308 self.usage = "usage: %prog [options] //depot/path[@revRange]"
Tommy Thorn354081d2008-02-03 10:38:51 -08004309 self.options += [
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004310 optparse.make_option("--destination", dest="cloneDestination",
4311 action='store', default=None,
Tommy Thorn354081d2008-02-03 10:38:51 -08004312 help="where to leave result of the clone"),
Pete Wyckoff38200072011-02-19 08:18:01 -05004313 optparse.make_option("--bare", dest="cloneBare",
4314 action="store_true", default=False),
Tommy Thorn354081d2008-02-03 10:38:51 -08004315 ]
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004316 self.cloneDestination = None
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004317 self.needsGit = False
Pete Wyckoff38200072011-02-19 08:18:01 -05004318 self.cloneBare = False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004319
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004320 def defaultDestination(self, args):
Joel Holdsworthc785e202022-04-01 15:24:57 +01004321 # TODO: use common prefix of args?
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004322 depotPath = args[0]
4323 depotDir = re.sub("(@[^@]*)$", "", depotPath)
4324 depotDir = re.sub("(#[^#]*)$", "", depotDir)
Toby Allsopp053d9e42008-02-05 09:41:43 +13004325 depotDir = re.sub(r"\.\.\.$", "", depotDir)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03004326 depotDir = re.sub(r"/$", "", depotDir)
4327 return os.path.split(depotDir)[1]
4328
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004329 def run(self, args):
4330 if len(args) < 1:
4331 return False
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004332
4333 if self.keepRepoPath and not self.cloneDestination:
4334 sys.stderr.write("Must specify destination for --keep-path\n")
4335 sys.exit(1)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004336
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004337 depotPaths = args
Simon Hausmann5e100b52007-06-07 21:12:25 +02004338
4339 if not self.cloneDestination and len(depotPaths) > 1:
4340 self.cloneDestination = depotPaths[-1]
4341 depotPaths = depotPaths[:-1]
4342
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004343 for p in depotPaths:
4344 if not p.startswith("//"):
Pete Wyckoff0f487d32013-01-26 22:11:06 -05004345 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004346 return False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004347
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004348 if not self.cloneDestination:
Marius Storm-Olsen98ad4fa2007-06-07 15:08:33 +02004349 self.cloneDestination = self.defaultDestination(args)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004350
Luke Diamandf2606b12018-06-19 09:04:10 +01004351 print("Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination))
Pete Wyckoff38200072011-02-19 08:18:01 -05004352
Kevin Greenc3bf3f12007-06-11 16:48:07 -04004353 if not os.path.exists(self.cloneDestination):
4354 os.makedirs(self.cloneDestination)
Robert Blum053fd0c2008-08-01 12:50:03 -07004355 chdir(self.cloneDestination)
Pete Wyckoff38200072011-02-19 08:18:01 -05004356
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004357 init_cmd = ["git", "init"]
Pete Wyckoff38200072011-02-19 08:18:01 -05004358 if self.cloneBare:
4359 init_cmd.append("--bare")
Brandon Caseya235e852013-01-26 11:14:33 -08004360 retcode = subprocess.call(init_cmd)
4361 if retcode:
Joel Holdsworth40e7cfd2022-01-06 21:41:56 +00004362 raise subprocess.CalledProcessError(retcode, init_cmd)
Pete Wyckoff38200072011-02-19 08:18:01 -05004363
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03004364 if not P4Sync.run(self, depotPaths):
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004365 return False
Pete Wyckoffc5959562013-01-14 19:47:01 -05004366
4367 # create a master branch and check out a work tree
4368 if gitBranchExists(self.branch):
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004369 system(["git", "branch", currentGitBranch(), self.branch])
Pete Wyckoffc5959562013-01-14 19:47:01 -05004370 if not self.cloneBare:
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004371 system(["git", "checkout", "-f"])
Pete Wyckoffc5959562013-01-14 19:47:01 -05004372 else:
Joel Holdsworth968e29e2022-04-01 15:24:55 +01004373 print('Not checking out any branch, use '
Luke Diamandf2606b12018-06-19 09:04:10 +01004374 '"git checkout -q -b master <branch>"')
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03004375
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05004376 # auto-set this variable if invoked with --use-client-spec
4377 if self.useClientSpec_from_options:
Joel Holdsworth8a470592022-01-06 21:40:34 +00004378 system(["git", "config", "--bool", "git-p4.useclientspec", "true"])
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05004379
Tao Klerksf7b5ff62022-04-30 19:26:52 +00004380 # persist any git-p4 encoding-handling config options passed in for clone:
4381 if gitConfig('git-p4.metadataDecodingStrategy'):
4382 system(["git", "config", "git-p4.metadataDecodingStrategy", gitConfig('git-p4.metadataDecodingStrategy')])
4383 if gitConfig('git-p4.metadataFallbackEncoding'):
4384 system(["git", "config", "git-p4.metadataFallbackEncoding", gitConfig('git-p4.metadataFallbackEncoding')])
4385 if gitConfig('git-p4.pathEncoding'):
4386 system(["git", "config", "git-p4.pathEncoding", gitConfig('git-p4.pathEncoding')])
4387
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02004388 return True
4389
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004390
Luke Diamand123f6312018-05-23 23:20:26 +01004391class P4Unshelve(Command):
4392 def __init__(self):
4393 Command.__init__(self)
4394 self.options = []
4395 self.origin = "HEAD"
4396 self.description = "Unshelve a P4 changelist into a git commit"
4397 self.usage = "usage: %prog [options] changelist"
4398 self.options += [
4399 optparse.make_option("--origin", dest="origin",
4400 help="Use this base revision instead of the default (%s)" % self.origin),
4401 ]
4402 self.verbose = False
4403 self.noCommit = False
Luke Diamand08813122018-10-15 12:14:07 +01004404 self.destbranch = "refs/remotes/p4-unshelved"
Luke Diamand123f6312018-05-23 23:20:26 +01004405
4406 def renameBranch(self, branch_name):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01004407 """Rename the existing branch to branch_name.N ."""
Luke Diamand123f6312018-05-23 23:20:26 +01004408
Joel Holdsworth12a77f52022-04-01 15:24:53 +01004409 for i in range(0, 1000):
Luke Diamand123f6312018-05-23 23:20:26 +01004410 backup_branch_name = "{0}.{1}".format(branch_name, i)
4411 if not gitBranchExists(backup_branch_name):
Joel Holdsworth4768af22022-04-01 15:25:02 +01004412 # Copy ref to backup
4413 gitUpdateRef(backup_branch_name, branch_name)
Luke Diamand123f6312018-05-23 23:20:26 +01004414 gitDeleteRef(branch_name)
Luke Diamand123f6312018-05-23 23:20:26 +01004415 print("renamed old unshelve branch to {0}".format(backup_branch_name))
4416 break
Moritz Baumann198551c2022-07-20 18:55:30 +00004417 else:
4418 sys.exit("gave up trying to rename existing branch {0}".format(branch_name))
Luke Diamand123f6312018-05-23 23:20:26 +01004419
4420 def findLastP4Revision(self, starting_point):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01004421 """Look back from starting_point for the first commit created by git-p4
4422 to find the P4 commit we are based on, and the depot-paths.
4423 """
Luke Diamand123f6312018-05-23 23:20:26 +01004424
4425 for parent in (range(65535)):
Luke Diamand0acbf592020-09-19 09:54:41 +01004426 log = extractLogMessageFromGitCommit("{0}~{1}".format(starting_point, parent))
Luke Diamand123f6312018-05-23 23:20:26 +01004427 settings = extractSettingsGitLog(log)
Luke Diamanddba1c9d2018-06-19 09:04:07 +01004428 if 'change' in settings:
Luke Diamand123f6312018-05-23 23:20:26 +01004429 return settings
4430
4431 sys.exit("could not find git-p4 commits in {0}".format(self.origin))
4432
Luke Diamand89143ac2018-10-15 12:14:08 +01004433 def createShelveParent(self, change, branch_name, sync, origin):
Joel Holdsworth59ef3fc2022-04-01 15:24:46 +01004434 """Create a commit matching the parent of the shelved changelist
4435 'change'.
4436 """
Luke Diamand89143ac2018-10-15 12:14:08 +01004437 parent_description = p4_describe(change, shelved=True)
4438 parent_description['desc'] = 'parent for shelved changelist {}\n'.format(change)
4439 files = sync.extractFilesFromCommit(parent_description, shelved=False, shelved_cl=change)
4440
4441 parent_files = []
4442 for f in files:
4443 # if it was added in the shelved changelist, it won't exist in the parent
4444 if f['action'] in self.add_actions:
4445 continue
4446
4447 # if it was deleted in the shelved changelist it must not be deleted
4448 # in the parent - we might even need to create it if the origin branch
4449 # does not have it
4450 if f['action'] in self.delete_actions:
4451 f['action'] = 'add'
4452
4453 parent_files.append(f)
4454
4455 sync.commit(parent_description, parent_files, branch_name,
4456 parent=origin, allow_empty=True)
4457 print("created parent commit for {0} based on {1} in {2}".format(
4458 change, self.origin, branch_name))
4459
Luke Diamand123f6312018-05-23 23:20:26 +01004460 def run(self, args):
4461 if len(args) != 1:
4462 return False
4463
4464 if not gitBranchExists(self.origin):
4465 sys.exit("origin branch {0} does not exist".format(self.origin))
4466
4467 sync = P4Sync()
4468 changes = args
Luke Diamand123f6312018-05-23 23:20:26 +01004469
Luke Diamand89143ac2018-10-15 12:14:08 +01004470 # only one change at a time
Luke Diamand123f6312018-05-23 23:20:26 +01004471 change = changes[0]
4472
4473 # if the target branch already exists, rename it
4474 branch_name = "{0}/{1}".format(self.destbranch, change)
4475 if gitBranchExists(branch_name):
4476 self.renameBranch(branch_name)
4477 sync.branch = branch_name
4478
4479 sync.verbose = self.verbose
4480 sync.suppress_meta_comment = True
4481
4482 settings = self.findLastP4Revision(self.origin)
Luke Diamand123f6312018-05-23 23:20:26 +01004483 sync.depotPaths = settings['depot-paths']
4484 sync.branchPrefixes = sync.depotPaths
4485
4486 sync.openStreams()
4487 sync.loadUserMapFromCache()
4488 sync.silent = True
Luke Diamand89143ac2018-10-15 12:14:08 +01004489
4490 # create a commit for the parent of the shelved changelist
4491 self.createShelveParent(change, branch_name, sync, self.origin)
4492
4493 # create the commit for the shelved changelist itself
4494 description = p4_describe(change, True)
4495 files = sync.extractFilesFromCommit(description, True, change)
4496
4497 sync.commit(description, files, branch_name, "")
Luke Diamand123f6312018-05-23 23:20:26 +01004498 sync.closeStreams()
4499
4500 print("unshelved changelist {0} into {1}".format(change, branch_name))
4501
4502 return True
4503
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004504
Simon Hausmann09d89de2007-06-20 23:10:28 +02004505class P4Branches(Command):
4506 def __init__(self):
4507 Command.__init__(self)
Joel Holdsworth84af8b82022-04-01 15:24:50 +01004508 self.options = []
Simon Hausmann09d89de2007-06-20 23:10:28 +02004509 self.description = ("Shows the git branches that hold imports and their "
4510 + "corresponding perforce depot paths")
4511 self.verbose = False
4512
4513 def run(self, args):
Simon Hausmann5ca44612007-08-24 17:44:16 +02004514 if originP4BranchesExist():
4515 createOrUpdateBranchesFromOrigin()
4516
Joel Holdsworth8a470592022-01-06 21:40:34 +00004517 for line in read_pipe_lines(["git", "rev-parse", "--symbolic", "--remotes"]):
Simon Hausmann09d89de2007-06-20 23:10:28 +02004518 line = line.strip()
4519
4520 if not line.startswith('p4/') or line == "p4/HEAD":
4521 continue
4522 branch = line
4523
4524 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4525 settings = extractSettingsGitLog(log)
4526
Luke Diamandf2606b12018-06-19 09:04:10 +01004527 print("%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"]))
Simon Hausmann09d89de2007-06-20 23:10:28 +02004528 return True
4529
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004530
Simon Hausmannb9847332007-03-20 20:54:23 +01004531class HelpFormatter(optparse.IndentedHelpFormatter):
4532 def __init__(self):
4533 optparse.IndentedHelpFormatter.__init__(self)
4534
4535 def format_description(self, description):
4536 if description:
4537 return description + "\n"
4538 else:
4539 return ""
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004540
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004541
Simon Hausmann86949ee2007-03-19 20:59:12 +01004542def printUsage(commands):
Luke Diamandf2606b12018-06-19 09:04:10 +01004543 print("usage: %s <command> [options]" % sys.argv[0])
4544 print("")
4545 print("valid commands: %s" % ", ".join(commands))
4546 print("")
4547 print("Try %s <command> --help for command specific help." % sys.argv[0])
4548 print("")
Simon Hausmann86949ee2007-03-19 20:59:12 +01004549
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004550
Simon Hausmann86949ee2007-03-19 20:59:12 +01004551commands = {
Joel Holdsworth2bcf6112022-04-01 15:24:56 +01004552 "submit": P4Submit,
4553 "commit": P4Submit,
4554 "sync": P4Sync,
4555 "rebase": P4Rebase,
4556 "clone": P4Clone,
4557 "branches": P4Branches,
4558 "unshelve": P4Unshelve,
Simon Hausmann86949ee2007-03-19 20:59:12 +01004559}
4560
Joel Holdsworthadf159b2022-04-01 15:24:43 +01004561
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004562def main():
4563 if len(sys.argv[1:]) == 0:
4564 printUsage(commands.keys())
4565 sys.exit(2)
Simon Hausmann86949ee2007-03-19 20:59:12 +01004566
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004567 cmdName = sys.argv[1]
4568 try:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004569 klass = commands[cmdName]
4570 cmd = klass()
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004571 except KeyError:
Luke Diamandf2606b12018-06-19 09:04:10 +01004572 print("unknown command %s" % cmdName)
4573 print("")
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004574 printUsage(commands.keys())
4575 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004576
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004577 options = cmd.options
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004578 cmd.gitdir = os.environ.get("GIT_DIR", None)
Simon Hausmann86949ee2007-03-19 20:59:12 +01004579
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004580 args = sys.argv[2:]
Simon Hausmanne20a9e52007-03-26 00:13:51 +02004581
Pete Wyckoffb0ccc802012-09-09 16:16:10 -04004582 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
Luke Diamand6a10b6a2012-04-24 09:33:23 +01004583 if cmd.needsGit:
4584 options.append(optparse.make_option("--git-dir", dest="gitdir"))
Simon Hausmanne20a9e52007-03-26 00:13:51 +02004585
Luke Diamand6a10b6a2012-04-24 09:33:23 +01004586 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4587 options,
Joel Holdsworth57fe2ce2022-04-01 15:24:51 +01004588 description=cmd.description,
4589 formatter=HelpFormatter())
Simon Hausmann86949ee2007-03-19 20:59:12 +01004590
Ben Keene608e3802019-12-16 14:02:20 +00004591 try:
Joel Holdsworth0874bb02022-04-01 15:24:52 +01004592 cmd, args = parser.parse_args(sys.argv[2:], cmd)
Ben Keene608e3802019-12-16 14:02:20 +00004593 except:
4594 parser.print_help()
4595 raise
4596
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004597 global verbose
4598 verbose = cmd.verbose
4599 if cmd.needsGit:
Joel Holdsworthda0134f2022-04-01 15:25:00 +01004600 if cmd.gitdir is None:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004601 cmd.gitdir = os.path.abspath(".git")
4602 if not isValidGitDir(cmd.gitdir):
Luke Diamand378f7be2016-12-13 21:51:28 +00004603 # "rev-parse --git-dir" without arguments will try $PWD/.git
Joel Holdsworth8a470592022-01-06 21:40:34 +00004604 cmd.gitdir = read_pipe(["git", "rev-parse", "--git-dir"]).strip()
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004605 if os.path.exists(cmd.gitdir):
Joel Holdsworth8a470592022-01-06 21:40:34 +00004606 cdup = read_pipe(["git", "rev-parse", "--show-cdup"]).strip()
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004607 if len(cdup) > 0:
Joel Holdsworth990547a2022-04-01 15:24:44 +01004608 chdir(cdup)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004609
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004610 if not isValidGitDir(cmd.gitdir):
4611 if isValidGitDir(cmd.gitdir + "/.git"):
4612 cmd.gitdir += "/.git"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004613 else:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004614 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
Simon Hausmann8910ac02007-03-26 08:18:55 +02004615
Luke Diamand378f7be2016-12-13 21:51:28 +00004616 # so git commands invoked from the P4 workspace will succeed
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004617 os.environ["GIT_DIR"] = cmd.gitdir
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004618
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004619 if not cmd.run(args):
4620 parser.print_help()
Pete Wyckoff09fca772011-12-24 21:07:39 -05004621 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004622
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004623
4624if __name__ == '__main__':
4625 main()