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