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