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