blob: 0354d4df5cadd5145245d755a8868608b849719f [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#
Eric S. Raymonda33faf22012-12-28 11:40:59 -050010import sys
11if sys.hexversion < 0x02040000:
12 # The limiter is the subprocess module
13 sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
14 sys.exit(1)
Pete Wyckofff629fa52013-01-26 22:11:05 -050015import os
16import optparse
17import marshal
18import subprocess
19import tempfile
20import time
21import platform
22import re
23import shutil
Pete Wyckoffd20f0f82013-01-26 22:11:19 -050024import stat
Lars Schneidera5db4b12015-09-26 09:55:03 +020025import zipfile
26import zlib
Dennis Kaarsemaker4b07cd22015-10-20 21:31:46 +020027import ctypes
Luke Diamanddf8a9e82016-12-17 01:00:40 +000028import errno
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -030029
Brandon Caseya235e852013-01-26 11:14:33 -080030try:
31 from subprocess import CalledProcessError
32except ImportError:
33 # from python2.7:subprocess.py
34 # Exception classes used by this module.
35 class CalledProcessError(Exception):
36 """This exception is raised when a process run by check_call() returns
37 a non-zero exit status. The exit status will be stored in the
38 returncode attribute."""
39 def __init__(self, returncode, cmd):
40 self.returncode = returncode
41 self.cmd = cmd
42 def __str__(self):
43 return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
44
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -030045verbose = False
Simon Hausmann86949ee2007-03-19 20:59:12 +010046
Luke Diamand06804c72012-04-11 17:21:24 +020047# Only labels/tags matching this will be imported/exported
Luke Diamandc8942a22012-04-11 17:21:24 +020048defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
Anand Kumria21a50752008-08-10 19:26:28 +010049
Luke Diamand3deed5e2018-06-08 21:32:48 +010050# The block size is reduced automatically if required
51defaultBlockSize = 1<<20
Luke Diamand1051ef02015-06-10 08:30:59 +010052
Luke Diamand0ef67ac2018-06-08 21:32:45 +010053p4_access_checked = False
Anand Kumria21a50752008-08-10 19:26:28 +010054
55def p4_build_cmd(cmd):
56 """Build a suitable p4 command line.
57
58 This consolidates building and returning a p4 command line into one
59 location. It means that hooking into the environment, or other configuration
60 can be done more easily.
61 """
Luke Diamand6de040d2011-10-16 10:47:52 -040062 real_cmd = ["p4"]
Anand Kumriaabcaf072008-08-10 19:26:31 +010063
64 user = gitConfig("git-p4.user")
65 if len(user) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040066 real_cmd += ["-u",user]
Anand Kumriaabcaf072008-08-10 19:26:31 +010067
68 password = gitConfig("git-p4.password")
69 if len(password) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040070 real_cmd += ["-P", password]
Anand Kumriaabcaf072008-08-10 19:26:31 +010071
72 port = gitConfig("git-p4.port")
73 if len(port) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040074 real_cmd += ["-p", port]
Anand Kumriaabcaf072008-08-10 19:26:31 +010075
76 host = gitConfig("git-p4.host")
77 if len(host) > 0:
Russell Myers41799aa2012-02-22 11:16:05 -080078 real_cmd += ["-H", host]
Anand Kumriaabcaf072008-08-10 19:26:31 +010079
80 client = gitConfig("git-p4.client")
81 if len(client) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040082 real_cmd += ["-c", client]
Anand Kumriaabcaf072008-08-10 19:26:31 +010083
Lars Schneider89a6ecc2016-12-04 15:03:11 +010084 retries = gitConfigInt("git-p4.retries")
85 if retries is None:
86 # Perform 3 retries by default
87 retries = 3
Igor Kushnirbc233522016-12-29 12:22:23 +020088 if retries > 0:
89 # Provide a way to not pass this option by setting git-p4.retries to 0
90 real_cmd += ["-r", str(retries)]
Luke Diamand6de040d2011-10-16 10:47:52 -040091
92 if isinstance(cmd,basestring):
93 real_cmd = ' '.join(real_cmd) + ' ' + cmd
94 else:
95 real_cmd += cmd
Luke Diamand0ef67ac2018-06-08 21:32:45 +010096
97 # now check that we can actually talk to the server
98 global p4_access_checked
99 if not p4_access_checked:
100 p4_access_checked = True # suppress access checks in p4_check_access itself
101 p4_check_access()
102
Anand Kumria21a50752008-08-10 19:26:28 +0100103 return real_cmd
104
Luke Diamand378f7be2016-12-13 21:51:28 +0000105def git_dir(path):
106 """ Return TRUE if the given path is a git directory (/path/to/dir/.git).
107 This won't automatically add ".git" to a directory.
108 """
109 d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
110 if not d or len(d) == 0:
111 return None
112 else:
113 return d
114
Miklós Fazekasbbd84862013-03-11 17:45:29 -0400115def chdir(path, is_client_path=False):
116 """Do chdir to the given path, and set the PWD environment
117 variable for use by P4. It does not look at getcwd() output.
118 Since we're not using the shell, it is necessary to set the
119 PWD environment variable explicitly.
120
121 Normally, expand the path to force it to be absolute. This
122 addresses the use of relative path names inside P4 settings,
123 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
124 as given; it looks for .p4config using PWD.
125
126 If is_client_path, the path was handed to us directly by p4,
127 and may be a symbolic link. Do not call os.getcwd() in this
128 case, because it will cause p4 to think that PWD is not inside
129 the client path.
130 """
131
132 os.chdir(path)
133 if not is_client_path:
134 path = os.getcwd()
135 os.environ['PWD'] = path
Robert Blum053fd0c2008-08-01 12:50:03 -0700136
Lars Schneider4d25dc42015-09-26 09:55:02 +0200137def calcDiskFree():
138 """Return free space in bytes on the disk of the given dirname."""
139 if platform.system() == 'Windows':
140 free_bytes = ctypes.c_ulonglong(0)
141 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
142 return free_bytes.value
143 else:
144 st = os.statvfs(os.getcwd())
145 return st.f_bavail * st.f_frsize
146
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -0300147def die(msg):
148 if verbose:
149 raise Exception(msg)
150 else:
151 sys.stderr.write(msg + "\n")
152 sys.exit(1)
153
Luke Diamand6de040d2011-10-16 10:47:52 -0400154def write_pipe(c, stdin):
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300155 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400156 sys.stderr.write('Writing pipe: %s\n' % str(c))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300157
Luke Diamand6de040d2011-10-16 10:47:52 -0400158 expand = isinstance(c,basestring)
159 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
160 pipe = p.stdin
161 val = pipe.write(stdin)
162 pipe.close()
163 if p.wait():
164 die('Command failed: %s' % str(c))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300165
166 return val
167
Luke Diamand6de040d2011-10-16 10:47:52 -0400168def p4_write_pipe(c, stdin):
Anand Kumriad9429192008-08-14 23:40:38 +0100169 real_cmd = p4_build_cmd(c)
Luke Diamand6de040d2011-10-16 10:47:52 -0400170 return write_pipe(real_cmd, stdin)
Anand Kumriad9429192008-08-14 23:40:38 +0100171
Luke Diamand78871bf2017-04-15 11:36:08 +0100172def read_pipe_full(c):
173 """ Read output from command. Returns a tuple
174 of the return status, stdout text and stderr
175 text.
176 """
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300177 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400178 sys.stderr.write('Reading pipe: %s\n' % str(c))
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -0300179
Luke Diamand6de040d2011-10-16 10:47:52 -0400180 expand = isinstance(c,basestring)
Lars Schneider1f5f3902015-09-21 12:01:41 +0200181 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
182 (out, err) = p.communicate()
Luke Diamand78871bf2017-04-15 11:36:08 +0100183 return (p.returncode, out, err)
184
185def read_pipe(c, ignore_error=False):
186 """ Read output from command. Returns the output text on
187 success. On failure, terminates execution, unless
188 ignore_error is True, when it returns an empty string.
189 """
190 (retcode, out, err) = read_pipe_full(c)
191 if retcode != 0:
192 if ignore_error:
193 out = ""
194 else:
195 die('Command failed: %s\nError: %s' % (str(c), err))
Lars Schneider1f5f3902015-09-21 12:01:41 +0200196 return out
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300197
Luke Diamand78871bf2017-04-15 11:36:08 +0100198def read_pipe_text(c):
199 """ Read output from a command with trailing whitespace stripped.
200 On error, returns None.
201 """
202 (retcode, out, err) = read_pipe_full(c)
203 if retcode != 0:
204 return None
205 else:
206 return out.rstrip()
207
Anand Kumriad9429192008-08-14 23:40:38 +0100208def p4_read_pipe(c, ignore_error=False):
209 real_cmd = p4_build_cmd(c)
210 return read_pipe(real_cmd, ignore_error)
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300211
Han-Wen Nienhuysbce4c5f2007-05-23 17:14:33 -0300212def read_pipe_lines(c):
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300213 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400214 sys.stderr.write('Reading pipe: %s\n' % str(c))
215
216 expand = isinstance(c, basestring)
217 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
218 pipe = p.stdout
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300219 val = pipe.readlines()
Luke Diamand6de040d2011-10-16 10:47:52 -0400220 if pipe.close() or p.wait():
221 die('Command failed: %s' % str(c))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300222
223 return val
Simon Hausmanncaace112007-05-15 14:57:57 +0200224
Anand Kumria23181212008-08-10 19:26:24 +0100225def p4_read_pipe_lines(c):
226 """Specifically invoke p4 on the command supplied. """
Anand Kumria155af832008-08-10 19:26:30 +0100227 real_cmd = p4_build_cmd(c)
Anand Kumria23181212008-08-10 19:26:24 +0100228 return read_pipe_lines(real_cmd)
229
Gary Gibbons8e9497c2012-07-12 19:29:00 -0400230def p4_has_command(cmd):
231 """Ask p4 for help on this command. If it returns an error, the
232 command does not exist in this version of p4."""
233 real_cmd = p4_build_cmd(["help", cmd])
234 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
235 stderr=subprocess.PIPE)
236 p.communicate()
237 return p.returncode == 0
238
Pete Wyckoff249da4c2012-11-23 17:35:35 -0500239def p4_has_move_command():
240 """See if the move command exists, that it supports -k, and that
241 it has not been administratively disabled. The arguments
242 must be correct, but the filenames do not have to exist. Use
243 ones with wildcards so even if they exist, it will fail."""
244
245 if not p4_has_command("move"):
246 return False
247 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
248 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
249 (out, err) = p.communicate()
250 # return code will be 1 in either case
251 if err.find("Invalid option") >= 0:
252 return False
253 if err.find("disabled") >= 0:
254 return False
255 # assume it failed because @... was invalid changelist
256 return True
257
Luke Diamandcbff4b22015-11-21 09:54:40 +0000258def system(cmd, ignore_error=False):
Luke Diamand6de040d2011-10-16 10:47:52 -0400259 expand = isinstance(cmd,basestring)
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300260 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400261 sys.stderr.write("executing %s\n" % str(cmd))
Brandon Caseya235e852013-01-26 11:14:33 -0800262 retcode = subprocess.call(cmd, shell=expand)
Luke Diamandcbff4b22015-11-21 09:54:40 +0000263 if retcode and not ignore_error:
Brandon Caseya235e852013-01-26 11:14:33 -0800264 raise CalledProcessError(retcode, cmd)
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -0300265
Luke Diamandcbff4b22015-11-21 09:54:40 +0000266 return retcode
267
Anand Kumriabf9320f2008-08-10 19:26:26 +0100268def p4_system(cmd):
269 """Specifically invoke p4 as the system command. """
Anand Kumria155af832008-08-10 19:26:30 +0100270 real_cmd = p4_build_cmd(cmd)
Luke Diamand6de040d2011-10-16 10:47:52 -0400271 expand = isinstance(real_cmd, basestring)
Brandon Caseya235e852013-01-26 11:14:33 -0800272 retcode = subprocess.call(real_cmd, shell=expand)
273 if retcode:
274 raise CalledProcessError(retcode, real_cmd)
Luke Diamand6de040d2011-10-16 10:47:52 -0400275
Luke Diamand0ef67ac2018-06-08 21:32:45 +0100276def die_bad_access(s):
277 die("failure accessing depot: {0}".format(s.rstrip()))
278
279def p4_check_access(min_expiration=1):
280 """ Check if we can access Perforce - account still logged in
281 """
282 results = p4CmdList(["login", "-s"])
283
284 if len(results) == 0:
285 # should never get here: always get either some results, or a p4ExitCode
286 assert("could not parse response from perforce")
287
288 result = results[0]
289
290 if 'p4ExitCode' in result:
291 # p4 returned non-zero status, e.g. P4PORT invalid, or p4 not in path
292 die_bad_access("could not run p4")
293
294 code = result.get("code")
295 if not code:
296 # we get here if we couldn't connect and there was nothing to unmarshal
297 die_bad_access("could not connect")
298
299 elif code == "stat":
300 expiry = result.get("TicketExpiration")
301 if expiry:
302 expiry = int(expiry)
303 if expiry > min_expiration:
304 # ok to carry on
305 return
306 else:
307 die_bad_access("perforce ticket expires in {0} seconds".format(expiry))
308
309 else:
310 # account without a timeout - all ok
311 return
312
313 elif code == "error":
314 data = result.get("data")
315 if data:
316 die_bad_access("p4 error: {0}".format(data))
317 else:
318 die_bad_access("unknown error")
319 else:
320 die_bad_access("unknown error code {0}".format(code))
321
Pete Wyckoff7f0e5962013-01-26 22:11:13 -0500322_p4_version_string = None
323def p4_version_string():
324 """Read the version string, showing just the last line, which
325 hopefully is the interesting version bit.
326
327 $ p4 -V
328 Perforce - The Fast Software Configuration Management System.
329 Copyright 1995-2011 Perforce Software. All rights reserved.
330 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
331 """
332 global _p4_version_string
333 if not _p4_version_string:
334 a = p4_read_pipe_lines(["-V"])
335 _p4_version_string = a[-1].rstrip()
336 return _p4_version_string
337
Luke Diamand6de040d2011-10-16 10:47:52 -0400338def p4_integrate(src, dest):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400339 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400340
Pete Wyckoff8d7ec362012-04-29 20:57:14 -0400341def p4_sync(f, *options):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400342 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400343
344def p4_add(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400345 # forcibly add file names with wildcards
346 if wildcard_present(f):
347 p4_system(["add", "-f", f])
348 else:
349 p4_system(["add", f])
Luke Diamand6de040d2011-10-16 10:47:52 -0400350
351def p4_delete(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400352 p4_system(["delete", wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400353
Romain Picarda02b8bc2016-01-12 13:43:47 +0100354def p4_edit(f, *options):
355 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400356
357def p4_revert(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400358 p4_system(["revert", wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400359
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400360def p4_reopen(type, f):
361 p4_system(["reopen", "-t", type, wildcard_encode(f)])
Anand Kumriabf9320f2008-08-10 19:26:26 +0100362
Luke Diamand46c609e2016-12-02 22:43:19 +0000363def p4_reopen_in_change(changelist, files):
364 cmd = ["reopen", "-c", str(changelist)] + files
365 p4_system(cmd)
366
Gary Gibbons8e9497c2012-07-12 19:29:00 -0400367def p4_move(src, dest):
368 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
369
Luke Diamand1051ef02015-06-10 08:30:59 +0100370def p4_last_change():
Miguel Torroja1997e912017-07-13 09:00:35 +0200371 results = p4CmdList(["changes", "-m", "1"], skip_info=True)
Luke Diamand1051ef02015-06-10 08:30:59 +0100372 return int(results[0]['change'])
373
Luke Diamand123f6312018-05-23 23:20:26 +0100374def p4_describe(change, shelved=False):
Pete Wyckoff18fa13d2012-11-23 17:35:34 -0500375 """Make sure it returns a valid result by checking for
376 the presence of field "time". Return a dict of the
377 results."""
378
Luke Diamand123f6312018-05-23 23:20:26 +0100379 cmd = ["describe", "-s"]
380 if shelved:
381 cmd += ["-S"]
382 cmd += [str(change)]
383
384 ds = p4CmdList(cmd, skip_info=True)
Pete Wyckoff18fa13d2012-11-23 17:35:34 -0500385 if len(ds) != 1:
386 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
387
388 d = ds[0]
389
390 if "p4ExitCode" in d:
391 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
392 str(d)))
393 if "code" in d:
394 if d["code"] == "error":
395 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
396
397 if "time" not in d:
398 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
399
400 return d
401
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -0400402#
403# Canonicalize the p4 type and return a tuple of the
404# base type, plus any modifiers. See "p4 help filetypes"
405# for a list and explanation.
406#
407def split_p4_type(p4type):
David Brownb9fc6ea2007-09-19 13:12:48 -0700408
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -0400409 p4_filetypes_historical = {
410 "ctempobj": "binary+Sw",
411 "ctext": "text+C",
412 "cxtext": "text+Cx",
413 "ktext": "text+k",
414 "kxtext": "text+kx",
415 "ltext": "text+F",
416 "tempobj": "binary+FSw",
417 "ubinary": "binary+F",
418 "uresource": "resource+F",
419 "uxbinary": "binary+Fx",
420 "xbinary": "binary+x",
421 "xltext": "text+Fx",
422 "xtempobj": "binary+Swx",
423 "xtext": "text+x",
424 "xunicode": "unicode+x",
425 "xutf16": "utf16+x",
426 }
427 if p4type in p4_filetypes_historical:
428 p4type = p4_filetypes_historical[p4type]
429 mods = ""
430 s = p4type.split("+")
431 base = s[0]
432 mods = ""
433 if len(s) > 1:
434 mods = s[1]
435 return (base, mods)
436
Luke Diamand60df0712012-02-23 07:51:30 +0000437#
438# return the raw p4 type of a file (text, text+ko, etc)
439#
Pete Wyckoff79467e62014-01-21 18:16:45 -0500440def p4_type(f):
441 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
Luke Diamand60df0712012-02-23 07:51:30 +0000442 return results[0]['headType']
443
444#
445# Given a type base and modifier, return a regexp matching
446# the keywords that can be expanded in the file
447#
448def p4_keywords_regexp_for_type(base, type_mods):
449 if base in ("text", "unicode", "binary"):
450 kwords = None
451 if "ko" in type_mods:
452 kwords = 'Id|Header'
453 elif "k" in type_mods:
454 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
455 else:
456 return None
457 pattern = r"""
458 \$ # Starts with a dollar, followed by...
459 (%s) # one of the keywords, followed by...
Pete Wyckoff6b2bf412012-11-04 17:04:02 -0500460 (:[^$\n]+)? # possibly an old expansion, followed by...
Luke Diamand60df0712012-02-23 07:51:30 +0000461 \$ # another dollar
462 """ % kwords
463 return pattern
464 else:
465 return None
466
467#
468# Given a file, return a regexp matching the possible
469# RCS keywords that will be expanded, or None for files
470# with kw expansion turned off.
471#
472def p4_keywords_regexp_for_file(file):
473 if not os.path.exists(file):
474 return None
475 else:
476 (type_base, type_mods) = split_p4_type(p4_type(file))
477 return p4_keywords_regexp_for_type(type_base, type_mods)
David Brownb9fc6ea2007-09-19 13:12:48 -0700478
Chris Pettittc65b6702007-11-01 20:43:14 -0700479def setP4ExecBit(file, mode):
480 # Reopens an already open file and changes the execute bit to match
481 # the execute bit setting in the passed in mode.
482
483 p4Type = "+x"
484
485 if not isModeExec(mode):
486 p4Type = getP4OpenedType(file)
487 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
488 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
489 if p4Type[-1] == "+":
490 p4Type = p4Type[0:-1]
491
Luke Diamand6de040d2011-10-16 10:47:52 -0400492 p4_reopen(p4Type, file)
Chris Pettittc65b6702007-11-01 20:43:14 -0700493
494def getP4OpenedType(file):
495 # Returns the perforce file type for the given file.
496
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400497 result = p4_read_pipe(["opened", wildcard_encode(file)])
Blair Holloway34a0dbf2015-04-04 09:46:03 +0100498 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
Chris Pettittc65b6702007-11-01 20:43:14 -0700499 if match:
500 return match.group(1)
501 else:
Marius Storm-Olsenf3e5ae42008-03-28 15:40:40 +0100502 die("Could not determine file type for %s (result: '%s')" % (file, result))
Chris Pettittc65b6702007-11-01 20:43:14 -0700503
Luke Diamand06804c72012-04-11 17:21:24 +0200504# Return the set of all p4 labels
505def getP4Labels(depotPaths):
506 labels = set()
507 if isinstance(depotPaths,basestring):
508 depotPaths = [depotPaths]
509
510 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
511 label = l['label']
512 labels.add(label)
513
514 return labels
515
516# Return the set of all git tags
517def getGitTags():
518 gitTags = set()
519 for line in read_pipe_lines(["git", "tag"]):
520 tag = line.strip()
521 gitTags.add(tag)
522 return gitTags
523
Chris Pettittb43b0a32007-11-01 20:43:13 -0700524def diffTreePattern():
525 # This is a simple generator for the diff tree regex pattern. This could be
526 # a class variable if this and parseDiffTreeEntry were a part of a class.
527 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
528 while True:
529 yield pattern
530
531def parseDiffTreeEntry(entry):
532 """Parses a single diff tree entry into its component elements.
533
534 See git-diff-tree(1) manpage for details about the format of the diff
535 output. This method returns a dictionary with the following elements:
536
537 src_mode - The mode of the source file
538 dst_mode - The mode of the destination file
539 src_sha1 - The sha1 for the source file
540 dst_sha1 - The sha1 fr the destination file
541 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
542 status_score - The score for the status (applicable for 'C' and 'R'
543 statuses). This is None if there is no score.
544 src - The path for the source file.
545 dst - The path for the destination file. This is only present for
546 copy or renames. If it is not present, this is None.
547
548 If the pattern is not matched, None is returned."""
549
550 match = diffTreePattern().next().match(entry)
551 if match:
552 return {
553 'src_mode': match.group(1),
554 'dst_mode': match.group(2),
555 'src_sha1': match.group(3),
556 'dst_sha1': match.group(4),
557 'status': match.group(5),
558 'status_score': match.group(6),
559 'src': match.group(7),
560 'dst': match.group(10)
561 }
562 return None
563
Chris Pettittc65b6702007-11-01 20:43:14 -0700564def isModeExec(mode):
565 # Returns True if the given git mode represents an executable file,
566 # otherwise False.
567 return mode[-3:] == "755"
568
Luke Diamand55bb3e32018-06-08 21:32:46 +0100569class P4Exception(Exception):
570 """ Base class for exceptions from the p4 client """
571 def __init__(self, exit_code):
572 self.p4ExitCode = exit_code
573
574class P4ServerException(P4Exception):
575 """ Base class for exceptions where we get some kind of marshalled up result from the server """
576 def __init__(self, exit_code, p4_result):
577 super(P4ServerException, self).__init__(exit_code)
578 self.p4_result = p4_result
579 self.code = p4_result[0]['code']
580 self.data = p4_result[0]['data']
581
582class P4RequestSizeException(P4ServerException):
583 """ One of the maxresults or maxscanrows errors """
584 def __init__(self, exit_code, p4_result, limit):
585 super(P4RequestSizeException, self).__init__(exit_code, p4_result)
586 self.limit = limit
587
Chris Pettittc65b6702007-11-01 20:43:14 -0700588def isModeExecChanged(src_mode, dst_mode):
589 return isModeExec(src_mode) != isModeExec(dst_mode)
590
Luke Diamand55bb3e32018-06-08 21:32:46 +0100591def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None, skip_info=False,
592 errors_as_exceptions=False):
Luke Diamand6de040d2011-10-16 10:47:52 -0400593
594 if isinstance(cmd,basestring):
595 cmd = "-G " + cmd
596 expand = True
597 else:
598 cmd = ["-G"] + cmd
599 expand = False
600
601 cmd = p4_build_cmd(cmd)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -0300602 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400603 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
Scott Lamb9f90c732007-07-15 20:58:10 -0700604
605 # Use a temporary file to avoid deadlocks without
606 # subprocess.communicate(), which would put another copy
607 # of stdout into memory.
608 stdin_file = None
609 if stdin is not None:
610 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
Luke Diamand6de040d2011-10-16 10:47:52 -0400611 if isinstance(stdin,basestring):
612 stdin_file.write(stdin)
613 else:
614 for i in stdin:
615 stdin_file.write(i + '\n')
Scott Lamb9f90c732007-07-15 20:58:10 -0700616 stdin_file.flush()
617 stdin_file.seek(0)
618
Luke Diamand6de040d2011-10-16 10:47:52 -0400619 p4 = subprocess.Popen(cmd,
620 shell=expand,
Scott Lamb9f90c732007-07-15 20:58:10 -0700621 stdin=stdin_file,
622 stdout=subprocess.PIPE)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100623
624 result = []
625 try:
626 while True:
Scott Lamb9f90c732007-07-15 20:58:10 -0700627 entry = marshal.load(p4.stdout)
Miguel Torroja1997e912017-07-13 09:00:35 +0200628 if skip_info:
629 if 'code' in entry and entry['code'] == 'info':
630 continue
Andrew Garberc3f61632011-04-07 02:01:21 -0400631 if cb is not None:
632 cb(entry)
633 else:
634 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100635 except EOFError:
636 pass
Scott Lamb9f90c732007-07-15 20:58:10 -0700637 exitCode = p4.wait()
638 if exitCode != 0:
Luke Diamand55bb3e32018-06-08 21:32:46 +0100639 if errors_as_exceptions:
640 if len(result) > 0:
641 data = result[0].get('data')
642 if data:
643 m = re.search('Too many rows scanned \(over (\d+)\)', data)
644 if not m:
645 m = re.search('Request too large \(over (\d+)\)', data)
646
647 if m:
648 limit = int(m.group(1))
649 raise P4RequestSizeException(exitCode, result, limit)
650
651 raise P4ServerException(exitCode, result)
652 else:
653 raise P4Exception(exitCode)
654 else:
655 entry = {}
656 entry["p4ExitCode"] = exitCode
657 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100658
659 return result
660
661def p4Cmd(cmd):
662 list = p4CmdList(cmd)
663 result = {}
664 for entry in list:
665 result.update(entry)
666 return result;
667
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100668def p4Where(depotPath):
669 if not depotPath.endswith("/"):
670 depotPath += "/"
Vitor Antunescd884102015-04-21 23:49:30 +0100671 depotPathLong = depotPath + "..."
672 outputList = p4CmdList(["where", depotPathLong])
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100673 output = None
674 for entry in outputList:
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100675 if "depotFile" in entry:
Vitor Antunescd884102015-04-21 23:49:30 +0100676 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
677 # The base path always ends with "/...".
678 if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100679 output = entry
680 break
681 elif "data" in entry:
682 data = entry.get("data")
683 space = data.find(" ")
684 if data[:space] == depotPath:
685 output = entry
686 break
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100687 if output == None:
688 return ""
Simon Hausmanndc524032007-05-21 09:34:56 +0200689 if output["code"] == "error":
690 return ""
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100691 clientPath = ""
692 if "path" in output:
693 clientPath = output.get("path")
694 elif "data" in output:
695 data = output.get("data")
696 lastSpace = data.rfind(" ")
697 clientPath = data[lastSpace + 1:]
698
699 if clientPath.endswith("..."):
700 clientPath = clientPath[:-3]
701 return clientPath
702
Simon Hausmann86949ee2007-03-19 20:59:12 +0100703def currentGitBranch():
Luke Diamandeff45112017-04-15 11:36:09 +0100704 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
Simon Hausmann86949ee2007-03-19 20:59:12 +0100705
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100706def isValidGitDir(path):
Luke Diamand378f7be2016-12-13 21:51:28 +0000707 return git_dir(path) != None
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100708
Simon Hausmann463e8af2007-05-17 09:13:54 +0200709def parseRevision(ref):
Han-Wen Nienhuysb25b2062007-05-23 18:49:35 -0300710 return read_pipe("git rev-parse %s" % ref).strip()
Simon Hausmann463e8af2007-05-17 09:13:54 +0200711
Pete Wyckoff28755db2011-12-24 21:07:40 -0500712def branchExists(ref):
713 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
714 ignore_error=True)
715 return len(rev) > 0
716
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100717def extractLogMessageFromGitCommit(commit):
718 logMessage = ""
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300719
720 ## fixme: title is first line of commit, not 1st paragraph.
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100721 foundTitle = False
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300722 for log in read_pipe_lines("git cat-file commit %s" % commit):
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100723 if not foundTitle:
724 if len(log) == 1:
Simon Hausmann1c094182007-05-01 23:15:48 +0200725 foundTitle = True
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100726 continue
727
728 logMessage += log
729 return logMessage
730
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -0300731def extractSettingsGitLog(log):
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100732 values = {}
733 for line in log.split("\n"):
734 line = line.strip()
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300735 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
736 if not m:
737 continue
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100738
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300739 assignments = m.group(1).split (':')
740 for a in assignments:
741 vals = a.split ('=')
742 key = vals[0].strip()
743 val = ('='.join (vals[1:])).strip()
744 if val.endswith ('\"') and val.startswith('"'):
745 val = val[1:-1]
746
747 values[key] = val
748
Simon Hausmann845b42c2007-06-07 09:19:34 +0200749 paths = values.get("depot-paths")
750 if not paths:
751 paths = values.get("depot-path")
Simon Hausmanna3fdd572007-06-07 22:54:32 +0200752 if paths:
753 values['depot-paths'] = paths.split(',')
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -0300754 return values
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100755
Simon Hausmann8136a632007-03-22 21:27:14 +0100756def gitBranchExists(branch):
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -0300757 proc = subprocess.Popen(["git", "rev-parse", branch],
758 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
Simon Hausmanncaace112007-05-15 14:57:57 +0200759 return proc.wait() == 0;
Simon Hausmann8136a632007-03-22 21:27:14 +0100760
Luke Diamand123f6312018-05-23 23:20:26 +0100761def gitUpdateRef(ref, newvalue):
762 subprocess.check_call(["git", "update-ref", ref, newvalue])
763
764def gitDeleteRef(ref):
765 subprocess.check_call(["git", "update-ref", "-d", ref])
766
John Chapman36bd8442008-11-08 14:22:49 +1100767_gitConfig = {}
Pete Wyckoffb345d6c2013-01-26 22:11:23 -0500768
Lars Schneider692e1792015-09-26 09:54:58 +0200769def gitConfig(key, typeSpecifier=None):
John Chapman36bd8442008-11-08 14:22:49 +1100770 if not _gitConfig.has_key(key):
Lars Schneider692e1792015-09-26 09:54:58 +0200771 cmd = [ "git", "config" ]
772 if typeSpecifier:
773 cmd += [ typeSpecifier ]
774 cmd += [ key ]
Pete Wyckoffb345d6c2013-01-26 22:11:23 -0500775 s = read_pipe(cmd, ignore_error=True)
776 _gitConfig[key] = s.strip()
John Chapman36bd8442008-11-08 14:22:49 +1100777 return _gitConfig[key]
Simon Hausmann01265102007-05-25 10:36:10 +0200778
Pete Wyckoff0d609032013-01-26 22:11:24 -0500779def gitConfigBool(key):
780 """Return a bool, using git config --bool. It is True only if the
781 variable is set to true, and False if set to false or not present
782 in the config."""
783
784 if not _gitConfig.has_key(key):
Lars Schneider692e1792015-09-26 09:54:58 +0200785 _gitConfig[key] = gitConfig(key, '--bool') == "true"
Simon Hausmann062410b2007-07-18 10:56:31 +0200786 return _gitConfig[key]
787
Lars Schneidercb1dafd2015-09-26 09:54:59 +0200788def gitConfigInt(key):
789 if not _gitConfig.has_key(key):
790 cmd = [ "git", "config", "--int", key ]
Simon Hausmannb9847332007-03-20 20:54:23 +0100791 s = read_pipe(cmd, ignore_error=True)
Simon Hausmann062410b2007-07-18 10:56:31 +0200792 v = s.strip()
Lars Schneidercb1dafd2015-09-26 09:54:59 +0200793 try:
794 _gitConfig[key] = int(gitConfig(key, '--int'))
795 except ValueError:
796 _gitConfig[key] = None
Simon Hausmann062410b2007-07-18 10:56:31 +0200797 return _gitConfig[key]
798
Vitor Antunes7199cf12011-08-19 00:44:05 +0100799def gitConfigList(key):
800 if not _gitConfig.has_key(key):
Pete Wyckoff2abba302013-01-26 22:11:22 -0500801 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
George Vanburghc3c2b052017-01-25 09:17:29 +0000802 _gitConfig[key] = s.strip().splitlines()
Lars Schneider7960e702015-09-26 09:55:00 +0200803 if _gitConfig[key] == ['']:
804 _gitConfig[key] = []
Vitor Antunes7199cf12011-08-19 00:44:05 +0100805 return _gitConfig[key]
806
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500807def p4BranchesInGit(branchesAreInRemotes=True):
808 """Find all the branches whose names start with "p4/", looking
809 in remotes or heads as specified by the argument. Return
810 a dictionary of { branch: revision } for each one found.
811 The branch names are the short names, without any
812 "p4/" prefix."""
813
Simon Hausmann062410b2007-07-18 10:56:31 +0200814 branches = {}
815
816 cmdline = "git rev-parse --symbolic "
817 if branchesAreInRemotes:
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500818 cmdline += "--remotes"
Simon Hausmann062410b2007-07-18 10:56:31 +0200819 else:
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500820 cmdline += "--branches"
Simon Hausmann062410b2007-07-18 10:56:31 +0200821
822 for line in read_pipe_lines(cmdline):
823 line = line.strip()
824
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500825 # only import to p4/
826 if not line.startswith('p4/'):
Simon Hausmann062410b2007-07-18 10:56:31 +0200827 continue
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500828 # special symbolic ref to p4/master
829 if line == "p4/HEAD":
830 continue
Simon Hausmann062410b2007-07-18 10:56:31 +0200831
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500832 # strip off p4/ prefix
833 branch = line[len("p4/"):]
Simon Hausmann062410b2007-07-18 10:56:31 +0200834
835 branches[branch] = parseRevision(line)
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500836
Simon Hausmann062410b2007-07-18 10:56:31 +0200837 return branches
838
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -0500839def branch_exists(branch):
840 """Make sure that the given ref name really exists."""
841
842 cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
843 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
844 out, _ = p.communicate()
845 if p.returncode:
846 return False
847 # expect exactly one line of output: the branch name
848 return out.rstrip() == branch
849
Simon Hausmann9ceab362007-06-22 00:01:57 +0200850def findUpstreamBranchPoint(head = "HEAD"):
Simon Hausmann86506fe2007-07-18 12:40:12 +0200851 branches = p4BranchesInGit()
852 # map from depot-path to branch name
853 branchByDepotPath = {}
854 for branch in branches.keys():
855 tip = branches[branch]
856 log = extractLogMessageFromGitCommit(tip)
857 settings = extractSettingsGitLog(log)
858 if settings.has_key("depot-paths"):
859 paths = ",".join(settings["depot-paths"])
860 branchByDepotPath[paths] = "remotes/p4/" + branch
861
Simon Hausmann27d2d812007-06-12 14:31:59 +0200862 settings = None
Simon Hausmann27d2d812007-06-12 14:31:59 +0200863 parent = 0
864 while parent < 65535:
Simon Hausmann9ceab362007-06-22 00:01:57 +0200865 commit = head + "~%s" % parent
Simon Hausmann27d2d812007-06-12 14:31:59 +0200866 log = extractLogMessageFromGitCommit(commit)
867 settings = extractSettingsGitLog(log)
Simon Hausmann86506fe2007-07-18 12:40:12 +0200868 if settings.has_key("depot-paths"):
869 paths = ",".join(settings["depot-paths"])
870 if branchByDepotPath.has_key(paths):
871 return [branchByDepotPath[paths], settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +0200872
Simon Hausmann86506fe2007-07-18 12:40:12 +0200873 parent = parent + 1
Simon Hausmann27d2d812007-06-12 14:31:59 +0200874
Simon Hausmann86506fe2007-07-18 12:40:12 +0200875 return ["", settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +0200876
Simon Hausmann5ca44612007-08-24 17:44:16 +0200877def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
878 if not silent:
879 print ("Creating/updating branch(es) in %s based on origin branch(es)"
880 % localRefPrefix)
881
882 originPrefix = "origin/p4/"
883
884 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
885 line = line.strip()
886 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
887 continue
888
889 headName = line[len(originPrefix):]
890 remoteHead = localRefPrefix + headName
891 originHead = line
892
893 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
894 if (not original.has_key('depot-paths')
895 or not original.has_key('change')):
896 continue
897
898 update = False
899 if not gitBranchExists(remoteHead):
900 if verbose:
901 print "creating %s" % remoteHead
902 update = True
903 else:
904 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
905 if settings.has_key('change') > 0:
906 if settings['depot-paths'] == original['depot-paths']:
907 originP4Change = int(original['change'])
908 p4Change = int(settings['change'])
909 if originP4Change > p4Change:
910 print ("%s (%s) is newer than %s (%s). "
911 "Updating p4 branch from origin."
912 % (originHead, originP4Change,
913 remoteHead, p4Change))
914 update = True
915 else:
916 print ("Ignoring: %s was imported from %s while "
917 "%s was imported from %s"
918 % (originHead, ','.join(original['depot-paths']),
919 remoteHead, ','.join(settings['depot-paths'])))
920
921 if update:
922 system("git update-ref %s %s" % (remoteHead, originHead))
923
924def originP4BranchesExist():
925 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
926
Simon Hausmann4f6432d2007-08-26 15:56:36 +0200927
Luke Diamand1051ef02015-06-10 08:30:59 +0100928def p4ParseNumericChangeRange(parts):
929 changeStart = int(parts[0][1:])
930 if parts[1] == '#head':
931 changeEnd = p4_last_change()
932 else:
933 changeEnd = int(parts[1])
934
935 return (changeStart, changeEnd)
936
937def chooseBlockSize(blockSize):
938 if blockSize:
939 return blockSize
940 else:
941 return defaultBlockSize
942
943def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
944 assert depotPaths
945
946 # Parse the change range into start and end. Try to find integer
947 # revision ranges as these can be broken up into blocks to avoid
948 # hitting server-side limits (maxrows, maxscanresults). But if
949 # that doesn't work, fall back to using the raw revision specifier
950 # strings, without using block mode.
951
Lex Spoon96b2d542015-04-20 11:00:20 -0400952 if changeRange is None or changeRange == '':
Luke Diamand1051ef02015-06-10 08:30:59 +0100953 changeStart = 1
954 changeEnd = p4_last_change()
955 block_size = chooseBlockSize(requestedBlockSize)
Lex Spoon96b2d542015-04-20 11:00:20 -0400956 else:
957 parts = changeRange.split(',')
958 assert len(parts) == 2
Luke Diamand1051ef02015-06-10 08:30:59 +0100959 try:
960 (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
961 block_size = chooseBlockSize(requestedBlockSize)
Luke Diamand8fa0abf2018-06-08 21:32:47 +0100962 except ValueError:
Luke Diamand1051ef02015-06-10 08:30:59 +0100963 changeStart = parts[0][1:]
964 changeEnd = parts[1]
965 if requestedBlockSize:
966 die("cannot use --changes-block-size with non-numeric revisions")
967 block_size = None
Lex Spoon96b2d542015-04-20 11:00:20 -0400968
George Vanburgh9943e5b2016-12-17 22:11:23 +0000969 changes = set()
Lex Spoon96b2d542015-04-20 11:00:20 -0400970
Sam Hocevar1f90a642015-12-19 09:39:40 +0000971 # Retrieve changes a block at a time, to prevent running
Luke Diamand3deed5e2018-06-08 21:32:48 +0100972 # into a MaxResults/MaxScanRows error from the server. If
973 # we _do_ hit one of those errors, turn down the block size
Luke Diamand1051ef02015-06-10 08:30:59 +0100974
Sam Hocevar1f90a642015-12-19 09:39:40 +0000975 while True:
976 cmd = ['changes']
Luke Diamand1051ef02015-06-10 08:30:59 +0100977
Sam Hocevar1f90a642015-12-19 09:39:40 +0000978 if block_size:
979 end = min(changeEnd, changeStart + block_size)
980 revisionRange = "%d,%d" % (changeStart, end)
981 else:
982 revisionRange = "%s,%s" % (changeStart, changeEnd)
Luke Diamand1051ef02015-06-10 08:30:59 +0100983
Sam Hocevar1f90a642015-12-19 09:39:40 +0000984 for p in depotPaths:
Luke Diamand1051ef02015-06-10 08:30:59 +0100985 cmd += ["%s...@%s" % (p, revisionRange)]
986
Luke Diamand3deed5e2018-06-08 21:32:48 +0100987 # fetch the changes
988 try:
989 result = p4CmdList(cmd, errors_as_exceptions=True)
990 except P4RequestSizeException as e:
991 if not block_size:
992 block_size = e.limit
993 elif block_size > e.limit:
994 block_size = e.limit
995 else:
996 block_size = max(2, block_size // 2)
997
998 if verbose: print("block size error, retrying with block size {0}".format(block_size))
999 continue
1000 except P4Exception as e:
1001 die('Error retrieving changes description ({0})'.format(e.p4ExitCode))
1002
Sam Hocevar1f90a642015-12-19 09:39:40 +00001003 # Insert changes in chronological order
Luke Diamand3deed5e2018-06-08 21:32:48 +01001004 for entry in reversed(result):
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001005 if not entry.has_key('change'):
1006 continue
1007 changes.add(int(entry['change']))
Luke Diamand1051ef02015-06-10 08:30:59 +01001008
Sam Hocevar1f90a642015-12-19 09:39:40 +00001009 if not block_size:
1010 break
Luke Diamand1051ef02015-06-10 08:30:59 +01001011
Sam Hocevar1f90a642015-12-19 09:39:40 +00001012 if end >= changeEnd:
1013 break
Luke Diamand1051ef02015-06-10 08:30:59 +01001014
Sam Hocevar1f90a642015-12-19 09:39:40 +00001015 changeStart = end + 1
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001016
Sam Hocevar1f90a642015-12-19 09:39:40 +00001017 changes = sorted(changes)
1018 return changes
Simon Hausmann4f6432d2007-08-26 15:56:36 +02001019
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01001020def p4PathStartsWith(path, prefix):
1021 # This method tries to remedy a potential mixed-case issue:
1022 #
1023 # If UserA adds //depot/DirA/file1
1024 # and UserB adds //depot/dira/file2
1025 #
1026 # we may or may not have a problem. If you have core.ignorecase=true,
1027 # we treat DirA and dira as the same directory
Pete Wyckoff0d609032013-01-26 22:11:24 -05001028 if gitConfigBool("core.ignorecase"):
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01001029 return path.lower().startswith(prefix.lower())
1030 return path.startswith(prefix)
1031
Pete Wyckoff543987b2012-02-25 20:06:25 -05001032def getClientSpec():
1033 """Look at the p4 client spec, create a View() object that contains
1034 all the mappings, and return it."""
1035
1036 specList = p4CmdList("client -o")
1037 if len(specList) != 1:
1038 die('Output from "client -o" is %d lines, expecting 1' %
1039 len(specList))
1040
1041 # dictionary of all client parameters
1042 entry = specList[0]
1043
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001044 # the //client/ name
1045 client_name = entry["Client"]
1046
Pete Wyckoff543987b2012-02-25 20:06:25 -05001047 # just the keys that start with "View"
1048 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
1049
1050 # hold this new View
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001051 view = View(client_name)
Pete Wyckoff543987b2012-02-25 20:06:25 -05001052
1053 # append the lines, in order, to the view
1054 for view_num in range(len(view_keys)):
1055 k = "View%d" % view_num
1056 if k not in view_keys:
1057 die("Expected view key %s missing" % k)
1058 view.append(entry[k])
1059
1060 return view
1061
1062def getClientRoot():
1063 """Grab the client directory."""
1064
1065 output = p4CmdList("client -o")
1066 if len(output) != 1:
1067 die('Output from "client -o" is %d lines, expecting 1' % len(output))
1068
1069 entry = output[0]
1070 if "Root" not in entry:
1071 die('Client has no "Root"')
1072
1073 return entry["Root"]
1074
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001075#
1076# P4 wildcards are not allowed in filenames. P4 complains
1077# if you simply add them, but you can force it with "-f", in
1078# which case it translates them into %xx encoding internally.
1079#
1080def wildcard_decode(path):
1081 # Search for and fix just these four characters. Do % last so
1082 # that fixing it does not inadvertently create new %-escapes.
1083 # Cannot have * in a filename in windows; untested as to
1084 # what p4 would do in such a case.
1085 if not platform.system() == "Windows":
1086 path = path.replace("%2A", "*")
1087 path = path.replace("%23", "#") \
1088 .replace("%40", "@") \
1089 .replace("%25", "%")
1090 return path
1091
1092def wildcard_encode(path):
1093 # do % first to avoid double-encoding the %s introduced here
1094 path = path.replace("%", "%25") \
1095 .replace("*", "%2A") \
1096 .replace("#", "%23") \
1097 .replace("@", "%40")
1098 return path
1099
1100def wildcard_present(path):
Brandon Casey598354c2013-01-26 11:14:32 -08001101 m = re.search("[*#@%]", path)
1102 return m is not None
Pete Wyckoff9d7d4462012-04-29 20:57:17 -04001103
Lars Schneidera5db4b12015-09-26 09:55:03 +02001104class LargeFileSystem(object):
1105 """Base class for large file system support."""
1106
1107 def __init__(self, writeToGitStream):
1108 self.largeFiles = set()
1109 self.writeToGitStream = writeToGitStream
1110
1111 def generatePointer(self, cloneDestination, contentFile):
1112 """Return the content of a pointer file that is stored in Git instead of
1113 the actual content."""
1114 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
1115
1116 def pushFile(self, localLargeFile):
1117 """Push the actual content which is not stored in the Git repository to
1118 a server."""
1119 assert False, "Method 'pushFile' required in " + self.__class__.__name__
1120
1121 def hasLargeFileExtension(self, relPath):
1122 return reduce(
1123 lambda a, b: a or b,
1124 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1125 False
1126 )
1127
1128 def generateTempFile(self, contents):
1129 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1130 for d in contents:
1131 contentFile.write(d)
1132 contentFile.close()
1133 return contentFile.name
1134
1135 def exceedsLargeFileThreshold(self, relPath, contents):
1136 if gitConfigInt('git-p4.largeFileThreshold'):
1137 contentsSize = sum(len(d) for d in contents)
1138 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1139 return True
1140 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1141 contentsSize = sum(len(d) for d in contents)
1142 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1143 return False
1144 contentTempFile = self.generateTempFile(contents)
1145 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1146 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
1147 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1148 zf.close()
1149 compressedContentsSize = zf.infolist()[0].compress_size
1150 os.remove(contentTempFile)
1151 os.remove(compressedContentFile.name)
1152 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1153 return True
1154 return False
1155
1156 def addLargeFile(self, relPath):
1157 self.largeFiles.add(relPath)
1158
1159 def removeLargeFile(self, relPath):
1160 self.largeFiles.remove(relPath)
1161
1162 def isLargeFile(self, relPath):
1163 return relPath in self.largeFiles
1164
1165 def processContent(self, git_mode, relPath, contents):
1166 """Processes the content of git fast import. This method decides if a
1167 file is stored in the large file system and handles all necessary
1168 steps."""
1169 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1170 contentTempFile = self.generateTempFile(contents)
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001171 (pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1172 if pointer_git_mode:
1173 git_mode = pointer_git_mode
1174 if localLargeFile:
1175 # Move temp file to final location in large file system
1176 largeFileDir = os.path.dirname(localLargeFile)
1177 if not os.path.isdir(largeFileDir):
1178 os.makedirs(largeFileDir)
1179 shutil.move(contentTempFile, localLargeFile)
1180 self.addLargeFile(relPath)
1181 if gitConfigBool('git-p4.largeFilePush'):
1182 self.pushFile(localLargeFile)
1183 if verbose:
1184 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
Lars Schneidera5db4b12015-09-26 09:55:03 +02001185 return (git_mode, contents)
1186
1187class MockLFS(LargeFileSystem):
1188 """Mock large file system for testing."""
1189
1190 def generatePointer(self, contentFile):
1191 """The pointer content is the original content prefixed with "pointer-".
1192 The local filename of the large file storage is derived from the file content.
1193 """
1194 with open(contentFile, 'r') as f:
1195 content = next(f)
1196 gitMode = '100644'
1197 pointerContents = 'pointer-' + content
1198 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1199 return (gitMode, pointerContents, localLargeFile)
1200
1201 def pushFile(self, localLargeFile):
1202 """The remote filename of the large file storage is the same as the local
1203 one but in a different directory.
1204 """
1205 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1206 if not os.path.exists(remotePath):
1207 os.makedirs(remotePath)
1208 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1209
Lars Schneiderb47d8072015-09-26 09:55:04 +02001210class GitLFS(LargeFileSystem):
1211 """Git LFS as backend for the git-p4 large file system.
1212 See https://git-lfs.github.com/ for details."""
1213
1214 def __init__(self, *args):
1215 LargeFileSystem.__init__(self, *args)
1216 self.baseGitAttributes = []
1217
1218 def generatePointer(self, contentFile):
1219 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1220 mode and content which is stored in the Git repository instead of
1221 the actual content. Return also the new location of the actual
1222 content.
1223 """
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001224 if os.path.getsize(contentFile) == 0:
1225 return (None, '', None)
1226
Lars Schneiderb47d8072015-09-26 09:55:04 +02001227 pointerProcess = subprocess.Popen(
1228 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1229 stdout=subprocess.PIPE
1230 )
1231 pointerFile = pointerProcess.stdout.read()
1232 if pointerProcess.wait():
1233 os.remove(contentFile)
1234 die('git-lfs pointer command failed. Did you install the extension?')
Lars Schneider82f25672016-04-28 08:26:33 +02001235
1236 # Git LFS removed the preamble in the output of the 'pointer' command
1237 # starting from version 1.2.0. Check for the preamble here to support
1238 # earlier versions.
1239 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1240 if pointerFile.startswith('Git LFS pointer for'):
1241 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1242
1243 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001244 localLargeFile = os.path.join(
1245 os.getcwd(),
1246 '.git', 'lfs', 'objects', oid[:2], oid[2:4],
1247 oid,
1248 )
1249 # LFS Spec states that pointer files should not have the executable bit set.
1250 gitMode = '100644'
Lars Schneider82f25672016-04-28 08:26:33 +02001251 return (gitMode, pointerFile, localLargeFile)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001252
1253 def pushFile(self, localLargeFile):
1254 uploadProcess = subprocess.Popen(
1255 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1256 )
1257 if uploadProcess.wait():
1258 die('git-lfs push command failed. Did you define a remote?')
1259
1260 def generateGitAttributes(self):
1261 return (
1262 self.baseGitAttributes +
1263 [
1264 '\n',
1265 '#\n',
1266 '# Git LFS (see https://git-lfs.github.com/)\n',
1267 '#\n',
1268 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001269 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001270 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1271 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001272 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001273 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1274 ]
1275 )
1276
1277 def addLargeFile(self, relPath):
1278 LargeFileSystem.addLargeFile(self, relPath)
1279 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1280
1281 def removeLargeFile(self, relPath):
1282 LargeFileSystem.removeLargeFile(self, relPath)
1283 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1284
1285 def processContent(self, git_mode, relPath, contents):
1286 if relPath == '.gitattributes':
1287 self.baseGitAttributes = contents
1288 return (git_mode, self.generateGitAttributes())
1289 else:
1290 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1291
Simon Hausmannb9847332007-03-20 20:54:23 +01001292class Command:
1293 def __init__(self):
1294 self.usage = "usage: %prog [options]"
Simon Hausmann8910ac02007-03-26 08:18:55 +02001295 self.needsGit = True
Luke Diamand6a10b6a2012-04-24 09:33:23 +01001296 self.verbose = False
Simon Hausmannb9847332007-03-20 20:54:23 +01001297
Luke Diamand8cf422d2017-12-21 11:06:14 +00001298 # This is required for the "append" cloneExclude action
1299 def ensure_value(self, attr, value):
1300 if not hasattr(self, attr) or getattr(self, attr) is None:
1301 setattr(self, attr, value)
1302 return getattr(self, attr)
1303
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001304class P4UserMap:
1305 def __init__(self):
1306 self.userMapFromPerforceServer = False
Luke Diamandaffb4742012-01-19 09:52:27 +00001307 self.myP4UserId = None
1308
1309 def p4UserId(self):
1310 if self.myP4UserId:
1311 return self.myP4UserId
1312
1313 results = p4CmdList("user -o")
1314 for r in results:
1315 if r.has_key('User'):
1316 self.myP4UserId = r['User']
1317 return r['User']
1318 die("Could not find your p4 user id")
1319
1320 def p4UserIsMe(self, p4User):
1321 # return True if the given p4 user is actually me
1322 me = self.p4UserId()
1323 if not p4User or p4User != me:
1324 return False
1325 else:
1326 return True
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001327
1328 def getUserCacheFilename(self):
1329 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1330 return home + "/.gitp4-usercache.txt"
1331
1332 def getUserMapFromPerforceServer(self):
1333 if self.userMapFromPerforceServer:
1334 return
1335 self.users = {}
1336 self.emails = {}
1337
1338 for output in p4CmdList("users"):
1339 if not output.has_key("User"):
1340 continue
1341 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1342 self.emails[output["Email"]] = output["User"]
1343
Lars Schneider10d08a12016-03-01 11:49:56 +01001344 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1345 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1346 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1347 if mapUser and len(mapUser[0]) == 3:
1348 user = mapUser[0][0]
1349 fullname = mapUser[0][1]
1350 email = mapUser[0][2]
1351 self.users[user] = fullname + " <" + email + ">"
1352 self.emails[email] = user
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001353
1354 s = ''
1355 for (key, val) in self.users.items():
1356 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1357
1358 open(self.getUserCacheFilename(), "wb").write(s)
1359 self.userMapFromPerforceServer = True
1360
1361 def loadUserMapFromCache(self):
1362 self.users = {}
1363 self.userMapFromPerforceServer = False
1364 try:
1365 cache = open(self.getUserCacheFilename(), "rb")
1366 lines = cache.readlines()
1367 cache.close()
1368 for line in lines:
1369 entry = line.strip().split("\t")
1370 self.users[entry[0]] = entry[1]
1371 except IOError:
1372 self.getUserMapFromPerforceServer()
1373
Simon Hausmannb9847332007-03-20 20:54:23 +01001374class P4Debug(Command):
Simon Hausmann86949ee2007-03-19 20:59:12 +01001375 def __init__(self):
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001376 Command.__init__(self)
Luke Diamand6a10b6a2012-04-24 09:33:23 +01001377 self.options = []
Simon Hausmannc8c39112007-03-19 21:02:30 +01001378 self.description = "A tool to debug the output of p4 -G."
Simon Hausmann8910ac02007-03-26 08:18:55 +02001379 self.needsGit = False
Simon Hausmann86949ee2007-03-19 20:59:12 +01001380
1381 def run(self, args):
Han-Wen Nienhuysb1ce9442007-05-23 18:49:35 -03001382 j = 0
Luke Diamand6de040d2011-10-16 10:47:52 -04001383 for output in p4CmdList(args):
Han-Wen Nienhuysb1ce9442007-05-23 18:49:35 -03001384 print 'Element: %d' % j
1385 j += 1
Simon Hausmann86949ee2007-03-19 20:59:12 +01001386 print output
Simon Hausmannb9847332007-03-20 20:54:23 +01001387 return True
Simon Hausmann86949ee2007-03-19 20:59:12 +01001388
Simon Hausmann58346842007-05-21 22:57:06 +02001389class P4RollBack(Command):
1390 def __init__(self):
1391 Command.__init__(self)
1392 self.options = [
Simon Hausmann0c66a782007-05-23 20:07:57 +02001393 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
Simon Hausmann58346842007-05-21 22:57:06 +02001394 ]
1395 self.description = "A tool to debug the multi-branch import. Don't use :)"
Simon Hausmann0c66a782007-05-23 20:07:57 +02001396 self.rollbackLocalBranches = False
Simon Hausmann58346842007-05-21 22:57:06 +02001397
1398 def run(self, args):
1399 if len(args) != 1:
1400 return False
1401 maxChange = int(args[0])
Simon Hausmann0c66a782007-05-23 20:07:57 +02001402
Simon Hausmannad192f22007-05-23 23:44:19 +02001403 if "p4ExitCode" in p4Cmd("changes -m 1"):
Simon Hausmann66a2f522007-05-23 23:40:48 +02001404 die("Problems executing p4");
1405
Simon Hausmann0c66a782007-05-23 20:07:57 +02001406 if self.rollbackLocalBranches:
1407 refPrefix = "refs/heads/"
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -03001408 lines = read_pipe_lines("git rev-parse --symbolic --branches")
Simon Hausmann0c66a782007-05-23 20:07:57 +02001409 else:
1410 refPrefix = "refs/remotes/"
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -03001411 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
Simon Hausmann0c66a782007-05-23 20:07:57 +02001412
1413 for line in lines:
1414 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
Han-Wen Nienhuysb25b2062007-05-23 18:49:35 -03001415 line = line.strip()
1416 ref = refPrefix + line
Simon Hausmann58346842007-05-21 22:57:06 +02001417 log = extractLogMessageFromGitCommit(ref)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001418 settings = extractSettingsGitLog(log)
1419
1420 depotPaths = settings['depot-paths']
1421 change = settings['change']
1422
Simon Hausmann58346842007-05-21 22:57:06 +02001423 changed = False
Simon Hausmann52102d42007-05-21 23:44:24 +02001424
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03001425 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
1426 for p in depotPaths]))) == 0:
Simon Hausmann52102d42007-05-21 23:44:24 +02001427 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1428 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1429 continue
1430
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001431 while change and int(change) > maxChange:
Simon Hausmann58346842007-05-21 22:57:06 +02001432 changed = True
Simon Hausmann52102d42007-05-21 23:44:24 +02001433 if self.verbose:
1434 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
Simon Hausmann58346842007-05-21 22:57:06 +02001435 system("git update-ref %s \"%s^\"" % (ref, ref))
1436 log = extractLogMessageFromGitCommit(ref)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001437 settings = extractSettingsGitLog(log)
1438
1439
1440 depotPaths = settings['depot-paths']
1441 change = settings['change']
Simon Hausmann58346842007-05-21 22:57:06 +02001442
1443 if changed:
Simon Hausmann52102d42007-05-21 23:44:24 +02001444 print "%s rewound to %s" % (ref, change)
Simon Hausmann58346842007-05-21 22:57:06 +02001445
1446 return True
1447
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001448class P4Submit(Command, P4UserMap):
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001449
1450 conflict_behavior_choices = ("ask", "skip", "quit")
1451
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001452 def __init__(self):
Simon Hausmannb9847332007-03-20 20:54:23 +01001453 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001454 P4UserMap.__init__(self)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001455 self.options = [
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001456 optparse.make_option("--origin", dest="origin"),
Vitor Antunesae901092011-02-20 01:18:24 +00001457 optparse.make_option("-M", dest="detectRenames", action="store_true"),
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001458 # preserve the user, requires relevant p4 permissions
1459 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02001460 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
Pete Wyckoffef739f02012-09-09 16:16:11 -04001461 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001462 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001463 optparse.make_option("--conflict", dest="conflict_behavior",
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001464 choices=self.conflict_behavior_choices),
1465 optparse.make_option("--branch", dest="branch"),
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001466 optparse.make_option("--shelve", dest="shelve", action="store_true",
1467 help="Shelve instead of submit. Shelved files are reverted, "
1468 "restoring the workspace to the state before the shelve"),
Luke Diamand8cf422d2017-12-21 11:06:14 +00001469 optparse.make_option("--update-shelve", dest="update_shelve", action="append", type="int",
Luke Diamand46c609e2016-12-02 22:43:19 +00001470 metavar="CHANGELIST",
Luke Diamand8cf422d2017-12-21 11:06:14 +00001471 help="update an existing shelved changelist, implies --shelve, "
Romain Merlandf55b87c2018-06-01 09:46:14 +02001472 "repeat in-order for multiple shelved changelists"),
1473 optparse.make_option("--commit", dest="commit", metavar="COMMIT",
1474 help="submit only the specified commit(s), one commit or xxx..xxx"),
1475 optparse.make_option("--disable-rebase", dest="disable_rebase", action="store_true",
1476 help="Disable rebase after submit is completed. Can be useful if you "
Luke Diamandb9d34db2018-06-08 21:32:44 +01001477 "work from a local git branch that is not master"),
1478 optparse.make_option("--disable-p4sync", dest="disable_p4sync", action="store_true",
1479 help="Skip Perforce sync of p4/master after submit or shelve"),
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001480 ]
1481 self.description = "Submit changes from git to the perforce depot."
Simon Hausmannc9b50e62007-03-29 19:15:24 +02001482 self.usage += " [name of git branch to submit into perforce depot]"
Simon Hausmann95124972007-03-23 09:16:07 +01001483 self.origin = ""
Vitor Antunesae901092011-02-20 01:18:24 +00001484 self.detectRenames = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05001485 self.preserveUser = gitConfigBool("git-p4.preserveUser")
Pete Wyckoffef739f02012-09-09 16:16:11 -04001486 self.dry_run = False
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001487 self.shelve = False
Luke Diamand8cf422d2017-12-21 11:06:14 +00001488 self.update_shelve = list()
Romain Merlandf55b87c2018-06-01 09:46:14 +02001489 self.commit = ""
Luke Diamand3b3477e2018-06-08 21:32:43 +01001490 self.disable_rebase = gitConfigBool("git-p4.disableRebase")
Luke Diamandb9d34db2018-06-08 21:32:44 +01001491 self.disable_p4sync = gitConfigBool("git-p4.disableP4Sync")
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001492 self.prepare_p4_only = False
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001493 self.conflict_behavior = None
Marius Storm-Olsenf7baba82007-06-07 14:07:01 +02001494 self.isWindows = (platform.system() == "Windows")
Luke Diamand06804c72012-04-11 17:21:24 +02001495 self.exportLabels = False
Pete Wyckoff249da4c2012-11-23 17:35:35 -05001496 self.p4HasMoveCommand = p4_has_move_command()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001497 self.branch = None
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001498
Lars Schneidera5db4b12015-09-26 09:55:03 +02001499 if gitConfig('git-p4.largeFileSystem'):
1500 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1501
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001502 def check(self):
1503 if len(p4CmdList("opened ...")) > 0:
1504 die("You have files opened with perforce! Close them before starting the sync.")
1505
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001506 def separate_jobs_from_description(self, message):
1507 """Extract and return a possible Jobs field in the commit
1508 message. It goes into a separate section in the p4 change
1509 specification.
1510
1511 A jobs line starts with "Jobs:" and looks like a new field
1512 in a form. Values are white-space separated on the same
1513 line or on following lines that start with a tab.
1514
1515 This does not parse and extract the full git commit message
1516 like a p4 form. It just sees the Jobs: line as a marker
1517 to pass everything from then on directly into the p4 form,
1518 but outside the description section.
1519
1520 Return a tuple (stripped log message, jobs string)."""
1521
1522 m = re.search(r'^Jobs:', message, re.MULTILINE)
1523 if m is None:
1524 return (message, None)
1525
1526 jobtext = message[m.start():]
1527 stripped_message = message[:m.start()].rstrip()
1528 return (stripped_message, jobtext)
1529
1530 def prepareLogMessage(self, template, message, jobs):
1531 """Edits the template returned from "p4 change -o" to insert
1532 the message in the Description field, and the jobs text in
1533 the Jobs field."""
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001534 result = ""
1535
Simon Hausmannedae1e22008-02-19 09:29:06 +01001536 inDescriptionSection = False
1537
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001538 for line in template.split("\n"):
1539 if line.startswith("#"):
1540 result += line + "\n"
1541 continue
1542
Simon Hausmannedae1e22008-02-19 09:29:06 +01001543 if inDescriptionSection:
Michael Horowitzc9dbab02011-02-25 21:31:13 -05001544 if line.startswith("Files:") or line.startswith("Jobs:"):
Simon Hausmannedae1e22008-02-19 09:29:06 +01001545 inDescriptionSection = False
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001546 # insert Jobs section
1547 if jobs:
1548 result += jobs + "\n"
Simon Hausmannedae1e22008-02-19 09:29:06 +01001549 else:
1550 continue
1551 else:
1552 if line.startswith("Description:"):
1553 inDescriptionSection = True
1554 line += "\n"
1555 for messageLine in message.split("\n"):
1556 line += "\t" + messageLine + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001557
Simon Hausmannedae1e22008-02-19 09:29:06 +01001558 result += line + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001559
1560 return result
1561
Luke Diamand60df0712012-02-23 07:51:30 +00001562 def patchRCSKeywords(self, file, pattern):
1563 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1564 (handle, outFileName) = tempfile.mkstemp(dir='.')
1565 try:
1566 outFile = os.fdopen(handle, "w+")
1567 inFile = open(file, "r")
1568 regexp = re.compile(pattern, re.VERBOSE)
1569 for line in inFile.readlines():
1570 line = regexp.sub(r'$\1$', line)
1571 outFile.write(line)
1572 inFile.close()
1573 outFile.close()
1574 # Forcibly overwrite the original file
1575 os.unlink(file)
1576 shutil.move(outFileName, file)
1577 except:
1578 # cleanup our temporary file
1579 os.unlink(outFileName)
1580 print "Failed to strip RCS keywords in %s" % file
1581 raise
1582
1583 print "Patched up RCS keywords in %s" % file
1584
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001585 def p4UserForCommit(self,id):
1586 # Return the tuple (perforce user,git email) for a given git commit id
1587 self.getUserMapFromPerforceServer()
Pete Wyckoff9bf28852013-01-26 22:11:20 -05001588 gitEmail = read_pipe(["git", "log", "--max-count=1",
1589 "--format=%ae", id])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001590 gitEmail = gitEmail.strip()
1591 if not self.emails.has_key(gitEmail):
1592 return (None,gitEmail)
1593 else:
1594 return (self.emails[gitEmail],gitEmail)
1595
1596 def checkValidP4Users(self,commits):
1597 # check if any git authors cannot be mapped to p4 users
1598 for id in commits:
1599 (user,email) = self.p4UserForCommit(id)
1600 if not user:
1601 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
Pete Wyckoff0d609032013-01-26 22:11:24 -05001602 if gitConfigBool("git-p4.allowMissingP4Users"):
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001603 print "%s" % msg
1604 else:
1605 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1606
1607 def lastP4Changelist(self):
1608 # Get back the last changelist number submitted in this client spec. This
1609 # then gets used to patch up the username in the change. If the same
1610 # client spec is being used by multiple processes then this might go
1611 # wrong.
1612 results = p4CmdList("client -o") # find the current client
1613 client = None
1614 for r in results:
1615 if r.has_key('Client'):
1616 client = r['Client']
1617 break
1618 if not client:
1619 die("could not get client spec")
Luke Diamand6de040d2011-10-16 10:47:52 -04001620 results = p4CmdList(["changes", "-c", client, "-m", "1"])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001621 for r in results:
1622 if r.has_key('change'):
1623 return r['change']
1624 die("Could not get changelist number for last submit - cannot patch up user details")
1625
1626 def modifyChangelistUser(self, changelist, newUser):
1627 # fixup the user field of a changelist after it has been submitted.
1628 changes = p4CmdList("change -o %s" % changelist)
Luke Diamandecdba362011-05-07 11:19:43 +01001629 if len(changes) != 1:
1630 die("Bad output from p4 change modifying %s to user %s" %
1631 (changelist, newUser))
1632
1633 c = changes[0]
1634 if c['User'] == newUser: return # nothing to do
1635 c['User'] = newUser
1636 input = marshal.dumps(c)
1637
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001638 result = p4CmdList("change -f -i", stdin=input)
1639 for r in result:
1640 if r.has_key('code'):
1641 if r['code'] == 'error':
1642 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1643 if r.has_key('data'):
1644 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1645 return
1646 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1647
1648 def canChangeChangelists(self):
1649 # check to see if we have p4 admin or super-user permissions, either of
1650 # which are required to modify changelists.
Luke Diamand52a48802012-01-19 09:52:25 +00001651 results = p4CmdList(["protects", self.depotPath])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001652 for r in results:
1653 if r.has_key('perm'):
1654 if r['perm'] == 'admin':
1655 return 1
1656 if r['perm'] == 'super':
1657 return 1
1658 return 0
1659
Luke Diamand46c609e2016-12-02 22:43:19 +00001660 def prepareSubmitTemplate(self, changelist=None):
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001661 """Run "p4 change -o" to grab a change specification template.
1662 This does not use "p4 -G", as it is nice to keep the submission
1663 template in original order, since a human might edit it.
1664
1665 Remove lines in the Files section that show changes to files
1666 outside the depot path we're committing into."""
1667
Sam Hocevarcbc69242015-12-19 09:39:39 +00001668 [upstream, settings] = findUpstreamBranchPoint()
1669
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001670 template = """\
1671# A Perforce Change Specification.
1672#
1673# Change: The change number. 'new' on a new changelist.
1674# Date: The date this specification was last modified.
1675# Client: The client on which the changelist was created. Read-only.
1676# User: The user who created the changelist.
1677# Status: Either 'pending' or 'submitted'. Read-only.
1678# Type: Either 'public' or 'restricted'. Default is 'public'.
1679# Description: Comments about the changelist. Required.
1680# Jobs: What opened jobs are to be closed by this changelist.
1681# You may delete jobs from this list. (New changelists only.)
1682# Files: What opened files from the default changelist are to be added
1683# to this changelist. You may delete files from this list.
1684# (New changelists only.)
1685"""
1686 files_list = []
Simon Hausmannea99c3a2007-08-08 17:06:55 +02001687 inFilesSection = False
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001688 change_entry = None
Luke Diamand46c609e2016-12-02 22:43:19 +00001689 args = ['change', '-o']
1690 if changelist:
1691 args.append(str(changelist))
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001692 for entry in p4CmdList(args):
1693 if not entry.has_key('code'):
1694 continue
1695 if entry['code'] == 'stat':
1696 change_entry = entry
1697 break
1698 if not change_entry:
1699 die('Failed to decode output of p4 change -o')
1700 for key, value in change_entry.iteritems():
1701 if key.startswith('File'):
1702 if settings.has_key('depot-paths'):
1703 if not [p for p in settings['depot-paths']
1704 if p4PathStartsWith(value, p)]:
1705 continue
Simon Hausmannea99c3a2007-08-08 17:06:55 +02001706 else:
Miguel Torrojab596b3b2017-07-13 09:00:34 +02001707 if not p4PathStartsWith(value, self.depotPath):
1708 continue
1709 files_list.append(value)
1710 continue
1711 # Output in the order expected by prepareLogMessage
1712 for key in ['Change', 'Client', 'User', 'Status', 'Description', 'Jobs']:
1713 if not change_entry.has_key(key):
1714 continue
1715 template += '\n'
1716 template += key + ':'
1717 if key == 'Description':
1718 template += '\n'
1719 for field_line in change_entry[key].splitlines():
1720 template += '\t'+field_line+'\n'
1721 if len(files_list) > 0:
1722 template += '\n'
1723 template += 'Files:\n'
1724 for path in files_list:
1725 template += '\t'+path+'\n'
Simon Hausmannea99c3a2007-08-08 17:06:55 +02001726 return template
1727
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001728 def edit_template(self, template_file):
1729 """Invoke the editor to let the user change the submission
1730 message. Return true if okay to continue with the submit."""
1731
1732 # if configured to skip the editing part, just submit
Pete Wyckoff0d609032013-01-26 22:11:24 -05001733 if gitConfigBool("git-p4.skipSubmitEdit"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001734 return True
1735
1736 # look at the modification time, to check later if the user saved
1737 # the file
1738 mtime = os.stat(template_file).st_mtime
1739
1740 # invoke the editor
Luke Diamandf95ceaf2012-04-24 09:33:21 +01001741 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001742 editor = os.environ.get("P4EDITOR")
1743 else:
1744 editor = read_pipe("git var GIT_EDITOR").strip()
Luke Diamand2dade7a2015-05-19 23:23:17 +01001745 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001746
1747 # If the file was not saved, prompt to see if this patch should
1748 # be skipped. But skip this verification step if configured so.
Pete Wyckoff0d609032013-01-26 22:11:24 -05001749 if gitConfigBool("git-p4.skipSubmitEditCheck"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001750 return True
1751
Pete Wyckoffd1652042011-12-17 12:39:03 -05001752 # modification time updated means user saved the file
1753 if os.stat(template_file).st_mtime > mtime:
1754 return True
1755
1756 while True:
1757 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1758 if response == 'y':
1759 return True
1760 if response == 'n':
1761 return False
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001762
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001763 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
Maxime Costeb4073bb2014-05-24 18:40:35 +01001764 # diff
1765 if os.environ.has_key("P4DIFF"):
1766 del(os.environ["P4DIFF"])
1767 diff = ""
1768 for editedFile in editedFiles:
1769 diff += p4_read_pipe(['diff', '-du',
1770 wildcard_encode(editedFile)])
1771
1772 # new file diff
1773 newdiff = ""
1774 for newFile in filesToAdd:
1775 newdiff += "==== new file ====\n"
1776 newdiff += "--- /dev/null\n"
1777 newdiff += "+++ %s\n" % newFile
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001778
1779 is_link = os.path.islink(newFile)
1780 expect_link = newFile in symlinks
1781
1782 if is_link and expect_link:
1783 newdiff += "+%s\n" % os.readlink(newFile)
1784 else:
1785 f = open(newFile, "r")
1786 for line in f.readlines():
1787 newdiff += "+" + line
1788 f.close()
Maxime Costeb4073bb2014-05-24 18:40:35 +01001789
Maxime Costee2a892e2014-06-11 14:09:59 +01001790 return (diff + newdiff).replace('\r\n', '\n')
Maxime Costeb4073bb2014-05-24 18:40:35 +01001791
Han-Wen Nienhuys7cb5cbe2007-05-23 16:55:48 -03001792 def applyCommit(self, id):
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04001793 """Apply one commit, return True if it succeeded."""
1794
1795 print "Applying", read_pipe(["git", "show", "-s",
1796 "--format=format:%h %s", id])
Vitor Antunesae901092011-02-20 01:18:24 +00001797
Luke Diamand848de9c2011-05-13 20:46:00 +01001798 (p4User, gitEmail) = self.p4UserForCommit(id)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001799
Gary Gibbons84cb0002012-07-04 09:40:19 -04001800 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001801 filesToAdd = set()
Romain Picarda02b8bc2016-01-12 13:43:47 +01001802 filesToChangeType = set()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001803 filesToDelete = set()
Simon Hausmannd336c152007-05-16 09:41:26 +02001804 editedFiles = set()
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001805 pureRenameCopy = set()
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001806 symlinks = set()
Chris Pettittc65b6702007-11-01 20:43:14 -07001807 filesToChangeExecBit = {}
Luke Diamand46c609e2016-12-02 22:43:19 +00001808 all_files = list()
Luke Diamand60df0712012-02-23 07:51:30 +00001809
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001810 for line in diff:
Chris Pettittb43b0a32007-11-01 20:43:13 -07001811 diff = parseDiffTreeEntry(line)
1812 modifier = diff['status']
1813 path = diff['src']
Luke Diamand46c609e2016-12-02 22:43:19 +00001814 all_files.append(path)
1815
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001816 if modifier == "M":
Luke Diamand6de040d2011-10-16 10:47:52 -04001817 p4_edit(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07001818 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1819 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmannd336c152007-05-16 09:41:26 +02001820 editedFiles.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001821 elif modifier == "A":
1822 filesToAdd.add(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07001823 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001824 if path in filesToDelete:
1825 filesToDelete.remove(path)
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001826
1827 dst_mode = int(diff['dst_mode'], 8)
1828 if dst_mode == 0120000:
1829 symlinks.add(path)
1830
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001831 elif modifier == "D":
1832 filesToDelete.add(path)
1833 if path in filesToAdd:
1834 filesToAdd.remove(path)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001835 elif modifier == "C":
1836 src, dest = diff['src'], diff['dst']
Luke Diamand6de040d2011-10-16 10:47:52 -04001837 p4_integrate(src, dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001838 pureRenameCopy.add(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001839 if diff['src_sha1'] != diff['dst_sha1']:
Luke Diamand6de040d2011-10-16 10:47:52 -04001840 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001841 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001842 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Luke Diamand6de040d2011-10-16 10:47:52 -04001843 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001844 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001845 filesToChangeExecBit[dest] = diff['dst_mode']
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05001846 if self.isWindows:
1847 # turn off read-only attribute
1848 os.chmod(dest, stat.S_IWRITE)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001849 os.unlink(dest)
1850 editedFiles.add(dest)
Chris Pettittd9a5f252007-10-15 22:15:06 -07001851 elif modifier == "R":
Chris Pettittb43b0a32007-11-01 20:43:13 -07001852 src, dest = diff['src'], diff['dst']
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001853 if self.p4HasMoveCommand:
1854 p4_edit(src) # src must be open before move
1855 p4_move(src, dest) # opens for (move/delete, move/add)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001856 else:
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001857 p4_integrate(src, dest)
1858 if diff['src_sha1'] != diff['dst_sha1']:
1859 p4_edit(dest)
1860 else:
1861 pureRenameCopy.add(dest)
Chris Pettittc65b6702007-11-01 20:43:14 -07001862 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001863 if not self.p4HasMoveCommand:
1864 p4_edit(dest) # with move: already open, writable
Chris Pettittc65b6702007-11-01 20:43:14 -07001865 filesToChangeExecBit[dest] = diff['dst_mode']
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001866 if not self.p4HasMoveCommand:
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05001867 if self.isWindows:
1868 os.chmod(dest, stat.S_IWRITE)
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001869 os.unlink(dest)
1870 filesToDelete.add(src)
Chris Pettittd9a5f252007-10-15 22:15:06 -07001871 editedFiles.add(dest)
Romain Picarda02b8bc2016-01-12 13:43:47 +01001872 elif modifier == "T":
1873 filesToChangeType.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001874 else:
1875 die("unknown modifier %s for %s" % (modifier, path))
1876
Tolga Ceylan749b6682014-05-06 22:48:54 -07001877 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
Simon Hausmann47a130b2007-05-20 16:33:21 +02001878 patchcmd = diffcmd + " | git apply "
Simon Hausmannc1b296b2007-05-20 16:55:05 +02001879 tryPatchCmd = patchcmd + "--check -"
1880 applyPatchCmd = patchcmd + "--check --apply -"
Luke Diamand60df0712012-02-23 07:51:30 +00001881 patch_succeeded = True
Simon Hausmann51a26402007-04-15 09:59:56 +02001882
Simon Hausmann47a130b2007-05-20 16:33:21 +02001883 if os.system(tryPatchCmd) != 0:
Luke Diamand60df0712012-02-23 07:51:30 +00001884 fixed_rcs_keywords = False
1885 patch_succeeded = False
Simon Hausmann51a26402007-04-15 09:59:56 +02001886 print "Unfortunately applying the change failed!"
Luke Diamand60df0712012-02-23 07:51:30 +00001887
1888 # Patch failed, maybe it's just RCS keyword woes. Look through
1889 # the patch to see if that's possible.
Pete Wyckoff0d609032013-01-26 22:11:24 -05001890 if gitConfigBool("git-p4.attemptRCSCleanup"):
Luke Diamand60df0712012-02-23 07:51:30 +00001891 file = None
1892 pattern = None
1893 kwfiles = {}
1894 for file in editedFiles | filesToDelete:
1895 # did this file's delta contain RCS keywords?
1896 pattern = p4_keywords_regexp_for_file(file)
1897
1898 if pattern:
1899 # this file is a possibility...look for RCS keywords.
1900 regexp = re.compile(pattern, re.VERBOSE)
1901 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1902 if regexp.search(line):
1903 if verbose:
1904 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1905 kwfiles[file] = pattern
1906 break
1907
1908 for file in kwfiles:
1909 if verbose:
1910 print "zapping %s with %s" % (line,pattern)
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05001911 # File is being deleted, so not open in p4. Must
1912 # disable the read-only bit on windows.
1913 if self.isWindows and file not in editedFiles:
1914 os.chmod(file, stat.S_IWRITE)
Luke Diamand60df0712012-02-23 07:51:30 +00001915 self.patchRCSKeywords(file, kwfiles[file])
1916 fixed_rcs_keywords = True
1917
1918 if fixed_rcs_keywords:
1919 print "Retrying the patch with RCS keywords cleaned up"
1920 if os.system(tryPatchCmd) == 0:
1921 patch_succeeded = True
1922
1923 if not patch_succeeded:
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04001924 for f in editedFiles:
1925 p4_revert(f)
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04001926 return False
Simon Hausmann51a26402007-04-15 09:59:56 +02001927
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001928 #
1929 # Apply the patch for real, and do add/delete/+x handling.
1930 #
Simon Hausmann47a130b2007-05-20 16:33:21 +02001931 system(applyPatchCmd)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001932
Romain Picarda02b8bc2016-01-12 13:43:47 +01001933 for f in filesToChangeType:
1934 p4_edit(f, "-t", "auto")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001935 for f in filesToAdd:
Luke Diamand6de040d2011-10-16 10:47:52 -04001936 p4_add(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001937 for f in filesToDelete:
Luke Diamand6de040d2011-10-16 10:47:52 -04001938 p4_revert(f)
1939 p4_delete(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001940
Chris Pettittc65b6702007-11-01 20:43:14 -07001941 # Set/clear executable bits
1942 for f in filesToChangeExecBit.keys():
1943 mode = filesToChangeExecBit[f]
1944 setP4ExecBit(f, mode)
1945
Luke Diamand8cf422d2017-12-21 11:06:14 +00001946 update_shelve = 0
1947 if len(self.update_shelve) > 0:
1948 update_shelve = self.update_shelve.pop(0)
1949 p4_reopen_in_change(update_shelve, all_files)
Luke Diamand46c609e2016-12-02 22:43:19 +00001950
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001951 #
1952 # Build p4 change description, starting with the contents
1953 # of the git commit message.
1954 #
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01001955 logMessage = extractLogMessageFromGitCommit(id)
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01001956 logMessage = logMessage.strip()
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001957 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001958
Luke Diamand8cf422d2017-12-21 11:06:14 +00001959 template = self.prepareSubmitTemplate(update_shelve)
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001960 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001961
1962 if self.preserveUser:
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001963 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001964
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001965 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1966 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1967 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1968 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1969
1970 separatorLine = "######## everything below this line is just the diff #######\n"
Maxime Costeb4073bb2014-05-24 18:40:35 +01001971 if not self.prepare_p4_only:
1972 submitTemplate += separatorLine
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001973 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001974
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001975 (handle, fileName) = tempfile.mkstemp()
Maxime Costee2a892e2014-06-11 14:09:59 +01001976 tmpFile = os.fdopen(handle, "w+b")
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001977 if self.isWindows:
1978 submitTemplate = submitTemplate.replace("\n", "\r\n")
Maxime Costeb4073bb2014-05-24 18:40:35 +01001979 tmpFile.write(submitTemplate)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001980 tmpFile.close()
1981
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001982 if self.prepare_p4_only:
1983 #
1984 # Leave the p4 tree prepared, and the submit template around
1985 # and let the user decide what to do next
1986 #
1987 print
1988 print "P4 workspace prepared for submission."
1989 print "To submit or revert, go to client workspace"
1990 print " " + self.clientPath
1991 print
1992 print "To submit, use \"p4 submit\" to write a new description,"
Luke Diamand10de86d2015-01-23 09:15:12 +00001993 print "or \"p4 submit -i <%s\" to use the one prepared by" \
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001994 " \"git p4\"." % fileName
1995 print "You can delete the file \"%s\" when finished." % fileName
1996
1997 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1998 print "To preserve change ownership by user %s, you must\n" \
1999 "do \"p4 change -f <change>\" after submitting and\n" \
2000 "edit the User field."
2001 if pureRenameCopy:
2002 print "After submitting, renamed files must be re-synced."
2003 print "Invoke \"p4 sync -f\" on each of these files:"
2004 for f in pureRenameCopy:
2005 print " " + f
2006
2007 print
2008 print "To revert the changes, use \"p4 revert ...\", and delete"
2009 print "the submit template file \"%s\"" % fileName
2010 if filesToAdd:
2011 print "Since the commit adds new files, they must be deleted:"
2012 for f in filesToAdd:
2013 print " " + f
2014 print
2015 return True
2016
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04002017 #
2018 # Let the user edit the change description, then submit it.
2019 #
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002020 submitted = False
Luke Diamandecdba362011-05-07 11:19:43 +01002021
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002022 try:
2023 if self.edit_template(fileName):
2024 # read the edited message and submit
2025 tmpFile = open(fileName, "rb")
2026 message = tmpFile.read()
2027 tmpFile.close()
2028 if self.isWindows:
2029 message = message.replace("\r\n", "\n")
2030 submitTemplate = message[:message.index(separatorLine)]
Luke Diamand46c609e2016-12-02 22:43:19 +00002031
Luke Diamand8cf422d2017-12-21 11:06:14 +00002032 if update_shelve:
Luke Diamand46c609e2016-12-02 22:43:19 +00002033 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
2034 elif self.shelve:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002035 p4_write_pipe(['shelve', '-i'], submitTemplate)
2036 else:
2037 p4_write_pipe(['submit', '-i'], submitTemplate)
2038 # The rename/copy happened by applying a patch that created a
2039 # new file. This leaves it writable, which confuses p4.
2040 for f in pureRenameCopy:
2041 p4_sync(f, "-f")
Luke Diamandecdba362011-05-07 11:19:43 +01002042
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002043 if self.preserveUser:
2044 if p4User:
2045 # Get last changelist number. Cannot easily get it from
2046 # the submit command output as the output is
2047 # unmarshalled.
2048 changelist = self.lastP4Changelist()
2049 self.modifyChangelistUser(changelist, p4User)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002050
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002051 submitted = True
2052
2053 finally:
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002054 # skip this patch
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002055 if not submitted or self.shelve:
2056 if self.shelve:
2057 print ("Reverting shelved files.")
2058 else:
2059 print ("Submission cancelled, undoing p4 changes.")
2060 for f in editedFiles | filesToDelete:
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002061 p4_revert(f)
2062 for f in filesToAdd:
2063 p4_revert(f)
2064 os.remove(f)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04002065
2066 os.remove(fileName)
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00002067 return submitted
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002068
Luke Diamand06804c72012-04-11 17:21:24 +02002069 # Export git tags as p4 labels. Create a p4 label and then tag
2070 # with that.
2071 def exportGitTags(self, gitTags):
Luke Diamandc8942a22012-04-11 17:21:24 +02002072 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
2073 if len(validLabelRegexp) == 0:
2074 validLabelRegexp = defaultLabelRegexp
2075 m = re.compile(validLabelRegexp)
Luke Diamand06804c72012-04-11 17:21:24 +02002076
2077 for name in gitTags:
2078
2079 if not m.match(name):
2080 if verbose:
Luke Diamand05a3cec2012-05-11 07:25:17 +01002081 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
Luke Diamand06804c72012-04-11 17:21:24 +02002082 continue
2083
2084 # Get the p4 commit this corresponds to
Luke Diamandc8942a22012-04-11 17:21:24 +02002085 logMessage = extractLogMessageFromGitCommit(name)
2086 values = extractSettingsGitLog(logMessage)
Luke Diamand06804c72012-04-11 17:21:24 +02002087
Luke Diamandc8942a22012-04-11 17:21:24 +02002088 if not values.has_key('change'):
Luke Diamand06804c72012-04-11 17:21:24 +02002089 # a tag pointing to something not sent to p4; ignore
2090 if verbose:
2091 print "git tag %s does not give a p4 commit" % name
2092 continue
Luke Diamandc8942a22012-04-11 17:21:24 +02002093 else:
2094 changelist = values['change']
Luke Diamand06804c72012-04-11 17:21:24 +02002095
2096 # Get the tag details.
2097 inHeader = True
2098 isAnnotated = False
2099 body = []
2100 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
2101 l = l.strip()
2102 if inHeader:
2103 if re.match(r'tag\s+', l):
2104 isAnnotated = True
2105 elif re.match(r'\s*$', l):
2106 inHeader = False
2107 continue
2108 else:
2109 body.append(l)
2110
2111 if not isAnnotated:
2112 body = ["lightweight tag imported by git p4\n"]
2113
2114 # Create the label - use the same view as the client spec we are using
2115 clientSpec = getClientSpec()
2116
2117 labelTemplate = "Label: %s\n" % name
2118 labelTemplate += "Description:\n"
2119 for b in body:
2120 labelTemplate += "\t" + b + "\n"
2121 labelTemplate += "View:\n"
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002122 for depot_side in clientSpec.mappings:
2123 labelTemplate += "\t%s\n" % depot_side
Luke Diamand06804c72012-04-11 17:21:24 +02002124
Pete Wyckoffef739f02012-09-09 16:16:11 -04002125 if self.dry_run:
2126 print "Would create p4 label %s for tag" % name
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002127 elif self.prepare_p4_only:
2128 print "Not creating p4 label %s for tag due to option" \
2129 " --prepare-p4-only" % name
Pete Wyckoffef739f02012-09-09 16:16:11 -04002130 else:
2131 p4_write_pipe(["label", "-i"], labelTemplate)
Luke Diamand06804c72012-04-11 17:21:24 +02002132
Pete Wyckoffef739f02012-09-09 16:16:11 -04002133 # Use the label
2134 p4_system(["tag", "-l", name] +
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002135 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
Luke Diamand06804c72012-04-11 17:21:24 +02002136
Pete Wyckoffef739f02012-09-09 16:16:11 -04002137 if verbose:
2138 print "created p4 label for tag %s" % name
Luke Diamand06804c72012-04-11 17:21:24 +02002139
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002140 def run(self, args):
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002141 if len(args) == 0:
2142 self.master = currentGitBranch()
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002143 elif len(args) == 1:
2144 self.master = args[0]
Pete Wyckoff28755db2011-12-24 21:07:40 -05002145 if not branchExists(self.master):
2146 die("Branch %s does not exist" % self.master)
Simon Hausmannc9b50e62007-03-29 19:15:24 +02002147 else:
2148 return False
2149
Luke Diamand8cf422d2017-12-21 11:06:14 +00002150 for i in self.update_shelve:
2151 if i <= 0:
2152 sys.exit("invalid changelist %d" % i)
2153
Luke Diamand00ad6e32015-11-21 09:54:41 +00002154 if self.master:
2155 allowSubmit = gitConfig("git-p4.allowSubmit")
2156 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
2157 die("%s is not in git-p4.allowSubmit" % self.master)
Jing Xue4c2d5d72008-06-22 14:12:39 -04002158
Simon Hausmann27d2d812007-06-12 14:31:59 +02002159 [upstream, settings] = findUpstreamBranchPoint()
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002160 self.depotPath = settings['depot-paths'][0]
Simon Hausmann27d2d812007-06-12 14:31:59 +02002161 if len(self.origin) == 0:
2162 self.origin = upstream
Simon Hausmanna3fdd572007-06-07 22:54:32 +02002163
Luke Diamand8cf422d2017-12-21 11:06:14 +00002164 if len(self.update_shelve) > 0:
Luke Diamand46c609e2016-12-02 22:43:19 +00002165 self.shelve = True
2166
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002167 if self.preserveUser:
2168 if not self.canChangeChangelists():
2169 die("Cannot preserve user names without p4 super-user or admin permissions")
2170
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04002171 # if not set from the command line, try the config file
2172 if self.conflict_behavior is None:
2173 val = gitConfig("git-p4.conflict")
2174 if val:
2175 if val not in self.conflict_behavior_choices:
2176 die("Invalid value '%s' for config git-p4.conflict" % val)
2177 else:
2178 val = "ask"
2179 self.conflict_behavior = val
2180
Simon Hausmanna3fdd572007-06-07 22:54:32 +02002181 if self.verbose:
2182 print "Origin branch is " + self.origin
Simon Hausmann95124972007-03-23 09:16:07 +01002183
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002184 if len(self.depotPath) == 0:
Simon Hausmann95124972007-03-23 09:16:07 +01002185 print "Internal error: cannot locate perforce depot path from existing branches"
2186 sys.exit(128)
2187
Pete Wyckoff543987b2012-02-25 20:06:25 -05002188 self.useClientSpec = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05002189 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff543987b2012-02-25 20:06:25 -05002190 self.useClientSpec = True
2191 if self.useClientSpec:
2192 self.clientSpecDirs = getClientSpec()
Simon Hausmann95124972007-03-23 09:16:07 +01002193
Ville Skyttä2e3a16b2016-08-09 11:53:38 +03002194 # Check for the existence of P4 branches
Vitor Antunescd884102015-04-21 23:49:30 +01002195 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2196
2197 if self.useClientSpec and not branchesDetected:
Pete Wyckoff543987b2012-02-25 20:06:25 -05002198 # all files are relative to the client spec
2199 self.clientPath = getClientRoot()
2200 else:
2201 self.clientPath = p4Where(self.depotPath)
2202
2203 if self.clientPath == "":
2204 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
Simon Hausmann95124972007-03-23 09:16:07 +01002205
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002206 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
Simon Hausmann7944f142007-05-21 11:04:26 +02002207 self.oldWorkingDirectory = os.getcwd()
Simon Hausmannc1b296b2007-05-20 16:55:05 +02002208
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002209 # ensure the clientPath exists
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002210 new_client_dir = False
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002211 if not os.path.exists(self.clientPath):
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002212 new_client_dir = True
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002213 os.makedirs(self.clientPath)
2214
Miklós Fazekasbbd84862013-03-11 17:45:29 -04002215 chdir(self.clientPath, is_client_path=True)
Pete Wyckoffef739f02012-09-09 16:16:11 -04002216 if self.dry_run:
2217 print "Would synchronize p4 checkout in %s" % self.clientPath
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002218 else:
Pete Wyckoffef739f02012-09-09 16:16:11 -04002219 print "Synchronizing p4 checkout..."
2220 if new_client_dir:
2221 # old one was destroyed, and maybe nobody told p4
2222 p4_sync("...", "-f")
2223 else:
2224 p4_sync("...")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002225 self.check()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002226
Simon Hausmann4c750c02008-02-19 09:37:16 +01002227 commits = []
Luke Diamand00ad6e32015-11-21 09:54:41 +00002228 if self.master:
Ævar Arnfjörð Bjarmason89f32a92018-05-10 12:43:00 +00002229 committish = self.master
Luke Diamand00ad6e32015-11-21 09:54:41 +00002230 else:
Ævar Arnfjörð Bjarmason89f32a92018-05-10 12:43:00 +00002231 committish = 'HEAD'
Luke Diamand00ad6e32015-11-21 09:54:41 +00002232
Romain Merlandf55b87c2018-06-01 09:46:14 +02002233 if self.commit != "":
2234 if self.commit.find("..") != -1:
2235 limits_ish = self.commit.split("..")
2236 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (limits_ish[0], limits_ish[1])]):
2237 commits.append(line.strip())
2238 commits.reverse()
2239 else:
2240 commits.append(self.commit)
2241 else:
Junio C Hamanoe6388992018-06-18 10:18:41 -07002242 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
Romain Merlandf55b87c2018-06-01 09:46:14 +02002243 commits.append(line.strip())
2244 commits.reverse()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002245
Pete Wyckoff0d609032013-01-26 22:11:24 -05002246 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
Luke Diamand848de9c2011-05-13 20:46:00 +01002247 self.checkAuthorship = False
2248 else:
2249 self.checkAuthorship = True
2250
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002251 if self.preserveUser:
2252 self.checkValidP4Users(commits)
2253
Gary Gibbons84cb0002012-07-04 09:40:19 -04002254 #
2255 # Build up a set of options to be passed to diff when
2256 # submitting each commit to p4.
2257 #
2258 if self.detectRenames:
2259 # command-line -M arg
2260 self.diffOpts = "-M"
2261 else:
2262 # If not explicitly set check the config variable
2263 detectRenames = gitConfig("git-p4.detectRenames")
2264
2265 if detectRenames.lower() == "false" or detectRenames == "":
2266 self.diffOpts = ""
2267 elif detectRenames.lower() == "true":
2268 self.diffOpts = "-M"
2269 else:
2270 self.diffOpts = "-M%s" % detectRenames
2271
2272 # no command-line arg for -C or --find-copies-harder, just
2273 # config variables
2274 detectCopies = gitConfig("git-p4.detectCopies")
2275 if detectCopies.lower() == "false" or detectCopies == "":
2276 pass
2277 elif detectCopies.lower() == "true":
2278 self.diffOpts += " -C"
2279 else:
2280 self.diffOpts += " -C%s" % detectCopies
2281
Pete Wyckoff0d609032013-01-26 22:11:24 -05002282 if gitConfigBool("git-p4.detectCopiesHarder"):
Gary Gibbons84cb0002012-07-04 09:40:19 -04002283 self.diffOpts += " --find-copies-harder"
2284
Luke Diamand8cf422d2017-12-21 11:06:14 +00002285 num_shelves = len(self.update_shelve)
2286 if num_shelves > 0 and num_shelves != len(commits):
2287 sys.exit("number of commits (%d) must match number of shelved changelist (%d)" %
2288 (len(commits), num_shelves))
2289
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002290 #
2291 # Apply the commits, one at a time. On failure, ask if should
2292 # continue to try the rest of the patches, or quit.
2293 #
Pete Wyckoffef739f02012-09-09 16:16:11 -04002294 if self.dry_run:
2295 print "Would apply"
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002296 applied = []
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002297 last = len(commits) - 1
2298 for i, commit in enumerate(commits):
Pete Wyckoffef739f02012-09-09 16:16:11 -04002299 if self.dry_run:
2300 print " ", read_pipe(["git", "show", "-s",
2301 "--format=format:%h %s", commit])
2302 ok = True
2303 else:
2304 ok = self.applyCommit(commit)
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002305 if ok:
2306 applied.append(commit)
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002307 else:
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002308 if self.prepare_p4_only and i < last:
2309 print "Processing only the first commit due to option" \
2310 " --prepare-p4-only"
2311 break
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002312 if i < last:
2313 quit = False
2314 while True:
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04002315 # prompt for what to do, or use the option/variable
2316 if self.conflict_behavior == "ask":
2317 print "What do you want to do?"
2318 response = raw_input("[s]kip this commit but apply"
2319 " the rest, or [q]uit? ")
2320 if not response:
2321 continue
2322 elif self.conflict_behavior == "skip":
2323 response = "s"
2324 elif self.conflict_behavior == "quit":
2325 response = "q"
2326 else:
2327 die("Unknown conflict_behavior '%s'" %
2328 self.conflict_behavior)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002329
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002330 if response[0] == "s":
2331 print "Skipping this commit, but applying the rest"
2332 break
2333 if response[0] == "q":
2334 print "Quitting"
2335 quit = True
2336 break
2337 if quit:
2338 break
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002339
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002340 chdir(self.oldWorkingDirectory)
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002341 shelved_applied = "shelved" if self.shelve else "applied"
Pete Wyckoffef739f02012-09-09 16:16:11 -04002342 if self.dry_run:
2343 pass
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002344 elif self.prepare_p4_only:
2345 pass
Pete Wyckoffef739f02012-09-09 16:16:11 -04002346 elif len(commits) == len(applied):
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002347 print ("All commits {0}!".format(shelved_applied))
Simon Hausmann14594f42007-08-22 09:07:15 +02002348
Simon Hausmann4c750c02008-02-19 09:37:16 +01002349 sync = P4Sync()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05002350 if self.branch:
2351 sync.branch = self.branch
Luke Diamandb9d34db2018-06-08 21:32:44 +01002352 if self.disable_p4sync:
2353 sync.sync_origin_only()
2354 else:
2355 sync.run([])
Simon Hausmann14594f42007-08-22 09:07:15 +02002356
Luke Diamandb9d34db2018-06-08 21:32:44 +01002357 if not self.disable_rebase:
2358 rebase = P4Rebase()
2359 rebase.rebase()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002360
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002361 else:
2362 if len(applied) == 0:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002363 print ("No commits {0}.".format(shelved_applied))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002364 else:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002365 print ("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002366 for c in commits:
2367 if c in applied:
2368 star = "*"
2369 else:
2370 star = " "
2371 print star, read_pipe(["git", "show", "-s",
2372 "--format=format:%h %s", c])
2373 print "You will have to do 'git p4 sync' and rebase."
2374
Pete Wyckoff0d609032013-01-26 22:11:24 -05002375 if gitConfigBool("git-p4.exportLabels"):
Luke Diamand06dcd152012-05-11 07:25:18 +01002376 self.exportLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02002377
2378 if self.exportLabels:
2379 p4Labels = getP4Labels(self.depotPath)
2380 gitTags = getGitTags()
2381
2382 missingGitTags = gitTags - p4Labels
2383 self.exportGitTags(missingGitTags)
2384
Ondřej Bílka98e023d2013-07-29 10:18:21 +02002385 # exit with error unless everything applied perfectly
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002386 if len(commits) != len(applied):
2387 sys.exit(1)
2388
Simon Hausmannb9847332007-03-20 20:54:23 +01002389 return True
2390
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002391class View(object):
2392 """Represent a p4 view ("p4 help views"), and map files in a
2393 repo according to the view."""
2394
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002395 def __init__(self, client_name):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002396 self.mappings = []
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002397 self.client_prefix = "//%s/" % client_name
2398 # cache results of "p4 where" to lookup client file locations
2399 self.client_spec_path_cache = {}
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002400
2401 def append(self, view_line):
2402 """Parse a view line, splitting it into depot and client
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002403 sides. Append to self.mappings, preserving order. This
2404 is only needed for tag creation."""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002405
2406 # Split the view line into exactly two words. P4 enforces
2407 # structure on these lines that simplifies this quite a bit.
2408 #
2409 # Either or both words may be double-quoted.
2410 # Single quotes do not matter.
2411 # Double-quote marks cannot occur inside the words.
2412 # A + or - prefix is also inside the quotes.
2413 # There are no quotes unless they contain a space.
2414 # The line is already white-space stripped.
2415 # The two words are separated by a single space.
2416 #
2417 if view_line[0] == '"':
2418 # First word is double quoted. Find its end.
2419 close_quote_index = view_line.find('"', 1)
2420 if close_quote_index <= 0:
2421 die("No first-word closing quote found: %s" % view_line)
2422 depot_side = view_line[1:close_quote_index]
2423 # skip closing quote and space
2424 rhs_index = close_quote_index + 1 + 1
2425 else:
2426 space_index = view_line.find(" ")
2427 if space_index <= 0:
2428 die("No word-splitting space found: %s" % view_line)
2429 depot_side = view_line[0:space_index]
2430 rhs_index = space_index + 1
2431
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002432 # prefix + means overlay on previous mapping
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002433 if depot_side.startswith("+"):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002434 depot_side = depot_side[1:]
2435
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002436 # prefix - means exclude this path, leave out of mappings
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002437 exclude = False
2438 if depot_side.startswith("-"):
2439 exclude = True
2440 depot_side = depot_side[1:]
2441
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002442 if not exclude:
2443 self.mappings.append(depot_side)
2444
2445 def convert_client_path(self, clientFile):
2446 # chop off //client/ part to make it relative
2447 if not clientFile.startswith(self.client_prefix):
2448 die("No prefix '%s' on clientFile '%s'" %
2449 (self.client_prefix, clientFile))
2450 return clientFile[len(self.client_prefix):]
2451
2452 def update_client_spec_path_cache(self, files):
2453 """ Caching file paths by "p4 where" batch query """
2454
2455 # List depot file paths exclude that already cached
2456 fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2457
2458 if len(fileArgs) == 0:
2459 return # All files in cache
2460
2461 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2462 for res in where_result:
2463 if "code" in res and res["code"] == "error":
2464 # assume error is "... file(s) not in client view"
2465 continue
2466 if "clientFile" not in res:
Pete Wyckoff20005442014-01-21 18:16:46 -05002467 die("No clientFile in 'p4 where' output")
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002468 if "unmap" in res:
2469 # it will list all of them, but only one not unmap-ped
2470 continue
Lars Schneidera0a50d82015-08-28 14:00:34 +02002471 if gitConfigBool("core.ignorecase"):
2472 res['depotFile'] = res['depotFile'].lower()
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002473 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2474
2475 # not found files or unmap files set to ""
2476 for depotFile in fileArgs:
Lars Schneidera0a50d82015-08-28 14:00:34 +02002477 if gitConfigBool("core.ignorecase"):
2478 depotFile = depotFile.lower()
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002479 if depotFile not in self.client_spec_path_cache:
2480 self.client_spec_path_cache[depotFile] = ""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002481
2482 def map_in_client(self, depot_path):
2483 """Return the relative location in the client where this
2484 depot file should live. Returns "" if the file should
2485 not be mapped in the client."""
2486
Lars Schneidera0a50d82015-08-28 14:00:34 +02002487 if gitConfigBool("core.ignorecase"):
2488 depot_path = depot_path.lower()
2489
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002490 if depot_path in self.client_spec_path_cache:
2491 return self.client_spec_path_cache[depot_path]
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002492
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002493 die( "Error: %s is not found in client spec path" % depot_path )
2494 return ""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002495
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002496class P4Sync(Command, P4UserMap):
Pete Wyckoff56c09342011-02-19 08:17:57 -05002497 delete_actions = ( "delete", "move/delete", "purge" )
2498
Simon Hausmannb9847332007-03-20 20:54:23 +01002499 def __init__(self):
2500 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002501 P4UserMap.__init__(self)
Simon Hausmannb9847332007-03-20 20:54:23 +01002502 self.options = [
2503 optparse.make_option("--branch", dest="branch"),
2504 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2505 optparse.make_option("--changesfile", dest="changesFile"),
2506 optparse.make_option("--silent", dest="silent", action="store_true"),
Simon Hausmannef48f902007-05-17 22:17:49 +02002507 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02002508 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Han-Wen Nienhuysd2c6dd32007-05-23 18:49:35 -03002509 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2510 help="Import into refs/heads/ , not refs/remotes"),
Lex Spoon96b2d542015-04-20 11:00:20 -04002511 optparse.make_option("--max-changes", dest="maxChanges",
2512 help="Maximum number of changes to import"),
2513 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2514 help="Internal block size to use when iteratively calling p4 changes"),
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03002515 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002516 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2517 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
Luke Diamand51334bb2015-01-17 20:56:38 +00002518 help="Only sync files that are included in the Perforce Client Spec"),
2519 optparse.make_option("-/", dest="cloneExclude",
2520 action="append", type="string",
2521 help="exclude depot path"),
Simon Hausmannb9847332007-03-20 20:54:23 +01002522 ]
2523 self.description = """Imports from Perforce into a git repository.\n
2524 example:
2525 //depot/my/project/ -- to import the current head
2526 //depot/my/project/@all -- to import everything
2527 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2528
2529 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2530
2531 self.usage += " //depot/path[@revRange]"
Simon Hausmannb9847332007-03-20 20:54:23 +01002532 self.silent = False
Reilly Grant1d7367d2009-09-10 00:02:38 -07002533 self.createdBranches = set()
2534 self.committedChanges = set()
Simon Hausmann569d1bd2007-03-22 21:34:16 +01002535 self.branch = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01002536 self.detectBranches = False
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02002537 self.detectLabels = False
Luke Diamand06804c72012-04-11 17:21:24 +02002538 self.importLabels = False
Simon Hausmannb9847332007-03-20 20:54:23 +01002539 self.changesFile = ""
Simon Hausmann01265102007-05-25 10:36:10 +02002540 self.syncWithOrigin = True
Simon Hausmanna028a982007-05-23 00:03:08 +02002541 self.importIntoRemotes = True
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02002542 self.maxChanges = ""
Luke Diamand1051ef02015-06-10 08:30:59 +01002543 self.changes_block_size = None
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -03002544 self.keepRepoPath = False
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002545 self.depotPaths = None
Simon Hausmann3c699642007-06-16 13:09:21 +02002546 self.p4BranchesInGit = []
Tommy Thorn354081d2008-02-03 10:38:51 -08002547 self.cloneExclude = []
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002548 self.useClientSpec = False
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05002549 self.useClientSpec_from_options = False
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002550 self.clientSpecDirs = None
Vitor Antunesfed23692012-01-25 23:48:22 +00002551 self.tempBranches = []
Lars Schneiderd6041762016-06-29 09:35:27 +02002552 self.tempBranchLocation = "refs/git-p4-tmp"
Lars Schneidera5db4b12015-09-26 09:55:03 +02002553 self.largeFileSystem = None
Luke Diamand123f6312018-05-23 23:20:26 +01002554 self.suppress_meta_comment = False
Lars Schneidera5db4b12015-09-26 09:55:03 +02002555
2556 if gitConfig('git-p4.largeFileSystem'):
2557 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2558 self.largeFileSystem = largeFileSystemConstructor(
2559 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2560 )
Simon Hausmannb9847332007-03-20 20:54:23 +01002561
Simon Hausmann01265102007-05-25 10:36:10 +02002562 if gitConfig("git-p4.syncFromOrigin") == "false":
2563 self.syncWithOrigin = False
2564
Luke Diamand123f6312018-05-23 23:20:26 +01002565 self.depotPaths = []
2566 self.changeRange = ""
2567 self.previousDepotPaths = []
2568 self.hasOrigin = False
2569
2570 # map from branch depot path to parent branch
2571 self.knownBranches = {}
2572 self.initialParents = {}
2573
2574 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
2575 self.labels = {}
2576
Vitor Antunesfed23692012-01-25 23:48:22 +00002577 # Force a checkpoint in fast-import and wait for it to finish
2578 def checkpoint(self):
2579 self.gitStream.write("checkpoint\n\n")
2580 self.gitStream.write("progress checkpoint\n\n")
2581 out = self.gitOutput.readline()
2582 if self.verbose:
2583 print "checkpoint finished: " + out
2584
Luke Diamand123f6312018-05-23 23:20:26 +01002585 def cmp_shelved(self, path, filerev, revision):
2586 """ Determine if a path at revision #filerev is the same as the file
2587 at revision @revision for a shelved changelist. If they don't match,
2588 unshelving won't be safe (we will get other changes mixed in).
2589
2590 This is comparing the revision that the shelved changelist is *based* on, not
2591 the shelved changelist itself.
2592 """
2593 ret = p4Cmd(["diff2", "{0}#{1}".format(path, filerev), "{0}@{1}".format(path, revision)])
2594 if verbose:
2595 print("p4 diff2 path %s filerev %s revision %s => %s" % (path, filerev, revision, ret))
2596 return ret["status"] == "identical"
2597
2598 def extractFilesFromCommit(self, commit, shelved=False, shelved_cl = 0, origin_revision = 0):
Tommy Thorn354081d2008-02-03 10:38:51 -08002599 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2600 for path in self.cloneExclude]
Simon Hausmannb9847332007-03-20 20:54:23 +01002601 files = []
2602 fnum = 0
2603 while commit.has_key("depotFile%s" % fnum):
2604 path = commit["depotFile%s" % fnum]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002605
Tommy Thorn354081d2008-02-03 10:38:51 -08002606 if [p for p in self.cloneExclude
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01002607 if p4PathStartsWith(path, p)]:
Tommy Thorn354081d2008-02-03 10:38:51 -08002608 found = False
2609 else:
2610 found = [p for p in self.depotPaths
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01002611 if p4PathStartsWith(path, p)]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002612 if not found:
Simon Hausmannb9847332007-03-20 20:54:23 +01002613 fnum = fnum + 1
2614 continue
2615
2616 file = {}
2617 file["path"] = path
2618 file["rev"] = commit["rev%s" % fnum]
2619 file["action"] = commit["action%s" % fnum]
2620 file["type"] = commit["type%s" % fnum]
Luke Diamand123f6312018-05-23 23:20:26 +01002621 if shelved:
2622 file["shelved_cl"] = int(shelved_cl)
2623
2624 # For shelved changelists, check that the revision of each file that the
2625 # shelve was based on matches the revision that we are using for the
2626 # starting point for git-fast-import (self.initialParent). Otherwise
2627 # the resulting diff will contain deltas from multiple commits.
2628
2629 if file["action"] != "add" and \
2630 not self.cmp_shelved(path, file["rev"], origin_revision):
2631 sys.exit("change {0} not based on {1} for {2}, cannot unshelve".format(
2632 commit["change"], self.initialParent, path))
2633
Simon Hausmannb9847332007-03-20 20:54:23 +01002634 files.append(file)
2635 fnum = fnum + 1
2636 return files
2637
Jan Durovec26e6a272016-04-19 19:49:41 +00002638 def extractJobsFromCommit(self, commit):
2639 jobs = []
2640 jnum = 0
2641 while commit.has_key("job%s" % jnum):
2642 job = commit["job%s" % jnum]
2643 jobs.append(job)
2644 jnum = jnum + 1
2645 return jobs
2646
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002647 def stripRepoPath(self, path, prefixes):
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002648 """When streaming files, this is called to map a p4 depot path
2649 to where it should go in git. The prefixes are either
2650 self.depotPaths, or self.branchPrefixes in the case of
2651 branch detection."""
2652
Ian Wienand39527102011-02-11 16:33:48 -08002653 if self.useClientSpec:
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002654 # branch detection moves files up a level (the branch name)
2655 # from what client spec interpretation gives
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002656 path = self.clientSpecDirs.map_in_client(path)
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002657 if self.detectBranches:
2658 for b in self.knownBranches:
2659 if path.startswith(b + "/"):
2660 path = path[len(b)+1:]
2661
2662 elif self.keepRepoPath:
2663 # Preserve everything in relative path name except leading
2664 # //depot/; just look at first prefix as they all should
2665 # be in the same depot.
2666 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2667 if p4PathStartsWith(path, depot):
2668 path = path[len(depot):]
Ian Wienand39527102011-02-11 16:33:48 -08002669
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002670 else:
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002671 for p in prefixes:
2672 if p4PathStartsWith(path, p):
2673 path = path[len(p):]
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002674 break
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002675
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002676 path = wildcard_decode(path)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002677 return path
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -03002678
Simon Hausmann71b112d2007-05-19 11:54:11 +02002679 def splitFilesIntoBranches(self, commit):
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002680 """Look at each depotFile in the commit to figure out to what
2681 branch it belongs."""
2682
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002683 if self.clientSpecDirs:
2684 files = self.extractFilesFromCommit(commit)
2685 self.clientSpecDirs.update_client_spec_path_cache(files)
2686
Simon Hausmannd5904672007-05-19 11:07:32 +02002687 branches = {}
Simon Hausmann71b112d2007-05-19 11:54:11 +02002688 fnum = 0
2689 while commit.has_key("depotFile%s" % fnum):
2690 path = commit["depotFile%s" % fnum]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002691 found = [p for p in self.depotPaths
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01002692 if p4PathStartsWith(path, p)]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002693 if not found:
Simon Hausmann71b112d2007-05-19 11:54:11 +02002694 fnum = fnum + 1
2695 continue
2696
2697 file = {}
2698 file["path"] = path
2699 file["rev"] = commit["rev%s" % fnum]
2700 file["action"] = commit["action%s" % fnum]
2701 file["type"] = commit["type%s" % fnum]
2702 fnum = fnum + 1
2703
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002704 # start with the full relative path where this file would
2705 # go in a p4 client
2706 if self.useClientSpec:
2707 relPath = self.clientSpecDirs.map_in_client(path)
2708 else:
2709 relPath = self.stripRepoPath(path, self.depotPaths)
Simon Hausmannb9847332007-03-20 20:54:23 +01002710
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02002711 for branch in self.knownBranches.keys():
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002712 # add a trailing slash so that a commit into qt/4.2foo
2713 # doesn't end up in qt/4.2, e.g.
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -03002714 if relPath.startswith(branch + "/"):
Simon Hausmannd5904672007-05-19 11:07:32 +02002715 if branch not in branches:
2716 branches[branch] = []
Simon Hausmann71b112d2007-05-19 11:54:11 +02002717 branches[branch].append(file)
Simon Hausmann6555b2c2007-06-17 11:25:34 +02002718 break
Simon Hausmannb9847332007-03-20 20:54:23 +01002719
2720 return branches
2721
Lars Schneidera5db4b12015-09-26 09:55:03 +02002722 def writeToGitStream(self, gitMode, relPath, contents):
2723 self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2724 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2725 for d in contents:
2726 self.gitStream.write(d)
2727 self.gitStream.write('\n')
2728
Lars Schneidera8b05162017-02-09 16:06:56 +01002729 def encodeWithUTF8(self, path):
2730 try:
2731 path.decode('ascii')
2732 except:
2733 encoding = 'utf8'
2734 if gitConfig('git-p4.pathEncoding'):
2735 encoding = gitConfig('git-p4.pathEncoding')
2736 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2737 if self.verbose:
2738 print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path)
2739 return path
2740
Luke Diamandb9327052009-07-30 00:13:46 +01002741 # output one file from the P4 stream
2742 # - helper for streamP4Files
2743
2744 def streamOneP4File(self, file, contents):
Luke Diamandb9327052009-07-30 00:13:46 +01002745 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
Lars Schneidera8b05162017-02-09 16:06:56 +01002746 relPath = self.encodeWithUTF8(relPath)
Luke Diamandb9327052009-07-30 00:13:46 +01002747 if verbose:
Lars Schneiderd2176a52015-09-26 09:55:01 +02002748 size = int(self.stream_file['fileSize'])
2749 sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2750 sys.stdout.flush()
Luke Diamandb9327052009-07-30 00:13:46 +01002751
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04002752 (type_base, type_mods) = split_p4_type(file["type"])
2753
2754 git_mode = "100644"
2755 if "x" in type_mods:
2756 git_mode = "100755"
2757 if type_base == "symlink":
2758 git_mode = "120000"
Alexandru Juncu1292df12013-08-08 16:17:38 +03002759 # p4 print on a symlink sometimes contains "target\n";
2760 # if it does, remove the newline
Evan Powersb39c3612010-02-16 00:44:08 -08002761 data = ''.join(contents)
Pete Wyckoff40f846c2014-01-21 18:16:40 -05002762 if not data:
2763 # Some version of p4 allowed creating a symlink that pointed
2764 # to nothing. This causes p4 errors when checking out such
2765 # a change, and errors here too. Work around it by ignoring
2766 # the bad symlink; hopefully a future change fixes it.
2767 print "\nIgnoring empty symlink in %s" % file['depotFile']
2768 return
2769 elif data[-1] == '\n':
Alexandru Juncu1292df12013-08-08 16:17:38 +03002770 contents = [data[:-1]]
2771 else:
2772 contents = [data]
Luke Diamandb9327052009-07-30 00:13:46 +01002773
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04002774 if type_base == "utf16":
Pete Wyckoff55aa5712011-09-17 19:16:14 -04002775 # p4 delivers different text in the python output to -G
2776 # than it does when using "print -o", or normal p4 client
2777 # operations. utf16 is converted to ascii or utf8, perhaps.
2778 # But ascii text saved as -t utf16 is completely mangled.
2779 # Invoke print -o to get the real contents.
Pete Wyckoff7f0e5962013-01-26 22:11:13 -05002780 #
2781 # On windows, the newlines will always be mangled by print, so put
2782 # them back too. This is not needed to the cygwin windows version,
2783 # just the native "NT" type.
2784 #
Lars Schneider1f5f3902015-09-21 12:01:41 +02002785 try:
2786 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2787 except Exception as e:
2788 if 'Translation of file content failed' in str(e):
2789 type_base = 'binary'
2790 else:
2791 raise e
2792 else:
2793 if p4_version_string().find('/NT') >= 0:
2794 text = text.replace('\r\n', '\n')
2795 contents = [ text ]
Pete Wyckoff55aa5712011-09-17 19:16:14 -04002796
Pete Wyckoff9f7ef0e2011-11-05 13:36:07 -04002797 if type_base == "apple":
2798 # Apple filetype files will be streamed as a concatenation of
2799 # its appledouble header and the contents. This is useless
2800 # on both macs and non-macs. If using "print -q -o xx", it
2801 # will create "xx" with the data, and "%xx" with the header.
2802 # This is also not very useful.
2803 #
2804 # Ideally, someday, this script can learn how to generate
2805 # appledouble files directly and import those to git, but
2806 # non-mac machines can never find a use for apple filetype.
2807 print "\nIgnoring apple filetype file %s" % file['depotFile']
2808 return
2809
Pete Wyckoff55aa5712011-09-17 19:16:14 -04002810 # Note that we do not try to de-mangle keywords on utf16 files,
2811 # even though in theory somebody may want that.
Luke Diamand60df0712012-02-23 07:51:30 +00002812 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2813 if pattern:
2814 regexp = re.compile(pattern, re.VERBOSE)
2815 text = ''.join(contents)
2816 text = regexp.sub(r'$\1$', text)
2817 contents = [ text ]
Luke Diamandb9327052009-07-30 00:13:46 +01002818
Lars Schneidera5db4b12015-09-26 09:55:03 +02002819 if self.largeFileSystem:
2820 (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01002821
Lars Schneidera5db4b12015-09-26 09:55:03 +02002822 self.writeToGitStream(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01002823
2824 def streamOneP4Deletion(self, file):
2825 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
Lars Schneidera8b05162017-02-09 16:06:56 +01002826 relPath = self.encodeWithUTF8(relPath)
Luke Diamandb9327052009-07-30 00:13:46 +01002827 if verbose:
Lars Schneiderd2176a52015-09-26 09:55:01 +02002828 sys.stdout.write("delete %s\n" % relPath)
2829 sys.stdout.flush()
Luke Diamandb9327052009-07-30 00:13:46 +01002830 self.gitStream.write("D %s\n" % relPath)
2831
Lars Schneidera5db4b12015-09-26 09:55:03 +02002832 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2833 self.largeFileSystem.removeLargeFile(relPath)
2834
Luke Diamandb9327052009-07-30 00:13:46 +01002835 # handle another chunk of streaming data
2836 def streamP4FilesCb(self, marshalled):
2837
Pete Wyckoff78189be2012-11-23 17:35:36 -05002838 # catch p4 errors and complain
2839 err = None
2840 if "code" in marshalled:
2841 if marshalled["code"] == "error":
2842 if "data" in marshalled:
2843 err = marshalled["data"].rstrip()
Lars Schneider4d25dc42015-09-26 09:55:02 +02002844
2845 if not err and 'fileSize' in self.stream_file:
2846 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2847 if required_bytes > 0:
2848 err = 'Not enough space left on %s! Free at least %i MB.' % (
2849 os.getcwd(), required_bytes/1024/1024
2850 )
2851
Pete Wyckoff78189be2012-11-23 17:35:36 -05002852 if err:
2853 f = None
2854 if self.stream_have_file_info:
2855 if "depotFile" in self.stream_file:
2856 f = self.stream_file["depotFile"]
2857 # force a failure in fast-import, else an empty
2858 # commit will be made
2859 self.gitStream.write("\n")
2860 self.gitStream.write("die-now\n")
2861 self.gitStream.close()
2862 # ignore errors, but make sure it exits first
2863 self.importProcess.wait()
2864 if f:
2865 die("Error from p4 print for %s: %s" % (f, err))
2866 else:
2867 die("Error from p4 print: %s" % err)
2868
Andrew Garberc3f61632011-04-07 02:01:21 -04002869 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2870 # start of a new file - output the old one first
2871 self.streamOneP4File(self.stream_file, self.stream_contents)
2872 self.stream_file = {}
2873 self.stream_contents = []
2874 self.stream_have_file_info = False
Luke Diamandb9327052009-07-30 00:13:46 +01002875
Andrew Garberc3f61632011-04-07 02:01:21 -04002876 # pick up the new file information... for the
2877 # 'data' field we need to append to our array
2878 for k in marshalled.keys():
2879 if k == 'data':
Lars Schneiderd2176a52015-09-26 09:55:01 +02002880 if 'streamContentSize' not in self.stream_file:
2881 self.stream_file['streamContentSize'] = 0
2882 self.stream_file['streamContentSize'] += len(marshalled['data'])
Andrew Garberc3f61632011-04-07 02:01:21 -04002883 self.stream_contents.append(marshalled['data'])
2884 else:
2885 self.stream_file[k] = marshalled[k]
Luke Diamandb9327052009-07-30 00:13:46 +01002886
Lars Schneiderd2176a52015-09-26 09:55:01 +02002887 if (verbose and
2888 'streamContentSize' in self.stream_file and
2889 'fileSize' in self.stream_file and
2890 'depotFile' in self.stream_file):
2891 size = int(self.stream_file["fileSize"])
2892 if size > 0:
2893 progress = 100*self.stream_file['streamContentSize']/size
2894 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
2895 sys.stdout.flush()
2896
Andrew Garberc3f61632011-04-07 02:01:21 -04002897 self.stream_have_file_info = True
Luke Diamandb9327052009-07-30 00:13:46 +01002898
2899 # Stream directly from "p4 files" into "git fast-import"
2900 def streamP4Files(self, files):
Simon Hausmann30b59402008-03-03 11:55:48 +01002901 filesForCommit = []
2902 filesToRead = []
Luke Diamandb9327052009-07-30 00:13:46 +01002903 filesToDelete = []
Simon Hausmann30b59402008-03-03 11:55:48 +01002904
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002905 for f in files:
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002906 filesForCommit.append(f)
2907 if f['action'] in self.delete_actions:
2908 filesToDelete.append(f)
2909 else:
2910 filesToRead.append(f)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03002911
Luke Diamandb9327052009-07-30 00:13:46 +01002912 # deleted files...
2913 for f in filesToDelete:
2914 self.streamOneP4Deletion(f)
2915
Simon Hausmann30b59402008-03-03 11:55:48 +01002916 if len(filesToRead) > 0:
Luke Diamandb9327052009-07-30 00:13:46 +01002917 self.stream_file = {}
2918 self.stream_contents = []
2919 self.stream_have_file_info = False
2920
Andrew Garberc3f61632011-04-07 02:01:21 -04002921 # curry self argument
2922 def streamP4FilesCbSelf(entry):
2923 self.streamP4FilesCb(entry)
Luke Diamandb9327052009-07-30 00:13:46 +01002924
Luke Diamand123f6312018-05-23 23:20:26 +01002925 fileArgs = []
2926 for f in filesToRead:
2927 if 'shelved_cl' in f:
2928 # Handle shelved CLs using the "p4 print file@=N" syntax to print
2929 # the contents
2930 fileArg = '%s@=%d' % (f['path'], f['shelved_cl'])
2931 else:
2932 fileArg = '%s#%s' % (f['path'], f['rev'])
2933
2934 fileArgs.append(fileArg)
Luke Diamand6de040d2011-10-16 10:47:52 -04002935
2936 p4CmdList(["-x", "-", "print"],
2937 stdin=fileArgs,
2938 cb=streamP4FilesCbSelf)
Han-Wen Nienhuysf2eda792007-05-23 18:49:35 -03002939
Luke Diamandb9327052009-07-30 00:13:46 +01002940 # do the last chunk
2941 if self.stream_file.has_key('depotFile'):
2942 self.streamOneP4File(self.stream_file, self.stream_contents)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03002943
Luke Diamandaffb4742012-01-19 09:52:27 +00002944 def make_email(self, userid):
2945 if userid in self.users:
2946 return self.users[userid]
2947 else:
2948 return "%s <a@b>" % userid
2949
Luke Diamand06804c72012-04-11 17:21:24 +02002950 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
Luke Diamandb43702a2015-08-27 08:18:58 +01002951 """ Stream a p4 tag.
2952 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2953 """
2954
Luke Diamand06804c72012-04-11 17:21:24 +02002955 if verbose:
2956 print "writing tag %s for commit %s" % (labelName, commit)
2957 gitStream.write("tag %s\n" % labelName)
2958 gitStream.write("from %s\n" % commit)
2959
2960 if labelDetails.has_key('Owner'):
2961 owner = labelDetails["Owner"]
2962 else:
2963 owner = None
2964
2965 # Try to use the owner of the p4 label, or failing that,
2966 # the current p4 user id.
2967 if owner:
2968 email = self.make_email(owner)
2969 else:
2970 email = self.make_email(self.p4UserId())
2971 tagger = "%s %s %s" % (email, epoch, self.tz)
2972
2973 gitStream.write("tagger %s\n" % tagger)
2974
2975 print "labelDetails=",labelDetails
2976 if labelDetails.has_key('Description'):
2977 description = labelDetails['Description']
2978 else:
2979 description = 'Label from git p4'
2980
2981 gitStream.write("data %d\n" % len(description))
2982 gitStream.write(description)
2983 gitStream.write("\n")
2984
Lars Schneider4ae048e2015-12-08 10:36:22 +01002985 def inClientSpec(self, path):
2986 if not self.clientSpecDirs:
2987 return True
2988 inClientSpec = self.clientSpecDirs.map_in_client(path)
2989 if not inClientSpec and self.verbose:
2990 print('Ignoring file outside of client spec: {0}'.format(path))
2991 return inClientSpec
2992
2993 def hasBranchPrefix(self, path):
2994 if not self.branchPrefixes:
2995 return True
2996 hasPrefix = [p for p in self.branchPrefixes
2997 if p4PathStartsWith(path, p)]
Andrew Oakley09667d02016-06-22 10:26:11 +01002998 if not hasPrefix and self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01002999 print('Ignoring file outside of prefix: {0}'.format(path))
3000 return hasPrefix
3001
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003002 def commit(self, details, files, branch, parent = ""):
Simon Hausmannb9847332007-03-20 20:54:23 +01003003 epoch = details["time"]
3004 author = details["user"]
Jan Durovec26e6a272016-04-19 19:49:41 +00003005 jobs = self.extractJobsFromCommit(details)
Simon Hausmannb9847332007-03-20 20:54:23 +01003006
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003007 if self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01003008 print('commit into {0}'.format(branch))
Han-Wen Nienhuys96e07dd2007-05-23 18:49:35 -03003009
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09003010 if self.clientSpecDirs:
3011 self.clientSpecDirs.update_client_spec_path_cache(files)
3012
Lars Schneider4ae048e2015-12-08 10:36:22 +01003013 files = [f for f in files
3014 if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
3015
3016 if not files and not gitConfigBool('git-p4.keepEmptyCommits'):
3017 print('Ignoring revision {0} as it would produce an empty commit.'
3018 .format(details['change']))
3019 return
3020
Simon Hausmannb9847332007-03-20 20:54:23 +01003021 self.gitStream.write("commit %s\n" % branch)
Luke Diamandb43702a2015-08-27 08:18:58 +01003022 self.gitStream.write("mark :%s\n" % details["change"])
Simon Hausmannb9847332007-03-20 20:54:23 +01003023 self.committedChanges.add(int(details["change"]))
3024 committer = ""
Simon Hausmannb607e712007-05-20 10:55:54 +02003025 if author not in self.users:
3026 self.getUserMapFromPerforceServer()
Luke Diamandaffb4742012-01-19 09:52:27 +00003027 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
Simon Hausmannb9847332007-03-20 20:54:23 +01003028
3029 self.gitStream.write("committer %s\n" % committer)
3030
3031 self.gitStream.write("data <<EOT\n")
3032 self.gitStream.write(details["desc"])
Jan Durovec26e6a272016-04-19 19:49:41 +00003033 if len(jobs) > 0:
3034 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
Luke Diamand123f6312018-05-23 23:20:26 +01003035
3036 if not self.suppress_meta_comment:
3037 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
3038 (','.join(self.branchPrefixes), details["change"]))
3039 if len(details['options']) > 0:
3040 self.gitStream.write(": options = %s" % details['options'])
3041 self.gitStream.write("]\n")
3042
3043 self.gitStream.write("EOT\n\n")
Simon Hausmannb9847332007-03-20 20:54:23 +01003044
3045 if len(parent) > 0:
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003046 if self.verbose:
3047 print "parent %s" % parent
Simon Hausmannb9847332007-03-20 20:54:23 +01003048 self.gitStream.write("from %s\n" % parent)
3049
Lars Schneider4ae048e2015-12-08 10:36:22 +01003050 self.streamP4Files(files)
Simon Hausmannb9847332007-03-20 20:54:23 +01003051 self.gitStream.write("\n")
3052
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003053 change = int(details["change"])
3054
Simon Hausmann9bda3a82007-05-19 12:05:40 +02003055 if self.labels.has_key(change):
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003056 label = self.labels[change]
3057 labelDetails = label[0]
3058 labelRevisions = label[1]
Simon Hausmann71b112d2007-05-19 11:54:11 +02003059 if self.verbose:
3060 print "Change %s is labelled %s" % (change, labelDetails)
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003061
Luke Diamand6de040d2011-10-16 10:47:52 -04003062 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003063 for p in self.branchPrefixes])
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003064
3065 if len(files) == len(labelRevisions):
3066
3067 cleanedFiles = {}
3068 for info in files:
Pete Wyckoff56c09342011-02-19 08:17:57 -05003069 if info["action"] in self.delete_actions:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003070 continue
3071 cleanedFiles[info["depotFile"]] = info["rev"]
3072
3073 if cleanedFiles == labelRevisions:
Luke Diamand06804c72012-04-11 17:21:24 +02003074 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003075
3076 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02003077 if not self.silent:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003078 print ("Tag %s does not match with change %s: files do not match."
3079 % (labelDetails["label"], change))
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003080
3081 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02003082 if not self.silent:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003083 print ("Tag %s does not match with change %s: file count is different."
3084 % (labelDetails["label"], change))
Simon Hausmannb9847332007-03-20 20:54:23 +01003085
Luke Diamand06804c72012-04-11 17:21:24 +02003086 # Build a dictionary of changelists and labels, for "detect-labels" option.
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003087 def getLabels(self):
3088 self.labels = {}
3089
Luke Diamand52a48802012-01-19 09:52:25 +00003090 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
Simon Hausmann10c32112007-04-08 10:15:47 +02003091 if len(l) > 0 and not self.silent:
Shun Kei Leung183f8432007-11-21 11:01:19 +08003092 print "Finding files belonging to labels in %s" % `self.depotPaths`
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003093
3094 for output in l:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003095 label = output["label"]
3096 revisions = {}
3097 newestChange = 0
Simon Hausmann71b112d2007-05-19 11:54:11 +02003098 if self.verbose:
3099 print "Querying files for label %s" % label
Luke Diamand6de040d2011-10-16 10:47:52 -04003100 for file in p4CmdList(["files"] +
3101 ["%s...@%s" % (p, label)
3102 for p in self.depotPaths]):
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003103 revisions[file["depotFile"]] = file["rev"]
3104 change = int(file["change"])
3105 if change > newestChange:
3106 newestChange = change
3107
Simon Hausmann9bda3a82007-05-19 12:05:40 +02003108 self.labels[newestChange] = [output, revisions]
3109
3110 if self.verbose:
3111 print "Label changes: %s" % self.labels.keys()
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02003112
Luke Diamand06804c72012-04-11 17:21:24 +02003113 # Import p4 labels as git tags. A direct mapping does not
3114 # exist, so assume that if all the files are at the same revision
3115 # then we can use that, or it's something more complicated we should
3116 # just ignore.
3117 def importP4Labels(self, stream, p4Labels):
3118 if verbose:
3119 print "import p4 labels: " + ' '.join(p4Labels)
3120
3121 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
Luke Diamandc8942a22012-04-11 17:21:24 +02003122 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
Luke Diamand06804c72012-04-11 17:21:24 +02003123 if len(validLabelRegexp) == 0:
3124 validLabelRegexp = defaultLabelRegexp
3125 m = re.compile(validLabelRegexp)
3126
3127 for name in p4Labels:
3128 commitFound = False
3129
3130 if not m.match(name):
3131 if verbose:
3132 print "label %s does not match regexp %s" % (name,validLabelRegexp)
3133 continue
3134
3135 if name in ignoredP4Labels:
3136 continue
3137
3138 labelDetails = p4CmdList(['label', "-o", name])[0]
3139
3140 # get the most recent changelist for each file in this label
3141 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
3142 for p in self.depotPaths])
3143
3144 if change.has_key('change'):
3145 # find the corresponding git commit; take the oldest commit
3146 changelist = int(change['change'])
Luke Diamandb43702a2015-08-27 08:18:58 +01003147 if changelist in self.committedChanges:
3148 gitCommit = ":%d" % changelist # use a fast-import mark
Luke Diamand06804c72012-04-11 17:21:24 +02003149 commitFound = True
Luke Diamandb43702a2015-08-27 08:18:58 +01003150 else:
3151 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
3152 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
3153 if len(gitCommit) == 0:
3154 print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
3155 else:
3156 commitFound = True
3157 gitCommit = gitCommit.strip()
3158
3159 if commitFound:
Luke Diamand06804c72012-04-11 17:21:24 +02003160 # Convert from p4 time format
3161 try:
3162 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
3163 except ValueError:
Pete Wyckoffa4e90542012-11-23 17:35:38 -05003164 print "Could not convert label time %s" % labelDetails['Update']
Luke Diamand06804c72012-04-11 17:21:24 +02003165 tmwhen = 1
3166
3167 when = int(time.mktime(tmwhen))
3168 self.streamTag(stream, name, labelDetails, gitCommit, when)
3169 if verbose:
3170 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
3171 else:
3172 if verbose:
3173 print "Label %s has no changelists - possibly deleted?" % name
3174
3175 if not commitFound:
3176 # We can't import this label; don't try again as it will get very
3177 # expensive repeatedly fetching all the files for labels that will
3178 # never be imported. If the label is moved in the future, the
3179 # ignore will need to be removed manually.
3180 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
3181
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003182 def guessProjectName(self):
3183 for p in self.depotPaths:
Simon Hausmann6e5295c2007-06-11 08:50:57 +02003184 if p.endswith("/"):
3185 p = p[:-1]
3186 p = p[p.strip().rfind("/") + 1:]
3187 if not p.endswith("/"):
3188 p += "/"
3189 return p
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003190
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003191 def getBranchMapping(self):
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003192 lostAndFoundBranches = set()
3193
Vitor Antunes8ace74c2011-08-19 00:44:04 +01003194 user = gitConfig("git-p4.branchUser")
3195 if len(user) > 0:
3196 command = "branches -u %s" % user
3197 else:
3198 command = "branches"
3199
3200 for info in p4CmdList(command):
Luke Diamand52a48802012-01-19 09:52:25 +00003201 details = p4Cmd(["branch", "-o", info["branch"]])
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003202 viewIdx = 0
3203 while details.has_key("View%s" % viewIdx):
3204 paths = details["View%s" % viewIdx].split(" ")
3205 viewIdx = viewIdx + 1
3206 # require standard //depot/foo/... //depot/bar/... mapping
3207 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
3208 continue
3209 source = paths[0]
3210 destination = paths[1]
Simon Hausmann6509e192007-06-07 09:41:53 +02003211 ## HACK
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01003212 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
Simon Hausmann6509e192007-06-07 09:41:53 +02003213 source = source[len(self.depotPaths[0]):-4]
3214 destination = destination[len(self.depotPaths[0]):-4]
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003215
Simon Hausmann1a2edf42007-06-17 15:10:24 +02003216 if destination in self.knownBranches:
3217 if not self.silent:
3218 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
3219 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
3220 continue
3221
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003222 self.knownBranches[destination] = source
3223
3224 lostAndFoundBranches.discard(destination)
3225
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003226 if source not in self.knownBranches:
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003227 lostAndFoundBranches.add(source)
3228
Vitor Antunes7199cf12011-08-19 00:44:05 +01003229 # Perforce does not strictly require branches to be defined, so we also
3230 # check git config for a branch list.
3231 #
3232 # Example of branch definition in git config file:
3233 # [git-p4]
3234 # branchList=main:branchA
3235 # branchList=main:branchB
3236 # branchList=branchA:branchC
3237 configBranches = gitConfigList("git-p4.branchList")
3238 for branch in configBranches:
3239 if branch:
3240 (source, destination) = branch.split(":")
3241 self.knownBranches[destination] = source
3242
3243 lostAndFoundBranches.discard(destination)
3244
3245 if source not in self.knownBranches:
3246 lostAndFoundBranches.add(source)
3247
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003248
3249 for branch in lostAndFoundBranches:
3250 self.knownBranches[branch] = branch
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003251
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01003252 def getBranchMappingFromGitBranches(self):
3253 branches = p4BranchesInGit(self.importIntoRemotes)
3254 for branch in branches.keys():
3255 if branch == "master":
3256 branch = "main"
3257 else:
3258 branch = branch[len(self.projectName):]
3259 self.knownBranches[branch] = branch
3260
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003261 def updateOptionDict(self, d):
3262 option_keys = {}
3263 if self.keepRepoPath:
3264 option_keys['keepRepoPath'] = 1
3265
3266 d["options"] = ' '.join(sorted(option_keys.keys()))
3267
3268 def readOptions(self, d):
3269 self.keepRepoPath = (d.has_key('options')
3270 and ('keepRepoPath' in d['options']))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003271
Simon Hausmann8134f692007-08-26 16:44:55 +02003272 def gitRefForBranch(self, branch):
3273 if branch == "main":
3274 return self.refPrefix + "master"
3275
3276 if len(branch) <= 0:
3277 return branch
3278
3279 return self.refPrefix + self.projectName + branch
3280
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003281 def gitCommitByP4Change(self, ref, change):
3282 if self.verbose:
3283 print "looking in ref " + ref + " for change %s using bisect..." % change
3284
3285 earliestCommit = ""
3286 latestCommit = parseRevision(ref)
3287
3288 while True:
3289 if self.verbose:
3290 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
3291 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3292 if len(next) == 0:
3293 if self.verbose:
3294 print "argh"
3295 return ""
3296 log = extractLogMessageFromGitCommit(next)
3297 settings = extractSettingsGitLog(log)
3298 currentChange = int(settings['change'])
3299 if self.verbose:
3300 print "current change %s" % currentChange
3301
3302 if currentChange == change:
3303 if self.verbose:
3304 print "found %s" % next
3305 return next
3306
3307 if currentChange < change:
3308 earliestCommit = "^%s" % next
3309 else:
3310 latestCommit = "%s" % next
3311
3312 return ""
3313
3314 def importNewBranch(self, branch, maxChange):
3315 # make fast-import flush all changes to disk and update the refs using the checkpoint
3316 # command so that we can try to find the branch parent in the git history
3317 self.gitStream.write("checkpoint\n\n");
3318 self.gitStream.flush();
3319 branchPrefix = self.depotPaths[0] + branch + "/"
3320 range = "@1,%s" % maxChange
3321 #print "prefix" + branchPrefix
Lex Spoon96b2d542015-04-20 11:00:20 -04003322 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003323 if len(changes) <= 0:
3324 return False
3325 firstChange = changes[0]
3326 #print "first change in branch: %s" % firstChange
3327 sourceBranch = self.knownBranches[branch]
3328 sourceDepotPath = self.depotPaths[0] + sourceBranch
3329 sourceRef = self.gitRefForBranch(sourceBranch)
3330 #print "source " + sourceBranch
3331
Luke Diamand52a48802012-01-19 09:52:25 +00003332 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003333 #print "branch parent: %s" % branchParentChange
3334 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3335 if len(gitParent) > 0:
3336 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3337 #print "parent git commit: %s" % gitParent
3338
3339 self.importChanges(changes)
3340 return True
3341
Vitor Antunesfed23692012-01-25 23:48:22 +00003342 def searchParent(self, parent, branch, target):
3343 parentFound = False
Pete Wyckoffc7d34882013-01-26 22:11:21 -05003344 for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3345 "--no-merges", parent]):
Vitor Antunesfed23692012-01-25 23:48:22 +00003346 blob = blob.strip()
3347 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3348 parentFound = True
3349 if self.verbose:
3350 print "Found parent of %s in commit %s" % (branch, blob)
3351 break
3352 if parentFound:
3353 return blob
3354 else:
3355 return None
3356
Luke Diamand123f6312018-05-23 23:20:26 +01003357 def importChanges(self, changes, shelved=False, origin_revision=0):
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003358 cnt = 1
3359 for change in changes:
Luke Diamand123f6312018-05-23 23:20:26 +01003360 description = p4_describe(change, shelved)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003361 self.updateOptionDict(description)
3362
3363 if not self.silent:
3364 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3365 sys.stdout.flush()
3366 cnt = cnt + 1
3367
3368 try:
3369 if self.detectBranches:
3370 branches = self.splitFilesIntoBranches(description)
3371 for branch in branches.keys():
3372 ## HACK --hwn
3373 branchPrefix = self.depotPaths[0] + branch + "/"
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003374 self.branchPrefixes = [ branchPrefix ]
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003375
3376 parent = ""
3377
3378 filesForCommit = branches[branch]
3379
3380 if self.verbose:
3381 print "branch is %s" % branch
3382
3383 self.updatedBranches.add(branch)
3384
3385 if branch not in self.createdBranches:
3386 self.createdBranches.add(branch)
3387 parent = self.knownBranches[branch]
3388 if parent == branch:
3389 parent = ""
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003390 else:
3391 fullBranch = self.projectName + branch
3392 if fullBranch not in self.p4BranchesInGit:
3393 if not self.silent:
3394 print("\n Importing new branch %s" % fullBranch);
3395 if self.importNewBranch(branch, change - 1):
3396 parent = ""
3397 self.p4BranchesInGit.append(fullBranch)
3398 if not self.silent:
3399 print("\n Resuming with change %s" % change);
3400
3401 if self.verbose:
3402 print "parent determined through known branches: %s" % parent
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003403
Simon Hausmann8134f692007-08-26 16:44:55 +02003404 branch = self.gitRefForBranch(branch)
3405 parent = self.gitRefForBranch(parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003406
3407 if self.verbose:
3408 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
3409
3410 if len(parent) == 0 and branch in self.initialParents:
3411 parent = self.initialParents[branch]
3412 del self.initialParents[branch]
3413
Vitor Antunesfed23692012-01-25 23:48:22 +00003414 blob = None
3415 if len(parent) > 0:
Pete Wyckoff4f9273d2013-01-26 22:11:04 -05003416 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
Vitor Antunesfed23692012-01-25 23:48:22 +00003417 if self.verbose:
3418 print "Creating temporary branch: " + tempBranch
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003419 self.commit(description, filesForCommit, tempBranch)
Vitor Antunesfed23692012-01-25 23:48:22 +00003420 self.tempBranches.append(tempBranch)
3421 self.checkpoint()
3422 blob = self.searchParent(parent, branch, tempBranch)
3423 if blob:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003424 self.commit(description, filesForCommit, branch, blob)
Vitor Antunesfed23692012-01-25 23:48:22 +00003425 else:
3426 if self.verbose:
3427 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003428 self.commit(description, filesForCommit, branch, parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003429 else:
Luke Diamand123f6312018-05-23 23:20:26 +01003430 files = self.extractFilesFromCommit(description, shelved, change, origin_revision)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003431 self.commit(description, files, self.branch,
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003432 self.initialParent)
Pete Wyckoff47497842013-01-14 19:47:04 -05003433 # only needed once, to connect to the previous commit
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003434 self.initialParent = ""
3435 except IOError:
3436 print self.gitError.read()
3437 sys.exit(1)
3438
Luke Diamandb9d34db2018-06-08 21:32:44 +01003439 def sync_origin_only(self):
3440 if self.syncWithOrigin:
3441 self.hasOrigin = originP4BranchesExist()
3442 if self.hasOrigin:
3443 if not self.silent:
3444 print 'Syncing with origin first, using "git fetch origin"'
3445 system("git fetch origin")
3446
Simon Hausmannc208a242007-08-26 16:07:18 +02003447 def importHeadRevision(self, revision):
3448 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
3449
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003450 details = {}
3451 details["user"] = "git perforce import user"
Pete Wyckoff1494fcb2011-02-19 08:17:56 -05003452 details["desc"] = ("Initial import of %s from the state at revision %s\n"
Simon Hausmannc208a242007-08-26 16:07:18 +02003453 % (' '.join(self.depotPaths), revision))
3454 details["change"] = revision
3455 newestRevision = 0
3456
3457 fileCnt = 0
Luke Diamand6de040d2011-10-16 10:47:52 -04003458 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3459
3460 for info in p4CmdList(["files"] + fileArgs):
Simon Hausmannc208a242007-08-26 16:07:18 +02003461
Pete Wyckoff68b28592011-02-19 08:17:55 -05003462 if 'code' in info and info['code'] == 'error':
Simon Hausmannc208a242007-08-26 16:07:18 +02003463 sys.stderr.write("p4 returned an error: %s\n"
3464 % info['data'])
Pete Wyckoffd88e7072011-02-19 08:17:58 -05003465 if info['data'].find("must refer to client") >= 0:
3466 sys.stderr.write("This particular p4 error is misleading.\n")
3467 sys.stderr.write("Perhaps the depot path was misspelled.\n");
3468 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
Simon Hausmannc208a242007-08-26 16:07:18 +02003469 sys.exit(1)
Pete Wyckoff68b28592011-02-19 08:17:55 -05003470 if 'p4ExitCode' in info:
3471 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
Simon Hausmannc208a242007-08-26 16:07:18 +02003472 sys.exit(1)
3473
3474
3475 change = int(info["change"])
3476 if change > newestRevision:
3477 newestRevision = change
3478
Pete Wyckoff56c09342011-02-19 08:17:57 -05003479 if info["action"] in self.delete_actions:
Simon Hausmannc208a242007-08-26 16:07:18 +02003480 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3481 #fileCnt = fileCnt + 1
3482 continue
3483
3484 for prop in ["depotFile", "rev", "action", "type" ]:
3485 details["%s%s" % (prop, fileCnt)] = info[prop]
3486
3487 fileCnt = fileCnt + 1
3488
3489 details["change"] = newestRevision
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003490
Pete Wyckoff9dcb9f22012-04-08 20:18:01 -04003491 # Use time from top-most change so that all git p4 clones of
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003492 # the same p4 repo have the same commit SHA1s.
Pete Wyckoff18fa13d2012-11-23 17:35:34 -05003493 res = p4_describe(newestRevision)
3494 details["time"] = res["time"]
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003495
Simon Hausmannc208a242007-08-26 16:07:18 +02003496 self.updateOptionDict(details)
3497 try:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003498 self.commit(details, self.extractFilesFromCommit(details), self.branch)
Simon Hausmannc208a242007-08-26 16:07:18 +02003499 except IOError:
3500 print "IO error with git fast-import. Is your git version recent enough?"
3501 print self.gitError.read()
3502
Luke Diamand123f6312018-05-23 23:20:26 +01003503 def openStreams(self):
3504 self.importProcess = subprocess.Popen(["git", "fast-import"],
3505 stdin=subprocess.PIPE,
3506 stdout=subprocess.PIPE,
3507 stderr=subprocess.PIPE);
3508 self.gitOutput = self.importProcess.stdout
3509 self.gitStream = self.importProcess.stdin
3510 self.gitError = self.importProcess.stderr
3511
3512 def closeStreams(self):
3513 self.gitStream.close()
3514 if self.importProcess.wait() != 0:
3515 die("fast-import failed: %s" % self.gitError.read())
3516 self.gitOutput.close()
3517 self.gitError.close()
Simon Hausmannc208a242007-08-26 16:07:18 +02003518
Simon Hausmannb9847332007-03-20 20:54:23 +01003519 def run(self, args):
Simon Hausmanna028a982007-05-23 00:03:08 +02003520 if self.importIntoRemotes:
3521 self.refPrefix = "refs/remotes/p4/"
3522 else:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02003523 self.refPrefix = "refs/heads/p4/"
Simon Hausmanna028a982007-05-23 00:03:08 +02003524
Luke Diamandb9d34db2018-06-08 21:32:44 +01003525 self.sync_origin_only()
Simon Hausmann10f880f2007-05-24 22:28:28 +02003526
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05003527 branch_arg_given = bool(self.branch)
Simon Hausmann569d1bd2007-03-22 21:34:16 +01003528 if len(self.branch) == 0:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02003529 self.branch = self.refPrefix + "master"
Simon Hausmanna028a982007-05-23 00:03:08 +02003530 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
Simon Hausmann48df6fd2007-05-17 21:18:53 +02003531 system("git update-ref %s refs/heads/p4" % self.branch)
Pete Wyckoff55d12432013-01-14 19:46:59 -05003532 system("git branch -D p4")
Simon Hausmann179caeb2007-03-22 22:17:42 +01003533
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05003534 # accept either the command-line option, or the configuration variable
3535 if self.useClientSpec:
3536 # will use this after clone to set the variable
3537 self.useClientSpec_from_options = True
3538 else:
Pete Wyckoff0d609032013-01-26 22:11:24 -05003539 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff09fca772011-12-24 21:07:39 -05003540 self.useClientSpec = True
3541 if self.useClientSpec:
Pete Wyckoff543987b2012-02-25 20:06:25 -05003542 self.clientSpecDirs = getClientSpec()
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01003543
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003544 # TODO: should always look at previous commits,
3545 # merge with previous imports, if possible.
3546 if args == []:
Simon Hausmannd414c742007-05-25 11:36:42 +02003547 if self.hasOrigin:
Simon Hausmann5ca44612007-08-24 17:44:16 +02003548 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
Pete Wyckoff3b650fc2013-01-14 19:46:58 -05003549
3550 # branches holds mapping from branch name to sha1
3551 branches = p4BranchesInGit(self.importIntoRemotes)
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05003552
3553 # restrict to just this one, disabling detect-branches
3554 if branch_arg_given:
3555 short = self.branch.split("/")[-1]
3556 if short in branches:
3557 self.p4BranchesInGit = [ short ]
3558 else:
3559 self.p4BranchesInGit = branches.keys()
Simon Hausmannabcd7902007-05-24 22:25:36 +02003560
3561 if len(self.p4BranchesInGit) > 1:
3562 if not self.silent:
3563 print "Importing from/into multiple branches"
3564 self.detectBranches = True
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05003565 for branch in branches.keys():
3566 self.initialParents[self.refPrefix + branch] = \
3567 branches[branch]
Simon Hausmann967f72e2007-03-23 09:30:41 +01003568
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003569 if self.verbose:
3570 print "branches: %s" % self.p4BranchesInGit
3571
3572 p4Change = 0
3573 for branch in self.p4BranchesInGit:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003574 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003575
3576 settings = extractSettingsGitLog(logMsg)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003577
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003578 self.readOptions(settings)
3579 if (settings.has_key('depot-paths')
3580 and settings.has_key ('change')):
3581 change = int(settings['change']) + 1
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003582 p4Change = max(p4Change, change)
3583
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003584 depotPaths = sorted(settings['depot-paths'])
3585 if self.previousDepotPaths == []:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003586 self.previousDepotPaths = depotPaths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003587 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003588 paths = []
3589 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
Vitor Antunes04d277b2011-08-19 00:44:03 +01003590 prev_list = prev.split("/")
3591 cur_list = cur.split("/")
3592 for i in range(0, min(len(cur_list), len(prev_list))):
3593 if cur_list[i] <> prev_list[i]:
Simon Hausmann583e1702007-06-07 09:37:13 +02003594 i = i - 1
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003595 break
3596
Vitor Antunes04d277b2011-08-19 00:44:03 +01003597 paths.append ("/".join(cur_list[:i + 1]))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003598
3599 self.previousDepotPaths = paths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003600
3601 if p4Change > 0:
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003602 self.depotPaths = sorted(self.previousDepotPaths)
Simon Hausmannd5904672007-05-19 11:07:32 +02003603 self.changeRange = "@%s,#head" % p4Change
Simon Hausmann341dc1c2007-05-21 00:39:16 +02003604 if not self.silent and not self.detectBranches:
Simon Hausmann967f72e2007-03-23 09:30:41 +01003605 print "Performing incremental import into %s git branch" % self.branch
Simon Hausmann569d1bd2007-03-22 21:34:16 +01003606
Pete Wyckoff40d69ac2013-01-14 19:47:03 -05003607 # accept multiple ref name abbreviations:
3608 # refs/foo/bar/branch -> use it exactly
3609 # p4/branch -> prepend refs/remotes/ or refs/heads/
3610 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
Simon Hausmannf9162f62007-05-17 09:02:45 +02003611 if not self.branch.startswith("refs/"):
Pete Wyckoff40d69ac2013-01-14 19:47:03 -05003612 if self.importIntoRemotes:
3613 prepend = "refs/remotes/"
3614 else:
3615 prepend = "refs/heads/"
3616 if not self.branch.startswith("p4/"):
3617 prepend += "p4/"
3618 self.branch = prepend + self.branch
Simon Hausmann179caeb2007-03-22 22:17:42 +01003619
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003620 if len(args) == 0 and self.depotPaths:
Simon Hausmannb9847332007-03-20 20:54:23 +01003621 if not self.silent:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003622 print "Depot paths: %s" % ' '.join(self.depotPaths)
Simon Hausmannb9847332007-03-20 20:54:23 +01003623 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003624 if self.depotPaths and self.depotPaths != args:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003625 print ("previous import used depot path %s and now %s was specified. "
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003626 "This doesn't work!" % (' '.join (self.depotPaths),
3627 ' '.join (args)))
Simon Hausmannb9847332007-03-20 20:54:23 +01003628 sys.exit(1)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003629
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003630 self.depotPaths = sorted(args)
Simon Hausmannb9847332007-03-20 20:54:23 +01003631
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003632 revision = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01003633 self.users = {}
Simon Hausmannb9847332007-03-20 20:54:23 +01003634
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05003635 # Make sure no revision specifiers are used when --changesfile
3636 # is specified.
3637 bad_changesfile = False
3638 if len(self.changesFile) > 0:
3639 for p in self.depotPaths:
3640 if p.find("@") >= 0 or p.find("#") >= 0:
3641 bad_changesfile = True
3642 break
3643 if bad_changesfile:
3644 die("Option --changesfile is incompatible with revision specifiers")
3645
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003646 newPaths = []
3647 for p in self.depotPaths:
3648 if p.find("@") != -1:
3649 atIdx = p.index("@")
3650 self.changeRange = p[atIdx:]
3651 if self.changeRange == "@all":
3652 self.changeRange = ""
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003653 elif ',' not in self.changeRange:
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003654 revision = self.changeRange
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003655 self.changeRange = ""
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07003656 p = p[:atIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003657 elif p.find("#") != -1:
3658 hashIdx = p.index("#")
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003659 revision = p[hashIdx:]
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07003660 p = p[:hashIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003661 elif self.previousDepotPaths == []:
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05003662 # pay attention to changesfile, if given, else import
3663 # the entire p4 tree at the head revision
3664 if len(self.changesFile) == 0:
3665 revision = "#head"
Simon Hausmannb9847332007-03-20 20:54:23 +01003666
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003667 p = re.sub ("\.\.\.$", "", p)
3668 if not p.endswith("/"):
3669 p += "/"
3670
3671 newPaths.append(p)
3672
3673 self.depotPaths = newPaths
3674
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003675 # --detect-branches may change this for each branch
3676 self.branchPrefixes = self.depotPaths
3677
Simon Hausmannb607e712007-05-20 10:55:54 +02003678 self.loadUserMapFromCache()
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02003679 self.labels = {}
3680 if self.detectLabels:
3681 self.getLabels();
Simon Hausmannb9847332007-03-20 20:54:23 +01003682
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003683 if self.detectBranches:
Simon Hausmanndf450922007-06-08 08:49:22 +02003684 ## FIXME - what's a P4 projectName ?
3685 self.projectName = self.guessProjectName()
3686
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01003687 if self.hasOrigin:
3688 self.getBranchMappingFromGitBranches()
3689 else:
3690 self.getBranchMapping()
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003691 if self.verbose:
3692 print "p4-git branches: %s" % self.p4BranchesInGit
3693 print "initial parents: %s" % self.initialParents
3694 for b in self.p4BranchesInGit:
3695 if b != "master":
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003696
3697 ## FIXME
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003698 b = b[len(self.projectName):]
3699 self.createdBranches.add(b)
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003700
Luke Diamand123f6312018-05-23 23:20:26 +01003701 self.openStreams()
Simon Hausmannb9847332007-03-20 20:54:23 +01003702
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003703 if revision:
Simon Hausmannc208a242007-08-26 16:07:18 +02003704 self.importHeadRevision(revision)
Simon Hausmannb9847332007-03-20 20:54:23 +01003705 else:
3706 changes = []
3707
Simon Hausmann0828ab12007-03-20 20:59:30 +01003708 if len(self.changesFile) > 0:
Simon Hausmannb9847332007-03-20 20:54:23 +01003709 output = open(self.changesFile).readlines()
Reilly Grant1d7367d2009-09-10 00:02:38 -07003710 changeSet = set()
Simon Hausmannb9847332007-03-20 20:54:23 +01003711 for line in output:
3712 changeSet.add(int(line))
3713
3714 for change in changeSet:
3715 changes.append(change)
3716
3717 changes.sort()
3718 else:
Pete Wyckoff9dcb9f22012-04-08 20:18:01 -04003719 # catch "git p4 sync" with no new branches, in a repo that
3720 # does not have any existing p4 branches
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05003721 if len(args) == 0:
3722 if not self.p4BranchesInGit:
3723 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3724
3725 # The default branch is master, unless --branch is used to
3726 # specify something else. Make sure it exists, or complain
3727 # nicely about how to use --branch.
3728 if not self.detectBranches:
3729 if not branch_exists(self.branch):
3730 if branch_arg_given:
3731 die("Error: branch %s does not exist." % self.branch)
3732 else:
3733 die("Error: no branch %s; perhaps specify one with --branch." %
3734 self.branch)
3735
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003736 if self.verbose:
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003737 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003738 self.changeRange)
Lex Spoon96b2d542015-04-20 11:00:20 -04003739 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
Simon Hausmannb9847332007-03-20 20:54:23 +01003740
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02003741 if len(self.maxChanges) > 0:
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07003742 changes = changes[:min(int(self.maxChanges), len(changes))]
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02003743
Simon Hausmannb9847332007-03-20 20:54:23 +01003744 if len(changes) == 0:
Simon Hausmann0828ab12007-03-20 20:59:30 +01003745 if not self.silent:
Simon Hausmann341dc1c2007-05-21 00:39:16 +02003746 print "No changes to import!"
Luke Diamand06804c72012-04-11 17:21:24 +02003747 else:
3748 if not self.silent and not self.detectBranches:
3749 print "Import destination: %s" % self.branch
Simon Hausmannb9847332007-03-20 20:54:23 +01003750
Luke Diamand06804c72012-04-11 17:21:24 +02003751 self.updatedBranches = set()
Simon Hausmanna9d1a272007-06-11 23:28:03 +02003752
Pete Wyckoff47497842013-01-14 19:47:04 -05003753 if not self.detectBranches:
3754 if args:
3755 # start a new branch
3756 self.initialParent = ""
3757 else:
3758 # build on a previous revision
3759 self.initialParent = parseRevision(self.branch)
3760
Luke Diamand06804c72012-04-11 17:21:24 +02003761 self.importChanges(changes)
Simon Hausmann341dc1c2007-05-21 00:39:16 +02003762
Luke Diamand06804c72012-04-11 17:21:24 +02003763 if not self.silent:
3764 print ""
3765 if len(self.updatedBranches) > 0:
3766 sys.stdout.write("Updated branches: ")
3767 for b in self.updatedBranches:
3768 sys.stdout.write("%s " % b)
3769 sys.stdout.write("\n")
Simon Hausmannb9847332007-03-20 20:54:23 +01003770
Pete Wyckoff0d609032013-01-26 22:11:24 -05003771 if gitConfigBool("git-p4.importLabels"):
Luke Diamand06dcd152012-05-11 07:25:18 +01003772 self.importLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02003773
3774 if self.importLabels:
3775 p4Labels = getP4Labels(self.depotPaths)
3776 gitTags = getGitTags()
3777
3778 missingP4Labels = p4Labels - gitTags
3779 self.importP4Labels(self.gitStream, missingP4Labels)
Simon Hausmannb9847332007-03-20 20:54:23 +01003780
Luke Diamand123f6312018-05-23 23:20:26 +01003781 self.closeStreams()
Simon Hausmannb9847332007-03-20 20:54:23 +01003782
Vitor Antunesfed23692012-01-25 23:48:22 +00003783 # Cleanup temporary branches created during import
3784 if self.tempBranches != []:
3785 for branch in self.tempBranches:
3786 read_pipe("git update-ref -d %s" % branch)
3787 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3788
Pete Wyckoff55d12432013-01-14 19:46:59 -05003789 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3790 # a convenient shortcut refname "p4".
3791 if self.importIntoRemotes:
3792 head_ref = self.refPrefix + "HEAD"
3793 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3794 system(["git", "symbolic-ref", head_ref, self.branch])
3795
Simon Hausmannb9847332007-03-20 20:54:23 +01003796 return True
3797
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003798class P4Rebase(Command):
3799 def __init__(self):
3800 Command.__init__(self)
Luke Diamand06804c72012-04-11 17:21:24 +02003801 self.options = [
3802 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02003803 ]
Luke Diamand06804c72012-04-11 17:21:24 +02003804 self.importLabels = False
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003805 self.description = ("Fetches the latest revision from perforce and "
3806 + "rebases the current work (branch) against it")
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003807
3808 def run(self, args):
3809 sync = P4Sync()
Luke Diamand06804c72012-04-11 17:21:24 +02003810 sync.importLabels = self.importLabels
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003811 sync.run([])
Simon Hausmannd7e38682007-06-12 14:34:46 +02003812
Simon Hausmann14594f42007-08-22 09:07:15 +02003813 return self.rebase()
3814
3815 def rebase(self):
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01003816 if os.system("git update-index --refresh") != 0:
Martin Ågren7560f542017-08-23 19:49:35 +02003817 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.");
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01003818 if len(read_pipe("git diff-index HEAD --")) > 0:
Veres Lajosf7e604e2013-06-19 07:37:24 +02003819 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01003820
Simon Hausmannd7e38682007-06-12 14:34:46 +02003821 [upstream, settings] = findUpstreamBranchPoint()
3822 if len(upstream) == 0:
3823 die("Cannot find upstream branchpoint for rebase")
3824
3825 # the branchpoint may be p4/foo~3, so strip off the parent
3826 upstream = re.sub("~[0-9]+$", "", upstream)
3827
3828 print "Rebasing the current branch onto %s" % upstream
Han-Wen Nienhuysb25b2062007-05-23 18:49:35 -03003829 oldHead = read_pipe("git rev-parse HEAD").strip()
Simon Hausmannd7e38682007-06-12 14:34:46 +02003830 system("git rebase %s" % upstream)
Vlad Dogaru4e49d952014-04-07 16:19:11 +03003831 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003832 return True
3833
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003834class P4Clone(P4Sync):
3835 def __init__(self):
3836 P4Sync.__init__(self)
3837 self.description = "Creates a new git repository and imports from Perforce into it"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003838 self.usage = "usage: %prog [options] //depot/path[@revRange]"
Tommy Thorn354081d2008-02-03 10:38:51 -08003839 self.options += [
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003840 optparse.make_option("--destination", dest="cloneDestination",
3841 action='store', default=None,
Tommy Thorn354081d2008-02-03 10:38:51 -08003842 help="where to leave result of the clone"),
Pete Wyckoff38200072011-02-19 08:18:01 -05003843 optparse.make_option("--bare", dest="cloneBare",
3844 action="store_true", default=False),
Tommy Thorn354081d2008-02-03 10:38:51 -08003845 ]
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003846 self.cloneDestination = None
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003847 self.needsGit = False
Pete Wyckoff38200072011-02-19 08:18:01 -05003848 self.cloneBare = False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003849
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003850 def defaultDestination(self, args):
3851 ## TODO: use common prefix of args?
3852 depotPath = args[0]
3853 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3854 depotDir = re.sub("(#[^#]*)$", "", depotDir)
Toby Allsopp053d9e42008-02-05 09:41:43 +13003855 depotDir = re.sub(r"\.\.\.$", "", depotDir)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003856 depotDir = re.sub(r"/$", "", depotDir)
3857 return os.path.split(depotDir)[1]
3858
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003859 def run(self, args):
3860 if len(args) < 1:
3861 return False
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003862
3863 if self.keepRepoPath and not self.cloneDestination:
3864 sys.stderr.write("Must specify destination for --keep-path\n")
3865 sys.exit(1)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003866
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003867 depotPaths = args
Simon Hausmann5e100b52007-06-07 21:12:25 +02003868
3869 if not self.cloneDestination and len(depotPaths) > 1:
3870 self.cloneDestination = depotPaths[-1]
3871 depotPaths = depotPaths[:-1]
3872
Tommy Thorn354081d2008-02-03 10:38:51 -08003873 self.cloneExclude = ["/"+p for p in self.cloneExclude]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003874 for p in depotPaths:
3875 if not p.startswith("//"):
Pete Wyckoff0f487d32013-01-26 22:11:06 -05003876 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003877 return False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003878
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003879 if not self.cloneDestination:
Marius Storm-Olsen98ad4fa2007-06-07 15:08:33 +02003880 self.cloneDestination = self.defaultDestination(args)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003881
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003882 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
Pete Wyckoff38200072011-02-19 08:18:01 -05003883
Kevin Greenc3bf3f12007-06-11 16:48:07 -04003884 if not os.path.exists(self.cloneDestination):
3885 os.makedirs(self.cloneDestination)
Robert Blum053fd0c2008-08-01 12:50:03 -07003886 chdir(self.cloneDestination)
Pete Wyckoff38200072011-02-19 08:18:01 -05003887
3888 init_cmd = [ "git", "init" ]
3889 if self.cloneBare:
3890 init_cmd.append("--bare")
Brandon Caseya235e852013-01-26 11:14:33 -08003891 retcode = subprocess.call(init_cmd)
3892 if retcode:
3893 raise CalledProcessError(retcode, init_cmd)
Pete Wyckoff38200072011-02-19 08:18:01 -05003894
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003895 if not P4Sync.run(self, depotPaths):
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003896 return False
Pete Wyckoffc5959562013-01-14 19:47:01 -05003897
3898 # create a master branch and check out a work tree
3899 if gitBranchExists(self.branch):
3900 system([ "git", "branch", "master", self.branch ])
3901 if not self.cloneBare:
3902 system([ "git", "checkout", "-f" ])
3903 else:
3904 print 'Not checking out any branch, use ' \
3905 '"git checkout -q -b master <branch>"'
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003906
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05003907 # auto-set this variable if invoked with --use-client-spec
3908 if self.useClientSpec_from_options:
3909 system("git config --bool git-p4.useclientspec true")
3910
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003911 return True
3912
Luke Diamand123f6312018-05-23 23:20:26 +01003913class P4Unshelve(Command):
3914 def __init__(self):
3915 Command.__init__(self)
3916 self.options = []
3917 self.origin = "HEAD"
3918 self.description = "Unshelve a P4 changelist into a git commit"
3919 self.usage = "usage: %prog [options] changelist"
3920 self.options += [
3921 optparse.make_option("--origin", dest="origin",
3922 help="Use this base revision instead of the default (%s)" % self.origin),
3923 ]
3924 self.verbose = False
3925 self.noCommit = False
3926 self.destbranch = "refs/remotes/p4/unshelved"
3927
3928 def renameBranch(self, branch_name):
3929 """ Rename the existing branch to branch_name.N
3930 """
3931
3932 found = True
3933 for i in range(0,1000):
3934 backup_branch_name = "{0}.{1}".format(branch_name, i)
3935 if not gitBranchExists(backup_branch_name):
3936 gitUpdateRef(backup_branch_name, branch_name) # copy ref to backup
3937 gitDeleteRef(branch_name)
3938 found = True
3939 print("renamed old unshelve branch to {0}".format(backup_branch_name))
3940 break
3941
3942 if not found:
3943 sys.exit("gave up trying to rename existing branch {0}".format(sync.branch))
3944
3945 def findLastP4Revision(self, starting_point):
3946 """ Look back from starting_point for the first commit created by git-p4
3947 to find the P4 commit we are based on, and the depot-paths.
3948 """
3949
3950 for parent in (range(65535)):
3951 log = extractLogMessageFromGitCommit("{0}^{1}".format(starting_point, parent))
3952 settings = extractSettingsGitLog(log)
3953 if settings.has_key('change'):
3954 return settings
3955
3956 sys.exit("could not find git-p4 commits in {0}".format(self.origin))
3957
3958 def run(self, args):
3959 if len(args) != 1:
3960 return False
3961
3962 if not gitBranchExists(self.origin):
3963 sys.exit("origin branch {0} does not exist".format(self.origin))
3964
3965 sync = P4Sync()
3966 changes = args
3967 sync.initialParent = self.origin
3968
3969 # use the first change in the list to construct the branch to unshelve into
3970 change = changes[0]
3971
3972 # if the target branch already exists, rename it
3973 branch_name = "{0}/{1}".format(self.destbranch, change)
3974 if gitBranchExists(branch_name):
3975 self.renameBranch(branch_name)
3976 sync.branch = branch_name
3977
3978 sync.verbose = self.verbose
3979 sync.suppress_meta_comment = True
3980
3981 settings = self.findLastP4Revision(self.origin)
3982 origin_revision = settings['change']
3983 sync.depotPaths = settings['depot-paths']
3984 sync.branchPrefixes = sync.depotPaths
3985
3986 sync.openStreams()
3987 sync.loadUserMapFromCache()
3988 sync.silent = True
3989 sync.importChanges(changes, shelved=True, origin_revision=origin_revision)
3990 sync.closeStreams()
3991
3992 print("unshelved changelist {0} into {1}".format(change, branch_name))
3993
3994 return True
3995
Simon Hausmann09d89de2007-06-20 23:10:28 +02003996class P4Branches(Command):
3997 def __init__(self):
3998 Command.__init__(self)
3999 self.options = [ ]
4000 self.description = ("Shows the git branches that hold imports and their "
4001 + "corresponding perforce depot paths")
4002 self.verbose = False
4003
4004 def run(self, args):
Simon Hausmann5ca44612007-08-24 17:44:16 +02004005 if originP4BranchesExist():
4006 createOrUpdateBranchesFromOrigin()
4007
Simon Hausmann09d89de2007-06-20 23:10:28 +02004008 cmdline = "git rev-parse --symbolic "
4009 cmdline += " --remotes"
4010
4011 for line in read_pipe_lines(cmdline):
4012 line = line.strip()
4013
4014 if not line.startswith('p4/') or line == "p4/HEAD":
4015 continue
4016 branch = line
4017
4018 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
4019 settings = extractSettingsGitLog(log)
4020
4021 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
4022 return True
4023
Simon Hausmannb9847332007-03-20 20:54:23 +01004024class HelpFormatter(optparse.IndentedHelpFormatter):
4025 def __init__(self):
4026 optparse.IndentedHelpFormatter.__init__(self)
4027
4028 def format_description(self, description):
4029 if description:
4030 return description + "\n"
4031 else:
4032 return ""
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004033
Simon Hausmann86949ee2007-03-19 20:59:12 +01004034def printUsage(commands):
4035 print "usage: %s <command> [options]" % sys.argv[0]
4036 print ""
4037 print "valid commands: %s" % ", ".join(commands)
4038 print ""
4039 print "Try %s <command> --help for command specific help." % sys.argv[0]
4040 print ""
4041
4042commands = {
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004043 "debug" : P4Debug,
4044 "submit" : P4Submit,
Marius Storm-Olsena9834f52007-10-09 16:16:09 +02004045 "commit" : P4Submit,
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004046 "sync" : P4Sync,
4047 "rebase" : P4Rebase,
4048 "clone" : P4Clone,
Simon Hausmann09d89de2007-06-20 23:10:28 +02004049 "rollback" : P4RollBack,
Luke Diamand123f6312018-05-23 23:20:26 +01004050 "branches" : P4Branches,
4051 "unshelve" : P4Unshelve,
Simon Hausmann86949ee2007-03-19 20:59:12 +01004052}
4053
Simon Hausmann86949ee2007-03-19 20:59:12 +01004054
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004055def main():
4056 if len(sys.argv[1:]) == 0:
4057 printUsage(commands.keys())
4058 sys.exit(2)
Simon Hausmann86949ee2007-03-19 20:59:12 +01004059
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004060 cmdName = sys.argv[1]
4061 try:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004062 klass = commands[cmdName]
4063 cmd = klass()
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004064 except KeyError:
4065 print "unknown command %s" % cmdName
4066 print ""
4067 printUsage(commands.keys())
4068 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004069
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004070 options = cmd.options
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004071 cmd.gitdir = os.environ.get("GIT_DIR", None)
Simon Hausmann86949ee2007-03-19 20:59:12 +01004072
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004073 args = sys.argv[2:]
Simon Hausmanne20a9e52007-03-26 00:13:51 +02004074
Pete Wyckoffb0ccc802012-09-09 16:16:10 -04004075 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
Luke Diamand6a10b6a2012-04-24 09:33:23 +01004076 if cmd.needsGit:
4077 options.append(optparse.make_option("--git-dir", dest="gitdir"))
Simon Hausmanne20a9e52007-03-26 00:13:51 +02004078
Luke Diamand6a10b6a2012-04-24 09:33:23 +01004079 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
4080 options,
4081 description = cmd.description,
4082 formatter = HelpFormatter())
Simon Hausmann86949ee2007-03-19 20:59:12 +01004083
Luke Diamand6a10b6a2012-04-24 09:33:23 +01004084 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004085 global verbose
4086 verbose = cmd.verbose
4087 if cmd.needsGit:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004088 if cmd.gitdir == None:
4089 cmd.gitdir = os.path.abspath(".git")
4090 if not isValidGitDir(cmd.gitdir):
Luke Diamand378f7be2016-12-13 21:51:28 +00004091 # "rev-parse --git-dir" without arguments will try $PWD/.git
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004092 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
4093 if os.path.exists(cmd.gitdir):
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004094 cdup = read_pipe("git rev-parse --show-cdup").strip()
4095 if len(cdup) > 0:
Robert Blum053fd0c2008-08-01 12:50:03 -07004096 chdir(cdup);
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004097
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004098 if not isValidGitDir(cmd.gitdir):
4099 if isValidGitDir(cmd.gitdir + "/.git"):
4100 cmd.gitdir += "/.git"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004101 else:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004102 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
Simon Hausmann8910ac02007-03-26 08:18:55 +02004103
Luke Diamand378f7be2016-12-13 21:51:28 +00004104 # so git commands invoked from the P4 workspace will succeed
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03004105 os.environ["GIT_DIR"] = cmd.gitdir
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004106
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004107 if not cmd.run(args):
4108 parser.print_help()
Pete Wyckoff09fca772011-12-24 21:07:39 -05004109 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01004110
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03004111
4112if __name__ == '__main__':
4113 main()