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