Skip to content

Commit ecb7cf9

Browse files
Pete Wyckoffgitster
authored andcommitted
git-p4: rewrite view handling
The old code was not very complete or robust. Redo it. This new code should be useful for a few possible additions in the future: - support for * and %%n wildcards - allowing ... inside paths - representing branch specs (not just client specs) - tracking changes to views Mark the remaining 12 tests in t9809 as fixed. Signed-off-by: Pete Wyckoff <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent e3e6864 commit ecb7cf9

File tree

2 files changed

+258
-101
lines changed

2 files changed

+258
-101
lines changed

contrib/fast-import/git-p4

Lines changed: 246 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,218 @@ class P4Submit(Command, P4UserMap):
11691169

11701170
return True
11711171

1172+
class View(object):
1173+
"""Represent a p4 view ("p4 help views"), and map files in a
1174+
repo according to the view."""
1175+
1176+
class Path(object):
1177+
"""A depot or client path, possibly containing wildcards.
1178+
The only one supported is ... at the end, currently.
1179+
Initialize with the full path, with //depot or //client."""
1180+
1181+
def __init__(self, path, is_depot):
1182+
self.path = path
1183+
self.is_depot = is_depot
1184+
self.find_wildcards()
1185+
# remember the prefix bit, useful for relative mappings
1186+
m = re.match("(//[^/]+/)", self.path)
1187+
if not m:
1188+
die("Path %s does not start with //prefix/" % self.path)
1189+
prefix = m.group(1)
1190+
if not self.is_depot:
1191+
# strip //client/ on client paths
1192+
self.path = self.path[len(prefix):]
1193+
1194+
def find_wildcards(self):
1195+
"""Make sure wildcards are valid, and set up internal
1196+
variables."""
1197+
1198+
self.ends_triple_dot = False
1199+
# There are three wildcards allowed in p4 views
1200+
# (see "p4 help views"). This code knows how to
1201+
# handle "..." (only at the end), but cannot deal with
1202+
# "%%n" or "*". Only check the depot_side, as p4 should
1203+
# validate that the client_side matches too.
1204+
if re.search(r'%%[1-9]', self.path):
1205+
die("Can't handle %%n wildcards in view: %s" % self.path)
1206+
if self.path.find("*") >= 0:
1207+
die("Can't handle * wildcards in view: %s" % self.path)
1208+
triple_dot_index = self.path.find("...")
1209+
if triple_dot_index >= 0:
1210+
if not self.path.endswith("..."):
1211+
die("Can handle ... wildcard only at end of path: %s" %
1212+
self.path)
1213+
self.ends_triple_dot = True
1214+
1215+
def ensure_compatible(self, other_path):
1216+
"""Make sure the wildcards agree."""
1217+
if self.ends_triple_dot != other_path.ends_triple_dot:
1218+
die("Both paths must end with ... if either does;\n" +
1219+
"paths: %s %s" % (self.path, other_path.path))
1220+
1221+
def match_wildcards(self, test_path):
1222+
"""See if this test_path matches us, and fill in the value
1223+
of the wildcards if so. Returns a tuple of
1224+
(True|False, wildcards[]). For now, only the ... at end
1225+
is supported, so at most one wildcard."""
1226+
if self.ends_triple_dot:
1227+
dotless = self.path[:-3]
1228+
if test_path.startswith(dotless):
1229+
wildcard = test_path[len(dotless):]
1230+
return (True, [ wildcard ])
1231+
else:
1232+
if test_path == self.path:
1233+
return (True, [])
1234+
return (False, [])
1235+
1236+
def match(self, test_path):
1237+
"""Just return if it matches; don't bother with the wildcards."""
1238+
b, _ = self.match_wildcards(test_path)
1239+
return b
1240+
1241+
def fill_in_wildcards(self, wildcards):
1242+
"""Return the relative path, with the wildcards filled in
1243+
if there are any."""
1244+
if self.ends_triple_dot:
1245+
return self.path[:-3] + wildcards[0]
1246+
else:
1247+
return self.path
1248+
1249+
class Mapping(object):
1250+
def __init__(self, depot_side, client_side, overlay, exclude):
1251+
# depot_side is without the trailing /... if it had one
1252+
self.depot_side = View.Path(depot_side, is_depot=True)
1253+
self.client_side = View.Path(client_side, is_depot=False)
1254+
self.overlay = overlay # started with "+"
1255+
self.exclude = exclude # started with "-"
1256+
assert not (self.overlay and self.exclude)
1257+
self.depot_side.ensure_compatible(self.client_side)
1258+
1259+
def __str__(self):
1260+
c = " "
1261+
if self.overlay:
1262+
c = "+"
1263+
if self.exclude:
1264+
c = "-"
1265+
return "View.Mapping: %s%s -> %s" % \
1266+
(c, self.depot_side, self.client_side)
1267+
1268+
def map_depot_to_client(self, depot_path):
1269+
"""Calculate the client path if using this mapping on the
1270+
given depot path; does not consider the effect of other
1271+
mappings in a view. Even excluded mappings are returned."""
1272+
matches, wildcards = self.depot_side.match_wildcards(depot_path)
1273+
if not matches:
1274+
return ""
1275+
client_path = self.client_side.fill_in_wildcards(wildcards)
1276+
return client_path
1277+
1278+
#
1279+
# View methods
1280+
#
1281+
def __init__(self):
1282+
self.mappings = []
1283+
1284+
def append(self, view_line):
1285+
"""Parse a view line, splitting it into depot and client
1286+
sides. Append to self.mappings, preserving order."""
1287+
1288+
# Split the view line into exactly two words. P4 enforces
1289+
# structure on these lines that simplifies this quite a bit.
1290+
#
1291+
# Either or both words may be double-quoted.
1292+
# Single quotes do not matter.
1293+
# Double-quote marks cannot occur inside the words.
1294+
# A + or - prefix is also inside the quotes.
1295+
# There are no quotes unless they contain a space.
1296+
# The line is already white-space stripped.
1297+
# The two words are separated by a single space.
1298+
#
1299+
if view_line[0] == '"':
1300+
# First word is double quoted. Find its end.
1301+
close_quote_index = view_line.find('"', 1)
1302+
if close_quote_index <= 0:
1303+
die("No first-word closing quote found: %s" % view_line)
1304+
depot_side = view_line[1:close_quote_index]
1305+
# skip closing quote and space
1306+
rhs_index = close_quote_index + 1 + 1
1307+
else:
1308+
space_index = view_line.find(" ")
1309+
if space_index <= 0:
1310+
die("No word-splitting space found: %s" % view_line)
1311+
depot_side = view_line[0:space_index]
1312+
rhs_index = space_index + 1
1313+
1314+
if view_line[rhs_index] == '"':
1315+
# Second word is double quoted. Make sure there is a
1316+
# double quote at the end too.
1317+
if not view_line.endswith('"'):
1318+
die("View line with rhs quote should end with one: %s" %
1319+
view_line)
1320+
# skip the quotes
1321+
client_side = view_line[rhs_index+1:-1]
1322+
else:
1323+
client_side = view_line[rhs_index:]
1324+
1325+
# prefix + means overlay on previous mapping
1326+
overlay = False
1327+
if depot_side.startswith("+"):
1328+
overlay = True
1329+
depot_side = depot_side[1:]
1330+
1331+
# prefix - means exclude this path
1332+
exclude = False
1333+
if depot_side.startswith("-"):
1334+
exclude = True
1335+
depot_side = depot_side[1:]
1336+
1337+
m = View.Mapping(depot_side, client_side, overlay, exclude)
1338+
self.mappings.append(m)
1339+
1340+
def map_in_client(self, depot_path):
1341+
"""Return the relative location in the client where this
1342+
depot file should live. Returns "" if the file should
1343+
not be mapped in the client."""
1344+
1345+
paths_filled = []
1346+
client_path = ""
1347+
1348+
# look at later entries first
1349+
for m in self.mappings[::-1]:
1350+
1351+
# see where will this path end up in the client
1352+
p = m.map_depot_to_client(depot_path)
1353+
1354+
if p == "":
1355+
# Depot path does not belong in client. Must remember
1356+
# this, as previous items should not cause files to
1357+
# exist in this path either. Remember that the list is
1358+
# being walked from the end, which has higher precedence.
1359+
# Overlap mappings do not exclude previous mappings.
1360+
if not m.overlay:
1361+
paths_filled.append(m.client_side)
1362+
1363+
else:
1364+
# This mapping matched; no need to search any further.
1365+
# But, the mapping could be rejected if the client path
1366+
# has already been claimed by an earlier mapping.
1367+
already_mapped_in_client = False
1368+
for f in paths_filled:
1369+
# this is View.Path.match
1370+
if f.match(p):
1371+
already_mapped_in_client = True
1372+
break
1373+
if not already_mapped_in_client:
1374+
# Include this file, unless it is from a line that
1375+
# explicitly said to exclude it.
1376+
if not m.exclude:
1377+
client_path = p
1378+
1379+
# a match, even if rejected, always stops the search
1380+
break
1381+
1382+
return client_path
1383+
11721384
class P4Sync(Command, P4UserMap):
11731385
delete_actions = ( "delete", "move/delete", "purge" )
11741386

@@ -1216,8 +1428,7 @@ class P4Sync(Command, P4UserMap):
12161428
self.p4BranchesInGit = []
12171429
self.cloneExclude = []
12181430
self.useClientSpec = False
1219-
self.clientSpecDirs = []
1220-
self.haveSingleFileClientViews = False
1431+
self.clientSpecDirs = None
12211432

12221433
if gitConfig("git-p4.syncFromOrigin") == "false":
12231434
self.syncWithOrigin = False
@@ -1268,30 +1479,7 @@ class P4Sync(Command, P4UserMap):
12681479

12691480
def stripRepoPath(self, path, prefixes):
12701481
if self.useClientSpec:
1271-
1272-
# if using the client spec, we use the output directory
1273-
# specified in the client. For example, a view
1274-
# //depot/foo/branch/... //client/branch/foo/...
1275-
# will end up putting all foo/branch files into
1276-
# branch/foo/
1277-
for val in self.clientSpecDirs:
1278-
if self.haveSingleFileClientViews and len(path) == abs(val[1][0]) and path == val[0]:
1279-
# since 'path' is a depot file path, if it matches the LeftMap,
1280-
# then the View is a single file mapping, so use the entire rightMap
1281-
# first two tests above avoid the last == test for common cases
1282-
path = val[1][1]
1283-
# now strip out the client (//client/...)
1284-
path = re.sub("^(//[^/]+/)", '', path)
1285-
# the rest is local client path
1286-
return path
1287-
1288-
if path.startswith(val[0]):
1289-
# replace the depot path with the client path
1290-
path = path.replace(val[0], val[1][1])
1291-
# now strip out the client (//client/...)
1292-
path = re.sub("^(//[^/]+/)", '', path)
1293-
# the rest is all path
1294-
return path
1482+
return self.clientSpecDirs.map_in_client(path)
12951483

12961484
if self.keepRepoPath:
12971485
prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
@@ -1441,19 +1629,17 @@ class P4Sync(Command, P4UserMap):
14411629
filesToDelete = []
14421630

14431631
for f in files:
1444-
includeFile = True
1445-
for val in self.clientSpecDirs:
1446-
if f['path'].startswith(val[0]):
1447-
if val[1][0] <= 0:
1448-
includeFile = False
1449-
break
1632+
# if using a client spec, only add the files that have
1633+
# a path in the client
1634+
if self.clientSpecDirs:
1635+
if self.clientSpecDirs.map_in_client(f['path']) == "":
1636+
continue
14501637

1451-
if includeFile:
1452-
filesForCommit.append(f)
1453-
if f['action'] in self.delete_actions:
1454-
filesToDelete.append(f)
1455-
else:
1456-
filesToRead.append(f)
1638+
filesForCommit.append(f)
1639+
if f['action'] in self.delete_actions:
1640+
filesToDelete.append(f)
1641+
else:
1642+
filesToRead.append(f)
14571643

14581644
# deleted files...
14591645
for f in filesToDelete:
@@ -1892,60 +2078,31 @@ class P4Sync(Command, P4UserMap):
18922078

18932079

18942080
def getClientSpec(self):
1895-
specList = p4CmdList( "client -o" )
1896-
temp = {}
1897-
for entry in specList:
1898-
for k,v in entry.iteritems():
1899-
if k.startswith("View"):
1900-
1901-
# p4 has these %%1 to %%9 arguments in specs to
1902-
# reorder paths; which we can't handle (yet :)
1903-
if re.search('%%\d', v) != None:
1904-
print "Sorry, can't handle %%n arguments in client specs"
1905-
sys.exit(1)
1906-
if re.search('\*', v) != None:
1907-
print "Sorry, can't handle * mappings in client specs"
1908-
sys.exit(1)
1909-
1910-
if v.startswith('"'):
1911-
start = 1
1912-
else:
1913-
start = 0
1914-
index = v.find("...")
1915-
1916-
# save the "client view"; i.e the RHS of the view
1917-
# line that tells the client where to put the
1918-
# files for this view.
1919-
1920-
# check for individual file mappings - those which have no '...'
1921-
if index < 0 :
1922-
v,cv = v.strip().split()
1923-
v = v[start:]
1924-
self.haveSingleFileClientViews = True
1925-
else:
1926-
cv = v[index+3:].strip() # +3 to remove previous '...'
1927-
cv=cv[:-3]
1928-
v = v[start:index]
1929-
1930-
# now save the view; +index means included, -index
1931-
# means it should be filtered out.
1932-
if v.startswith("-"):
1933-
v = v[1:]
1934-
include = -len(v)
1935-
else:
1936-
include = len(v)
2081+
specList = p4CmdList("client -o")
2082+
if len(specList) != 1:
2083+
die('Output from "client -o" is %d lines, expecting 1' %
2084+
len(specList))
2085+
2086+
# dictionary of all client parameters
2087+
entry = specList[0]
2088+
2089+
# just the keys that start with "View"
2090+
view_keys = [ k for k in entry.keys() if k.startswith("View") ]
2091+
2092+
# hold this new View
2093+
view = View()
19372094

1938-
# store the View #number for sorting
1939-
# and the View string itself (this last for documentation)
1940-
temp[v] = (include, cv, int(k[4:]),k)
2095+
# append the lines, in order, to the view
2096+
for view_num in range(len(view_keys)):
2097+
k = "View%d" % view_num
2098+
if k not in view_keys:
2099+
die("Expected view key %s missing" % k)
2100+
view.append(entry[k])
19412101

1942-
self.clientSpecDirs = temp.items()
1943-
# Perforce ViewNN with higher #numbers override those with lower
1944-
# reverse sort on the View #number
1945-
self.clientSpecDirs.sort( lambda x, y: y[1][2] - x[1][2] )
2102+
self.clientSpecDirs = view
19462103
if self.verbose:
1947-
for val in self.clientSpecDirs:
1948-
print "clientSpecDirs: %s %s" % (val[0],val[1])
2104+
for i, m in enumerate(self.clientSpecDirs.mappings):
2105+
print "clientSpecDirs %d: %s" % (i, str(m))
19492106

19502107
def run(self, args):
19512108
self.depotPaths = []

0 commit comments

Comments
 (0)