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