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