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