blob: 8d151da91b9699e804f4d28b865af7f44138bfc1 [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 Diamand1051ef02015-06-10 08:30:59 +010050# Grab changes in blocks of this many revisions, unless otherwise requested
51defaultBlockSize = 512
52
Anand Kumria21a50752008-08-10 19:26:28 +010053def p4_build_cmd(cmd):
54 """Build a suitable p4 command line.
55
56 This consolidates building and returning a p4 command line into one
57 location. It means that hooking into the environment, or other configuration
58 can be done more easily.
59 """
Luke Diamand6de040d2011-10-16 10:47:52 -040060 real_cmd = ["p4"]
Anand Kumriaabcaf072008-08-10 19:26:31 +010061
62 user = gitConfig("git-p4.user")
63 if len(user) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040064 real_cmd += ["-u",user]
Anand Kumriaabcaf072008-08-10 19:26:31 +010065
66 password = gitConfig("git-p4.password")
67 if len(password) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040068 real_cmd += ["-P", password]
Anand Kumriaabcaf072008-08-10 19:26:31 +010069
70 port = gitConfig("git-p4.port")
71 if len(port) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040072 real_cmd += ["-p", port]
Anand Kumriaabcaf072008-08-10 19:26:31 +010073
74 host = gitConfig("git-p4.host")
75 if len(host) > 0:
Russell Myers41799aa2012-02-22 11:16:05 -080076 real_cmd += ["-H", host]
Anand Kumriaabcaf072008-08-10 19:26:31 +010077
78 client = gitConfig("git-p4.client")
79 if len(client) > 0:
Luke Diamand6de040d2011-10-16 10:47:52 -040080 real_cmd += ["-c", client]
Anand Kumriaabcaf072008-08-10 19:26:31 +010081
Lars Schneider89a6ecc2016-12-04 15:03:11 +010082 retries = gitConfigInt("git-p4.retries")
83 if retries is None:
84 # Perform 3 retries by default
85 retries = 3
Igor Kushnirbc233522016-12-29 12:22:23 +020086 if retries > 0:
87 # Provide a way to not pass this option by setting git-p4.retries to 0
88 real_cmd += ["-r", str(retries)]
Luke Diamand6de040d2011-10-16 10:47:52 -040089
90 if isinstance(cmd,basestring):
91 real_cmd = ' '.join(real_cmd) + ' ' + cmd
92 else:
93 real_cmd += cmd
Anand Kumria21a50752008-08-10 19:26:28 +010094 return real_cmd
95
Luke Diamand378f7be2016-12-13 21:51:28 +000096def git_dir(path):
97 """ Return TRUE if the given path is a git directory (/path/to/dir/.git).
98 This won't automatically add ".git" to a directory.
99 """
100 d = read_pipe(["git", "--git-dir", path, "rev-parse", "--git-dir"], True).strip()
101 if not d or len(d) == 0:
102 return None
103 else:
104 return d
105
Miklós Fazekasbbd84862013-03-11 17:45:29 -0400106def chdir(path, is_client_path=False):
107 """Do chdir to the given path, and set the PWD environment
108 variable for use by P4. It does not look at getcwd() output.
109 Since we're not using the shell, it is necessary to set the
110 PWD environment variable explicitly.
111
112 Normally, expand the path to force it to be absolute. This
113 addresses the use of relative path names inside P4 settings,
114 e.g. P4CONFIG=.p4config. P4 does not simply open the filename
115 as given; it looks for .p4config using PWD.
116
117 If is_client_path, the path was handed to us directly by p4,
118 and may be a symbolic link. Do not call os.getcwd() in this
119 case, because it will cause p4 to think that PWD is not inside
120 the client path.
121 """
122
123 os.chdir(path)
124 if not is_client_path:
125 path = os.getcwd()
126 os.environ['PWD'] = path
Robert Blum053fd0c2008-08-01 12:50:03 -0700127
Lars Schneider4d25dc42015-09-26 09:55:02 +0200128def calcDiskFree():
129 """Return free space in bytes on the disk of the given dirname."""
130 if platform.system() == 'Windows':
131 free_bytes = ctypes.c_ulonglong(0)
132 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
133 return free_bytes.value
134 else:
135 st = os.statvfs(os.getcwd())
136 return st.f_bavail * st.f_frsize
137
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -0300138def die(msg):
139 if verbose:
140 raise Exception(msg)
141 else:
142 sys.stderr.write(msg + "\n")
143 sys.exit(1)
144
Luke Diamand6de040d2011-10-16 10:47:52 -0400145def write_pipe(c, stdin):
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300146 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400147 sys.stderr.write('Writing pipe: %s\n' % str(c))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300148
Luke Diamand6de040d2011-10-16 10:47:52 -0400149 expand = isinstance(c,basestring)
150 p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
151 pipe = p.stdin
152 val = pipe.write(stdin)
153 pipe.close()
154 if p.wait():
155 die('Command failed: %s' % str(c))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300156
157 return val
158
Luke Diamand6de040d2011-10-16 10:47:52 -0400159def p4_write_pipe(c, stdin):
Anand Kumriad9429192008-08-14 23:40:38 +0100160 real_cmd = p4_build_cmd(c)
Luke Diamand6de040d2011-10-16 10:47:52 -0400161 return write_pipe(real_cmd, stdin)
Anand Kumriad9429192008-08-14 23:40:38 +0100162
Luke Diamand78871bf2017-04-15 11:36:08 +0100163def read_pipe_full(c):
164 """ Read output from command. Returns a tuple
165 of the return status, stdout text and stderr
166 text.
167 """
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300168 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400169 sys.stderr.write('Reading pipe: %s\n' % str(c))
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -0300170
Luke Diamand6de040d2011-10-16 10:47:52 -0400171 expand = isinstance(c,basestring)
Lars Schneider1f5f3902015-09-21 12:01:41 +0200172 p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
173 (out, err) = p.communicate()
Luke Diamand78871bf2017-04-15 11:36:08 +0100174 return (p.returncode, out, err)
175
176def read_pipe(c, ignore_error=False):
177 """ Read output from command. Returns the output text on
178 success. On failure, terminates execution, unless
179 ignore_error is True, when it returns an empty string.
180 """
181 (retcode, out, err) = read_pipe_full(c)
182 if retcode != 0:
183 if ignore_error:
184 out = ""
185 else:
186 die('Command failed: %s\nError: %s' % (str(c), err))
Lars Schneider1f5f3902015-09-21 12:01:41 +0200187 return out
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300188
Luke Diamand78871bf2017-04-15 11:36:08 +0100189def read_pipe_text(c):
190 """ Read output from a command with trailing whitespace stripped.
191 On error, returns None.
192 """
193 (retcode, out, err) = read_pipe_full(c)
194 if retcode != 0:
195 return None
196 else:
197 return out.rstrip()
198
Anand Kumriad9429192008-08-14 23:40:38 +0100199def p4_read_pipe(c, ignore_error=False):
200 real_cmd = p4_build_cmd(c)
201 return read_pipe(real_cmd, ignore_error)
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300202
Han-Wen Nienhuysbce4c5f2007-05-23 17:14:33 -0300203def read_pipe_lines(c):
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300204 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400205 sys.stderr.write('Reading pipe: %s\n' % str(c))
206
207 expand = isinstance(c, basestring)
208 p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
209 pipe = p.stdout
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300210 val = pipe.readlines()
Luke Diamand6de040d2011-10-16 10:47:52 -0400211 if pipe.close() or p.wait():
212 die('Command failed: %s' % str(c))
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300213
214 return val
Simon Hausmanncaace112007-05-15 14:57:57 +0200215
Anand Kumria23181212008-08-10 19:26:24 +0100216def p4_read_pipe_lines(c):
217 """Specifically invoke p4 on the command supplied. """
Anand Kumria155af832008-08-10 19:26:30 +0100218 real_cmd = p4_build_cmd(c)
Anand Kumria23181212008-08-10 19:26:24 +0100219 return read_pipe_lines(real_cmd)
220
Gary Gibbons8e9497c2012-07-12 19:29:00 -0400221def p4_has_command(cmd):
222 """Ask p4 for help on this command. If it returns an error, the
223 command does not exist in this version of p4."""
224 real_cmd = p4_build_cmd(["help", cmd])
225 p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
226 stderr=subprocess.PIPE)
227 p.communicate()
228 return p.returncode == 0
229
Pete Wyckoff249da4c2012-11-23 17:35:35 -0500230def p4_has_move_command():
231 """See if the move command exists, that it supports -k, and that
232 it has not been administratively disabled. The arguments
233 must be correct, but the filenames do not have to exist. Use
234 ones with wildcards so even if they exist, it will fail."""
235
236 if not p4_has_command("move"):
237 return False
238 cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
239 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
240 (out, err) = p.communicate()
241 # return code will be 1 in either case
242 if err.find("Invalid option") >= 0:
243 return False
244 if err.find("disabled") >= 0:
245 return False
246 # assume it failed because @... was invalid changelist
247 return True
248
Luke Diamandcbff4b22015-11-21 09:54:40 +0000249def system(cmd, ignore_error=False):
Luke Diamand6de040d2011-10-16 10:47:52 -0400250 expand = isinstance(cmd,basestring)
Han-Wen Nienhuys4addad22007-05-23 18:49:35 -0300251 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400252 sys.stderr.write("executing %s\n" % str(cmd))
Brandon Caseya235e852013-01-26 11:14:33 -0800253 retcode = subprocess.call(cmd, shell=expand)
Luke Diamandcbff4b22015-11-21 09:54:40 +0000254 if retcode and not ignore_error:
Brandon Caseya235e852013-01-26 11:14:33 -0800255 raise CalledProcessError(retcode, cmd)
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -0300256
Luke Diamandcbff4b22015-11-21 09:54:40 +0000257 return retcode
258
Anand Kumriabf9320f2008-08-10 19:26:26 +0100259def p4_system(cmd):
260 """Specifically invoke p4 as the system command. """
Anand Kumria155af832008-08-10 19:26:30 +0100261 real_cmd = p4_build_cmd(cmd)
Luke Diamand6de040d2011-10-16 10:47:52 -0400262 expand = isinstance(real_cmd, basestring)
Brandon Caseya235e852013-01-26 11:14:33 -0800263 retcode = subprocess.call(real_cmd, shell=expand)
264 if retcode:
265 raise CalledProcessError(retcode, real_cmd)
Luke Diamand6de040d2011-10-16 10:47:52 -0400266
Pete Wyckoff7f0e5962013-01-26 22:11:13 -0500267_p4_version_string = None
268def p4_version_string():
269 """Read the version string, showing just the last line, which
270 hopefully is the interesting version bit.
271
272 $ p4 -V
273 Perforce - The Fast Software Configuration Management System.
274 Copyright 1995-2011 Perforce Software. All rights reserved.
275 Rev. P4/NTX86/2011.1/393975 (2011/12/16).
276 """
277 global _p4_version_string
278 if not _p4_version_string:
279 a = p4_read_pipe_lines(["-V"])
280 _p4_version_string = a[-1].rstrip()
281 return _p4_version_string
282
Luke Diamand6de040d2011-10-16 10:47:52 -0400283def p4_integrate(src, dest):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400284 p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400285
Pete Wyckoff8d7ec362012-04-29 20:57:14 -0400286def p4_sync(f, *options):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400287 p4_system(["sync"] + list(options) + [wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400288
289def p4_add(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400290 # forcibly add file names with wildcards
291 if wildcard_present(f):
292 p4_system(["add", "-f", f])
293 else:
294 p4_system(["add", f])
Luke Diamand6de040d2011-10-16 10:47:52 -0400295
296def p4_delete(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400297 p4_system(["delete", wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400298
Romain Picarda02b8bc2016-01-12 13:43:47 +0100299def p4_edit(f, *options):
300 p4_system(["edit"] + list(options) + [wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400301
302def p4_revert(f):
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400303 p4_system(["revert", wildcard_encode(f)])
Luke Diamand6de040d2011-10-16 10:47:52 -0400304
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400305def p4_reopen(type, f):
306 p4_system(["reopen", "-t", type, wildcard_encode(f)])
Anand Kumriabf9320f2008-08-10 19:26:26 +0100307
Luke Diamand46c609e2016-12-02 22:43:19 +0000308def p4_reopen_in_change(changelist, files):
309 cmd = ["reopen", "-c", str(changelist)] + files
310 p4_system(cmd)
311
Gary Gibbons8e9497c2012-07-12 19:29:00 -0400312def p4_move(src, dest):
313 p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
314
Luke Diamand1051ef02015-06-10 08:30:59 +0100315def p4_last_change():
316 results = p4CmdList(["changes", "-m", "1"])
317 return int(results[0]['change'])
318
Pete Wyckoff18fa13d2012-11-23 17:35:34 -0500319def p4_describe(change):
320 """Make sure it returns a valid result by checking for
321 the presence of field "time". Return a dict of the
322 results."""
323
324 ds = p4CmdList(["describe", "-s", str(change)])
325 if len(ds) != 1:
326 die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
327
328 d = ds[0]
329
330 if "p4ExitCode" in d:
331 die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
332 str(d)))
333 if "code" in d:
334 if d["code"] == "error":
335 die("p4 describe -s %d returned error code: %s" % (change, str(d)))
336
337 if "time" not in d:
338 die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
339
340 return d
341
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -0400342#
343# Canonicalize the p4 type and return a tuple of the
344# base type, plus any modifiers. See "p4 help filetypes"
345# for a list and explanation.
346#
347def split_p4_type(p4type):
David Brownb9fc6ea2007-09-19 13:12:48 -0700348
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -0400349 p4_filetypes_historical = {
350 "ctempobj": "binary+Sw",
351 "ctext": "text+C",
352 "cxtext": "text+Cx",
353 "ktext": "text+k",
354 "kxtext": "text+kx",
355 "ltext": "text+F",
356 "tempobj": "binary+FSw",
357 "ubinary": "binary+F",
358 "uresource": "resource+F",
359 "uxbinary": "binary+Fx",
360 "xbinary": "binary+x",
361 "xltext": "text+Fx",
362 "xtempobj": "binary+Swx",
363 "xtext": "text+x",
364 "xunicode": "unicode+x",
365 "xutf16": "utf16+x",
366 }
367 if p4type in p4_filetypes_historical:
368 p4type = p4_filetypes_historical[p4type]
369 mods = ""
370 s = p4type.split("+")
371 base = s[0]
372 mods = ""
373 if len(s) > 1:
374 mods = s[1]
375 return (base, mods)
376
Luke Diamand60df0712012-02-23 07:51:30 +0000377#
378# return the raw p4 type of a file (text, text+ko, etc)
379#
Pete Wyckoff79467e62014-01-21 18:16:45 -0500380def p4_type(f):
381 results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
Luke Diamand60df0712012-02-23 07:51:30 +0000382 return results[0]['headType']
383
384#
385# Given a type base and modifier, return a regexp matching
386# the keywords that can be expanded in the file
387#
388def p4_keywords_regexp_for_type(base, type_mods):
389 if base in ("text", "unicode", "binary"):
390 kwords = None
391 if "ko" in type_mods:
392 kwords = 'Id|Header'
393 elif "k" in type_mods:
394 kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
395 else:
396 return None
397 pattern = r"""
398 \$ # Starts with a dollar, followed by...
399 (%s) # one of the keywords, followed by...
Pete Wyckoff6b2bf412012-11-04 17:04:02 -0500400 (:[^$\n]+)? # possibly an old expansion, followed by...
Luke Diamand60df0712012-02-23 07:51:30 +0000401 \$ # another dollar
402 """ % kwords
403 return pattern
404 else:
405 return None
406
407#
408# Given a file, return a regexp matching the possible
409# RCS keywords that will be expanded, or None for files
410# with kw expansion turned off.
411#
412def p4_keywords_regexp_for_file(file):
413 if not os.path.exists(file):
414 return None
415 else:
416 (type_base, type_mods) = split_p4_type(p4_type(file))
417 return p4_keywords_regexp_for_type(type_base, type_mods)
David Brownb9fc6ea2007-09-19 13:12:48 -0700418
Chris Pettittc65b6702007-11-01 20:43:14 -0700419def setP4ExecBit(file, mode):
420 # Reopens an already open file and changes the execute bit to match
421 # the execute bit setting in the passed in mode.
422
423 p4Type = "+x"
424
425 if not isModeExec(mode):
426 p4Type = getP4OpenedType(file)
427 p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
428 p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
429 if p4Type[-1] == "+":
430 p4Type = p4Type[0:-1]
431
Luke Diamand6de040d2011-10-16 10:47:52 -0400432 p4_reopen(p4Type, file)
Chris Pettittc65b6702007-11-01 20:43:14 -0700433
434def getP4OpenedType(file):
435 # Returns the perforce file type for the given file.
436
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400437 result = p4_read_pipe(["opened", wildcard_encode(file)])
Blair Holloway34a0dbf2015-04-04 09:46:03 +0100438 match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
Chris Pettittc65b6702007-11-01 20:43:14 -0700439 if match:
440 return match.group(1)
441 else:
Marius Storm-Olsenf3e5ae42008-03-28 15:40:40 +0100442 die("Could not determine file type for %s (result: '%s')" % (file, result))
Chris Pettittc65b6702007-11-01 20:43:14 -0700443
Luke Diamand06804c72012-04-11 17:21:24 +0200444# Return the set of all p4 labels
445def getP4Labels(depotPaths):
446 labels = set()
447 if isinstance(depotPaths,basestring):
448 depotPaths = [depotPaths]
449
450 for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
451 label = l['label']
452 labels.add(label)
453
454 return labels
455
456# Return the set of all git tags
457def getGitTags():
458 gitTags = set()
459 for line in read_pipe_lines(["git", "tag"]):
460 tag = line.strip()
461 gitTags.add(tag)
462 return gitTags
463
Chris Pettittb43b0a32007-11-01 20:43:13 -0700464def diffTreePattern():
465 # This is a simple generator for the diff tree regex pattern. This could be
466 # a class variable if this and parseDiffTreeEntry were a part of a class.
467 pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
468 while True:
469 yield pattern
470
471def parseDiffTreeEntry(entry):
472 """Parses a single diff tree entry into its component elements.
473
474 See git-diff-tree(1) manpage for details about the format of the diff
475 output. This method returns a dictionary with the following elements:
476
477 src_mode - The mode of the source file
478 dst_mode - The mode of the destination file
479 src_sha1 - The sha1 for the source file
480 dst_sha1 - The sha1 fr the destination file
481 status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
482 status_score - The score for the status (applicable for 'C' and 'R'
483 statuses). This is None if there is no score.
484 src - The path for the source file.
485 dst - The path for the destination file. This is only present for
486 copy or renames. If it is not present, this is None.
487
488 If the pattern is not matched, None is returned."""
489
490 match = diffTreePattern().next().match(entry)
491 if match:
492 return {
493 'src_mode': match.group(1),
494 'dst_mode': match.group(2),
495 'src_sha1': match.group(3),
496 'dst_sha1': match.group(4),
497 'status': match.group(5),
498 'status_score': match.group(6),
499 'src': match.group(7),
500 'dst': match.group(10)
501 }
502 return None
503
Chris Pettittc65b6702007-11-01 20:43:14 -0700504def isModeExec(mode):
505 # Returns True if the given git mode represents an executable file,
506 # otherwise False.
507 return mode[-3:] == "755"
508
509def isModeExecChanged(src_mode, dst_mode):
510 return isModeExec(src_mode) != isModeExec(dst_mode)
511
Luke Diamandb9327052009-07-30 00:13:46 +0100512def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
Luke Diamand6de040d2011-10-16 10:47:52 -0400513
514 if isinstance(cmd,basestring):
515 cmd = "-G " + cmd
516 expand = True
517 else:
518 cmd = ["-G"] + cmd
519 expand = False
520
521 cmd = p4_build_cmd(cmd)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -0300522 if verbose:
Luke Diamand6de040d2011-10-16 10:47:52 -0400523 sys.stderr.write("Opening pipe: %s\n" % str(cmd))
Scott Lamb9f90c732007-07-15 20:58:10 -0700524
525 # Use a temporary file to avoid deadlocks without
526 # subprocess.communicate(), which would put another copy
527 # of stdout into memory.
528 stdin_file = None
529 if stdin is not None:
530 stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
Luke Diamand6de040d2011-10-16 10:47:52 -0400531 if isinstance(stdin,basestring):
532 stdin_file.write(stdin)
533 else:
534 for i in stdin:
535 stdin_file.write(i + '\n')
Scott Lamb9f90c732007-07-15 20:58:10 -0700536 stdin_file.flush()
537 stdin_file.seek(0)
538
Luke Diamand6de040d2011-10-16 10:47:52 -0400539 p4 = subprocess.Popen(cmd,
540 shell=expand,
Scott Lamb9f90c732007-07-15 20:58:10 -0700541 stdin=stdin_file,
542 stdout=subprocess.PIPE)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100543
544 result = []
545 try:
546 while True:
Scott Lamb9f90c732007-07-15 20:58:10 -0700547 entry = marshal.load(p4.stdout)
Andrew Garberc3f61632011-04-07 02:01:21 -0400548 if cb is not None:
549 cb(entry)
550 else:
551 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100552 except EOFError:
553 pass
Scott Lamb9f90c732007-07-15 20:58:10 -0700554 exitCode = p4.wait()
555 if exitCode != 0:
Simon Hausmannac3e0d72007-05-23 23:32:32 +0200556 entry = {}
557 entry["p4ExitCode"] = exitCode
558 result.append(entry)
Simon Hausmann86949ee2007-03-19 20:59:12 +0100559
560 return result
561
562def p4Cmd(cmd):
563 list = p4CmdList(cmd)
564 result = {}
565 for entry in list:
566 result.update(entry)
567 return result;
568
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100569def p4Where(depotPath):
570 if not depotPath.endswith("/"):
571 depotPath += "/"
Vitor Antunescd884102015-04-21 23:49:30 +0100572 depotPathLong = depotPath + "..."
573 outputList = p4CmdList(["where", depotPathLong])
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100574 output = None
575 for entry in outputList:
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100576 if "depotFile" in entry:
Vitor Antunescd884102015-04-21 23:49:30 +0100577 # Search for the base client side depot path, as long as it starts with the branch's P4 path.
578 # The base path always ends with "/...".
579 if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
Tor Arvid Lund75bc9572008-12-09 16:41:50 +0100580 output = entry
581 break
582 elif "data" in entry:
583 data = entry.get("data")
584 space = data.find(" ")
585 if data[:space] == depotPath:
586 output = entry
587 break
Tor Arvid Lund7f705dc2008-12-04 14:37:33 +0100588 if output == None:
589 return ""
Simon Hausmanndc524032007-05-21 09:34:56 +0200590 if output["code"] == "error":
591 return ""
Simon Hausmanncb2c9db2007-03-24 09:15:11 +0100592 clientPath = ""
593 if "path" in output:
594 clientPath = output.get("path")
595 elif "data" in output:
596 data = output.get("data")
597 lastSpace = data.rfind(" ")
598 clientPath = data[lastSpace + 1:]
599
600 if clientPath.endswith("..."):
601 clientPath = clientPath[:-3]
602 return clientPath
603
Simon Hausmann86949ee2007-03-19 20:59:12 +0100604def currentGitBranch():
Luke Diamandeff45112017-04-15 11:36:09 +0100605 return read_pipe_text(["git", "symbolic-ref", "--short", "-q", "HEAD"])
Simon Hausmann86949ee2007-03-19 20:59:12 +0100606
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100607def isValidGitDir(path):
Luke Diamand378f7be2016-12-13 21:51:28 +0000608 return git_dir(path) != None
Simon Hausmann4f5cf762007-03-19 22:25:17 +0100609
Simon Hausmann463e8af2007-05-17 09:13:54 +0200610def parseRevision(ref):
Han-Wen Nienhuysb25b2062007-05-23 18:49:35 -0300611 return read_pipe("git rev-parse %s" % ref).strip()
Simon Hausmann463e8af2007-05-17 09:13:54 +0200612
Pete Wyckoff28755db2011-12-24 21:07:40 -0500613def branchExists(ref):
614 rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
615 ignore_error=True)
616 return len(rev) > 0
617
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100618def extractLogMessageFromGitCommit(commit):
619 logMessage = ""
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300620
621 ## fixme: title is first line of commit, not 1st paragraph.
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100622 foundTitle = False
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -0300623 for log in read_pipe_lines("git cat-file commit %s" % commit):
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100624 if not foundTitle:
625 if len(log) == 1:
Simon Hausmann1c094182007-05-01 23:15:48 +0200626 foundTitle = True
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100627 continue
628
629 logMessage += log
630 return logMessage
631
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -0300632def extractSettingsGitLog(log):
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100633 values = {}
634 for line in log.split("\n"):
635 line = line.strip()
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300636 m = re.search (r"^ *\[git-p4: (.*)\]$", line)
637 if not m:
638 continue
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100639
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -0300640 assignments = m.group(1).split (':')
641 for a in assignments:
642 vals = a.split ('=')
643 key = vals[0].strip()
644 val = ('='.join (vals[1:])).strip()
645 if val.endswith ('\"') and val.startswith('"'):
646 val = val[1:-1]
647
648 values[key] = val
649
Simon Hausmann845b42c2007-06-07 09:19:34 +0200650 paths = values.get("depot-paths")
651 if not paths:
652 paths = values.get("depot-path")
Simon Hausmanna3fdd572007-06-07 22:54:32 +0200653 if paths:
654 values['depot-paths'] = paths.split(',')
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -0300655 return values
Simon Hausmann6ae8de82007-03-22 21:10:25 +0100656
Simon Hausmann8136a632007-03-22 21:27:14 +0100657def gitBranchExists(branch):
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -0300658 proc = subprocess.Popen(["git", "rev-parse", branch],
659 stderr=subprocess.PIPE, stdout=subprocess.PIPE);
Simon Hausmanncaace112007-05-15 14:57:57 +0200660 return proc.wait() == 0;
Simon Hausmann8136a632007-03-22 21:27:14 +0100661
John Chapman36bd8442008-11-08 14:22:49 +1100662_gitConfig = {}
Pete Wyckoffb345d6c2013-01-26 22:11:23 -0500663
Lars Schneider692e1792015-09-26 09:54:58 +0200664def gitConfig(key, typeSpecifier=None):
John Chapman36bd8442008-11-08 14:22:49 +1100665 if not _gitConfig.has_key(key):
Lars Schneider692e1792015-09-26 09:54:58 +0200666 cmd = [ "git", "config" ]
667 if typeSpecifier:
668 cmd += [ typeSpecifier ]
669 cmd += [ key ]
Pete Wyckoffb345d6c2013-01-26 22:11:23 -0500670 s = read_pipe(cmd, ignore_error=True)
671 _gitConfig[key] = s.strip()
John Chapman36bd8442008-11-08 14:22:49 +1100672 return _gitConfig[key]
Simon Hausmann01265102007-05-25 10:36:10 +0200673
Pete Wyckoff0d609032013-01-26 22:11:24 -0500674def gitConfigBool(key):
675 """Return a bool, using git config --bool. It is True only if the
676 variable is set to true, and False if set to false or not present
677 in the config."""
678
679 if not _gitConfig.has_key(key):
Lars Schneider692e1792015-09-26 09:54:58 +0200680 _gitConfig[key] = gitConfig(key, '--bool') == "true"
Simon Hausmann062410b2007-07-18 10:56:31 +0200681 return _gitConfig[key]
682
Lars Schneidercb1dafd2015-09-26 09:54:59 +0200683def gitConfigInt(key):
684 if not _gitConfig.has_key(key):
685 cmd = [ "git", "config", "--int", key ]
Simon Hausmannb9847332007-03-20 20:54:23 +0100686 s = read_pipe(cmd, ignore_error=True)
Simon Hausmann062410b2007-07-18 10:56:31 +0200687 v = s.strip()
Lars Schneidercb1dafd2015-09-26 09:54:59 +0200688 try:
689 _gitConfig[key] = int(gitConfig(key, '--int'))
690 except ValueError:
691 _gitConfig[key] = None
Simon Hausmann062410b2007-07-18 10:56:31 +0200692 return _gitConfig[key]
693
Vitor Antunes7199cf12011-08-19 00:44:05 +0100694def gitConfigList(key):
695 if not _gitConfig.has_key(key):
Pete Wyckoff2abba302013-01-26 22:11:22 -0500696 s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
George Vanburghc3c2b052017-01-25 09:17:29 +0000697 _gitConfig[key] = s.strip().splitlines()
Lars Schneider7960e702015-09-26 09:55:00 +0200698 if _gitConfig[key] == ['']:
699 _gitConfig[key] = []
Vitor Antunes7199cf12011-08-19 00:44:05 +0100700 return _gitConfig[key]
701
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500702def p4BranchesInGit(branchesAreInRemotes=True):
703 """Find all the branches whose names start with "p4/", looking
704 in remotes or heads as specified by the argument. Return
705 a dictionary of { branch: revision } for each one found.
706 The branch names are the short names, without any
707 "p4/" prefix."""
708
Simon Hausmann062410b2007-07-18 10:56:31 +0200709 branches = {}
710
711 cmdline = "git rev-parse --symbolic "
712 if branchesAreInRemotes:
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500713 cmdline += "--remotes"
Simon Hausmann062410b2007-07-18 10:56:31 +0200714 else:
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500715 cmdline += "--branches"
Simon Hausmann062410b2007-07-18 10:56:31 +0200716
717 for line in read_pipe_lines(cmdline):
718 line = line.strip()
719
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500720 # only import to p4/
721 if not line.startswith('p4/'):
Simon Hausmann062410b2007-07-18 10:56:31 +0200722 continue
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500723 # special symbolic ref to p4/master
724 if line == "p4/HEAD":
725 continue
Simon Hausmann062410b2007-07-18 10:56:31 +0200726
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500727 # strip off p4/ prefix
728 branch = line[len("p4/"):]
Simon Hausmann062410b2007-07-18 10:56:31 +0200729
730 branches[branch] = parseRevision(line)
Pete Wyckoff2c8037e2013-01-14 19:46:57 -0500731
Simon Hausmann062410b2007-07-18 10:56:31 +0200732 return branches
733
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -0500734def branch_exists(branch):
735 """Make sure that the given ref name really exists."""
736
737 cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
738 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
739 out, _ = p.communicate()
740 if p.returncode:
741 return False
742 # expect exactly one line of output: the branch name
743 return out.rstrip() == branch
744
Simon Hausmann9ceab362007-06-22 00:01:57 +0200745def findUpstreamBranchPoint(head = "HEAD"):
Simon Hausmann86506fe2007-07-18 12:40:12 +0200746 branches = p4BranchesInGit()
747 # map from depot-path to branch name
748 branchByDepotPath = {}
749 for branch in branches.keys():
750 tip = branches[branch]
751 log = extractLogMessageFromGitCommit(tip)
752 settings = extractSettingsGitLog(log)
753 if settings.has_key("depot-paths"):
754 paths = ",".join(settings["depot-paths"])
755 branchByDepotPath[paths] = "remotes/p4/" + branch
756
Simon Hausmann27d2d812007-06-12 14:31:59 +0200757 settings = None
Simon Hausmann27d2d812007-06-12 14:31:59 +0200758 parent = 0
759 while parent < 65535:
Simon Hausmann9ceab362007-06-22 00:01:57 +0200760 commit = head + "~%s" % parent
Simon Hausmann27d2d812007-06-12 14:31:59 +0200761 log = extractLogMessageFromGitCommit(commit)
762 settings = extractSettingsGitLog(log)
Simon Hausmann86506fe2007-07-18 12:40:12 +0200763 if settings.has_key("depot-paths"):
764 paths = ",".join(settings["depot-paths"])
765 if branchByDepotPath.has_key(paths):
766 return [branchByDepotPath[paths], settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +0200767
Simon Hausmann86506fe2007-07-18 12:40:12 +0200768 parent = parent + 1
Simon Hausmann27d2d812007-06-12 14:31:59 +0200769
Simon Hausmann86506fe2007-07-18 12:40:12 +0200770 return ["", settings]
Simon Hausmann27d2d812007-06-12 14:31:59 +0200771
Simon Hausmann5ca44612007-08-24 17:44:16 +0200772def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
773 if not silent:
774 print ("Creating/updating branch(es) in %s based on origin branch(es)"
775 % localRefPrefix)
776
777 originPrefix = "origin/p4/"
778
779 for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
780 line = line.strip()
781 if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
782 continue
783
784 headName = line[len(originPrefix):]
785 remoteHead = localRefPrefix + headName
786 originHead = line
787
788 original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
789 if (not original.has_key('depot-paths')
790 or not original.has_key('change')):
791 continue
792
793 update = False
794 if not gitBranchExists(remoteHead):
795 if verbose:
796 print "creating %s" % remoteHead
797 update = True
798 else:
799 settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
800 if settings.has_key('change') > 0:
801 if settings['depot-paths'] == original['depot-paths']:
802 originP4Change = int(original['change'])
803 p4Change = int(settings['change'])
804 if originP4Change > p4Change:
805 print ("%s (%s) is newer than %s (%s). "
806 "Updating p4 branch from origin."
807 % (originHead, originP4Change,
808 remoteHead, p4Change))
809 update = True
810 else:
811 print ("Ignoring: %s was imported from %s while "
812 "%s was imported from %s"
813 % (originHead, ','.join(original['depot-paths']),
814 remoteHead, ','.join(settings['depot-paths'])))
815
816 if update:
817 system("git update-ref %s %s" % (remoteHead, originHead))
818
819def originP4BranchesExist():
820 return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
821
Simon Hausmann4f6432d2007-08-26 15:56:36 +0200822
Luke Diamand1051ef02015-06-10 08:30:59 +0100823def p4ParseNumericChangeRange(parts):
824 changeStart = int(parts[0][1:])
825 if parts[1] == '#head':
826 changeEnd = p4_last_change()
827 else:
828 changeEnd = int(parts[1])
829
830 return (changeStart, changeEnd)
831
832def chooseBlockSize(blockSize):
833 if blockSize:
834 return blockSize
835 else:
836 return defaultBlockSize
837
838def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
839 assert depotPaths
840
841 # Parse the change range into start and end. Try to find integer
842 # revision ranges as these can be broken up into blocks to avoid
843 # hitting server-side limits (maxrows, maxscanresults). But if
844 # that doesn't work, fall back to using the raw revision specifier
845 # strings, without using block mode.
846
Lex Spoon96b2d542015-04-20 11:00:20 -0400847 if changeRange is None or changeRange == '':
Luke Diamand1051ef02015-06-10 08:30:59 +0100848 changeStart = 1
849 changeEnd = p4_last_change()
850 block_size = chooseBlockSize(requestedBlockSize)
Lex Spoon96b2d542015-04-20 11:00:20 -0400851 else:
852 parts = changeRange.split(',')
853 assert len(parts) == 2
Luke Diamand1051ef02015-06-10 08:30:59 +0100854 try:
855 (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
856 block_size = chooseBlockSize(requestedBlockSize)
857 except:
858 changeStart = parts[0][1:]
859 changeEnd = parts[1]
860 if requestedBlockSize:
861 die("cannot use --changes-block-size with non-numeric revisions")
862 block_size = None
Lex Spoon96b2d542015-04-20 11:00:20 -0400863
George Vanburgh9943e5b2016-12-17 22:11:23 +0000864 changes = set()
Lex Spoon96b2d542015-04-20 11:00:20 -0400865
Sam Hocevar1f90a642015-12-19 09:39:40 +0000866 # Retrieve changes a block at a time, to prevent running
867 # into a MaxResults/MaxScanRows error from the server.
Luke Diamand1051ef02015-06-10 08:30:59 +0100868
Sam Hocevar1f90a642015-12-19 09:39:40 +0000869 while True:
870 cmd = ['changes']
Luke Diamand1051ef02015-06-10 08:30:59 +0100871
Sam Hocevar1f90a642015-12-19 09:39:40 +0000872 if block_size:
873 end = min(changeEnd, changeStart + block_size)
874 revisionRange = "%d,%d" % (changeStart, end)
875 else:
876 revisionRange = "%s,%s" % (changeStart, changeEnd)
Luke Diamand1051ef02015-06-10 08:30:59 +0100877
Sam Hocevar1f90a642015-12-19 09:39:40 +0000878 for p in depotPaths:
Luke Diamand1051ef02015-06-10 08:30:59 +0100879 cmd += ["%s...@%s" % (p, revisionRange)]
880
Sam Hocevar1f90a642015-12-19 09:39:40 +0000881 # Insert changes in chronological order
882 for line in reversed(p4_read_pipe_lines(cmd)):
George Vanburgh9943e5b2016-12-17 22:11:23 +0000883 changes.add(int(line.split(" ")[1]))
Luke Diamand1051ef02015-06-10 08:30:59 +0100884
Sam Hocevar1f90a642015-12-19 09:39:40 +0000885 if not block_size:
886 break
Luke Diamand1051ef02015-06-10 08:30:59 +0100887
Sam Hocevar1f90a642015-12-19 09:39:40 +0000888 if end >= changeEnd:
889 break
Luke Diamand1051ef02015-06-10 08:30:59 +0100890
Sam Hocevar1f90a642015-12-19 09:39:40 +0000891 changeStart = end + 1
Simon Hausmann4f6432d2007-08-26 15:56:36 +0200892
Sam Hocevar1f90a642015-12-19 09:39:40 +0000893 changes = sorted(changes)
894 return changes
Simon Hausmann4f6432d2007-08-26 15:56:36 +0200895
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +0100896def p4PathStartsWith(path, prefix):
897 # This method tries to remedy a potential mixed-case issue:
898 #
899 # If UserA adds //depot/DirA/file1
900 # and UserB adds //depot/dira/file2
901 #
902 # we may or may not have a problem. If you have core.ignorecase=true,
903 # we treat DirA and dira as the same directory
Pete Wyckoff0d609032013-01-26 22:11:24 -0500904 if gitConfigBool("core.ignorecase"):
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +0100905 return path.lower().startswith(prefix.lower())
906 return path.startswith(prefix)
907
Pete Wyckoff543987b2012-02-25 20:06:25 -0500908def getClientSpec():
909 """Look at the p4 client spec, create a View() object that contains
910 all the mappings, and return it."""
911
912 specList = p4CmdList("client -o")
913 if len(specList) != 1:
914 die('Output from "client -o" is %d lines, expecting 1' %
915 len(specList))
916
917 # dictionary of all client parameters
918 entry = specList[0]
919
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +0900920 # the //client/ name
921 client_name = entry["Client"]
922
Pete Wyckoff543987b2012-02-25 20:06:25 -0500923 # just the keys that start with "View"
924 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
925
926 # hold this new View
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +0900927 view = View(client_name)
Pete Wyckoff543987b2012-02-25 20:06:25 -0500928
929 # append the lines, in order, to the view
930 for view_num in range(len(view_keys)):
931 k = "View%d" % view_num
932 if k not in view_keys:
933 die("Expected view key %s missing" % k)
934 view.append(entry[k])
935
936 return view
937
938def getClientRoot():
939 """Grab the client directory."""
940
941 output = p4CmdList("client -o")
942 if len(output) != 1:
943 die('Output from "client -o" is %d lines, expecting 1' % len(output))
944
945 entry = output[0]
946 if "Root" not in entry:
947 die('Client has no "Root"')
948
949 return entry["Root"]
950
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400951#
952# P4 wildcards are not allowed in filenames. P4 complains
953# if you simply add them, but you can force it with "-f", in
954# which case it translates them into %xx encoding internally.
955#
956def wildcard_decode(path):
957 # Search for and fix just these four characters. Do % last so
958 # that fixing it does not inadvertently create new %-escapes.
959 # Cannot have * in a filename in windows; untested as to
960 # what p4 would do in such a case.
961 if not platform.system() == "Windows":
962 path = path.replace("%2A", "*")
963 path = path.replace("%23", "#") \
964 .replace("%40", "@") \
965 .replace("%25", "%")
966 return path
967
968def wildcard_encode(path):
969 # do % first to avoid double-encoding the %s introduced here
970 path = path.replace("%", "%25") \
971 .replace("*", "%2A") \
972 .replace("#", "%23") \
973 .replace("@", "%40")
974 return path
975
976def wildcard_present(path):
Brandon Casey598354c2013-01-26 11:14:32 -0800977 m = re.search("[*#@%]", path)
978 return m is not None
Pete Wyckoff9d7d4462012-04-29 20:57:17 -0400979
Lars Schneidera5db4b12015-09-26 09:55:03 +0200980class LargeFileSystem(object):
981 """Base class for large file system support."""
982
983 def __init__(self, writeToGitStream):
984 self.largeFiles = set()
985 self.writeToGitStream = writeToGitStream
986
987 def generatePointer(self, cloneDestination, contentFile):
988 """Return the content of a pointer file that is stored in Git instead of
989 the actual content."""
990 assert False, "Method 'generatePointer' required in " + self.__class__.__name__
991
992 def pushFile(self, localLargeFile):
993 """Push the actual content which is not stored in the Git repository to
994 a server."""
995 assert False, "Method 'pushFile' required in " + self.__class__.__name__
996
997 def hasLargeFileExtension(self, relPath):
998 return reduce(
999 lambda a, b: a or b,
1000 [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
1001 False
1002 )
1003
1004 def generateTempFile(self, contents):
1005 contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1006 for d in contents:
1007 contentFile.write(d)
1008 contentFile.close()
1009 return contentFile.name
1010
1011 def exceedsLargeFileThreshold(self, relPath, contents):
1012 if gitConfigInt('git-p4.largeFileThreshold'):
1013 contentsSize = sum(len(d) for d in contents)
1014 if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
1015 return True
1016 if gitConfigInt('git-p4.largeFileCompressedThreshold'):
1017 contentsSize = sum(len(d) for d in contents)
1018 if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
1019 return False
1020 contentTempFile = self.generateTempFile(contents)
1021 compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
1022 zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
1023 zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
1024 zf.close()
1025 compressedContentsSize = zf.infolist()[0].compress_size
1026 os.remove(contentTempFile)
1027 os.remove(compressedContentFile.name)
1028 if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
1029 return True
1030 return False
1031
1032 def addLargeFile(self, relPath):
1033 self.largeFiles.add(relPath)
1034
1035 def removeLargeFile(self, relPath):
1036 self.largeFiles.remove(relPath)
1037
1038 def isLargeFile(self, relPath):
1039 return relPath in self.largeFiles
1040
1041 def processContent(self, git_mode, relPath, contents):
1042 """Processes the content of git fast import. This method decides if a
1043 file is stored in the large file system and handles all necessary
1044 steps."""
1045 if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
1046 contentTempFile = self.generateTempFile(contents)
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001047 (pointer_git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
1048 if pointer_git_mode:
1049 git_mode = pointer_git_mode
1050 if localLargeFile:
1051 # Move temp file to final location in large file system
1052 largeFileDir = os.path.dirname(localLargeFile)
1053 if not os.path.isdir(largeFileDir):
1054 os.makedirs(largeFileDir)
1055 shutil.move(contentTempFile, localLargeFile)
1056 self.addLargeFile(relPath)
1057 if gitConfigBool('git-p4.largeFilePush'):
1058 self.pushFile(localLargeFile)
1059 if verbose:
1060 sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
Lars Schneidera5db4b12015-09-26 09:55:03 +02001061 return (git_mode, contents)
1062
1063class MockLFS(LargeFileSystem):
1064 """Mock large file system for testing."""
1065
1066 def generatePointer(self, contentFile):
1067 """The pointer content is the original content prefixed with "pointer-".
1068 The local filename of the large file storage is derived from the file content.
1069 """
1070 with open(contentFile, 'r') as f:
1071 content = next(f)
1072 gitMode = '100644'
1073 pointerContents = 'pointer-' + content
1074 localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
1075 return (gitMode, pointerContents, localLargeFile)
1076
1077 def pushFile(self, localLargeFile):
1078 """The remote filename of the large file storage is the same as the local
1079 one but in a different directory.
1080 """
1081 remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
1082 if not os.path.exists(remotePath):
1083 os.makedirs(remotePath)
1084 shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
1085
Lars Schneiderb47d8072015-09-26 09:55:04 +02001086class GitLFS(LargeFileSystem):
1087 """Git LFS as backend for the git-p4 large file system.
1088 See https://git-lfs.github.com/ for details."""
1089
1090 def __init__(self, *args):
1091 LargeFileSystem.__init__(self, *args)
1092 self.baseGitAttributes = []
1093
1094 def generatePointer(self, contentFile):
1095 """Generate a Git LFS pointer for the content. Return LFS Pointer file
1096 mode and content which is stored in the Git repository instead of
1097 the actual content. Return also the new location of the actual
1098 content.
1099 """
Lars Schneiderd5eb3cf2016-12-04 17:03:37 +01001100 if os.path.getsize(contentFile) == 0:
1101 return (None, '', None)
1102
Lars Schneiderb47d8072015-09-26 09:55:04 +02001103 pointerProcess = subprocess.Popen(
1104 ['git', 'lfs', 'pointer', '--file=' + contentFile],
1105 stdout=subprocess.PIPE
1106 )
1107 pointerFile = pointerProcess.stdout.read()
1108 if pointerProcess.wait():
1109 os.remove(contentFile)
1110 die('git-lfs pointer command failed. Did you install the extension?')
Lars Schneider82f25672016-04-28 08:26:33 +02001111
1112 # Git LFS removed the preamble in the output of the 'pointer' command
1113 # starting from version 1.2.0. Check for the preamble here to support
1114 # earlier versions.
1115 # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
1116 if pointerFile.startswith('Git LFS pointer for'):
1117 pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
1118
1119 oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001120 localLargeFile = os.path.join(
1121 os.getcwd(),
1122 '.git', 'lfs', 'objects', oid[:2], oid[2:4],
1123 oid,
1124 )
1125 # LFS Spec states that pointer files should not have the executable bit set.
1126 gitMode = '100644'
Lars Schneider82f25672016-04-28 08:26:33 +02001127 return (gitMode, pointerFile, localLargeFile)
Lars Schneiderb47d8072015-09-26 09:55:04 +02001128
1129 def pushFile(self, localLargeFile):
1130 uploadProcess = subprocess.Popen(
1131 ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
1132 )
1133 if uploadProcess.wait():
1134 die('git-lfs push command failed. Did you define a remote?')
1135
1136 def generateGitAttributes(self):
1137 return (
1138 self.baseGitAttributes +
1139 [
1140 '\n',
1141 '#\n',
1142 '# Git LFS (see https://git-lfs.github.com/)\n',
1143 '#\n',
1144 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001145 ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001146 for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
1147 ] +
Lars Schneider862f9312016-12-18 20:01:40 +01001148 ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs diff=lfs merge=lfs -text\n'
Lars Schneiderb47d8072015-09-26 09:55:04 +02001149 for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
1150 ]
1151 )
1152
1153 def addLargeFile(self, relPath):
1154 LargeFileSystem.addLargeFile(self, relPath)
1155 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1156
1157 def removeLargeFile(self, relPath):
1158 LargeFileSystem.removeLargeFile(self, relPath)
1159 self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
1160
1161 def processContent(self, git_mode, relPath, contents):
1162 if relPath == '.gitattributes':
1163 self.baseGitAttributes = contents
1164 return (git_mode, self.generateGitAttributes())
1165 else:
1166 return LargeFileSystem.processContent(self, git_mode, relPath, contents)
1167
Simon Hausmannb9847332007-03-20 20:54:23 +01001168class Command:
1169 def __init__(self):
1170 self.usage = "usage: %prog [options]"
Simon Hausmann8910ac02007-03-26 08:18:55 +02001171 self.needsGit = True
Luke Diamand6a10b6a2012-04-24 09:33:23 +01001172 self.verbose = False
Simon Hausmannb9847332007-03-20 20:54:23 +01001173
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001174class P4UserMap:
1175 def __init__(self):
1176 self.userMapFromPerforceServer = False
Luke Diamandaffb4742012-01-19 09:52:27 +00001177 self.myP4UserId = None
1178
1179 def p4UserId(self):
1180 if self.myP4UserId:
1181 return self.myP4UserId
1182
1183 results = p4CmdList("user -o")
1184 for r in results:
1185 if r.has_key('User'):
1186 self.myP4UserId = r['User']
1187 return r['User']
1188 die("Could not find your p4 user id")
1189
1190 def p4UserIsMe(self, p4User):
1191 # return True if the given p4 user is actually me
1192 me = self.p4UserId()
1193 if not p4User or p4User != me:
1194 return False
1195 else:
1196 return True
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001197
1198 def getUserCacheFilename(self):
1199 home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1200 return home + "/.gitp4-usercache.txt"
1201
1202 def getUserMapFromPerforceServer(self):
1203 if self.userMapFromPerforceServer:
1204 return
1205 self.users = {}
1206 self.emails = {}
1207
1208 for output in p4CmdList("users"):
1209 if not output.has_key("User"):
1210 continue
1211 self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1212 self.emails[output["Email"]] = output["User"]
1213
Lars Schneider10d08a12016-03-01 11:49:56 +01001214 mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
1215 for mapUserConfig in gitConfigList("git-p4.mapUser"):
1216 mapUser = mapUserConfigRegex.findall(mapUserConfig)
1217 if mapUser and len(mapUser[0]) == 3:
1218 user = mapUser[0][0]
1219 fullname = mapUser[0][1]
1220 email = mapUser[0][2]
1221 self.users[user] = fullname + " <" + email + ">"
1222 self.emails[email] = user
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001223
1224 s = ''
1225 for (key, val) in self.users.items():
1226 s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1227
1228 open(self.getUserCacheFilename(), "wb").write(s)
1229 self.userMapFromPerforceServer = True
1230
1231 def loadUserMapFromCache(self):
1232 self.users = {}
1233 self.userMapFromPerforceServer = False
1234 try:
1235 cache = open(self.getUserCacheFilename(), "rb")
1236 lines = cache.readlines()
1237 cache.close()
1238 for line in lines:
1239 entry = line.strip().split("\t")
1240 self.users[entry[0]] = entry[1]
1241 except IOError:
1242 self.getUserMapFromPerforceServer()
1243
Simon Hausmannb9847332007-03-20 20:54:23 +01001244class P4Debug(Command):
Simon Hausmann86949ee2007-03-19 20:59:12 +01001245 def __init__(self):
Simon Hausmann6ae8de82007-03-22 21:10:25 +01001246 Command.__init__(self)
Luke Diamand6a10b6a2012-04-24 09:33:23 +01001247 self.options = []
Simon Hausmannc8c39112007-03-19 21:02:30 +01001248 self.description = "A tool to debug the output of p4 -G."
Simon Hausmann8910ac02007-03-26 08:18:55 +02001249 self.needsGit = False
Simon Hausmann86949ee2007-03-19 20:59:12 +01001250
1251 def run(self, args):
Han-Wen Nienhuysb1ce9442007-05-23 18:49:35 -03001252 j = 0
Luke Diamand6de040d2011-10-16 10:47:52 -04001253 for output in p4CmdList(args):
Han-Wen Nienhuysb1ce9442007-05-23 18:49:35 -03001254 print 'Element: %d' % j
1255 j += 1
Simon Hausmann86949ee2007-03-19 20:59:12 +01001256 print output
Simon Hausmannb9847332007-03-20 20:54:23 +01001257 return True
Simon Hausmann86949ee2007-03-19 20:59:12 +01001258
Simon Hausmann58346842007-05-21 22:57:06 +02001259class P4RollBack(Command):
1260 def __init__(self):
1261 Command.__init__(self)
1262 self.options = [
Simon Hausmann0c66a782007-05-23 20:07:57 +02001263 optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
Simon Hausmann58346842007-05-21 22:57:06 +02001264 ]
1265 self.description = "A tool to debug the multi-branch import. Don't use :)"
Simon Hausmann0c66a782007-05-23 20:07:57 +02001266 self.rollbackLocalBranches = False
Simon Hausmann58346842007-05-21 22:57:06 +02001267
1268 def run(self, args):
1269 if len(args) != 1:
1270 return False
1271 maxChange = int(args[0])
Simon Hausmann0c66a782007-05-23 20:07:57 +02001272
Simon Hausmannad192f22007-05-23 23:44:19 +02001273 if "p4ExitCode" in p4Cmd("changes -m 1"):
Simon Hausmann66a2f522007-05-23 23:40:48 +02001274 die("Problems executing p4");
1275
Simon Hausmann0c66a782007-05-23 20:07:57 +02001276 if self.rollbackLocalBranches:
1277 refPrefix = "refs/heads/"
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -03001278 lines = read_pipe_lines("git rev-parse --symbolic --branches")
Simon Hausmann0c66a782007-05-23 20:07:57 +02001279 else:
1280 refPrefix = "refs/remotes/"
Han-Wen Nienhuysb016d392007-05-23 17:10:46 -03001281 lines = read_pipe_lines("git rev-parse --symbolic --remotes")
Simon Hausmann0c66a782007-05-23 20:07:57 +02001282
1283 for line in lines:
1284 if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
Han-Wen Nienhuysb25b2062007-05-23 18:49:35 -03001285 line = line.strip()
1286 ref = refPrefix + line
Simon Hausmann58346842007-05-21 22:57:06 +02001287 log = extractLogMessageFromGitCommit(ref)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001288 settings = extractSettingsGitLog(log)
1289
1290 depotPaths = settings['depot-paths']
1291 change = settings['change']
1292
Simon Hausmann58346842007-05-21 22:57:06 +02001293 changed = False
Simon Hausmann52102d42007-05-21 23:44:24 +02001294
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03001295 if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
1296 for p in depotPaths]))) == 0:
Simon Hausmann52102d42007-05-21 23:44:24 +02001297 print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
1298 system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
1299 continue
1300
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001301 while change and int(change) > maxChange:
Simon Hausmann58346842007-05-21 22:57:06 +02001302 changed = True
Simon Hausmann52102d42007-05-21 23:44:24 +02001303 if self.verbose:
1304 print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
Simon Hausmann58346842007-05-21 22:57:06 +02001305 system("git update-ref %s \"%s^\"" % (ref, ref))
1306 log = extractLogMessageFromGitCommit(ref)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03001307 settings = extractSettingsGitLog(log)
1308
1309
1310 depotPaths = settings['depot-paths']
1311 change = settings['change']
Simon Hausmann58346842007-05-21 22:57:06 +02001312
1313 if changed:
Simon Hausmann52102d42007-05-21 23:44:24 +02001314 print "%s rewound to %s" % (ref, change)
Simon Hausmann58346842007-05-21 22:57:06 +02001315
1316 return True
1317
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001318class P4Submit(Command, P4UserMap):
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001319
1320 conflict_behavior_choices = ("ask", "skip", "quit")
1321
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001322 def __init__(self):
Simon Hausmannb9847332007-03-20 20:54:23 +01001323 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001324 P4UserMap.__init__(self)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001325 self.options = [
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001326 optparse.make_option("--origin", dest="origin"),
Vitor Antunesae901092011-02-20 01:18:24 +00001327 optparse.make_option("-M", dest="detectRenames", action="store_true"),
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001328 # preserve the user, requires relevant p4 permissions
1329 optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02001330 optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
Pete Wyckoffef739f02012-09-09 16:16:11 -04001331 optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001332 optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001333 optparse.make_option("--conflict", dest="conflict_behavior",
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001334 choices=self.conflict_behavior_choices),
1335 optparse.make_option("--branch", dest="branch"),
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001336 optparse.make_option("--shelve", dest="shelve", action="store_true",
1337 help="Shelve instead of submit. Shelved files are reverted, "
1338 "restoring the workspace to the state before the shelve"),
Luke Diamand46c609e2016-12-02 22:43:19 +00001339 optparse.make_option("--update-shelve", dest="update_shelve", action="store", type="int",
1340 metavar="CHANGELIST",
1341 help="update an existing shelved changelist, implies --shelve")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001342 ]
1343 self.description = "Submit changes from git to the perforce depot."
Simon Hausmannc9b50e62007-03-29 19:15:24 +02001344 self.usage += " [name of git branch to submit into perforce depot]"
Simon Hausmann95124972007-03-23 09:16:07 +01001345 self.origin = ""
Vitor Antunesae901092011-02-20 01:18:24 +00001346 self.detectRenames = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05001347 self.preserveUser = gitConfigBool("git-p4.preserveUser")
Pete Wyckoffef739f02012-09-09 16:16:11 -04001348 self.dry_run = False
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001349 self.shelve = False
Luke Diamand46c609e2016-12-02 22:43:19 +00001350 self.update_shelve = None
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001351 self.prepare_p4_only = False
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04001352 self.conflict_behavior = None
Marius Storm-Olsenf7baba82007-06-07 14:07:01 +02001353 self.isWindows = (platform.system() == "Windows")
Luke Diamand06804c72012-04-11 17:21:24 +02001354 self.exportLabels = False
Pete Wyckoff249da4c2012-11-23 17:35:35 -05001355 self.p4HasMoveCommand = p4_has_move_command()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05001356 self.branch = None
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001357
Lars Schneidera5db4b12015-09-26 09:55:03 +02001358 if gitConfig('git-p4.largeFileSystem'):
1359 die("Large file system not supported for git-p4 submit command. Please remove it from config.")
1360
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001361 def check(self):
1362 if len(p4CmdList("opened ...")) > 0:
1363 die("You have files opened with perforce! Close them before starting the sync.")
1364
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001365 def separate_jobs_from_description(self, message):
1366 """Extract and return a possible Jobs field in the commit
1367 message. It goes into a separate section in the p4 change
1368 specification.
1369
1370 A jobs line starts with "Jobs:" and looks like a new field
1371 in a form. Values are white-space separated on the same
1372 line or on following lines that start with a tab.
1373
1374 This does not parse and extract the full git commit message
1375 like a p4 form. It just sees the Jobs: line as a marker
1376 to pass everything from then on directly into the p4 form,
1377 but outside the description section.
1378
1379 Return a tuple (stripped log message, jobs string)."""
1380
1381 m = re.search(r'^Jobs:', message, re.MULTILINE)
1382 if m is None:
1383 return (message, None)
1384
1385 jobtext = message[m.start():]
1386 stripped_message = message[:m.start()].rstrip()
1387 return (stripped_message, jobtext)
1388
1389 def prepareLogMessage(self, template, message, jobs):
1390 """Edits the template returned from "p4 change -o" to insert
1391 the message in the Description field, and the jobs text in
1392 the Jobs field."""
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001393 result = ""
1394
Simon Hausmannedae1e22008-02-19 09:29:06 +01001395 inDescriptionSection = False
1396
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001397 for line in template.split("\n"):
1398 if line.startswith("#"):
1399 result += line + "\n"
1400 continue
1401
Simon Hausmannedae1e22008-02-19 09:29:06 +01001402 if inDescriptionSection:
Michael Horowitzc9dbab02011-02-25 21:31:13 -05001403 if line.startswith("Files:") or line.startswith("Jobs:"):
Simon Hausmannedae1e22008-02-19 09:29:06 +01001404 inDescriptionSection = False
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001405 # insert Jobs section
1406 if jobs:
1407 result += jobs + "\n"
Simon Hausmannedae1e22008-02-19 09:29:06 +01001408 else:
1409 continue
1410 else:
1411 if line.startswith("Description:"):
1412 inDescriptionSection = True
1413 line += "\n"
1414 for messageLine in message.split("\n"):
1415 line += "\t" + messageLine + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001416
Simon Hausmannedae1e22008-02-19 09:29:06 +01001417 result += line + "\n"
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001418
1419 return result
1420
Luke Diamand60df0712012-02-23 07:51:30 +00001421 def patchRCSKeywords(self, file, pattern):
1422 # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
1423 (handle, outFileName) = tempfile.mkstemp(dir='.')
1424 try:
1425 outFile = os.fdopen(handle, "w+")
1426 inFile = open(file, "r")
1427 regexp = re.compile(pattern, re.VERBOSE)
1428 for line in inFile.readlines():
1429 line = regexp.sub(r'$\1$', line)
1430 outFile.write(line)
1431 inFile.close()
1432 outFile.close()
1433 # Forcibly overwrite the original file
1434 os.unlink(file)
1435 shutil.move(outFileName, file)
1436 except:
1437 # cleanup our temporary file
1438 os.unlink(outFileName)
1439 print "Failed to strip RCS keywords in %s" % file
1440 raise
1441
1442 print "Patched up RCS keywords in %s" % file
1443
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001444 def p4UserForCommit(self,id):
1445 # Return the tuple (perforce user,git email) for a given git commit id
1446 self.getUserMapFromPerforceServer()
Pete Wyckoff9bf28852013-01-26 22:11:20 -05001447 gitEmail = read_pipe(["git", "log", "--max-count=1",
1448 "--format=%ae", id])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001449 gitEmail = gitEmail.strip()
1450 if not self.emails.has_key(gitEmail):
1451 return (None,gitEmail)
1452 else:
1453 return (self.emails[gitEmail],gitEmail)
1454
1455 def checkValidP4Users(self,commits):
1456 # check if any git authors cannot be mapped to p4 users
1457 for id in commits:
1458 (user,email) = self.p4UserForCommit(id)
1459 if not user:
1460 msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
Pete Wyckoff0d609032013-01-26 22:11:24 -05001461 if gitConfigBool("git-p4.allowMissingP4Users"):
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001462 print "%s" % msg
1463 else:
1464 die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
1465
1466 def lastP4Changelist(self):
1467 # Get back the last changelist number submitted in this client spec. This
1468 # then gets used to patch up the username in the change. If the same
1469 # client spec is being used by multiple processes then this might go
1470 # wrong.
1471 results = p4CmdList("client -o") # find the current client
1472 client = None
1473 for r in results:
1474 if r.has_key('Client'):
1475 client = r['Client']
1476 break
1477 if not client:
1478 die("could not get client spec")
Luke Diamand6de040d2011-10-16 10:47:52 -04001479 results = p4CmdList(["changes", "-c", client, "-m", "1"])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001480 for r in results:
1481 if r.has_key('change'):
1482 return r['change']
1483 die("Could not get changelist number for last submit - cannot patch up user details")
1484
1485 def modifyChangelistUser(self, changelist, newUser):
1486 # fixup the user field of a changelist after it has been submitted.
1487 changes = p4CmdList("change -o %s" % changelist)
Luke Diamandecdba362011-05-07 11:19:43 +01001488 if len(changes) != 1:
1489 die("Bad output from p4 change modifying %s to user %s" %
1490 (changelist, newUser))
1491
1492 c = changes[0]
1493 if c['User'] == newUser: return # nothing to do
1494 c['User'] = newUser
1495 input = marshal.dumps(c)
1496
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001497 result = p4CmdList("change -f -i", stdin=input)
1498 for r in result:
1499 if r.has_key('code'):
1500 if r['code'] == 'error':
1501 die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
1502 if r.has_key('data'):
1503 print("Updated user field for changelist %s to %s" % (changelist, newUser))
1504 return
1505 die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
1506
1507 def canChangeChangelists(self):
1508 # check to see if we have p4 admin or super-user permissions, either of
1509 # which are required to modify changelists.
Luke Diamand52a48802012-01-19 09:52:25 +00001510 results = p4CmdList(["protects", self.depotPath])
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001511 for r in results:
1512 if r.has_key('perm'):
1513 if r['perm'] == 'admin':
1514 return 1
1515 if r['perm'] == 'super':
1516 return 1
1517 return 0
1518
Luke Diamand46c609e2016-12-02 22:43:19 +00001519 def prepareSubmitTemplate(self, changelist=None):
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001520 """Run "p4 change -o" to grab a change specification template.
1521 This does not use "p4 -G", as it is nice to keep the submission
1522 template in original order, since a human might edit it.
1523
1524 Remove lines in the Files section that show changes to files
1525 outside the depot path we're committing into."""
1526
Sam Hocevarcbc69242015-12-19 09:39:39 +00001527 [upstream, settings] = findUpstreamBranchPoint()
1528
Simon Hausmannea99c3a2007-08-08 17:06:55 +02001529 template = ""
1530 inFilesSection = False
Luke Diamand46c609e2016-12-02 22:43:19 +00001531 args = ['change', '-o']
1532 if changelist:
1533 args.append(str(changelist))
1534
1535 for line in p4_read_pipe_lines(args):
Marius Storm-Olsenf3e5ae42008-03-28 15:40:40 +01001536 if line.endswith("\r\n"):
1537 line = line[:-2] + "\n"
Simon Hausmannea99c3a2007-08-08 17:06:55 +02001538 if inFilesSection:
1539 if line.startswith("\t"):
1540 # path starts and ends with a tab
1541 path = line[1:]
1542 lastTab = path.rfind("\t")
1543 if lastTab != -1:
1544 path = path[:lastTab]
Sam Hocevarcbc69242015-12-19 09:39:39 +00001545 if settings.has_key('depot-paths'):
1546 if not [p for p in settings['depot-paths']
1547 if p4PathStartsWith(path, p)]:
1548 continue
1549 else:
1550 if not p4PathStartsWith(path, self.depotPath):
1551 continue
Simon Hausmannea99c3a2007-08-08 17:06:55 +02001552 else:
1553 inFilesSection = False
1554 else:
1555 if line.startswith("Files:"):
1556 inFilesSection = True
1557
1558 template += line
1559
1560 return template
1561
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001562 def edit_template(self, template_file):
1563 """Invoke the editor to let the user change the submission
1564 message. Return true if okay to continue with the submit."""
1565
1566 # if configured to skip the editing part, just submit
Pete Wyckoff0d609032013-01-26 22:11:24 -05001567 if gitConfigBool("git-p4.skipSubmitEdit"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001568 return True
1569
1570 # look at the modification time, to check later if the user saved
1571 # the file
1572 mtime = os.stat(template_file).st_mtime
1573
1574 # invoke the editor
Luke Diamandf95ceaf2012-04-24 09:33:21 +01001575 if os.environ.has_key("P4EDITOR") and (os.environ.get("P4EDITOR") != ""):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001576 editor = os.environ.get("P4EDITOR")
1577 else:
1578 editor = read_pipe("git var GIT_EDITOR").strip()
Luke Diamand2dade7a2015-05-19 23:23:17 +01001579 system(["sh", "-c", ('%s "$@"' % editor), editor, template_file])
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001580
1581 # If the file was not saved, prompt to see if this patch should
1582 # be skipped. But skip this verification step if configured so.
Pete Wyckoff0d609032013-01-26 22:11:24 -05001583 if gitConfigBool("git-p4.skipSubmitEditCheck"):
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001584 return True
1585
Pete Wyckoffd1652042011-12-17 12:39:03 -05001586 # modification time updated means user saved the file
1587 if os.stat(template_file).st_mtime > mtime:
1588 return True
1589
1590 while True:
1591 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
1592 if response == 'y':
1593 return True
1594 if response == 'n':
1595 return False
Pete Wyckoff7c766e52011-12-04 19:22:45 -05001596
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001597 def get_diff_description(self, editedFiles, filesToAdd, symlinks):
Maxime Costeb4073bb2014-05-24 18:40:35 +01001598 # diff
1599 if os.environ.has_key("P4DIFF"):
1600 del(os.environ["P4DIFF"])
1601 diff = ""
1602 for editedFile in editedFiles:
1603 diff += p4_read_pipe(['diff', '-du',
1604 wildcard_encode(editedFile)])
1605
1606 # new file diff
1607 newdiff = ""
1608 for newFile in filesToAdd:
1609 newdiff += "==== new file ====\n"
1610 newdiff += "--- /dev/null\n"
1611 newdiff += "+++ %s\n" % newFile
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001612
1613 is_link = os.path.islink(newFile)
1614 expect_link = newFile in symlinks
1615
1616 if is_link and expect_link:
1617 newdiff += "+%s\n" % os.readlink(newFile)
1618 else:
1619 f = open(newFile, "r")
1620 for line in f.readlines():
1621 newdiff += "+" + line
1622 f.close()
Maxime Costeb4073bb2014-05-24 18:40:35 +01001623
Maxime Costee2a892e2014-06-11 14:09:59 +01001624 return (diff + newdiff).replace('\r\n', '\n')
Maxime Costeb4073bb2014-05-24 18:40:35 +01001625
Han-Wen Nienhuys7cb5cbe2007-05-23 16:55:48 -03001626 def applyCommit(self, id):
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04001627 """Apply one commit, return True if it succeeded."""
1628
1629 print "Applying", read_pipe(["git", "show", "-s",
1630 "--format=format:%h %s", id])
Vitor Antunesae901092011-02-20 01:18:24 +00001631
Luke Diamand848de9c2011-05-13 20:46:00 +01001632 (p4User, gitEmail) = self.p4UserForCommit(id)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001633
Gary Gibbons84cb0002012-07-04 09:40:19 -04001634 diff = read_pipe_lines("git diff-tree -r %s \"%s^\" \"%s\"" % (self.diffOpts, id, id))
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001635 filesToAdd = set()
Romain Picarda02b8bc2016-01-12 13:43:47 +01001636 filesToChangeType = set()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001637 filesToDelete = set()
Simon Hausmannd336c152007-05-16 09:41:26 +02001638 editedFiles = set()
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001639 pureRenameCopy = set()
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001640 symlinks = set()
Chris Pettittc65b6702007-11-01 20:43:14 -07001641 filesToChangeExecBit = {}
Luke Diamand46c609e2016-12-02 22:43:19 +00001642 all_files = list()
Luke Diamand60df0712012-02-23 07:51:30 +00001643
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001644 for line in diff:
Chris Pettittb43b0a32007-11-01 20:43:13 -07001645 diff = parseDiffTreeEntry(line)
1646 modifier = diff['status']
1647 path = diff['src']
Luke Diamand46c609e2016-12-02 22:43:19 +00001648 all_files.append(path)
1649
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001650 if modifier == "M":
Luke Diamand6de040d2011-10-16 10:47:52 -04001651 p4_edit(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07001652 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
1653 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmannd336c152007-05-16 09:41:26 +02001654 editedFiles.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001655 elif modifier == "A":
1656 filesToAdd.add(path)
Chris Pettittc65b6702007-11-01 20:43:14 -07001657 filesToChangeExecBit[path] = diff['dst_mode']
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001658 if path in filesToDelete:
1659 filesToDelete.remove(path)
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001660
1661 dst_mode = int(diff['dst_mode'], 8)
1662 if dst_mode == 0120000:
1663 symlinks.add(path)
1664
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001665 elif modifier == "D":
1666 filesToDelete.add(path)
1667 if path in filesToAdd:
1668 filesToAdd.remove(path)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001669 elif modifier == "C":
1670 src, dest = diff['src'], diff['dst']
Luke Diamand6de040d2011-10-16 10:47:52 -04001671 p4_integrate(src, dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001672 pureRenameCopy.add(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001673 if diff['src_sha1'] != diff['dst_sha1']:
Luke Diamand6de040d2011-10-16 10:47:52 -04001674 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001675 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001676 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Luke Diamand6de040d2011-10-16 10:47:52 -04001677 p4_edit(dest)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001678 pureRenameCopy.discard(dest)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001679 filesToChangeExecBit[dest] = diff['dst_mode']
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05001680 if self.isWindows:
1681 # turn off read-only attribute
1682 os.chmod(dest, stat.S_IWRITE)
Vitor Antunes4fddb412011-02-20 01:18:25 +00001683 os.unlink(dest)
1684 editedFiles.add(dest)
Chris Pettittd9a5f252007-10-15 22:15:06 -07001685 elif modifier == "R":
Chris Pettittb43b0a32007-11-01 20:43:13 -07001686 src, dest = diff['src'], diff['dst']
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001687 if self.p4HasMoveCommand:
1688 p4_edit(src) # src must be open before move
1689 p4_move(src, dest) # opens for (move/delete, move/add)
Pete Wyckoffb6ad6dc2012-04-29 20:57:16 -04001690 else:
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001691 p4_integrate(src, dest)
1692 if diff['src_sha1'] != diff['dst_sha1']:
1693 p4_edit(dest)
1694 else:
1695 pureRenameCopy.add(dest)
Chris Pettittc65b6702007-11-01 20:43:14 -07001696 if isModeExecChanged(diff['src_mode'], diff['dst_mode']):
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001697 if not self.p4HasMoveCommand:
1698 p4_edit(dest) # with move: already open, writable
Chris Pettittc65b6702007-11-01 20:43:14 -07001699 filesToChangeExecBit[dest] = diff['dst_mode']
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001700 if not self.p4HasMoveCommand:
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05001701 if self.isWindows:
1702 os.chmod(dest, stat.S_IWRITE)
Gary Gibbons8e9497c2012-07-12 19:29:00 -04001703 os.unlink(dest)
1704 filesToDelete.add(src)
Chris Pettittd9a5f252007-10-15 22:15:06 -07001705 editedFiles.add(dest)
Romain Picarda02b8bc2016-01-12 13:43:47 +01001706 elif modifier == "T":
1707 filesToChangeType.add(path)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001708 else:
1709 die("unknown modifier %s for %s" % (modifier, path))
1710
Tolga Ceylan749b6682014-05-06 22:48:54 -07001711 diffcmd = "git diff-tree --full-index -p \"%s\"" % (id)
Simon Hausmann47a130b2007-05-20 16:33:21 +02001712 patchcmd = diffcmd + " | git apply "
Simon Hausmannc1b296b2007-05-20 16:55:05 +02001713 tryPatchCmd = patchcmd + "--check -"
1714 applyPatchCmd = patchcmd + "--check --apply -"
Luke Diamand60df0712012-02-23 07:51:30 +00001715 patch_succeeded = True
Simon Hausmann51a26402007-04-15 09:59:56 +02001716
Simon Hausmann47a130b2007-05-20 16:33:21 +02001717 if os.system(tryPatchCmd) != 0:
Luke Diamand60df0712012-02-23 07:51:30 +00001718 fixed_rcs_keywords = False
1719 patch_succeeded = False
Simon Hausmann51a26402007-04-15 09:59:56 +02001720 print "Unfortunately applying the change failed!"
Luke Diamand60df0712012-02-23 07:51:30 +00001721
1722 # Patch failed, maybe it's just RCS keyword woes. Look through
1723 # the patch to see if that's possible.
Pete Wyckoff0d609032013-01-26 22:11:24 -05001724 if gitConfigBool("git-p4.attemptRCSCleanup"):
Luke Diamand60df0712012-02-23 07:51:30 +00001725 file = None
1726 pattern = None
1727 kwfiles = {}
1728 for file in editedFiles | filesToDelete:
1729 # did this file's delta contain RCS keywords?
1730 pattern = p4_keywords_regexp_for_file(file)
1731
1732 if pattern:
1733 # this file is a possibility...look for RCS keywords.
1734 regexp = re.compile(pattern, re.VERBOSE)
1735 for line in read_pipe_lines(["git", "diff", "%s^..%s" % (id, id), file]):
1736 if regexp.search(line):
1737 if verbose:
1738 print "got keyword match on %s in %s in %s" % (pattern, line, file)
1739 kwfiles[file] = pattern
1740 break
1741
1742 for file in kwfiles:
1743 if verbose:
1744 print "zapping %s with %s" % (line,pattern)
Pete Wyckoffd20f0f82013-01-26 22:11:19 -05001745 # File is being deleted, so not open in p4. Must
1746 # disable the read-only bit on windows.
1747 if self.isWindows and file not in editedFiles:
1748 os.chmod(file, stat.S_IWRITE)
Luke Diamand60df0712012-02-23 07:51:30 +00001749 self.patchRCSKeywords(file, kwfiles[file])
1750 fixed_rcs_keywords = True
1751
1752 if fixed_rcs_keywords:
1753 print "Retrying the patch with RCS keywords cleaned up"
1754 if os.system(tryPatchCmd) == 0:
1755 patch_succeeded = True
1756
1757 if not patch_succeeded:
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04001758 for f in editedFiles:
1759 p4_revert(f)
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04001760 return False
Simon Hausmann51a26402007-04-15 09:59:56 +02001761
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001762 #
1763 # Apply the patch for real, and do add/delete/+x handling.
1764 #
Simon Hausmann47a130b2007-05-20 16:33:21 +02001765 system(applyPatchCmd)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001766
Romain Picarda02b8bc2016-01-12 13:43:47 +01001767 for f in filesToChangeType:
1768 p4_edit(f, "-t", "auto")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001769 for f in filesToAdd:
Luke Diamand6de040d2011-10-16 10:47:52 -04001770 p4_add(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001771 for f in filesToDelete:
Luke Diamand6de040d2011-10-16 10:47:52 -04001772 p4_revert(f)
1773 p4_delete(f)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001774
Chris Pettittc65b6702007-11-01 20:43:14 -07001775 # Set/clear executable bits
1776 for f in filesToChangeExecBit.keys():
1777 mode = filesToChangeExecBit[f]
1778 setP4ExecBit(f, mode)
1779
Luke Diamand46c609e2016-12-02 22:43:19 +00001780 if self.update_shelve:
1781 print("all_files = %s" % str(all_files))
1782 p4_reopen_in_change(self.update_shelve, all_files)
1783
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001784 #
1785 # Build p4 change description, starting with the contents
1786 # of the git commit message.
1787 #
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01001788 logMessage = extractLogMessageFromGitCommit(id)
Simon Hausmann0e36f2d2008-02-19 09:33:08 +01001789 logMessage = logMessage.strip()
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001790 (logMessage, jobs) = self.separate_jobs_from_description(logMessage)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001791
Luke Diamand46c609e2016-12-02 22:43:19 +00001792 template = self.prepareSubmitTemplate(self.update_shelve)
Pete Wyckofff19cb0a2012-07-04 09:34:20 -04001793 submitTemplate = self.prepareLogMessage(template, logMessage, jobs)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001794
1795 if self.preserveUser:
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001796 submitTemplate += "\n######## Actual user %s, modified after commit\n" % p4User
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001797
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001798 if self.checkAuthorship and not self.p4UserIsMe(p4User):
1799 submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail
1800 submitTemplate += "######## Use option --preserve-user to modify authorship.\n"
1801 submitTemplate += "######## Variable git-p4.skipUserNameCheck hides this message.\n"
1802
1803 separatorLine = "######## everything below this line is just the diff #######\n"
Maxime Costeb4073bb2014-05-24 18:40:35 +01001804 if not self.prepare_p4_only:
1805 submitTemplate += separatorLine
Luke Diamanddf8a9e82016-12-17 01:00:40 +00001806 submitTemplate += self.get_diff_description(editedFiles, filesToAdd, symlinks)
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001807
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001808 (handle, fileName) = tempfile.mkstemp()
Maxime Costee2a892e2014-06-11 14:09:59 +01001809 tmpFile = os.fdopen(handle, "w+b")
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001810 if self.isWindows:
1811 submitTemplate = submitTemplate.replace("\n", "\r\n")
Maxime Costeb4073bb2014-05-24 18:40:35 +01001812 tmpFile.write(submitTemplate)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001813 tmpFile.close()
1814
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001815 if self.prepare_p4_only:
1816 #
1817 # Leave the p4 tree prepared, and the submit template around
1818 # and let the user decide what to do next
1819 #
1820 print
1821 print "P4 workspace prepared for submission."
1822 print "To submit or revert, go to client workspace"
1823 print " " + self.clientPath
1824 print
1825 print "To submit, use \"p4 submit\" to write a new description,"
Luke Diamand10de86d2015-01-23 09:15:12 +00001826 print "or \"p4 submit -i <%s\" to use the one prepared by" \
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001827 " \"git p4\"." % fileName
1828 print "You can delete the file \"%s\" when finished." % fileName
1829
1830 if self.preserveUser and p4User and not self.p4UserIsMe(p4User):
1831 print "To preserve change ownership by user %s, you must\n" \
1832 "do \"p4 change -f <change>\" after submitting and\n" \
1833 "edit the User field."
1834 if pureRenameCopy:
1835 print "After submitting, renamed files must be re-synced."
1836 print "Invoke \"p4 sync -f\" on each of these files:"
1837 for f in pureRenameCopy:
1838 print " " + f
1839
1840 print
1841 print "To revert the changes, use \"p4 revert ...\", and delete"
1842 print "the submit template file \"%s\"" % fileName
1843 if filesToAdd:
1844 print "Since the commit adds new files, they must be deleted:"
1845 for f in filesToAdd:
1846 print " " + f
1847 print
1848 return True
1849
Pete Wyckoff55ac2ed2012-09-09 16:16:08 -04001850 #
1851 # Let the user edit the change description, then submit it.
1852 #
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00001853 submitted = False
Luke Diamandecdba362011-05-07 11:19:43 +01001854
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00001855 try:
1856 if self.edit_template(fileName):
1857 # read the edited message and submit
1858 tmpFile = open(fileName, "rb")
1859 message = tmpFile.read()
1860 tmpFile.close()
1861 if self.isWindows:
1862 message = message.replace("\r\n", "\n")
1863 submitTemplate = message[:message.index(separatorLine)]
Luke Diamand46c609e2016-12-02 22:43:19 +00001864
1865 if self.update_shelve:
1866 p4_write_pipe(['shelve', '-r', '-i'], submitTemplate)
1867 elif self.shelve:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001868 p4_write_pipe(['shelve', '-i'], submitTemplate)
1869 else:
1870 p4_write_pipe(['submit', '-i'], submitTemplate)
1871 # The rename/copy happened by applying a patch that created a
1872 # new file. This leaves it writable, which confuses p4.
1873 for f in pureRenameCopy:
1874 p4_sync(f, "-f")
Luke Diamandecdba362011-05-07 11:19:43 +01001875
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00001876 if self.preserveUser:
1877 if p4User:
1878 # Get last changelist number. Cannot easily get it from
1879 # the submit command output as the output is
1880 # unmarshalled.
1881 changelist = self.lastP4Changelist()
1882 self.modifyChangelistUser(changelist, p4User)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001883
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00001884 submitted = True
1885
1886 finally:
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001887 # skip this patch
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00001888 if not submitted or self.shelve:
1889 if self.shelve:
1890 print ("Reverting shelved files.")
1891 else:
1892 print ("Submission cancelled, undoing p4 changes.")
1893 for f in editedFiles | filesToDelete:
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00001894 p4_revert(f)
1895 for f in filesToAdd:
1896 p4_revert(f)
1897 os.remove(f)
Pete Wyckoffc47178d2012-07-04 09:34:18 -04001898
1899 os.remove(fileName)
GIRARD Etienneb7638fe2015-11-24 07:43:59 +00001900 return submitted
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001901
Luke Diamand06804c72012-04-11 17:21:24 +02001902 # Export git tags as p4 labels. Create a p4 label and then tag
1903 # with that.
1904 def exportGitTags(self, gitTags):
Luke Diamandc8942a22012-04-11 17:21:24 +02001905 validLabelRegexp = gitConfig("git-p4.labelExportRegexp")
1906 if len(validLabelRegexp) == 0:
1907 validLabelRegexp = defaultLabelRegexp
1908 m = re.compile(validLabelRegexp)
Luke Diamand06804c72012-04-11 17:21:24 +02001909
1910 for name in gitTags:
1911
1912 if not m.match(name):
1913 if verbose:
Luke Diamand05a3cec2012-05-11 07:25:17 +01001914 print "tag %s does not match regexp %s" % (name, validLabelRegexp)
Luke Diamand06804c72012-04-11 17:21:24 +02001915 continue
1916
1917 # Get the p4 commit this corresponds to
Luke Diamandc8942a22012-04-11 17:21:24 +02001918 logMessage = extractLogMessageFromGitCommit(name)
1919 values = extractSettingsGitLog(logMessage)
Luke Diamand06804c72012-04-11 17:21:24 +02001920
Luke Diamandc8942a22012-04-11 17:21:24 +02001921 if not values.has_key('change'):
Luke Diamand06804c72012-04-11 17:21:24 +02001922 # a tag pointing to something not sent to p4; ignore
1923 if verbose:
1924 print "git tag %s does not give a p4 commit" % name
1925 continue
Luke Diamandc8942a22012-04-11 17:21:24 +02001926 else:
1927 changelist = values['change']
Luke Diamand06804c72012-04-11 17:21:24 +02001928
1929 # Get the tag details.
1930 inHeader = True
1931 isAnnotated = False
1932 body = []
1933 for l in read_pipe_lines(["git", "cat-file", "-p", name]):
1934 l = l.strip()
1935 if inHeader:
1936 if re.match(r'tag\s+', l):
1937 isAnnotated = True
1938 elif re.match(r'\s*$', l):
1939 inHeader = False
1940 continue
1941 else:
1942 body.append(l)
1943
1944 if not isAnnotated:
1945 body = ["lightweight tag imported by git p4\n"]
1946
1947 # Create the label - use the same view as the client spec we are using
1948 clientSpec = getClientSpec()
1949
1950 labelTemplate = "Label: %s\n" % name
1951 labelTemplate += "Description:\n"
1952 for b in body:
1953 labelTemplate += "\t" + b + "\n"
1954 labelTemplate += "View:\n"
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001955 for depot_side in clientSpec.mappings:
1956 labelTemplate += "\t%s\n" % depot_side
Luke Diamand06804c72012-04-11 17:21:24 +02001957
Pete Wyckoffef739f02012-09-09 16:16:11 -04001958 if self.dry_run:
1959 print "Would create p4 label %s for tag" % name
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04001960 elif self.prepare_p4_only:
1961 print "Not creating p4 label %s for tag due to option" \
1962 " --prepare-p4-only" % name
Pete Wyckoffef739f02012-09-09 16:16:11 -04001963 else:
1964 p4_write_pipe(["label", "-i"], labelTemplate)
Luke Diamand06804c72012-04-11 17:21:24 +02001965
Pete Wyckoffef739f02012-09-09 16:16:11 -04001966 # Use the label
1967 p4_system(["tag", "-l", name] +
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09001968 ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
Luke Diamand06804c72012-04-11 17:21:24 +02001969
Pete Wyckoffef739f02012-09-09 16:16:11 -04001970 if verbose:
1971 print "created p4 label for tag %s" % name
Luke Diamand06804c72012-04-11 17:21:24 +02001972
Simon Hausmann4f5cf762007-03-19 22:25:17 +01001973 def run(self, args):
Simon Hausmannc9b50e62007-03-29 19:15:24 +02001974 if len(args) == 0:
1975 self.master = currentGitBranch()
Simon Hausmannc9b50e62007-03-29 19:15:24 +02001976 elif len(args) == 1:
1977 self.master = args[0]
Pete Wyckoff28755db2011-12-24 21:07:40 -05001978 if not branchExists(self.master):
1979 die("Branch %s does not exist" % self.master)
Simon Hausmannc9b50e62007-03-29 19:15:24 +02001980 else:
1981 return False
1982
Luke Diamand00ad6e32015-11-21 09:54:41 +00001983 if self.master:
1984 allowSubmit = gitConfig("git-p4.allowSubmit")
1985 if len(allowSubmit) > 0 and not self.master in allowSubmit.split(","):
1986 die("%s is not in git-p4.allowSubmit" % self.master)
Jing Xue4c2d5d72008-06-22 14:12:39 -04001987
Simon Hausmann27d2d812007-06-12 14:31:59 +02001988 [upstream, settings] = findUpstreamBranchPoint()
Simon Hausmannea99c3a2007-08-08 17:06:55 +02001989 self.depotPath = settings['depot-paths'][0]
Simon Hausmann27d2d812007-06-12 14:31:59 +02001990 if len(self.origin) == 0:
1991 self.origin = upstream
Simon Hausmanna3fdd572007-06-07 22:54:32 +02001992
Luke Diamand46c609e2016-12-02 22:43:19 +00001993 if self.update_shelve:
1994 self.shelve = True
1995
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01001996 if self.preserveUser:
1997 if not self.canChangeChangelists():
1998 die("Cannot preserve user names without p4 super-user or admin permissions")
1999
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04002000 # if not set from the command line, try the config file
2001 if self.conflict_behavior is None:
2002 val = gitConfig("git-p4.conflict")
2003 if val:
2004 if val not in self.conflict_behavior_choices:
2005 die("Invalid value '%s' for config git-p4.conflict" % val)
2006 else:
2007 val = "ask"
2008 self.conflict_behavior = val
2009
Simon Hausmanna3fdd572007-06-07 22:54:32 +02002010 if self.verbose:
2011 print "Origin branch is " + self.origin
Simon Hausmann95124972007-03-23 09:16:07 +01002012
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002013 if len(self.depotPath) == 0:
Simon Hausmann95124972007-03-23 09:16:07 +01002014 print "Internal error: cannot locate perforce depot path from existing branches"
2015 sys.exit(128)
2016
Pete Wyckoff543987b2012-02-25 20:06:25 -05002017 self.useClientSpec = False
Pete Wyckoff0d609032013-01-26 22:11:24 -05002018 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff543987b2012-02-25 20:06:25 -05002019 self.useClientSpec = True
2020 if self.useClientSpec:
2021 self.clientSpecDirs = getClientSpec()
Simon Hausmann95124972007-03-23 09:16:07 +01002022
Ville Skyttä2e3a16b2016-08-09 11:53:38 +03002023 # Check for the existence of P4 branches
Vitor Antunescd884102015-04-21 23:49:30 +01002024 branchesDetected = (len(p4BranchesInGit().keys()) > 1)
2025
2026 if self.useClientSpec and not branchesDetected:
Pete Wyckoff543987b2012-02-25 20:06:25 -05002027 # all files are relative to the client spec
2028 self.clientPath = getClientRoot()
2029 else:
2030 self.clientPath = p4Where(self.depotPath)
2031
2032 if self.clientPath == "":
2033 die("Error: Cannot locate perforce checkout of %s in client view" % self.depotPath)
Simon Hausmann95124972007-03-23 09:16:07 +01002034
Simon Hausmannea99c3a2007-08-08 17:06:55 +02002035 print "Perforce checkout for depot path %s located at %s" % (self.depotPath, self.clientPath)
Simon Hausmann7944f142007-05-21 11:04:26 +02002036 self.oldWorkingDirectory = os.getcwd()
Simon Hausmannc1b296b2007-05-20 16:55:05 +02002037
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002038 # ensure the clientPath exists
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002039 new_client_dir = False
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002040 if not os.path.exists(self.clientPath):
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002041 new_client_dir = True
Gary Gibbons0591cfa2011-12-09 18:48:14 -05002042 os.makedirs(self.clientPath)
2043
Miklós Fazekasbbd84862013-03-11 17:45:29 -04002044 chdir(self.clientPath, is_client_path=True)
Pete Wyckoffef739f02012-09-09 16:16:11 -04002045 if self.dry_run:
2046 print "Would synchronize p4 checkout in %s" % self.clientPath
Pete Wyckoff8d7ec362012-04-29 20:57:14 -04002047 else:
Pete Wyckoffef739f02012-09-09 16:16:11 -04002048 print "Synchronizing p4 checkout..."
2049 if new_client_dir:
2050 # old one was destroyed, and maybe nobody told p4
2051 p4_sync("...", "-f")
2052 else:
2053 p4_sync("...")
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002054 self.check()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002055
Simon Hausmann4c750c02008-02-19 09:37:16 +01002056 commits = []
Luke Diamand00ad6e32015-11-21 09:54:41 +00002057 if self.master:
2058 commitish = self.master
2059 else:
2060 commitish = 'HEAD'
2061
2062 for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
Simon Hausmann4c750c02008-02-19 09:37:16 +01002063 commits.append(line.strip())
2064 commits.reverse()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002065
Pete Wyckoff0d609032013-01-26 22:11:24 -05002066 if self.preserveUser or gitConfigBool("git-p4.skipUserNameCheck"):
Luke Diamand848de9c2011-05-13 20:46:00 +01002067 self.checkAuthorship = False
2068 else:
2069 self.checkAuthorship = True
2070
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002071 if self.preserveUser:
2072 self.checkValidP4Users(commits)
2073
Gary Gibbons84cb0002012-07-04 09:40:19 -04002074 #
2075 # Build up a set of options to be passed to diff when
2076 # submitting each commit to p4.
2077 #
2078 if self.detectRenames:
2079 # command-line -M arg
2080 self.diffOpts = "-M"
2081 else:
2082 # If not explicitly set check the config variable
2083 detectRenames = gitConfig("git-p4.detectRenames")
2084
2085 if detectRenames.lower() == "false" or detectRenames == "":
2086 self.diffOpts = ""
2087 elif detectRenames.lower() == "true":
2088 self.diffOpts = "-M"
2089 else:
2090 self.diffOpts = "-M%s" % detectRenames
2091
2092 # no command-line arg for -C or --find-copies-harder, just
2093 # config variables
2094 detectCopies = gitConfig("git-p4.detectCopies")
2095 if detectCopies.lower() == "false" or detectCopies == "":
2096 pass
2097 elif detectCopies.lower() == "true":
2098 self.diffOpts += " -C"
2099 else:
2100 self.diffOpts += " -C%s" % detectCopies
2101
Pete Wyckoff0d609032013-01-26 22:11:24 -05002102 if gitConfigBool("git-p4.detectCopiesHarder"):
Gary Gibbons84cb0002012-07-04 09:40:19 -04002103 self.diffOpts += " --find-copies-harder"
2104
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002105 #
2106 # Apply the commits, one at a time. On failure, ask if should
2107 # continue to try the rest of the patches, or quit.
2108 #
Pete Wyckoffef739f02012-09-09 16:16:11 -04002109 if self.dry_run:
2110 print "Would apply"
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002111 applied = []
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002112 last = len(commits) - 1
2113 for i, commit in enumerate(commits):
Pete Wyckoffef739f02012-09-09 16:16:11 -04002114 if self.dry_run:
2115 print " ", read_pipe(["git", "show", "-s",
2116 "--format=format:%h %s", commit])
2117 ok = True
2118 else:
2119 ok = self.applyCommit(commit)
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002120 if ok:
2121 applied.append(commit)
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002122 else:
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002123 if self.prepare_p4_only and i < last:
2124 print "Processing only the first commit due to option" \
2125 " --prepare-p4-only"
2126 break
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002127 if i < last:
2128 quit = False
2129 while True:
Pete Wyckoff6bbfd132012-09-09 16:16:13 -04002130 # prompt for what to do, or use the option/variable
2131 if self.conflict_behavior == "ask":
2132 print "What do you want to do?"
2133 response = raw_input("[s]kip this commit but apply"
2134 " the rest, or [q]uit? ")
2135 if not response:
2136 continue
2137 elif self.conflict_behavior == "skip":
2138 response = "s"
2139 elif self.conflict_behavior == "quit":
2140 response = "q"
2141 else:
2142 die("Unknown conflict_behavior '%s'" %
2143 self.conflict_behavior)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002144
Pete Wyckoff7e5dd9f2012-09-09 16:16:05 -04002145 if response[0] == "s":
2146 print "Skipping this commit, but applying the rest"
2147 break
2148 if response[0] == "q":
2149 print "Quitting"
2150 quit = True
2151 break
2152 if quit:
2153 break
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002154
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002155 chdir(self.oldWorkingDirectory)
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002156 shelved_applied = "shelved" if self.shelve else "applied"
Pete Wyckoffef739f02012-09-09 16:16:11 -04002157 if self.dry_run:
2158 pass
Pete Wyckoff728b7ad2012-09-09 16:16:12 -04002159 elif self.prepare_p4_only:
2160 pass
Pete Wyckoffef739f02012-09-09 16:16:11 -04002161 elif len(commits) == len(applied):
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002162 print ("All commits {0}!".format(shelved_applied))
Simon Hausmann14594f42007-08-22 09:07:15 +02002163
Simon Hausmann4c750c02008-02-19 09:37:16 +01002164 sync = P4Sync()
Pete Wyckoff44e8d262013-01-14 19:47:08 -05002165 if self.branch:
2166 sync.branch = self.branch
Simon Hausmann4c750c02008-02-19 09:37:16 +01002167 sync.run([])
Simon Hausmann14594f42007-08-22 09:07:15 +02002168
Simon Hausmann4c750c02008-02-19 09:37:16 +01002169 rebase = P4Rebase()
2170 rebase.rebase()
Simon Hausmann4f5cf762007-03-19 22:25:17 +01002171
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002172 else:
2173 if len(applied) == 0:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002174 print ("No commits {0}.".format(shelved_applied))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002175 else:
Vinicius Kursancewb34fa572016-11-28 09:33:18 +00002176 print ("{0} only the commits marked with '*':".format(shelved_applied.capitalize()))
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002177 for c in commits:
2178 if c in applied:
2179 star = "*"
2180 else:
2181 star = " "
2182 print star, read_pipe(["git", "show", "-s",
2183 "--format=format:%h %s", c])
2184 print "You will have to do 'git p4 sync' and rebase."
2185
Pete Wyckoff0d609032013-01-26 22:11:24 -05002186 if gitConfigBool("git-p4.exportLabels"):
Luke Diamand06dcd152012-05-11 07:25:18 +01002187 self.exportLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02002188
2189 if self.exportLabels:
2190 p4Labels = getP4Labels(self.depotPath)
2191 gitTags = getGitTags()
2192
2193 missingGitTags = gitTags - p4Labels
2194 self.exportGitTags(missingGitTags)
2195
Ondřej Bílka98e023d2013-07-29 10:18:21 +02002196 # exit with error unless everything applied perfectly
Pete Wyckoff67b0fe22012-09-09 16:16:03 -04002197 if len(commits) != len(applied):
2198 sys.exit(1)
2199
Simon Hausmannb9847332007-03-20 20:54:23 +01002200 return True
2201
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002202class View(object):
2203 """Represent a p4 view ("p4 help views"), and map files in a
2204 repo according to the view."""
2205
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002206 def __init__(self, client_name):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002207 self.mappings = []
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002208 self.client_prefix = "//%s/" % client_name
2209 # cache results of "p4 where" to lookup client file locations
2210 self.client_spec_path_cache = {}
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002211
2212 def append(self, view_line):
2213 """Parse a view line, splitting it into depot and client
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002214 sides. Append to self.mappings, preserving order. This
2215 is only needed for tag creation."""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002216
2217 # Split the view line into exactly two words. P4 enforces
2218 # structure on these lines that simplifies this quite a bit.
2219 #
2220 # Either or both words may be double-quoted.
2221 # Single quotes do not matter.
2222 # Double-quote marks cannot occur inside the words.
2223 # A + or - prefix is also inside the quotes.
2224 # There are no quotes unless they contain a space.
2225 # The line is already white-space stripped.
2226 # The two words are separated by a single space.
2227 #
2228 if view_line[0] == '"':
2229 # First word is double quoted. Find its end.
2230 close_quote_index = view_line.find('"', 1)
2231 if close_quote_index <= 0:
2232 die("No first-word closing quote found: %s" % view_line)
2233 depot_side = view_line[1:close_quote_index]
2234 # skip closing quote and space
2235 rhs_index = close_quote_index + 1 + 1
2236 else:
2237 space_index = view_line.find(" ")
2238 if space_index <= 0:
2239 die("No word-splitting space found: %s" % view_line)
2240 depot_side = view_line[0:space_index]
2241 rhs_index = space_index + 1
2242
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002243 # prefix + means overlay on previous mapping
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002244 if depot_side.startswith("+"):
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002245 depot_side = depot_side[1:]
2246
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002247 # prefix - means exclude this path, leave out of mappings
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002248 exclude = False
2249 if depot_side.startswith("-"):
2250 exclude = True
2251 depot_side = depot_side[1:]
2252
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002253 if not exclude:
2254 self.mappings.append(depot_side)
2255
2256 def convert_client_path(self, clientFile):
2257 # chop off //client/ part to make it relative
2258 if not clientFile.startswith(self.client_prefix):
2259 die("No prefix '%s' on clientFile '%s'" %
2260 (self.client_prefix, clientFile))
2261 return clientFile[len(self.client_prefix):]
2262
2263 def update_client_spec_path_cache(self, files):
2264 """ Caching file paths by "p4 where" batch query """
2265
2266 # List depot file paths exclude that already cached
2267 fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
2268
2269 if len(fileArgs) == 0:
2270 return # All files in cache
2271
2272 where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
2273 for res in where_result:
2274 if "code" in res and res["code"] == "error":
2275 # assume error is "... file(s) not in client view"
2276 continue
2277 if "clientFile" not in res:
Pete Wyckoff20005442014-01-21 18:16:46 -05002278 die("No clientFile in 'p4 where' output")
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002279 if "unmap" in res:
2280 # it will list all of them, but only one not unmap-ped
2281 continue
Lars Schneidera0a50d82015-08-28 14:00:34 +02002282 if gitConfigBool("core.ignorecase"):
2283 res['depotFile'] = res['depotFile'].lower()
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002284 self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
2285
2286 # not found files or unmap files set to ""
2287 for depotFile in fileArgs:
Lars Schneidera0a50d82015-08-28 14:00:34 +02002288 if gitConfigBool("core.ignorecase"):
2289 depotFile = depotFile.lower()
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002290 if depotFile not in self.client_spec_path_cache:
2291 self.client_spec_path_cache[depotFile] = ""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002292
2293 def map_in_client(self, depot_path):
2294 """Return the relative location in the client where this
2295 depot file should live. Returns "" if the file should
2296 not be mapped in the client."""
2297
Lars Schneidera0a50d82015-08-28 14:00:34 +02002298 if gitConfigBool("core.ignorecase"):
2299 depot_path = depot_path.lower()
2300
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002301 if depot_path in self.client_spec_path_cache:
2302 return self.client_spec_path_cache[depot_path]
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002303
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002304 die( "Error: %s is not found in client spec path" % depot_path )
2305 return ""
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002306
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002307class P4Sync(Command, P4UserMap):
Pete Wyckoff56c09342011-02-19 08:17:57 -05002308 delete_actions = ( "delete", "move/delete", "purge" )
2309
Simon Hausmannb9847332007-03-20 20:54:23 +01002310 def __init__(self):
2311 Command.__init__(self)
Luke Diamand3ea2cfd2011-04-21 20:50:23 +01002312 P4UserMap.__init__(self)
Simon Hausmannb9847332007-03-20 20:54:23 +01002313 self.options = [
2314 optparse.make_option("--branch", dest="branch"),
2315 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
2316 optparse.make_option("--changesfile", dest="changesFile"),
2317 optparse.make_option("--silent", dest="silent", action="store_true"),
Simon Hausmannef48f902007-05-17 22:17:49 +02002318 optparse.make_option("--detect-labels", dest="detectLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02002319 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Han-Wen Nienhuysd2c6dd32007-05-23 18:49:35 -03002320 optparse.make_option("--import-local", dest="importIntoRemotes", action="store_false",
2321 help="Import into refs/heads/ , not refs/remotes"),
Lex Spoon96b2d542015-04-20 11:00:20 -04002322 optparse.make_option("--max-changes", dest="maxChanges",
2323 help="Maximum number of changes to import"),
2324 optparse.make_option("--changes-block-size", dest="changes_block_size", type="int",
2325 help="Internal block size to use when iteratively calling p4 changes"),
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03002326 optparse.make_option("--keep-path", dest="keepRepoPath", action='store_true',
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002327 help="Keep entire BRANCH/DIR/SUBDIR prefix during import"),
2328 optparse.make_option("--use-client-spec", dest="useClientSpec", action='store_true',
Luke Diamand51334bb2015-01-17 20:56:38 +00002329 help="Only sync files that are included in the Perforce Client Spec"),
2330 optparse.make_option("-/", dest="cloneExclude",
2331 action="append", type="string",
2332 help="exclude depot path"),
Simon Hausmannb9847332007-03-20 20:54:23 +01002333 ]
2334 self.description = """Imports from Perforce into a git repository.\n
2335 example:
2336 //depot/my/project/ -- to import the current head
2337 //depot/my/project/@all -- to import everything
2338 //depot/my/project/@1,6 -- to import only from revision 1 to 6
2339
2340 (a ... is not needed in the path p4 specification, it's added implicitly)"""
2341
2342 self.usage += " //depot/path[@revRange]"
Simon Hausmannb9847332007-03-20 20:54:23 +01002343 self.silent = False
Reilly Grant1d7367d2009-09-10 00:02:38 -07002344 self.createdBranches = set()
2345 self.committedChanges = set()
Simon Hausmann569d1bd2007-03-22 21:34:16 +01002346 self.branch = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01002347 self.detectBranches = False
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02002348 self.detectLabels = False
Luke Diamand06804c72012-04-11 17:21:24 +02002349 self.importLabels = False
Simon Hausmannb9847332007-03-20 20:54:23 +01002350 self.changesFile = ""
Simon Hausmann01265102007-05-25 10:36:10 +02002351 self.syncWithOrigin = True
Simon Hausmanna028a982007-05-23 00:03:08 +02002352 self.importIntoRemotes = True
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02002353 self.maxChanges = ""
Luke Diamand1051ef02015-06-10 08:30:59 +01002354 self.changes_block_size = None
Han-Wen Nienhuys8b41a972007-05-23 18:20:53 -03002355 self.keepRepoPath = False
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002356 self.depotPaths = None
Simon Hausmann3c699642007-06-16 13:09:21 +02002357 self.p4BranchesInGit = []
Tommy Thorn354081d2008-02-03 10:38:51 -08002358 self.cloneExclude = []
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002359 self.useClientSpec = False
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05002360 self.useClientSpec_from_options = False
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002361 self.clientSpecDirs = None
Vitor Antunesfed23692012-01-25 23:48:22 +00002362 self.tempBranches = []
Lars Schneiderd6041762016-06-29 09:35:27 +02002363 self.tempBranchLocation = "refs/git-p4-tmp"
Lars Schneidera5db4b12015-09-26 09:55:03 +02002364 self.largeFileSystem = None
2365
2366 if gitConfig('git-p4.largeFileSystem'):
2367 largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
2368 self.largeFileSystem = largeFileSystemConstructor(
2369 lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
2370 )
Simon Hausmannb9847332007-03-20 20:54:23 +01002371
Simon Hausmann01265102007-05-25 10:36:10 +02002372 if gitConfig("git-p4.syncFromOrigin") == "false":
2373 self.syncWithOrigin = False
2374
Luke Diamand51334bb2015-01-17 20:56:38 +00002375 # This is required for the "append" cloneExclude action
2376 def ensure_value(self, attr, value):
2377 if not hasattr(self, attr) or getattr(self, attr) is None:
2378 setattr(self, attr, value)
2379 return getattr(self, attr)
2380
Vitor Antunesfed23692012-01-25 23:48:22 +00002381 # Force a checkpoint in fast-import and wait for it to finish
2382 def checkpoint(self):
2383 self.gitStream.write("checkpoint\n\n")
2384 self.gitStream.write("progress checkpoint\n\n")
2385 out = self.gitOutput.readline()
2386 if self.verbose:
2387 print "checkpoint finished: " + out
2388
Simon Hausmannb9847332007-03-20 20:54:23 +01002389 def extractFilesFromCommit(self, commit):
Tommy Thorn354081d2008-02-03 10:38:51 -08002390 self.cloneExclude = [re.sub(r"\.\.\.$", "", path)
2391 for path in self.cloneExclude]
Simon Hausmannb9847332007-03-20 20:54:23 +01002392 files = []
2393 fnum = 0
2394 while commit.has_key("depotFile%s" % fnum):
2395 path = commit["depotFile%s" % fnum]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002396
Tommy Thorn354081d2008-02-03 10:38:51 -08002397 if [p for p in self.cloneExclude
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01002398 if p4PathStartsWith(path, p)]:
Tommy Thorn354081d2008-02-03 10:38:51 -08002399 found = False
2400 else:
2401 found = [p for p in self.depotPaths
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01002402 if p4PathStartsWith(path, p)]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002403 if not found:
Simon Hausmannb9847332007-03-20 20:54:23 +01002404 fnum = fnum + 1
2405 continue
2406
2407 file = {}
2408 file["path"] = path
2409 file["rev"] = commit["rev%s" % fnum]
2410 file["action"] = commit["action%s" % fnum]
2411 file["type"] = commit["type%s" % fnum]
2412 files.append(file)
2413 fnum = fnum + 1
2414 return files
2415
Jan Durovec26e6a272016-04-19 19:49:41 +00002416 def extractJobsFromCommit(self, commit):
2417 jobs = []
2418 jnum = 0
2419 while commit.has_key("job%s" % jnum):
2420 job = commit["job%s" % jnum]
2421 jobs.append(job)
2422 jnum = jnum + 1
2423 return jobs
2424
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002425 def stripRepoPath(self, path, prefixes):
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002426 """When streaming files, this is called to map a p4 depot path
2427 to where it should go in git. The prefixes are either
2428 self.depotPaths, or self.branchPrefixes in the case of
2429 branch detection."""
2430
Ian Wienand39527102011-02-11 16:33:48 -08002431 if self.useClientSpec:
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002432 # branch detection moves files up a level (the branch name)
2433 # from what client spec interpretation gives
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002434 path = self.clientSpecDirs.map_in_client(path)
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002435 if self.detectBranches:
2436 for b in self.knownBranches:
2437 if path.startswith(b + "/"):
2438 path = path[len(b)+1:]
2439
2440 elif self.keepRepoPath:
2441 # Preserve everything in relative path name except leading
2442 # //depot/; just look at first prefix as they all should
2443 # be in the same depot.
2444 depot = re.sub("^(//[^/]+/).*", r'\1', prefixes[0])
2445 if p4PathStartsWith(path, depot):
2446 path = path[len(depot):]
Ian Wienand39527102011-02-11 16:33:48 -08002447
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002448 else:
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002449 for p in prefixes:
2450 if p4PathStartsWith(path, p):
2451 path = path[len(p):]
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002452 break
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002453
Pete Wyckoff0d1696e2012-08-11 12:55:03 -04002454 path = wildcard_decode(path)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002455 return path
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -03002456
Simon Hausmann71b112d2007-05-19 11:54:11 +02002457 def splitFilesIntoBranches(self, commit):
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002458 """Look at each depotFile in the commit to figure out to what
2459 branch it belongs."""
2460
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002461 if self.clientSpecDirs:
2462 files = self.extractFilesFromCommit(commit)
2463 self.clientSpecDirs.update_client_spec_path_cache(files)
2464
Simon Hausmannd5904672007-05-19 11:07:32 +02002465 branches = {}
Simon Hausmann71b112d2007-05-19 11:54:11 +02002466 fnum = 0
2467 while commit.has_key("depotFile%s" % fnum):
2468 path = commit["depotFile%s" % fnum]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002469 found = [p for p in self.depotPaths
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01002470 if p4PathStartsWith(path, p)]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03002471 if not found:
Simon Hausmann71b112d2007-05-19 11:54:11 +02002472 fnum = fnum + 1
2473 continue
2474
2475 file = {}
2476 file["path"] = path
2477 file["rev"] = commit["rev%s" % fnum]
2478 file["action"] = commit["action%s" % fnum]
2479 file["type"] = commit["type%s" % fnum]
2480 fnum = fnum + 1
2481
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002482 # start with the full relative path where this file would
2483 # go in a p4 client
2484 if self.useClientSpec:
2485 relPath = self.clientSpecDirs.map_in_client(path)
2486 else:
2487 relPath = self.stripRepoPath(path, self.depotPaths)
Simon Hausmannb9847332007-03-20 20:54:23 +01002488
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02002489 for branch in self.knownBranches.keys():
Pete Wyckoff21ef5df2012-08-11 12:55:04 -04002490 # add a trailing slash so that a commit into qt/4.2foo
2491 # doesn't end up in qt/4.2, e.g.
Han-Wen Nienhuys6754a292007-05-23 17:41:50 -03002492 if relPath.startswith(branch + "/"):
Simon Hausmannd5904672007-05-19 11:07:32 +02002493 if branch not in branches:
2494 branches[branch] = []
Simon Hausmann71b112d2007-05-19 11:54:11 +02002495 branches[branch].append(file)
Simon Hausmann6555b2c2007-06-17 11:25:34 +02002496 break
Simon Hausmannb9847332007-03-20 20:54:23 +01002497
2498 return branches
2499
Lars Schneidera5db4b12015-09-26 09:55:03 +02002500 def writeToGitStream(self, gitMode, relPath, contents):
2501 self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
2502 self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
2503 for d in contents:
2504 self.gitStream.write(d)
2505 self.gitStream.write('\n')
2506
Lars Schneidera8b05162017-02-09 16:06:56 +01002507 def encodeWithUTF8(self, path):
2508 try:
2509 path.decode('ascii')
2510 except:
2511 encoding = 'utf8'
2512 if gitConfig('git-p4.pathEncoding'):
2513 encoding = gitConfig('git-p4.pathEncoding')
2514 path = path.decode(encoding, 'replace').encode('utf8', 'replace')
2515 if self.verbose:
2516 print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, path)
2517 return path
2518
Luke Diamandb9327052009-07-30 00:13:46 +01002519 # output one file from the P4 stream
2520 # - helper for streamP4Files
2521
2522 def streamOneP4File(self, file, contents):
Luke Diamandb9327052009-07-30 00:13:46 +01002523 relPath = self.stripRepoPath(file['depotFile'], self.branchPrefixes)
Lars Schneidera8b05162017-02-09 16:06:56 +01002524 relPath = self.encodeWithUTF8(relPath)
Luke Diamandb9327052009-07-30 00:13:46 +01002525 if verbose:
Lars Schneiderd2176a52015-09-26 09:55:01 +02002526 size = int(self.stream_file['fileSize'])
2527 sys.stdout.write('\r%s --> %s (%i MB)\n' % (file['depotFile'], relPath, size/1024/1024))
2528 sys.stdout.flush()
Luke Diamandb9327052009-07-30 00:13:46 +01002529
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04002530 (type_base, type_mods) = split_p4_type(file["type"])
2531
2532 git_mode = "100644"
2533 if "x" in type_mods:
2534 git_mode = "100755"
2535 if type_base == "symlink":
2536 git_mode = "120000"
Alexandru Juncu1292df12013-08-08 16:17:38 +03002537 # p4 print on a symlink sometimes contains "target\n";
2538 # if it does, remove the newline
Evan Powersb39c3612010-02-16 00:44:08 -08002539 data = ''.join(contents)
Pete Wyckoff40f846c2014-01-21 18:16:40 -05002540 if not data:
2541 # Some version of p4 allowed creating a symlink that pointed
2542 # to nothing. This causes p4 errors when checking out such
2543 # a change, and errors here too. Work around it by ignoring
2544 # the bad symlink; hopefully a future change fixes it.
2545 print "\nIgnoring empty symlink in %s" % file['depotFile']
2546 return
2547 elif data[-1] == '\n':
Alexandru Juncu1292df12013-08-08 16:17:38 +03002548 contents = [data[:-1]]
2549 else:
2550 contents = [data]
Luke Diamandb9327052009-07-30 00:13:46 +01002551
Pete Wyckoff9cffb8c2011-10-16 10:45:01 -04002552 if type_base == "utf16":
Pete Wyckoff55aa5712011-09-17 19:16:14 -04002553 # p4 delivers different text in the python output to -G
2554 # than it does when using "print -o", or normal p4 client
2555 # operations. utf16 is converted to ascii or utf8, perhaps.
2556 # But ascii text saved as -t utf16 is completely mangled.
2557 # Invoke print -o to get the real contents.
Pete Wyckoff7f0e5962013-01-26 22:11:13 -05002558 #
2559 # On windows, the newlines will always be mangled by print, so put
2560 # them back too. This is not needed to the cygwin windows version,
2561 # just the native "NT" type.
2562 #
Lars Schneider1f5f3902015-09-21 12:01:41 +02002563 try:
2564 text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
2565 except Exception as e:
2566 if 'Translation of file content failed' in str(e):
2567 type_base = 'binary'
2568 else:
2569 raise e
2570 else:
2571 if p4_version_string().find('/NT') >= 0:
2572 text = text.replace('\r\n', '\n')
2573 contents = [ text ]
Pete Wyckoff55aa5712011-09-17 19:16:14 -04002574
Pete Wyckoff9f7ef0e2011-11-05 13:36:07 -04002575 if type_base == "apple":
2576 # Apple filetype files will be streamed as a concatenation of
2577 # its appledouble header and the contents. This is useless
2578 # on both macs and non-macs. If using "print -q -o xx", it
2579 # will create "xx" with the data, and "%xx" with the header.
2580 # This is also not very useful.
2581 #
2582 # Ideally, someday, this script can learn how to generate
2583 # appledouble files directly and import those to git, but
2584 # non-mac machines can never find a use for apple filetype.
2585 print "\nIgnoring apple filetype file %s" % file['depotFile']
2586 return
2587
Pete Wyckoff55aa5712011-09-17 19:16:14 -04002588 # Note that we do not try to de-mangle keywords on utf16 files,
2589 # even though in theory somebody may want that.
Luke Diamand60df0712012-02-23 07:51:30 +00002590 pattern = p4_keywords_regexp_for_type(type_base, type_mods)
2591 if pattern:
2592 regexp = re.compile(pattern, re.VERBOSE)
2593 text = ''.join(contents)
2594 text = regexp.sub(r'$\1$', text)
2595 contents = [ text ]
Luke Diamandb9327052009-07-30 00:13:46 +01002596
Lars Schneidera5db4b12015-09-26 09:55:03 +02002597 if self.largeFileSystem:
2598 (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01002599
Lars Schneidera5db4b12015-09-26 09:55:03 +02002600 self.writeToGitStream(git_mode, relPath, contents)
Luke Diamandb9327052009-07-30 00:13:46 +01002601
2602 def streamOneP4Deletion(self, file):
2603 relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
Lars Schneidera8b05162017-02-09 16:06:56 +01002604 relPath = self.encodeWithUTF8(relPath)
Luke Diamandb9327052009-07-30 00:13:46 +01002605 if verbose:
Lars Schneiderd2176a52015-09-26 09:55:01 +02002606 sys.stdout.write("delete %s\n" % relPath)
2607 sys.stdout.flush()
Luke Diamandb9327052009-07-30 00:13:46 +01002608 self.gitStream.write("D %s\n" % relPath)
2609
Lars Schneidera5db4b12015-09-26 09:55:03 +02002610 if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
2611 self.largeFileSystem.removeLargeFile(relPath)
2612
Luke Diamandb9327052009-07-30 00:13:46 +01002613 # handle another chunk of streaming data
2614 def streamP4FilesCb(self, marshalled):
2615
Pete Wyckoff78189be2012-11-23 17:35:36 -05002616 # catch p4 errors and complain
2617 err = None
2618 if "code" in marshalled:
2619 if marshalled["code"] == "error":
2620 if "data" in marshalled:
2621 err = marshalled["data"].rstrip()
Lars Schneider4d25dc42015-09-26 09:55:02 +02002622
2623 if not err and 'fileSize' in self.stream_file:
2624 required_bytes = int((4 * int(self.stream_file["fileSize"])) - calcDiskFree())
2625 if required_bytes > 0:
2626 err = 'Not enough space left on %s! Free at least %i MB.' % (
2627 os.getcwd(), required_bytes/1024/1024
2628 )
2629
Pete Wyckoff78189be2012-11-23 17:35:36 -05002630 if err:
2631 f = None
2632 if self.stream_have_file_info:
2633 if "depotFile" in self.stream_file:
2634 f = self.stream_file["depotFile"]
2635 # force a failure in fast-import, else an empty
2636 # commit will be made
2637 self.gitStream.write("\n")
2638 self.gitStream.write("die-now\n")
2639 self.gitStream.close()
2640 # ignore errors, but make sure it exits first
2641 self.importProcess.wait()
2642 if f:
2643 die("Error from p4 print for %s: %s" % (f, err))
2644 else:
2645 die("Error from p4 print: %s" % err)
2646
Andrew Garberc3f61632011-04-07 02:01:21 -04002647 if marshalled.has_key('depotFile') and self.stream_have_file_info:
2648 # start of a new file - output the old one first
2649 self.streamOneP4File(self.stream_file, self.stream_contents)
2650 self.stream_file = {}
2651 self.stream_contents = []
2652 self.stream_have_file_info = False
Luke Diamandb9327052009-07-30 00:13:46 +01002653
Andrew Garberc3f61632011-04-07 02:01:21 -04002654 # pick up the new file information... for the
2655 # 'data' field we need to append to our array
2656 for k in marshalled.keys():
2657 if k == 'data':
Lars Schneiderd2176a52015-09-26 09:55:01 +02002658 if 'streamContentSize' not in self.stream_file:
2659 self.stream_file['streamContentSize'] = 0
2660 self.stream_file['streamContentSize'] += len(marshalled['data'])
Andrew Garberc3f61632011-04-07 02:01:21 -04002661 self.stream_contents.append(marshalled['data'])
2662 else:
2663 self.stream_file[k] = marshalled[k]
Luke Diamandb9327052009-07-30 00:13:46 +01002664
Lars Schneiderd2176a52015-09-26 09:55:01 +02002665 if (verbose and
2666 'streamContentSize' in self.stream_file and
2667 'fileSize' in self.stream_file and
2668 'depotFile' in self.stream_file):
2669 size = int(self.stream_file["fileSize"])
2670 if size > 0:
2671 progress = 100*self.stream_file['streamContentSize']/size
2672 sys.stdout.write('\r%s %d%% (%i MB)' % (self.stream_file['depotFile'], progress, int(size/1024/1024)))
2673 sys.stdout.flush()
2674
Andrew Garberc3f61632011-04-07 02:01:21 -04002675 self.stream_have_file_info = True
Luke Diamandb9327052009-07-30 00:13:46 +01002676
2677 # Stream directly from "p4 files" into "git fast-import"
2678 def streamP4Files(self, files):
Simon Hausmann30b59402008-03-03 11:55:48 +01002679 filesForCommit = []
2680 filesToRead = []
Luke Diamandb9327052009-07-30 00:13:46 +01002681 filesToDelete = []
Simon Hausmann30b59402008-03-03 11:55:48 +01002682
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01002683 for f in files:
Pete Wyckoffecb7cf92012-01-02 18:05:53 -05002684 filesForCommit.append(f)
2685 if f['action'] in self.delete_actions:
2686 filesToDelete.append(f)
2687 else:
2688 filesToRead.append(f)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03002689
Luke Diamandb9327052009-07-30 00:13:46 +01002690 # deleted files...
2691 for f in filesToDelete:
2692 self.streamOneP4Deletion(f)
2693
Simon Hausmann30b59402008-03-03 11:55:48 +01002694 if len(filesToRead) > 0:
Luke Diamandb9327052009-07-30 00:13:46 +01002695 self.stream_file = {}
2696 self.stream_contents = []
2697 self.stream_have_file_info = False
2698
Andrew Garberc3f61632011-04-07 02:01:21 -04002699 # curry self argument
2700 def streamP4FilesCbSelf(entry):
2701 self.streamP4FilesCb(entry)
Luke Diamandb9327052009-07-30 00:13:46 +01002702
Luke Diamand6de040d2011-10-16 10:47:52 -04002703 fileArgs = ['%s#%s' % (f['path'], f['rev']) for f in filesToRead]
2704
2705 p4CmdList(["-x", "-", "print"],
2706 stdin=fileArgs,
2707 cb=streamP4FilesCbSelf)
Han-Wen Nienhuysf2eda792007-05-23 18:49:35 -03002708
Luke Diamandb9327052009-07-30 00:13:46 +01002709 # do the last chunk
2710 if self.stream_file.has_key('depotFile'):
2711 self.streamOneP4File(self.stream_file, self.stream_contents)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03002712
Luke Diamandaffb4742012-01-19 09:52:27 +00002713 def make_email(self, userid):
2714 if userid in self.users:
2715 return self.users[userid]
2716 else:
2717 return "%s <a@b>" % userid
2718
Luke Diamand06804c72012-04-11 17:21:24 +02002719 def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
Luke Diamandb43702a2015-08-27 08:18:58 +01002720 """ Stream a p4 tag.
2721 commit is either a git commit, or a fast-import mark, ":<p4commit>"
2722 """
2723
Luke Diamand06804c72012-04-11 17:21:24 +02002724 if verbose:
2725 print "writing tag %s for commit %s" % (labelName, commit)
2726 gitStream.write("tag %s\n" % labelName)
2727 gitStream.write("from %s\n" % commit)
2728
2729 if labelDetails.has_key('Owner'):
2730 owner = labelDetails["Owner"]
2731 else:
2732 owner = None
2733
2734 # Try to use the owner of the p4 label, or failing that,
2735 # the current p4 user id.
2736 if owner:
2737 email = self.make_email(owner)
2738 else:
2739 email = self.make_email(self.p4UserId())
2740 tagger = "%s %s %s" % (email, epoch, self.tz)
2741
2742 gitStream.write("tagger %s\n" % tagger)
2743
2744 print "labelDetails=",labelDetails
2745 if labelDetails.has_key('Description'):
2746 description = labelDetails['Description']
2747 else:
2748 description = 'Label from git p4'
2749
2750 gitStream.write("data %d\n" % len(description))
2751 gitStream.write(description)
2752 gitStream.write("\n")
2753
Lars Schneider4ae048e2015-12-08 10:36:22 +01002754 def inClientSpec(self, path):
2755 if not self.clientSpecDirs:
2756 return True
2757 inClientSpec = self.clientSpecDirs.map_in_client(path)
2758 if not inClientSpec and self.verbose:
2759 print('Ignoring file outside of client spec: {0}'.format(path))
2760 return inClientSpec
2761
2762 def hasBranchPrefix(self, path):
2763 if not self.branchPrefixes:
2764 return True
2765 hasPrefix = [p for p in self.branchPrefixes
2766 if p4PathStartsWith(path, p)]
Andrew Oakley09667d02016-06-22 10:26:11 +01002767 if not hasPrefix and self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01002768 print('Ignoring file outside of prefix: {0}'.format(path))
2769 return hasPrefix
2770
Pete Wyckoffe63231e2012-08-11 12:55:02 -04002771 def commit(self, details, files, branch, parent = ""):
Simon Hausmannb9847332007-03-20 20:54:23 +01002772 epoch = details["time"]
2773 author = details["user"]
Jan Durovec26e6a272016-04-19 19:49:41 +00002774 jobs = self.extractJobsFromCommit(details)
Simon Hausmannb9847332007-03-20 20:54:23 +01002775
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02002776 if self.verbose:
Lars Schneider4ae048e2015-12-08 10:36:22 +01002777 print('commit into {0}'.format(branch))
Han-Wen Nienhuys96e07dd2007-05-23 18:49:35 -03002778
Kazuki Saitoh9d57c4a2013-08-30 19:02:06 +09002779 if self.clientSpecDirs:
2780 self.clientSpecDirs.update_client_spec_path_cache(files)
2781
Lars Schneider4ae048e2015-12-08 10:36:22 +01002782 files = [f for f in files
2783 if self.inClientSpec(f['path']) and self.hasBranchPrefix(f['path'])]
2784
2785 if not files and not gitConfigBool('git-p4.keepEmptyCommits'):
2786 print('Ignoring revision {0} as it would produce an empty commit.'
2787 .format(details['change']))
2788 return
2789
Simon Hausmannb9847332007-03-20 20:54:23 +01002790 self.gitStream.write("commit %s\n" % branch)
Luke Diamandb43702a2015-08-27 08:18:58 +01002791 self.gitStream.write("mark :%s\n" % details["change"])
Simon Hausmannb9847332007-03-20 20:54:23 +01002792 self.committedChanges.add(int(details["change"]))
2793 committer = ""
Simon Hausmannb607e712007-05-20 10:55:54 +02002794 if author not in self.users:
2795 self.getUserMapFromPerforceServer()
Luke Diamandaffb4742012-01-19 09:52:27 +00002796 committer = "%s %s %s" % (self.make_email(author), epoch, self.tz)
Simon Hausmannb9847332007-03-20 20:54:23 +01002797
2798 self.gitStream.write("committer %s\n" % committer)
2799
2800 self.gitStream.write("data <<EOT\n")
2801 self.gitStream.write(details["desc"])
Jan Durovec26e6a272016-04-19 19:49:41 +00002802 if len(jobs) > 0:
2803 self.gitStream.write("\nJobs: %s" % (' '.join(jobs)))
Pete Wyckoffe63231e2012-08-11 12:55:02 -04002804 self.gitStream.write("\n[git-p4: depot-paths = \"%s\": change = %s" %
2805 (','.join(self.branchPrefixes), details["change"]))
Simon Hausmann6581de02007-06-11 10:01:58 +02002806 if len(details['options']) > 0:
2807 self.gitStream.write(": options = %s" % details['options'])
2808 self.gitStream.write("]\nEOT\n\n")
Simon Hausmannb9847332007-03-20 20:54:23 +01002809
2810 if len(parent) > 0:
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02002811 if self.verbose:
2812 print "parent %s" % parent
Simon Hausmannb9847332007-03-20 20:54:23 +01002813 self.gitStream.write("from %s\n" % parent)
2814
Lars Schneider4ae048e2015-12-08 10:36:22 +01002815 self.streamP4Files(files)
Simon Hausmannb9847332007-03-20 20:54:23 +01002816 self.gitStream.write("\n")
2817
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002818 change = int(details["change"])
2819
Simon Hausmann9bda3a82007-05-19 12:05:40 +02002820 if self.labels.has_key(change):
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002821 label = self.labels[change]
2822 labelDetails = label[0]
2823 labelRevisions = label[1]
Simon Hausmann71b112d2007-05-19 11:54:11 +02002824 if self.verbose:
2825 print "Change %s is labelled %s" % (change, labelDetails)
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002826
Luke Diamand6de040d2011-10-16 10:47:52 -04002827 files = p4CmdList(["files"] + ["%s...@%s" % (p, change)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04002828 for p in self.branchPrefixes])
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002829
2830 if len(files) == len(labelRevisions):
2831
2832 cleanedFiles = {}
2833 for info in files:
Pete Wyckoff56c09342011-02-19 08:17:57 -05002834 if info["action"] in self.delete_actions:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002835 continue
2836 cleanedFiles[info["depotFile"]] = info["rev"]
2837
2838 if cleanedFiles == labelRevisions:
Luke Diamand06804c72012-04-11 17:21:24 +02002839 self.streamTag(self.gitStream, 'tag_%s' % labelDetails['label'], labelDetails, branch, epoch)
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002840
2841 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02002842 if not self.silent:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03002843 print ("Tag %s does not match with change %s: files do not match."
2844 % (labelDetails["label"], change))
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002845
2846 else:
Simon Hausmanna46668f2007-03-28 17:05:38 +02002847 if not self.silent:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03002848 print ("Tag %s does not match with change %s: file count is different."
2849 % (labelDetails["label"], change))
Simon Hausmannb9847332007-03-20 20:54:23 +01002850
Luke Diamand06804c72012-04-11 17:21:24 +02002851 # Build a dictionary of changelists and labels, for "detect-labels" option.
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002852 def getLabels(self):
2853 self.labels = {}
2854
Luke Diamand52a48802012-01-19 09:52:25 +00002855 l = p4CmdList(["labels"] + ["%s..." % p for p in self.depotPaths])
Simon Hausmann10c32112007-04-08 10:15:47 +02002856 if len(l) > 0 and not self.silent:
Shun Kei Leung183f8432007-11-21 11:01:19 +08002857 print "Finding files belonging to labels in %s" % `self.depotPaths`
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02002858
2859 for output in l:
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002860 label = output["label"]
2861 revisions = {}
2862 newestChange = 0
Simon Hausmann71b112d2007-05-19 11:54:11 +02002863 if self.verbose:
2864 print "Querying files for label %s" % label
Luke Diamand6de040d2011-10-16 10:47:52 -04002865 for file in p4CmdList(["files"] +
2866 ["%s...@%s" % (p, label)
2867 for p in self.depotPaths]):
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002868 revisions[file["depotFile"]] = file["rev"]
2869 change = int(file["change"])
2870 if change > newestChange:
2871 newestChange = change
2872
Simon Hausmann9bda3a82007-05-19 12:05:40 +02002873 self.labels[newestChange] = [output, revisions]
2874
2875 if self.verbose:
2876 print "Label changes: %s" % self.labels.keys()
Simon Hausmann1f4ba1c2007-03-26 22:34:34 +02002877
Luke Diamand06804c72012-04-11 17:21:24 +02002878 # Import p4 labels as git tags. A direct mapping does not
2879 # exist, so assume that if all the files are at the same revision
2880 # then we can use that, or it's something more complicated we should
2881 # just ignore.
2882 def importP4Labels(self, stream, p4Labels):
2883 if verbose:
2884 print "import p4 labels: " + ' '.join(p4Labels)
2885
2886 ignoredP4Labels = gitConfigList("git-p4.ignoredP4Labels")
Luke Diamandc8942a22012-04-11 17:21:24 +02002887 validLabelRegexp = gitConfig("git-p4.labelImportRegexp")
Luke Diamand06804c72012-04-11 17:21:24 +02002888 if len(validLabelRegexp) == 0:
2889 validLabelRegexp = defaultLabelRegexp
2890 m = re.compile(validLabelRegexp)
2891
2892 for name in p4Labels:
2893 commitFound = False
2894
2895 if not m.match(name):
2896 if verbose:
2897 print "label %s does not match regexp %s" % (name,validLabelRegexp)
2898 continue
2899
2900 if name in ignoredP4Labels:
2901 continue
2902
2903 labelDetails = p4CmdList(['label', "-o", name])[0]
2904
2905 # get the most recent changelist for each file in this label
2906 change = p4Cmd(["changes", "-m", "1"] + ["%s...@%s" % (p, name)
2907 for p in self.depotPaths])
2908
2909 if change.has_key('change'):
2910 # find the corresponding git commit; take the oldest commit
2911 changelist = int(change['change'])
Luke Diamandb43702a2015-08-27 08:18:58 +01002912 if changelist in self.committedChanges:
2913 gitCommit = ":%d" % changelist # use a fast-import mark
Luke Diamand06804c72012-04-11 17:21:24 +02002914 commitFound = True
Luke Diamandb43702a2015-08-27 08:18:58 +01002915 else:
2916 gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
2917 "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
2918 if len(gitCommit) == 0:
2919 print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
2920 else:
2921 commitFound = True
2922 gitCommit = gitCommit.strip()
2923
2924 if commitFound:
Luke Diamand06804c72012-04-11 17:21:24 +02002925 # Convert from p4 time format
2926 try:
2927 tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")
2928 except ValueError:
Pete Wyckoffa4e90542012-11-23 17:35:38 -05002929 print "Could not convert label time %s" % labelDetails['Update']
Luke Diamand06804c72012-04-11 17:21:24 +02002930 tmwhen = 1
2931
2932 when = int(time.mktime(tmwhen))
2933 self.streamTag(stream, name, labelDetails, gitCommit, when)
2934 if verbose:
2935 print "p4 label %s mapped to git commit %s" % (name, gitCommit)
2936 else:
2937 if verbose:
2938 print "Label %s has no changelists - possibly deleted?" % name
2939
2940 if not commitFound:
2941 # We can't import this label; don't try again as it will get very
2942 # expensive repeatedly fetching all the files for labels that will
2943 # never be imported. If the label is moved in the future, the
2944 # ignore will need to be removed manually.
2945 system(["git", "config", "--add", "git-p4.ignoredP4Labels", name])
2946
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03002947 def guessProjectName(self):
2948 for p in self.depotPaths:
Simon Hausmann6e5295c2007-06-11 08:50:57 +02002949 if p.endswith("/"):
2950 p = p[:-1]
2951 p = p[p.strip().rfind("/") + 1:]
2952 if not p.endswith("/"):
2953 p += "/"
2954 return p
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03002955
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02002956 def getBranchMapping(self):
Simon Hausmann6555b2c2007-06-17 11:25:34 +02002957 lostAndFoundBranches = set()
2958
Vitor Antunes8ace74c2011-08-19 00:44:04 +01002959 user = gitConfig("git-p4.branchUser")
2960 if len(user) > 0:
2961 command = "branches -u %s" % user
2962 else:
2963 command = "branches"
2964
2965 for info in p4CmdList(command):
Luke Diamand52a48802012-01-19 09:52:25 +00002966 details = p4Cmd(["branch", "-o", info["branch"]])
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02002967 viewIdx = 0
2968 while details.has_key("View%s" % viewIdx):
2969 paths = details["View%s" % viewIdx].split(" ")
2970 viewIdx = viewIdx + 1
2971 # require standard //depot/foo/... //depot/bar/... mapping
2972 if len(paths) != 2 or not paths[0].endswith("/...") or not paths[1].endswith("/..."):
2973 continue
2974 source = paths[0]
2975 destination = paths[1]
Simon Hausmann6509e192007-06-07 09:41:53 +02002976 ## HACK
Tor Arvid Lundd53de8b2011-03-15 13:08:02 +01002977 if p4PathStartsWith(source, self.depotPaths[0]) and p4PathStartsWith(destination, self.depotPaths[0]):
Simon Hausmann6509e192007-06-07 09:41:53 +02002978 source = source[len(self.depotPaths[0]):-4]
2979 destination = destination[len(self.depotPaths[0]):-4]
Simon Hausmann6555b2c2007-06-17 11:25:34 +02002980
Simon Hausmann1a2edf42007-06-17 15:10:24 +02002981 if destination in self.knownBranches:
2982 if not self.silent:
2983 print "p4 branch %s defines a mapping from %s to %s" % (info["branch"], source, destination)
2984 print "but there exists another mapping from %s to %s already!" % (self.knownBranches[destination], destination)
2985 continue
2986
Simon Hausmann6555b2c2007-06-17 11:25:34 +02002987 self.knownBranches[destination] = source
2988
2989 lostAndFoundBranches.discard(destination)
2990
Simon Hausmann29bdbac2007-05-19 10:23:12 +02002991 if source not in self.knownBranches:
Simon Hausmann6555b2c2007-06-17 11:25:34 +02002992 lostAndFoundBranches.add(source)
2993
Vitor Antunes7199cf12011-08-19 00:44:05 +01002994 # Perforce does not strictly require branches to be defined, so we also
2995 # check git config for a branch list.
2996 #
2997 # Example of branch definition in git config file:
2998 # [git-p4]
2999 # branchList=main:branchA
3000 # branchList=main:branchB
3001 # branchList=branchA:branchC
3002 configBranches = gitConfigList("git-p4.branchList")
3003 for branch in configBranches:
3004 if branch:
3005 (source, destination) = branch.split(":")
3006 self.knownBranches[destination] = source
3007
3008 lostAndFoundBranches.discard(destination)
3009
3010 if source not in self.knownBranches:
3011 lostAndFoundBranches.add(source)
3012
Simon Hausmann6555b2c2007-06-17 11:25:34 +02003013
3014 for branch in lostAndFoundBranches:
3015 self.knownBranches[branch] = branch
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003016
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01003017 def getBranchMappingFromGitBranches(self):
3018 branches = p4BranchesInGit(self.importIntoRemotes)
3019 for branch in branches.keys():
3020 if branch == "master":
3021 branch = "main"
3022 else:
3023 branch = branch[len(self.projectName):]
3024 self.knownBranches[branch] = branch
3025
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003026 def updateOptionDict(self, d):
3027 option_keys = {}
3028 if self.keepRepoPath:
3029 option_keys['keepRepoPath'] = 1
3030
3031 d["options"] = ' '.join(sorted(option_keys.keys()))
3032
3033 def readOptions(self, d):
3034 self.keepRepoPath = (d.has_key('options')
3035 and ('keepRepoPath' in d['options']))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003036
Simon Hausmann8134f692007-08-26 16:44:55 +02003037 def gitRefForBranch(self, branch):
3038 if branch == "main":
3039 return self.refPrefix + "master"
3040
3041 if len(branch) <= 0:
3042 return branch
3043
3044 return self.refPrefix + self.projectName + branch
3045
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003046 def gitCommitByP4Change(self, ref, change):
3047 if self.verbose:
3048 print "looking in ref " + ref + " for change %s using bisect..." % change
3049
3050 earliestCommit = ""
3051 latestCommit = parseRevision(ref)
3052
3053 while True:
3054 if self.verbose:
3055 print "trying: earliest %s latest %s" % (earliestCommit, latestCommit)
3056 next = read_pipe("git rev-list --bisect %s %s" % (latestCommit, earliestCommit)).strip()
3057 if len(next) == 0:
3058 if self.verbose:
3059 print "argh"
3060 return ""
3061 log = extractLogMessageFromGitCommit(next)
3062 settings = extractSettingsGitLog(log)
3063 currentChange = int(settings['change'])
3064 if self.verbose:
3065 print "current change %s" % currentChange
3066
3067 if currentChange == change:
3068 if self.verbose:
3069 print "found %s" % next
3070 return next
3071
3072 if currentChange < change:
3073 earliestCommit = "^%s" % next
3074 else:
3075 latestCommit = "%s" % next
3076
3077 return ""
3078
3079 def importNewBranch(self, branch, maxChange):
3080 # make fast-import flush all changes to disk and update the refs using the checkpoint
3081 # command so that we can try to find the branch parent in the git history
3082 self.gitStream.write("checkpoint\n\n");
3083 self.gitStream.flush();
3084 branchPrefix = self.depotPaths[0] + branch + "/"
3085 range = "@1,%s" % maxChange
3086 #print "prefix" + branchPrefix
Lex Spoon96b2d542015-04-20 11:00:20 -04003087 changes = p4ChangesForPaths([branchPrefix], range, self.changes_block_size)
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003088 if len(changes) <= 0:
3089 return False
3090 firstChange = changes[0]
3091 #print "first change in branch: %s" % firstChange
3092 sourceBranch = self.knownBranches[branch]
3093 sourceDepotPath = self.depotPaths[0] + sourceBranch
3094 sourceRef = self.gitRefForBranch(sourceBranch)
3095 #print "source " + sourceBranch
3096
Luke Diamand52a48802012-01-19 09:52:25 +00003097 branchParentChange = int(p4Cmd(["changes", "-m", "1", "%s...@1,%s" % (sourceDepotPath, firstChange)])["change"])
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003098 #print "branch parent: %s" % branchParentChange
3099 gitParent = self.gitCommitByP4Change(sourceRef, branchParentChange)
3100 if len(gitParent) > 0:
3101 self.initialParents[self.gitRefForBranch(branch)] = gitParent
3102 #print "parent git commit: %s" % gitParent
3103
3104 self.importChanges(changes)
3105 return True
3106
Vitor Antunesfed23692012-01-25 23:48:22 +00003107 def searchParent(self, parent, branch, target):
3108 parentFound = False
Pete Wyckoffc7d34882013-01-26 22:11:21 -05003109 for blob in read_pipe_lines(["git", "rev-list", "--reverse",
3110 "--no-merges", parent]):
Vitor Antunesfed23692012-01-25 23:48:22 +00003111 blob = blob.strip()
3112 if len(read_pipe(["git", "diff-tree", blob, target])) == 0:
3113 parentFound = True
3114 if self.verbose:
3115 print "Found parent of %s in commit %s" % (branch, blob)
3116 break
3117 if parentFound:
3118 return blob
3119 else:
3120 return None
3121
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003122 def importChanges(self, changes):
3123 cnt = 1
3124 for change in changes:
Pete Wyckoff18fa13d2012-11-23 17:35:34 -05003125 description = p4_describe(change)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003126 self.updateOptionDict(description)
3127
3128 if not self.silent:
3129 sys.stdout.write("\rImporting revision %s (%s%%)" % (change, cnt * 100 / len(changes)))
3130 sys.stdout.flush()
3131 cnt = cnt + 1
3132
3133 try:
3134 if self.detectBranches:
3135 branches = self.splitFilesIntoBranches(description)
3136 for branch in branches.keys():
3137 ## HACK --hwn
3138 branchPrefix = self.depotPaths[0] + branch + "/"
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003139 self.branchPrefixes = [ branchPrefix ]
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003140
3141 parent = ""
3142
3143 filesForCommit = branches[branch]
3144
3145 if self.verbose:
3146 print "branch is %s" % branch
3147
3148 self.updatedBranches.add(branch)
3149
3150 if branch not in self.createdBranches:
3151 self.createdBranches.add(branch)
3152 parent = self.knownBranches[branch]
3153 if parent == branch:
3154 parent = ""
Simon Hausmann1ca3d712007-08-26 17:36:55 +02003155 else:
3156 fullBranch = self.projectName + branch
3157 if fullBranch not in self.p4BranchesInGit:
3158 if not self.silent:
3159 print("\n Importing new branch %s" % fullBranch);
3160 if self.importNewBranch(branch, change - 1):
3161 parent = ""
3162 self.p4BranchesInGit.append(fullBranch)
3163 if not self.silent:
3164 print("\n Resuming with change %s" % change);
3165
3166 if self.verbose:
3167 print "parent determined through known branches: %s" % parent
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003168
Simon Hausmann8134f692007-08-26 16:44:55 +02003169 branch = self.gitRefForBranch(branch)
3170 parent = self.gitRefForBranch(parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003171
3172 if self.verbose:
3173 print "looking for initial parent for %s; current parent is %s" % (branch, parent)
3174
3175 if len(parent) == 0 and branch in self.initialParents:
3176 parent = self.initialParents[branch]
3177 del self.initialParents[branch]
3178
Vitor Antunesfed23692012-01-25 23:48:22 +00003179 blob = None
3180 if len(parent) > 0:
Pete Wyckoff4f9273d2013-01-26 22:11:04 -05003181 tempBranch = "%s/%d" % (self.tempBranchLocation, change)
Vitor Antunesfed23692012-01-25 23:48:22 +00003182 if self.verbose:
3183 print "Creating temporary branch: " + tempBranch
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003184 self.commit(description, filesForCommit, tempBranch)
Vitor Antunesfed23692012-01-25 23:48:22 +00003185 self.tempBranches.append(tempBranch)
3186 self.checkpoint()
3187 blob = self.searchParent(parent, branch, tempBranch)
3188 if blob:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003189 self.commit(description, filesForCommit, branch, blob)
Vitor Antunesfed23692012-01-25 23:48:22 +00003190 else:
3191 if self.verbose:
3192 print "Parent of %s not found. Committing into head of %s" % (branch, parent)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003193 self.commit(description, filesForCommit, branch, parent)
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003194 else:
3195 files = self.extractFilesFromCommit(description)
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003196 self.commit(description, files, self.branch,
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003197 self.initialParent)
Pete Wyckoff47497842013-01-14 19:47:04 -05003198 # only needed once, to connect to the previous commit
Simon Hausmanne87f37a2007-08-26 16:00:52 +02003199 self.initialParent = ""
3200 except IOError:
3201 print self.gitError.read()
3202 sys.exit(1)
3203
Simon Hausmannc208a242007-08-26 16:07:18 +02003204 def importHeadRevision(self, revision):
3205 print "Doing initial import of %s from revision %s into %s" % (' '.join(self.depotPaths), revision, self.branch)
3206
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003207 details = {}
3208 details["user"] = "git perforce import user"
Pete Wyckoff1494fcb2011-02-19 08:17:56 -05003209 details["desc"] = ("Initial import of %s from the state at revision %s\n"
Simon Hausmannc208a242007-08-26 16:07:18 +02003210 % (' '.join(self.depotPaths), revision))
3211 details["change"] = revision
3212 newestRevision = 0
3213
3214 fileCnt = 0
Luke Diamand6de040d2011-10-16 10:47:52 -04003215 fileArgs = ["%s...%s" % (p,revision) for p in self.depotPaths]
3216
3217 for info in p4CmdList(["files"] + fileArgs):
Simon Hausmannc208a242007-08-26 16:07:18 +02003218
Pete Wyckoff68b28592011-02-19 08:17:55 -05003219 if 'code' in info and info['code'] == 'error':
Simon Hausmannc208a242007-08-26 16:07:18 +02003220 sys.stderr.write("p4 returned an error: %s\n"
3221 % info['data'])
Pete Wyckoffd88e7072011-02-19 08:17:58 -05003222 if info['data'].find("must refer to client") >= 0:
3223 sys.stderr.write("This particular p4 error is misleading.\n")
3224 sys.stderr.write("Perhaps the depot path was misspelled.\n");
3225 sys.stderr.write("Depot path: %s\n" % " ".join(self.depotPaths))
Simon Hausmannc208a242007-08-26 16:07:18 +02003226 sys.exit(1)
Pete Wyckoff68b28592011-02-19 08:17:55 -05003227 if 'p4ExitCode' in info:
3228 sys.stderr.write("p4 exitcode: %s\n" % info['p4ExitCode'])
Simon Hausmannc208a242007-08-26 16:07:18 +02003229 sys.exit(1)
3230
3231
3232 change = int(info["change"])
3233 if change > newestRevision:
3234 newestRevision = change
3235
Pete Wyckoff56c09342011-02-19 08:17:57 -05003236 if info["action"] in self.delete_actions:
Simon Hausmannc208a242007-08-26 16:07:18 +02003237 # don't increase the file cnt, otherwise details["depotFile123"] will have gaps!
3238 #fileCnt = fileCnt + 1
3239 continue
3240
3241 for prop in ["depotFile", "rev", "action", "type" ]:
3242 details["%s%s" % (prop, fileCnt)] = info[prop]
3243
3244 fileCnt = fileCnt + 1
3245
3246 details["change"] = newestRevision
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003247
Pete Wyckoff9dcb9f22012-04-08 20:18:01 -04003248 # Use time from top-most change so that all git p4 clones of
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003249 # the same p4 repo have the same commit SHA1s.
Pete Wyckoff18fa13d2012-11-23 17:35:34 -05003250 res = p4_describe(newestRevision)
3251 details["time"] = res["time"]
Pete Wyckoff4e2e6ce2011-07-31 09:45:55 -04003252
Simon Hausmannc208a242007-08-26 16:07:18 +02003253 self.updateOptionDict(details)
3254 try:
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003255 self.commit(details, self.extractFilesFromCommit(details), self.branch)
Simon Hausmannc208a242007-08-26 16:07:18 +02003256 except IOError:
3257 print "IO error with git fast-import. Is your git version recent enough?"
3258 print self.gitError.read()
3259
3260
Simon Hausmannb9847332007-03-20 20:54:23 +01003261 def run(self, args):
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003262 self.depotPaths = []
Simon Hausmann179caeb2007-03-22 22:17:42 +01003263 self.changeRange = ""
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003264 self.previousDepotPaths = []
Pete Wyckoff991a2de2013-01-14 19:46:56 -05003265 self.hasOrigin = False
Han-Wen Nienhuysce6f33c2007-05-23 16:46:29 -03003266
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003267 # map from branch depot path to parent branch
3268 self.knownBranches = {}
3269 self.initialParents = {}
Simon Hausmann179caeb2007-03-22 22:17:42 +01003270
Simon Hausmanna028a982007-05-23 00:03:08 +02003271 if self.importIntoRemotes:
3272 self.refPrefix = "refs/remotes/p4/"
3273 else:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02003274 self.refPrefix = "refs/heads/p4/"
Simon Hausmanna028a982007-05-23 00:03:08 +02003275
Pete Wyckoff991a2de2013-01-14 19:46:56 -05003276 if self.syncWithOrigin:
3277 self.hasOrigin = originP4BranchesExist()
3278 if self.hasOrigin:
3279 if not self.silent:
3280 print 'Syncing with origin first, using "git fetch origin"'
3281 system("git fetch origin")
Simon Hausmann10f880f2007-05-24 22:28:28 +02003282
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05003283 branch_arg_given = bool(self.branch)
Simon Hausmann569d1bd2007-03-22 21:34:16 +01003284 if len(self.branch) == 0:
Marius Storm-Olsendb775552007-06-07 15:13:59 +02003285 self.branch = self.refPrefix + "master"
Simon Hausmanna028a982007-05-23 00:03:08 +02003286 if gitBranchExists("refs/heads/p4") and self.importIntoRemotes:
Simon Hausmann48df6fd2007-05-17 21:18:53 +02003287 system("git update-ref %s refs/heads/p4" % self.branch)
Pete Wyckoff55d12432013-01-14 19:46:59 -05003288 system("git branch -D p4")
Simon Hausmann179caeb2007-03-22 22:17:42 +01003289
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05003290 # accept either the command-line option, or the configuration variable
3291 if self.useClientSpec:
3292 # will use this after clone to set the variable
3293 self.useClientSpec_from_options = True
3294 else:
Pete Wyckoff0d609032013-01-26 22:11:24 -05003295 if gitConfigBool("git-p4.useclientspec"):
Pete Wyckoff09fca772011-12-24 21:07:39 -05003296 self.useClientSpec = True
3297 if self.useClientSpec:
Pete Wyckoff543987b2012-02-25 20:06:25 -05003298 self.clientSpecDirs = getClientSpec()
Tor Arvid Lund3a70cdf2008-02-18 15:22:08 +01003299
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003300 # TODO: should always look at previous commits,
3301 # merge with previous imports, if possible.
3302 if args == []:
Simon Hausmannd414c742007-05-25 11:36:42 +02003303 if self.hasOrigin:
Simon Hausmann5ca44612007-08-24 17:44:16 +02003304 createOrUpdateBranchesFromOrigin(self.refPrefix, self.silent)
Pete Wyckoff3b650fc2013-01-14 19:46:58 -05003305
3306 # branches holds mapping from branch name to sha1
3307 branches = p4BranchesInGit(self.importIntoRemotes)
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05003308
3309 # restrict to just this one, disabling detect-branches
3310 if branch_arg_given:
3311 short = self.branch.split("/")[-1]
3312 if short in branches:
3313 self.p4BranchesInGit = [ short ]
3314 else:
3315 self.p4BranchesInGit = branches.keys()
Simon Hausmannabcd7902007-05-24 22:25:36 +02003316
3317 if len(self.p4BranchesInGit) > 1:
3318 if not self.silent:
3319 print "Importing from/into multiple branches"
3320 self.detectBranches = True
Pete Wyckoff8c9e8b62013-01-14 19:47:06 -05003321 for branch in branches.keys():
3322 self.initialParents[self.refPrefix + branch] = \
3323 branches[branch]
Simon Hausmann967f72e2007-03-23 09:30:41 +01003324
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003325 if self.verbose:
3326 print "branches: %s" % self.p4BranchesInGit
3327
3328 p4Change = 0
3329 for branch in self.p4BranchesInGit:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003330 logMsg = extractLogMessageFromGitCommit(self.refPrefix + branch)
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003331
3332 settings = extractSettingsGitLog(logMsg)
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003333
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003334 self.readOptions(settings)
3335 if (settings.has_key('depot-paths')
3336 and settings.has_key ('change')):
3337 change = int(settings['change']) + 1
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003338 p4Change = max(p4Change, change)
3339
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003340 depotPaths = sorted(settings['depot-paths'])
3341 if self.previousDepotPaths == []:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003342 self.previousDepotPaths = depotPaths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003343 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003344 paths = []
3345 for (prev, cur) in zip(self.previousDepotPaths, depotPaths):
Vitor Antunes04d277b2011-08-19 00:44:03 +01003346 prev_list = prev.split("/")
3347 cur_list = cur.split("/")
3348 for i in range(0, min(len(cur_list), len(prev_list))):
3349 if cur_list[i] <> prev_list[i]:
Simon Hausmann583e1702007-06-07 09:37:13 +02003350 i = i - 1
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003351 break
3352
Vitor Antunes04d277b2011-08-19 00:44:03 +01003353 paths.append ("/".join(cur_list[:i + 1]))
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003354
3355 self.previousDepotPaths = paths
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003356
3357 if p4Change > 0:
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003358 self.depotPaths = sorted(self.previousDepotPaths)
Simon Hausmannd5904672007-05-19 11:07:32 +02003359 self.changeRange = "@%s,#head" % p4Change
Simon Hausmann341dc1c2007-05-21 00:39:16 +02003360 if not self.silent and not self.detectBranches:
Simon Hausmann967f72e2007-03-23 09:30:41 +01003361 print "Performing incremental import into %s git branch" % self.branch
Simon Hausmann569d1bd2007-03-22 21:34:16 +01003362
Pete Wyckoff40d69ac2013-01-14 19:47:03 -05003363 # accept multiple ref name abbreviations:
3364 # refs/foo/bar/branch -> use it exactly
3365 # p4/branch -> prepend refs/remotes/ or refs/heads/
3366 # branch -> prepend refs/remotes/p4/ or refs/heads/p4/
Simon Hausmannf9162f62007-05-17 09:02:45 +02003367 if not self.branch.startswith("refs/"):
Pete Wyckoff40d69ac2013-01-14 19:47:03 -05003368 if self.importIntoRemotes:
3369 prepend = "refs/remotes/"
3370 else:
3371 prepend = "refs/heads/"
3372 if not self.branch.startswith("p4/"):
3373 prepend += "p4/"
3374 self.branch = prepend + self.branch
Simon Hausmann179caeb2007-03-22 22:17:42 +01003375
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003376 if len(args) == 0 and self.depotPaths:
Simon Hausmannb9847332007-03-20 20:54:23 +01003377 if not self.silent:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003378 print "Depot paths: %s" % ' '.join(self.depotPaths)
Simon Hausmannb9847332007-03-20 20:54:23 +01003379 else:
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003380 if self.depotPaths and self.depotPaths != args:
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003381 print ("previous import used depot path %s and now %s was specified. "
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003382 "This doesn't work!" % (' '.join (self.depotPaths),
3383 ' '.join (args)))
Simon Hausmannb9847332007-03-20 20:54:23 +01003384 sys.exit(1)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003385
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003386 self.depotPaths = sorted(args)
Simon Hausmannb9847332007-03-20 20:54:23 +01003387
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003388 revision = ""
Simon Hausmannb9847332007-03-20 20:54:23 +01003389 self.users = {}
Simon Hausmannb9847332007-03-20 20:54:23 +01003390
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05003391 # Make sure no revision specifiers are used when --changesfile
3392 # is specified.
3393 bad_changesfile = False
3394 if len(self.changesFile) > 0:
3395 for p in self.depotPaths:
3396 if p.find("@") >= 0 or p.find("#") >= 0:
3397 bad_changesfile = True
3398 break
3399 if bad_changesfile:
3400 die("Option --changesfile is incompatible with revision specifiers")
3401
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003402 newPaths = []
3403 for p in self.depotPaths:
3404 if p.find("@") != -1:
3405 atIdx = p.index("@")
3406 self.changeRange = p[atIdx:]
3407 if self.changeRange == "@all":
3408 self.changeRange = ""
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003409 elif ',' not in self.changeRange:
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003410 revision = self.changeRange
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003411 self.changeRange = ""
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07003412 p = p[:atIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003413 elif p.find("#") != -1:
3414 hashIdx = p.index("#")
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003415 revision = p[hashIdx:]
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07003416 p = p[:hashIdx]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003417 elif self.previousDepotPaths == []:
Pete Wyckoff58c8bc72011-12-24 21:07:35 -05003418 # pay attention to changesfile, if given, else import
3419 # the entire p4 tree at the head revision
3420 if len(self.changesFile) == 0:
3421 revision = "#head"
Simon Hausmannb9847332007-03-20 20:54:23 +01003422
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003423 p = re.sub ("\.\.\.$", "", p)
3424 if not p.endswith("/"):
3425 p += "/"
3426
3427 newPaths.append(p)
3428
3429 self.depotPaths = newPaths
3430
Pete Wyckoffe63231e2012-08-11 12:55:02 -04003431 # --detect-branches may change this for each branch
3432 self.branchPrefixes = self.depotPaths
3433
Simon Hausmannb607e712007-05-20 10:55:54 +02003434 self.loadUserMapFromCache()
Simon Hausmanncb53e1f2007-04-08 00:12:02 +02003435 self.labels = {}
3436 if self.detectLabels:
3437 self.getLabels();
Simon Hausmannb9847332007-03-20 20:54:23 +01003438
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003439 if self.detectBranches:
Simon Hausmanndf450922007-06-08 08:49:22 +02003440 ## FIXME - what's a P4 projectName ?
3441 self.projectName = self.guessProjectName()
3442
Simon Hausmann38f9f5e2007-11-15 10:38:45 +01003443 if self.hasOrigin:
3444 self.getBranchMappingFromGitBranches()
3445 else:
3446 self.getBranchMapping()
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003447 if self.verbose:
3448 print "p4-git branches: %s" % self.p4BranchesInGit
3449 print "initial parents: %s" % self.initialParents
3450 for b in self.p4BranchesInGit:
3451 if b != "master":
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003452
3453 ## FIXME
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003454 b = b[len(self.projectName):]
3455 self.createdBranches.add(b)
Simon Hausmann4b97ffb2007-05-18 21:45:23 +02003456
Simon Hausmannf291b4e2007-04-14 11:21:50 +02003457 self.tz = "%+03d%02d" % (- time.timezone / 3600, ((- time.timezone % 3600) / 60))
Simon Hausmannb9847332007-03-20 20:54:23 +01003458
Pete Wyckoff78189be2012-11-23 17:35:36 -05003459 self.importProcess = subprocess.Popen(["git", "fast-import"],
3460 stdin=subprocess.PIPE,
3461 stdout=subprocess.PIPE,
3462 stderr=subprocess.PIPE);
3463 self.gitOutput = self.importProcess.stdout
3464 self.gitStream = self.importProcess.stdin
3465 self.gitError = self.importProcess.stderr
Simon Hausmannb9847332007-03-20 20:54:23 +01003466
Simon Hausmann1c49fc12007-08-26 16:04:34 +02003467 if revision:
Simon Hausmannc208a242007-08-26 16:07:18 +02003468 self.importHeadRevision(revision)
Simon Hausmannb9847332007-03-20 20:54:23 +01003469 else:
3470 changes = []
3471
Simon Hausmann0828ab12007-03-20 20:59:30 +01003472 if len(self.changesFile) > 0:
Simon Hausmannb9847332007-03-20 20:54:23 +01003473 output = open(self.changesFile).readlines()
Reilly Grant1d7367d2009-09-10 00:02:38 -07003474 changeSet = set()
Simon Hausmannb9847332007-03-20 20:54:23 +01003475 for line in output:
3476 changeSet.add(int(line))
3477
3478 for change in changeSet:
3479 changes.append(change)
3480
3481 changes.sort()
3482 else:
Pete Wyckoff9dcb9f22012-04-08 20:18:01 -04003483 # catch "git p4 sync" with no new branches, in a repo that
3484 # does not have any existing p4 branches
Pete Wyckoff5a8e84c2013-01-14 19:47:05 -05003485 if len(args) == 0:
3486 if not self.p4BranchesInGit:
3487 die("No remote p4 branches. Perhaps you never did \"git p4 clone\" in here.")
3488
3489 # The default branch is master, unless --branch is used to
3490 # specify something else. Make sure it exists, or complain
3491 # nicely about how to use --branch.
3492 if not self.detectBranches:
3493 if not branch_exists(self.branch):
3494 if branch_arg_given:
3495 die("Error: branch %s does not exist." % self.branch)
3496 else:
3497 die("Error: no branch %s; perhaps specify one with --branch." %
3498 self.branch)
3499
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003500 if self.verbose:
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003501 print "Getting p4 changes for %s...%s" % (', '.join(self.depotPaths),
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003502 self.changeRange)
Lex Spoon96b2d542015-04-20 11:00:20 -04003503 changes = p4ChangesForPaths(self.depotPaths, self.changeRange, self.changes_block_size)
Simon Hausmannb9847332007-03-20 20:54:23 +01003504
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02003505 if len(self.maxChanges) > 0:
Han-Wen Nienhuys7fcff9d2007-07-23 15:56:37 -07003506 changes = changes[:min(int(self.maxChanges), len(changes))]
Simon Hausmann01a9c9c2007-05-23 00:07:35 +02003507
Simon Hausmannb9847332007-03-20 20:54:23 +01003508 if len(changes) == 0:
Simon Hausmann0828ab12007-03-20 20:59:30 +01003509 if not self.silent:
Simon Hausmann341dc1c2007-05-21 00:39:16 +02003510 print "No changes to import!"
Luke Diamand06804c72012-04-11 17:21:24 +02003511 else:
3512 if not self.silent and not self.detectBranches:
3513 print "Import destination: %s" % self.branch
Simon Hausmannb9847332007-03-20 20:54:23 +01003514
Luke Diamand06804c72012-04-11 17:21:24 +02003515 self.updatedBranches = set()
Simon Hausmanna9d1a272007-06-11 23:28:03 +02003516
Pete Wyckoff47497842013-01-14 19:47:04 -05003517 if not self.detectBranches:
3518 if args:
3519 # start a new branch
3520 self.initialParent = ""
3521 else:
3522 # build on a previous revision
3523 self.initialParent = parseRevision(self.branch)
3524
Luke Diamand06804c72012-04-11 17:21:24 +02003525 self.importChanges(changes)
Simon Hausmann341dc1c2007-05-21 00:39:16 +02003526
Luke Diamand06804c72012-04-11 17:21:24 +02003527 if not self.silent:
3528 print ""
3529 if len(self.updatedBranches) > 0:
3530 sys.stdout.write("Updated branches: ")
3531 for b in self.updatedBranches:
3532 sys.stdout.write("%s " % b)
3533 sys.stdout.write("\n")
Simon Hausmannb9847332007-03-20 20:54:23 +01003534
Pete Wyckoff0d609032013-01-26 22:11:24 -05003535 if gitConfigBool("git-p4.importLabels"):
Luke Diamand06dcd152012-05-11 07:25:18 +01003536 self.importLabels = True
Luke Diamand06804c72012-04-11 17:21:24 +02003537
3538 if self.importLabels:
3539 p4Labels = getP4Labels(self.depotPaths)
3540 gitTags = getGitTags()
3541
3542 missingP4Labels = p4Labels - gitTags
3543 self.importP4Labels(self.gitStream, missingP4Labels)
Simon Hausmannb9847332007-03-20 20:54:23 +01003544
Simon Hausmannb9847332007-03-20 20:54:23 +01003545 self.gitStream.close()
Pete Wyckoff78189be2012-11-23 17:35:36 -05003546 if self.importProcess.wait() != 0:
Simon Hausmann29bdbac2007-05-19 10:23:12 +02003547 die("fast-import failed: %s" % self.gitError.read())
Simon Hausmannb9847332007-03-20 20:54:23 +01003548 self.gitOutput.close()
3549 self.gitError.close()
3550
Vitor Antunesfed23692012-01-25 23:48:22 +00003551 # Cleanup temporary branches created during import
3552 if self.tempBranches != []:
3553 for branch in self.tempBranches:
3554 read_pipe("git update-ref -d %s" % branch)
3555 os.rmdir(os.path.join(os.environ.get("GIT_DIR", ".git"), self.tempBranchLocation))
3556
Pete Wyckoff55d12432013-01-14 19:46:59 -05003557 # Create a symbolic ref p4/HEAD pointing to p4/<branch> to allow
3558 # a convenient shortcut refname "p4".
3559 if self.importIntoRemotes:
3560 head_ref = self.refPrefix + "HEAD"
3561 if not gitBranchExists(head_ref) and gitBranchExists(self.branch):
3562 system(["git", "symbolic-ref", head_ref, self.branch])
3563
Simon Hausmannb9847332007-03-20 20:54:23 +01003564 return True
3565
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003566class P4Rebase(Command):
3567 def __init__(self):
3568 Command.__init__(self)
Luke Diamand06804c72012-04-11 17:21:24 +02003569 self.options = [
3570 optparse.make_option("--import-labels", dest="importLabels", action="store_true"),
Luke Diamand06804c72012-04-11 17:21:24 +02003571 ]
Luke Diamand06804c72012-04-11 17:21:24 +02003572 self.importLabels = False
Han-Wen Nienhuyscebdf5a2007-05-23 16:53:11 -03003573 self.description = ("Fetches the latest revision from perforce and "
3574 + "rebases the current work (branch) against it")
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003575
3576 def run(self, args):
3577 sync = P4Sync()
Luke Diamand06804c72012-04-11 17:21:24 +02003578 sync.importLabels = self.importLabels
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003579 sync.run([])
Simon Hausmannd7e38682007-06-12 14:34:46 +02003580
Simon Hausmann14594f42007-08-22 09:07:15 +02003581 return self.rebase()
3582
3583 def rebase(self):
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01003584 if os.system("git update-index --refresh") != 0:
3585 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.");
3586 if len(read_pipe("git diff-index HEAD --")) > 0:
Veres Lajosf7e604e2013-06-19 07:37:24 +02003587 die("You have uncommitted changes. Please commit them before rebasing or stash them away with git stash.");
Simon Hausmann36ee4ee2008-01-07 14:21:45 +01003588
Simon Hausmannd7e38682007-06-12 14:34:46 +02003589 [upstream, settings] = findUpstreamBranchPoint()
3590 if len(upstream) == 0:
3591 die("Cannot find upstream branchpoint for rebase")
3592
3593 # the branchpoint may be p4/foo~3, so strip off the parent
3594 upstream = re.sub("~[0-9]+$", "", upstream)
3595
3596 print "Rebasing the current branch onto %s" % upstream
Han-Wen Nienhuysb25b2062007-05-23 18:49:35 -03003597 oldHead = read_pipe("git rev-parse HEAD").strip()
Simon Hausmannd7e38682007-06-12 14:34:46 +02003598 system("git rebase %s" % upstream)
Vlad Dogaru4e49d952014-04-07 16:19:11 +03003599 system("git diff-tree --stat --summary -M %s HEAD --" % oldHead)
Simon Hausmann01ce1fe2007-04-07 23:46:50 +02003600 return True
3601
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003602class P4Clone(P4Sync):
3603 def __init__(self):
3604 P4Sync.__init__(self)
3605 self.description = "Creates a new git repository and imports from Perforce into it"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003606 self.usage = "usage: %prog [options] //depot/path[@revRange]"
Tommy Thorn354081d2008-02-03 10:38:51 -08003607 self.options += [
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003608 optparse.make_option("--destination", dest="cloneDestination",
3609 action='store', default=None,
Tommy Thorn354081d2008-02-03 10:38:51 -08003610 help="where to leave result of the clone"),
Pete Wyckoff38200072011-02-19 08:18:01 -05003611 optparse.make_option("--bare", dest="cloneBare",
3612 action="store_true", default=False),
Tommy Thorn354081d2008-02-03 10:38:51 -08003613 ]
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003614 self.cloneDestination = None
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003615 self.needsGit = False
Pete Wyckoff38200072011-02-19 08:18:01 -05003616 self.cloneBare = False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003617
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003618 def defaultDestination(self, args):
3619 ## TODO: use common prefix of args?
3620 depotPath = args[0]
3621 depotDir = re.sub("(@[^@]*)$", "", depotPath)
3622 depotDir = re.sub("(#[^#]*)$", "", depotDir)
Toby Allsopp053d9e42008-02-05 09:41:43 +13003623 depotDir = re.sub(r"\.\.\.$", "", depotDir)
Han-Wen Nienhuys6a49f8e2007-05-23 18:49:35 -03003624 depotDir = re.sub(r"/$", "", depotDir)
3625 return os.path.split(depotDir)[1]
3626
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003627 def run(self, args):
3628 if len(args) < 1:
3629 return False
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003630
3631 if self.keepRepoPath and not self.cloneDestination:
3632 sys.stderr.write("Must specify destination for --keep-path\n")
3633 sys.exit(1)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003634
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003635 depotPaths = args
Simon Hausmann5e100b52007-06-07 21:12:25 +02003636
3637 if not self.cloneDestination and len(depotPaths) > 1:
3638 self.cloneDestination = depotPaths[-1]
3639 depotPaths = depotPaths[:-1]
3640
Tommy Thorn354081d2008-02-03 10:38:51 -08003641 self.cloneExclude = ["/"+p for p in self.cloneExclude]
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003642 for p in depotPaths:
3643 if not p.startswith("//"):
Pete Wyckoff0f487d32013-01-26 22:11:06 -05003644 sys.stderr.write('Depot paths must start with "//": %s\n' % p)
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003645 return False
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003646
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003647 if not self.cloneDestination:
Marius Storm-Olsen98ad4fa2007-06-07 15:08:33 +02003648 self.cloneDestination = self.defaultDestination(args)
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003649
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003650 print "Importing from %s into %s" % (', '.join(depotPaths), self.cloneDestination)
Pete Wyckoff38200072011-02-19 08:18:01 -05003651
Kevin Greenc3bf3f12007-06-11 16:48:07 -04003652 if not os.path.exists(self.cloneDestination):
3653 os.makedirs(self.cloneDestination)
Robert Blum053fd0c2008-08-01 12:50:03 -07003654 chdir(self.cloneDestination)
Pete Wyckoff38200072011-02-19 08:18:01 -05003655
3656 init_cmd = [ "git", "init" ]
3657 if self.cloneBare:
3658 init_cmd.append("--bare")
Brandon Caseya235e852013-01-26 11:14:33 -08003659 retcode = subprocess.call(init_cmd)
3660 if retcode:
3661 raise CalledProcessError(retcode, init_cmd)
Pete Wyckoff38200072011-02-19 08:18:01 -05003662
Han-Wen Nienhuys6326aa52007-05-23 18:49:35 -03003663 if not P4Sync.run(self, depotPaths):
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003664 return False
Pete Wyckoffc5959562013-01-14 19:47:01 -05003665
3666 # create a master branch and check out a work tree
3667 if gitBranchExists(self.branch):
3668 system([ "git", "branch", "master", self.branch ])
3669 if not self.cloneBare:
3670 system([ "git", "checkout", "-f" ])
3671 else:
3672 print 'Not checking out any branch, use ' \
3673 '"git checkout -q -b master <branch>"'
Han-Wen Nienhuys86dff6b2007-05-23 18:49:35 -03003674
Pete Wyckoffa93d33e2012-02-25 20:06:24 -05003675 # auto-set this variable if invoked with --use-client-spec
3676 if self.useClientSpec_from_options:
3677 system("git config --bool git-p4.useclientspec true")
3678
Simon Hausmannf9a3a4f2007-04-08 10:08:26 +02003679 return True
3680
Simon Hausmann09d89de2007-06-20 23:10:28 +02003681class P4Branches(Command):
3682 def __init__(self):
3683 Command.__init__(self)
3684 self.options = [ ]
3685 self.description = ("Shows the git branches that hold imports and their "
3686 + "corresponding perforce depot paths")
3687 self.verbose = False
3688
3689 def run(self, args):
Simon Hausmann5ca44612007-08-24 17:44:16 +02003690 if originP4BranchesExist():
3691 createOrUpdateBranchesFromOrigin()
3692
Simon Hausmann09d89de2007-06-20 23:10:28 +02003693 cmdline = "git rev-parse --symbolic "
3694 cmdline += " --remotes"
3695
3696 for line in read_pipe_lines(cmdline):
3697 line = line.strip()
3698
3699 if not line.startswith('p4/') or line == "p4/HEAD":
3700 continue
3701 branch = line
3702
3703 log = extractLogMessageFromGitCommit("refs/remotes/%s" % branch)
3704 settings = extractSettingsGitLog(log)
3705
3706 print "%s <= %s (%s)" % (branch, ",".join(settings["depot-paths"]), settings["change"])
3707 return True
3708
Simon Hausmannb9847332007-03-20 20:54:23 +01003709class HelpFormatter(optparse.IndentedHelpFormatter):
3710 def __init__(self):
3711 optparse.IndentedHelpFormatter.__init__(self)
3712
3713 def format_description(self, description):
3714 if description:
3715 return description + "\n"
3716 else:
3717 return ""
Simon Hausmann4f5cf762007-03-19 22:25:17 +01003718
Simon Hausmann86949ee2007-03-19 20:59:12 +01003719def printUsage(commands):
3720 print "usage: %s <command> [options]" % sys.argv[0]
3721 print ""
3722 print "valid commands: %s" % ", ".join(commands)
3723 print ""
3724 print "Try %s <command> --help for command specific help." % sys.argv[0]
3725 print ""
3726
3727commands = {
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003728 "debug" : P4Debug,
3729 "submit" : P4Submit,
Marius Storm-Olsena9834f52007-10-09 16:16:09 +02003730 "commit" : P4Submit,
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003731 "sync" : P4Sync,
3732 "rebase" : P4Rebase,
3733 "clone" : P4Clone,
Simon Hausmann09d89de2007-06-20 23:10:28 +02003734 "rollback" : P4RollBack,
3735 "branches" : P4Branches
Simon Hausmann86949ee2007-03-19 20:59:12 +01003736}
3737
Simon Hausmann86949ee2007-03-19 20:59:12 +01003738
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003739def main():
3740 if len(sys.argv[1:]) == 0:
3741 printUsage(commands.keys())
3742 sys.exit(2)
Simon Hausmann86949ee2007-03-19 20:59:12 +01003743
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003744 cmdName = sys.argv[1]
3745 try:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003746 klass = commands[cmdName]
3747 cmd = klass()
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003748 except KeyError:
3749 print "unknown command %s" % cmdName
3750 print ""
3751 printUsage(commands.keys())
3752 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01003753
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003754 options = cmd.options
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003755 cmd.gitdir = os.environ.get("GIT_DIR", None)
Simon Hausmann86949ee2007-03-19 20:59:12 +01003756
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003757 args = sys.argv[2:]
Simon Hausmanne20a9e52007-03-26 00:13:51 +02003758
Pete Wyckoffb0ccc802012-09-09 16:16:10 -04003759 options.append(optparse.make_option("--verbose", "-v", dest="verbose", action="store_true"))
Luke Diamand6a10b6a2012-04-24 09:33:23 +01003760 if cmd.needsGit:
3761 options.append(optparse.make_option("--git-dir", dest="gitdir"))
Simon Hausmanne20a9e52007-03-26 00:13:51 +02003762
Luke Diamand6a10b6a2012-04-24 09:33:23 +01003763 parser = optparse.OptionParser(cmd.usage.replace("%prog", "%prog " + cmdName),
3764 options,
3765 description = cmd.description,
3766 formatter = HelpFormatter())
Simon Hausmann86949ee2007-03-19 20:59:12 +01003767
Luke Diamand6a10b6a2012-04-24 09:33:23 +01003768 (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003769 global verbose
3770 verbose = cmd.verbose
3771 if cmd.needsGit:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003772 if cmd.gitdir == None:
3773 cmd.gitdir = os.path.abspath(".git")
3774 if not isValidGitDir(cmd.gitdir):
Luke Diamand378f7be2016-12-13 21:51:28 +00003775 # "rev-parse --git-dir" without arguments will try $PWD/.git
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003776 cmd.gitdir = read_pipe("git rev-parse --git-dir").strip()
3777 if os.path.exists(cmd.gitdir):
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003778 cdup = read_pipe("git rev-parse --show-cdup").strip()
3779 if len(cdup) > 0:
Robert Blum053fd0c2008-08-01 12:50:03 -07003780 chdir(cdup);
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003781
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003782 if not isValidGitDir(cmd.gitdir):
3783 if isValidGitDir(cmd.gitdir + "/.git"):
3784 cmd.gitdir += "/.git"
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003785 else:
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003786 die("fatal: cannot locate git repository at %s" % cmd.gitdir)
Simon Hausmann8910ac02007-03-26 08:18:55 +02003787
Luke Diamand378f7be2016-12-13 21:51:28 +00003788 # so git commands invoked from the P4 workspace will succeed
Han-Wen Nienhuysb86f7372007-05-23 18:49:35 -03003789 os.environ["GIT_DIR"] = cmd.gitdir
Simon Hausmann4f5cf762007-03-19 22:25:17 +01003790
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003791 if not cmd.run(args):
3792 parser.print_help()
Pete Wyckoff09fca772011-12-24 21:07:39 -05003793 sys.exit(2)
Simon Hausmann4f5cf762007-03-19 22:25:17 +01003794
Han-Wen Nienhuysbb6e09b2007-05-23 18:49:35 -03003795
3796if __name__ == '__main__':
3797 main()