@@ -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+
11721384class 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