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