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