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