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