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 <pw@padd.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Pete Wyckoff committed
Jan 2, 2012 at 18:05 UTC
ecb7cf98a7d02e943ce44b083639c75011f3d8ec
2 files changed
+258
-101
contrib/fast-import/git-p4
+246
-89
@@ -1169,6 +1169,218 @@ class P4Submit(Command, P4UserMap):
1169
1170
return True
1171
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
+
1384
class P4Sync(Command, P4UserMap):
1385
delete_actions = ( "delete", "move/delete", "purge" )
1386
@@ -1216,8 +1428,7 @@ class P4Sync(Command, P4UserMap):
1428
self.p4BranchesInGit = []
1429
self.cloneExclude = []
1430
self.useClientSpec = False
1219
- self.clientSpecDirs = []
1220
- self.haveSingleFileClientViews = False
1431
+ self.clientSpecDirs = None
1432
1433
if gitConfig("git-p4.syncFromOrigin") == "false":
1434
self.syncWithOrigin = False
@@ -1268,30 +1479,7 @@ class P4Sync(Command, P4UserMap):
1479
1480
def stripRepoPath(self, path, prefixes):
1481
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)
1483
1484
if self.keepRepoPath:
1485
prefixes = [re.sub("^(//[^/]+/).*", r'\1', prefixes[0])]
@@ -1441,19 +1629,17 @@ class P4Sync(Command, P4UserMap):
1629
filesToDelete = []
1630
1631
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
1637
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)
1643
1644
# deleted files...
1645
for f in filesToDelete:
@@ -1892,60 +2078,31 @@ class P4Sync(Command, P4UserMap):
2078
2079
2080
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()
2094
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])
2101
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
2103
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))
2106
2107
def run(self, args):
2108
self.depotPaths = []
t/t9809-git-p4-client-view.sh
+12
-12
@@ -107,7 +107,7 @@ test_expect_success 'wildcard ... only supported at end of spec' '
107
test_must_fail "$GITP4" clone --use-client-spec --dest="$git" //depot
108
'
109
110
-test_expect_failure 'basic map' '
110
+test_expect_success 'basic map' '
111
client_view "//depot/dir1/... //client/cli1/..." &&
112
files="cli1/file11 cli1/file12" &&
113
client_verify $files &&
@@ -116,7 +116,7 @@ test_expect_failure 'basic map' '
116
git_verify $files
117
'
118
119
-test_expect_failure 'client view with no mappings' '
119
+test_expect_success 'client view with no mappings' '
120
client_view &&
121
client_verify &&
122
test_when_finished cleanup_git &&
@@ -124,7 +124,7 @@ test_expect_failure 'client view with no mappings' '
124
git_verify
125
'
126
127
-test_expect_failure 'single file map' '
127
+test_expect_success 'single file map' '
128
client_view "//depot/dir1/file11 //client/file11" &&
129
files="file11" &&
130
client_verify $files &&
@@ -144,7 +144,7 @@ test_expect_success 'later mapping takes precedence (entire repo)' '
144
git_verify $files
145
'
146
147
-test_expect_failure 'later mapping takes precedence (partial repo)' '
147
+test_expect_success 'later mapping takes precedence (partial repo)' '
148
client_view "//depot/dir1/... //client/..." \
149
"//depot/dir2/... //client/..." &&
150
files="file21 file22" &&
@@ -159,7 +159,7 @@ test_expect_failure 'later mapping takes precedence (partial repo)' '
159
# dir1 cannot go to cli12 since it was filled by dir2
160
# dir1 also does not go to cli3, since the second rule
161
# noticed that it matched, but was already filled
162
-test_expect_failure 'depot path matching rejected client path' '
162
+test_expect_success 'depot path matching rejected client path' '
163
client_view "//depot/dir1/... //client/cli3/..." \
164
"//depot/dir1/... //client/cli12/..." \
165
"//depot/dir2/... //client/cli12/..." &&
@@ -172,7 +172,7 @@ test_expect_failure 'depot path matching rejected client path' '
172
173
# since both have the same //client/..., the exclusion
174
# rule keeps everything out
175
-test_expect_failure 'exclusion wildcard, client rhs same (odd)' '
175
+test_expect_success 'exclusion wildcard, client rhs same (odd)' '
176
client_view "//depot/... //client/..." \
177
"-//depot/dir2/... //client/..." &&
178
client_verify &&
@@ -201,7 +201,7 @@ test_expect_success 'exclusion single file' '
201
git_verify $files
202
'
203
204
-test_expect_failure 'overlay wildcard' '
204
+test_expect_success 'overlay wildcard' '
205
client_view "//depot/dir1/... //client/cli/..." \
206
"+//depot/dir2/... //client/cli/...\n" &&
207
files="cli/file11 cli/file12 cli/file21 cli/file22" &&
@@ -211,7 +211,7 @@ test_expect_failure 'overlay wildcard' '
211
git_verify $files
212
'
213
214
-test_expect_failure 'overlay single file' '
214
+test_expect_success 'overlay single file' '
215
client_view "//depot/dir1/... //client/cli/..." \
216
"+//depot/dir2/file21 //client/cli/file21" &&
217
files="cli/file11 cli/file12 cli/file21" &&
@@ -221,7 +221,7 @@ test_expect_failure 'overlay single file' '
221
git_verify $files
222
'
223
224
-test_expect_failure 'exclusion with later inclusion' '
224
+test_expect_success 'exclusion with later inclusion' '
225
client_view "//depot/... //client/..." \
226
"-//depot/dir2/... //client/dir2/..." \
227
"//depot/dir2/... //client/dir2incl/..." &&
@@ -232,7 +232,7 @@ test_expect_failure 'exclusion with later inclusion' '
232
git_verify $files
233
'
234
235
-test_expect_failure 'quotes on rhs only' '
235
+test_expect_success 'quotes on rhs only' '
236
client_view "//depot/dir1/... \"//client/cdir 1/...\"" &&
237
client_verify "cdir 1/file11" "cdir 1/file12" &&
238
test_when_finished cleanup_git &&
@@ -266,7 +266,7 @@ test_expect_success 'rename files to introduce spaces' '
266
"dir 2/file21" "dir 2/file22"
267
'
268
269
-test_expect_failure 'quotes on lhs only' '
269
+test_expect_success 'quotes on lhs only' '
270
client_view "\"//depot/dir 1/...\" //client/cdir1/..." &&
271
files="cdir1/file11 cdir1/file12" &&
272
client_verify $files &&
@@ -275,7 +275,7 @@ test_expect_failure 'quotes on lhs only' '
275
client_verify $files
276
'
277
278
-test_expect_failure 'quotes on both sides' '
278
+test_expect_success 'quotes on both sides' '
279
client_view "\"//depot/dir 1/...\" \"//client/cdir 1/...\"" &&
280
client_verify "cdir 1/file11" "cdir 1/file12" &&
281
test_when_finished cleanup_git &&