git p4: implement view spec wildcards with "p4 where"

"git p4" does not support many of the view wildcards, such as * and %%n. It only knows the common ... mapping, and exclusions. Redo the entire wildcard code around the idea of directly querying the p4 server for the mapping. For each commit, invoke "p4 where" with committed file paths as args and use the client mapping to decide where the file goes in git. This simplifies a lot of code, and adds support for all wildcards supported by p4. Downside is that there is probably a 20%-ish slowdown with this approach. [pw: redo code and tests] Signed-off-by: Kazuki Saitoh <ksaitoh560@gmail.com> Signed-off-by: Pete Wyckoff <pw@padd.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kazuki Saitoh committed Aug 30, 2013 at 19:02 UTC 9d57c4a6977997aa8150b333fbe2b88988e392d3
1 file changed +59 -164
git-p4.py
+59 -164
@@ -780,11 +780,14 @@ def getClientSpec():
780 # dictionary of all client parameters
781 entry = specList[0]
782
783 + # the //client/ name
784 + client_name = entry["Client"]
785 +
786 # just the keys that start with "View"
787 view_keys = [ k for k in entry.keys() if k.startswith("View") ]
788
789 # hold this new View
787 - view = View()
790 + view = View(client_name)
791
792 # append the lines, in order, to the view
793 for view_num in range(len(view_keys)):
@@ -1555,8 +1558,8 @@ class P4Submit(Command, P4UserMap):
1558 for b in body:
1559 labelTemplate += "\t" + b + "\n"
1560 labelTemplate += "View:\n"
1558 - for mapping in clientSpec.mappings:
1559 - labelTemplate += "\t%s\n" % mapping.depot_side.path
1561 + for depot_side in clientSpec.mappings:
1562 + labelTemplate += "\t%s\n" % depot_side
1563
1564 if self.dry_run:
1565 print "Would create p4 label %s for tag" % name
@@ -1568,7 +1571,7 @@ class P4Submit(Command, P4UserMap):
1571
1572 # Use the label
1573 p4_system(["tag", "-l", name] +
1571 - ["%s@%s" % (mapping.depot_side.path, changelist) for mapping in clientSpec.mappings])
1574 + ["%s@%s" % (depot_side, changelist) for depot_side in clientSpec.mappings])
1575
1576 if verbose:
1577 print "created p4 label for tag %s" % name
@@ -1796,117 +1799,16 @@ class View(object):
1799 """Represent a p4 view ("p4 help views"), and map files in a
1800 repo according to the view."""
1801
1799 - class Path(object):
1800 - """A depot or client path, possibly containing wildcards.
1801 - The only one supported is ... at the end, currently.
1802 - Initialize with the full path, with //depot or //client."""
1803 -
1804 - def __init__(self, path, is_depot):
1805 - self.path = path
1806 - self.is_depot = is_depot
1807 - self.find_wildcards()
1808 - # remember the prefix bit, useful for relative mappings
1809 - m = re.match("(//[^/]+/)", self.path)
1810 - if not m:
1811 - die("Path %s does not start with //prefix/" % self.path)
1812 - prefix = m.group(1)
1813 - if not self.is_depot:
1814 - # strip //client/ on client paths
1815 - self.path = self.path[len(prefix):]
1816 -
1817 - def find_wildcards(self):
1818 - """Make sure wildcards are valid, and set up internal
1819 - variables."""
1820 -
1821 - self.ends_triple_dot = False
1822 - # There are three wildcards allowed in p4 views
1823 - # (see "p4 help views"). This code knows how to
1824 - # handle "..." (only at the end), but cannot deal with
1825 - # "%%n" or "*". Only check the depot_side, as p4 should
1826 - # validate that the client_side matches too.
1827 - if re.search(r'%%[1-9]', self.path):
1828 - die("Can't handle %%n wildcards in view: %s" % self.path)
1829 - if self.path.find("*") >= 0:
1830 - die("Can't handle * wildcards in view: %s" % self.path)
1831 - triple_dot_index = self.path.find("...")
1832 - if triple_dot_index >= 0:
1833 - if triple_dot_index != len(self.path) - 3:
1834 - die("Can handle only single ... wildcard, at end: %s" %
1835 - self.path)
1836 - self.ends_triple_dot = True
1837 -
1838 - def ensure_compatible(self, other_path):
1839 - """Make sure the wildcards agree."""
1840 - if self.ends_triple_dot != other_path.ends_triple_dot:
1841 - die("Both paths must end with ... if either does;\n" +
1842 - "paths: %s %s" % (self.path, other_path.path))
1843 -
1844 - def match_wildcards(self, test_path):
1845 - """See if this test_path matches us, and fill in the value
1846 - of the wildcards if so. Returns a tuple of
1847 - (True|False, wildcards[]). For now, only the ... at end
1848 - is supported, so at most one wildcard."""
1849 - if self.ends_triple_dot:
1850 - dotless = self.path[:-3]
1851 - if test_path.startswith(dotless):
1852 - wildcard = test_path[len(dotless):]
1853 - return (True, [ wildcard ])
1854 - else:
1855 - if test_path == self.path:
1856 - return (True, [])
1857 - return (False, [])
1858 -
1859 - def match(self, test_path):
1860 - """Just return if it matches; don't bother with the wildcards."""
1861 - b, _ = self.match_wildcards(test_path)
1862 - return b
1863 -
1864 - def fill_in_wildcards(self, wildcards):
1865 - """Return the relative path, with the wildcards filled in
1866 - if there are any."""
1867 - if self.ends_triple_dot:
1868 - return self.path[:-3] + wildcards[0]
1869 - else:
1870 - return self.path
1871 -
1872 - class Mapping(object):
1873 - def __init__(self, depot_side, client_side, overlay, exclude):
1874 - # depot_side is without the trailing /... if it had one
1875 - self.depot_side = View.Path(depot_side, is_depot=True)
1876 - self.client_side = View.Path(client_side, is_depot=False)
1877 - self.overlay = overlay # started with "+"
1878 - self.exclude = exclude # started with "-"
1879 - assert not (self.overlay and self.exclude)
1880 - self.depot_side.ensure_compatible(self.client_side)
1881 -
1882 - def __str__(self):
1883 - c = " "
1884 - if self.overlay:
1885 - c = "+"
1886 - if self.exclude:
1887 - c = "-"
1888 - return "View.Mapping: %s%s -> %s" % \
1889 - (c, self.depot_side.path, self.client_side.path)
1890 -
1891 - def map_depot_to_client(self, depot_path):
1892 - """Calculate the client path if using this mapping on the
1893 - given depot path; does not consider the effect of other
1894 - mappings in a view. Even excluded mappings are returned."""
1895 - matches, wildcards = self.depot_side.match_wildcards(depot_path)
1896 - if not matches:
1897 - return ""
1898 - client_path = self.client_side.fill_in_wildcards(wildcards)
1899 - return client_path
1900 -
1901 - #
1902 - # View methods
1903 - #
1904 - def __init__(self):
1802 + def __init__(self, client_name):
1803 self.mappings = []
1804 + self.client_prefix = "//%s/" % client_name
1805 + # cache results of "p4 where" to lookup client file locations
1806 + self.client_spec_path_cache = {}
1807
1808 def append(self, view_line):
1809 """Parse a view line, splitting it into depot and client
1909 - sides. Append to self.mappings, preserving order."""
1810 + sides. Append to self.mappings, preserving order. This
1811 + is only needed for tag creation."""
1812
1813 # Split the view line into exactly two words. P4 enforces
1814 # structure on these lines that simplifies this quite a bit.
@@ -1934,76 +1836,62 @@ class View(object):
1836 depot_side = view_line[0:space_index]
1837 rhs_index = space_index + 1
1838
1937 - if view_line[rhs_index] == '"':
1938 - # Second word is double quoted. Make sure there is a
1939 - # double quote at the end too.
1940 - if not view_line.endswith('"'):
1941 - die("View line with rhs quote should end with one: %s" %
1942 - view_line)
1943 - # skip the quotes
1944 - client_side = view_line[rhs_index+1:-1]
1945 - else:
1946 - client_side = view_line[rhs_index:]
1947 -
1839 # prefix + means overlay on previous mapping
1949 - overlay = False
1840 if depot_side.startswith("+"):
1951 - overlay = True
1841 depot_side = depot_side[1:]
1842
1954 - # prefix - means exclude this path
1843 + # prefix - means exclude this path, leave out of mappings
1844 exclude = False
1845 if depot_side.startswith("-"):
1846 exclude = True
1847 depot_side = depot_side[1:]
1848
1960 - m = View.Mapping(depot_side, client_side, overlay, exclude)
1961 - self.mappings.append(m)
1849 + if not exclude:
1850 + self.mappings.append(depot_side)
1851
1963 - def map_in_client(self, depot_path):
1964 - """Return the relative location in the client where this
1965 - depot file should live. Returns "" if the file should
1966 - not be mapped in the client."""
1852 + def convert_client_path(self, clientFile):
1853 + # chop off //client/ part to make it relative
1854 + if not clientFile.startswith(self.client_prefix):
1855 + die("No prefix '%s' on clientFile '%s'" %
1856 + (self.client_prefix, clientFile))
1857 + return clientFile[len(self.client_prefix):]
1858
1968 - paths_filled = []
1969 - client_path = ""
1859 + def update_client_spec_path_cache(self, files):
1860 + """ Caching file paths by "p4 where" batch query """
1861
1971 - # look at later entries first
1972 - for m in self.mappings[::-1]:
1862 + # List depot file paths exclude that already cached
1863 + fileArgs = [f['path'] for f in files if f['path'] not in self.client_spec_path_cache]
1864
1974 - # see where will this path end up in the client
1975 - p = m.map_depot_to_client(depot_path)
1865 + if len(fileArgs) == 0:
1866 + return # All files in cache
1867
1977 - if p == "":
1978 - # Depot path does not belong in client. Must remember
1979 - # this, as previous items should not cause files to
1980 - # exist in this path either. Remember that the list is
1981 - # being walked from the end, which has higher precedence.
1982 - # Overlap mappings do not exclude previous mappings.
1983 - if not m.overlay:
1984 - paths_filled.append(m.client_side)
1868 + where_result = p4CmdList(["-x", "-", "where"], stdin=fileArgs)
1869 + for res in where_result:
1870 + if "code" in res and res["code"] == "error":
1871 + # assume error is "... file(s) not in client view"
1872 + continue
1873 + if "clientFile" not in res:
1874 + die("No clientFile from 'p4 where %s'" % depot_path)
1875 + if "unmap" in res:
1876 + # it will list all of them, but only one not unmap-ped
1877 + continue
1878 + self.client_spec_path_cache[res['depotFile']] = self.convert_client_path(res["clientFile"])
1879
1986 - else:
1987 - # This mapping matched; no need to search any further.
1988 - # But, the mapping could be rejected if the client path
1989 - # has already been claimed by an earlier mapping (i.e.
1990 - # one later in the list, which we are walking backwards).
1991 - already_mapped_in_client = False
1992 - for f in paths_filled:
1993 - # this is View.Path.match
1994 - if f.match(p):
1995 - already_mapped_in_client = True
1996 - break
1997 - if not already_mapped_in_client:
1998 - # Include this file, unless it is from a line that
1999 - # explicitly said to exclude it.
2000 - if not m.exclude:
2001 - client_path = p
1880 + # not found files or unmap files set to ""
1881 + for depotFile in fileArgs:
1882 + if depotFile not in self.client_spec_path_cache:
1883 + self.client_spec_path_cache[depotFile] = ""
1884
2003 - # a match, even if rejected, always stops the search
2004 - break
1885 + def map_in_client(self, depot_path):
1886 + """Return the relative location in the client where this
1887 + depot file should live. Returns "" if the file should
1888 + not be mapped in the client."""
1889
2006 - return client_path
1890 + if depot_path in self.client_spec_path_cache:
1891 + return self.client_spec_path_cache[depot_path]
1892 +
1893 + die( "Error: %s is not found in client spec path" % depot_path )
1894 + return ""
1895
1896 class P4Sync(Command, P4UserMap):
1897 delete_actions = ( "delete", "move/delete", "purge" )
@@ -2130,6 +2018,10 @@ class P4Sync(Command, P4UserMap):
2018 """Look at each depotFile in the commit to figure out to what
2019 branch it belongs."""
2020
2021 + if self.clientSpecDirs:
2022 + files = self.extractFilesFromCommit(commit)
2023 + self.clientSpecDirs.update_client_spec_path_cache(files)
2024 +
2025 branches = {}
2026 fnum = 0
2027 while commit.has_key("depotFile%s" % fnum):
@@ -2379,6 +2271,9 @@ class P4Sync(Command, P4UserMap):
2271 else:
2272 sys.stderr.write("Ignoring file outside of prefix: %s\n" % f['path'])
2273
2274 + if self.clientSpecDirs:
2275 + self.clientSpecDirs.update_client_spec_path_cache(files)
2276 +
2277 self.gitStream.write("commit %s\n" % branch)
2278 # gitStream.write("mark :%s\n" % details["change"])
2279 self.committedChanges.add(int(details["change"]))