// // P4Workspace.m // Perforce // // Created by Adam Czubernat on 02/10/2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "P4Workspace.h" #import "PSFileEvents.h" #import "P4Defaults.h" NSString * const P4SyncStartedNotification = @"P4SyncStartedNotification"; NSString * const P4SyncProgressNotification = @"P4SyncProgressNotification"; NSString * const P4SyncFinishedNotification = @"P4SyncFinishedNotification"; NSString * const P4SubmitFinishedNotification = @"P4SubmitFinishedNotification"; NSString * const P4ChangelistUpdatedNotification = @"P4ChangelistUpdatedNotification"; NSString * const P4MappingUpdatedNotification = @"P4MappingUpdatedNotification"; NSString * const P4UnreadUpdatedNotification = @"P4UnreadUpdatedNotification"; NSString * const P4ShelvedChangelistDescription = @"P4 Shelved progress"; NSString * const kDefaultLastFileEventId = @"FSEventsStreamLastEventId"; @interface P4Workspace () { P4Connection *listConnection; // Listing actions UI P4Connection *editConnection; // For checkout add delete and move actions P4Connection *syncConnection; // Sync and submit PSFileEvents *fileEventsP4; PSFileEvents *fileEventsUI; NSMutableArray *observers; NSString *address; NSString *username; NSString *workspace; NSString *root; NSFileManager *filemanager; __weak P4Operation *syncOperation; NSTimer *autosyncTimer; NSTimeInterval autosyncInterval; NSMutableDictionary *clientSpecs; NSMutableArray *clientViews; NSMutableArray *clientPaths; NSMutableArray *clientMappings; NSDictionary *changelist; NSMutableArray *unreadList; NSDictionary *usersList; NSNumber *shelvedChange; NSMutableArray *shelvedList; NSString *searchUrl; NSString *searchTicket; struct { unsigned int connected:1; unsigned int loggedIn:1; unsigned int loginInProgress:1; unsigned int synchronizing:1; } flags; } - (void)autosync; - (void)handleClientResponse:(NSArray *)response; - (void)handleChangelistResponse:(NSArray *)response; - (void)handlePendingResponse:(NSArray *)response; - (P4Operation *)mappingSet:(BOOL)set files:(NSArray *)paths sync:(NSArray *)syncArgs response:(P4ResponseBlock_t)responseBlock; - (NSMutableArray *)observers; - (NSArray *)contentOfDirectory:(NSString *)path; - (NSArray *)contentOfDirectories:(NSArray *)paths; - (NSArray *)pathsWithDirectoryStarSuffix:(NSArray *)paths; - (NSArray *)pathsWithDirectoryDotsSuffix:(NSArray *)paths; // Appends '...' suffixes to directory paths and exludes subpaths - (NSArray *)pathsWithEncodingChecked:(NSArray *)paths; - (NSArray *)pathsDecoded:(NSArray *)paths; - (BOOL) pathsHaveSpecialCharacters:(NSArray *)paths; @end @implementation P4Credentials @synthesize username, password, address, resumeSession; @end @implementation P4Workspace @synthesize address, username, workspace, root; + (P4Workspace *)sharedInstance { static P4Workspace *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[P4Workspace alloc] init]; }); return sharedInstance; } - (void)addObserver:(id )observer { if (!observers) { // Non-retaining array CFArrayCallBacks callbacks = { 0, NULL, NULL, CFCopyDescription, CFEqual }; observers = CFBridgingRelease(CFArrayCreateMutable(NULL, 0, &callbacks)); } if (![observers containsObject:observer]) [observers addObject:observer]; } - (void)removeObserver:(id )observer { if ([observers containsObject:observer]) [observers removeObject:observer]; } - (id)init { if (self = [super init]) { filemanager = [NSFileManager defaultManager]; } return self; } - (NSString *)ticket { return [editConnection ticket]; } #pragma mark Login - (BOOL)isConnected { return flags.connected; } - (void)connectWithCredentials:(P4Credentials *)credentials response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert([NSThread isMainThread], @"Not in the main thread"); // Reset connections [editConnection disconnect]; [listConnection disconnect]; [syncConnection disconnect]; editConnection = listConnection = syncConnection = nil; flags.connected = NO; address = credentials.address; username = credentials.username; [editConnection = [[P4Connection alloc] initWithName:@"P4.Edit"] connectWithHost:address username:username response:^(P4Operation *operation, NSArray *response) { flags.connected = !operation.errors; // Check for errors if (!flags.connected) { responseBlock(operation, response); return; }; // Get server info (for unicode) [editConnection run:@"info" arguments:nil prompt:nil input:nil receive:nil response:^(P4Operation *operation, NSArray *response) { PSLog(@"Connected %@@%@", username, address); // Get server info NSDictionary *info = [response lastObject]; NSString *unicode = [info objectForKey:@"unicode"]; [editConnection setCharset:unicode ? @"utf8" : @""]; NSString *ignore = [[NSBundle mainBundle] pathForResource:@"p4ignore" ofType:nil]; [editConnection setIgnoreFile:ignore]; // Create auxiliary connections listConnection = [[P4Connection alloc] initWithConnection:editConnection name:@"P4.List"]; syncConnection = [[P4Connection alloc] initWithConnection:editConnection name:@"P4.Sync"]; responseBlock(operation, response); }]; }]; } - (BOOL)isLoggedIn { return flags.loggedIn; } - (void)loginWithCredentials:(P4Credentials *)credentials response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert([NSThread isMainThread], @"Not in main thread"); NSAssert(editConnection, @"Login with null connection"); NSAssert([editConnection isConnected], @"Login with disconnected connection"); NSAssert(!flags.loginInProgress, @"Already logging in"); NSAssert(!flags.loggedIn, @"Already logged in"); NSAssert(!workspace, @"Login should have unset workspace"); NSAssert(!root, @"Login should have unset workspace root"); flags.loggedIn = NO; flags.loginInProgress = YES; [editConnection run:@"login" arguments:@[ credentials.resumeSession ? @"-s" : @"-a" ] prompt:credentials.password ?: @"" input:nil receive:nil response:^(P4Operation *operation, NSArray *response) { // Bail out if errors if (operation.errors) { flags.loginInProgress = NO; flags.loggedIn = NO; responseBlock(operation, response); return; } if (credentials.resumeSession) { // Retrieve previous search ticket searchTicket = [P4Defaults sharedInstance].searchTicket; } else { // Retrieve initial ticket as p4search searchTicket = [editConnection ticket]; [P4Defaults sharedInstance].searchTicket = searchTicket; } // Retrieve search service url [listConnection run:@"key" arguments:@[ @"p4search.url" ] response:^(P4Operation *operation, NSArray *response) { NSString *url = [[response lastObject] objectForKey:@"value"]; PSLog(@"p4search.url = \"%@\"", url); if (!url || ![url isKindOfClass:[NSString class]]) { url = [P4Defaults sharedInstance].searchUrl; PSLog(@"Defaults p4search.url = \"%@\"", url); } PSLogStore(@"p4search.url", @"%@", url); // Check for url scheme if (![url hasPrefix:@"http:/"] && ![url hasPrefix:@"https:/"]) url = [@"http://" stringByAppendingPathComponent:url]; // Append search API paths url = [url stringByAppendingPath:@"api/search"]; searchUrl = url; }]; flags.loginInProgress = NO; flags.loggedIn = YES; responseBlock(operation, response); }]; } - (void)loginWithSSO:(P4Credentials *)credentials response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; PSLog(@"SSO > Login..."); NSAssert([NSThread isMainThread], @"Not in main thread"); NSAssert(editConnection, @"Login with null connection"); NSAssert([editConnection isConnected], @"Login with disconnected connection"); NSAssert(!flags.loginInProgress, @"Already logging in"); NSAssert(!flags.loggedIn, @"Already logged in"); [editConnection runBlock:^(P4ThreadOperation *operation) { // Check if sso-client script exists NSString *script = @"/usr/local/sso/sso-client.sh"; NSURL *url = [NSURL fileURLWithPath:script isDirectory:NO]; BOOL exists = [url checkResourceIsReachableAndReturnError:NULL]; NSNumber *executable = nil; // Check if executable [url getResourceValue:&executable forKey:NSURLIsExecutableKey error:NULL]; // Notify if /usr/local script is not executable if (exists && !executable.boolValue) { [operation addError:[NSError errorWithFormat:@"SSO error script %@ is not executable", script]]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ responseBlock(operation, nil); }]; return; } // If not found get built-in script if (!exists) { script = [[NSBundle mainBundle] pathForResource:@"sso-client.sh" ofType:nil]; url = [NSURL fileURLWithPath:script isDirectory:NO]; exists = [url checkResourceIsReachableAndReturnError:NULL]; [url getResourceValue:&executable forKey:NSURLIsExecutableKey error:NULL]; } // Fail if cannot run the script if (!exists || !executable.boolValue) { NSString *reason = !exists ? @"doesn't exist" : @"is not executable"; [operation addError:[NSError errorWithFormat:@"SSO error script %@ %@", script, reason]]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ responseBlock(operation, nil); }]; return; } PSLog(@"SSO > Running script %@", script); // Set environment variables NSString *env; env = [[NSBundle mainBundle] resourcePath]; if (setenv("BASE", [env UTF8String], 1)) PSLog(@"Error > setenv %@ errno : %d", env, errno); env = [NSString stringWithFormat:@"\"%@\" %%serverAddress%%", script]; if (setenv("P4LOGINSSO", [env UTF8String], 1)) PSLog(@"Error > setenv %@ errno : %d", env, errno); // PSLog(@"Env variables:\n%@", [[NSProcessInfo processInfo] environment]); // Login without password using SSO [[NSOperationQueue mainQueue] addOperationWithBlock:^{ PSLog(@"SSO > Authenticating..."); [self loginWithCredentials:credentials response:^(P4Operation *operation, NSArray *response) { // Unset environment variable unsetenv("P4LOGINSSO"); responseBlock(operation, response); }]; }]; }]; } - (void)reloginWithCredentials:(P4Credentials *)credentials response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(editConnection, @"Relogin with null connection"); NSAssert([editConnection isConnected], @"Relogin with disconnected connection"); flags.loggedIn = NO; flags.loginInProgress = YES; [editConnection run:@"logout" arguments:nil response:^(P4Operation *operation, NSArray *response) { [editConnection run:@"login" arguments:@[ @"-a" ] prompt:credentials.password input:nil receive:nil response:^(P4Operation *operation, NSArray *response) { NSString *ticket = [editConnection ticket]; if (ticket) { // Retrieve initial ticket as p4search searchTicket = ticket; [P4Defaults sharedInstance].searchTicket = searchTicket; // Reset connections to new ticket [syncConnection setTicket:ticket]; [listConnection setTicket:ticket]; } flags.loginInProgress = NO; flags.loggedIn = !operation.errors; responseBlock(operation, response); }]; }]; } - (void)logout:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; // Invalidate sync timer [autosyncTimer invalidate]; autosyncTimer = nil; autosyncInterval = 0; [editConnection run:@"logout" arguments:nil response:^(P4Operation *operation, NSArray *response) { [editConnection disconnect]; [listConnection disconnect]; [syncConnection disconnect]; editConnection = listConnection = syncConnection = nil; flags.connected = NO; fileEventsP4 = nil; fileEventsUI = nil; address = nil; username = nil; workspace = nil; root = nil; flags.loggedIn = NO; clientSpecs = nil; clientViews = nil; clientPaths = nil; clientMappings = nil; changelist = nil; unreadList = nil; usersList = nil; shelvedChange = nil; shelvedList = nil; searchUrl = nil; searchTicket = nil; responseBlock(operation, response); }]; } - (void)listDepots:(P4ResponseBlock_t)responseBlock { [listConnection run:@"depots" arguments:nil response:responseBlock]; } - (void)listWorkspaces:(P4ResponseBlock_t)responseBlock { [listConnection run:@"clients" arguments:@[ @"-u", username ] response:responseBlock]; } #pragma mark Workspace - (void)setWorkspace:(NSString *)name response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; name = name ?: @""; [editConnection run:@"client" arguments:@[ @"-o", @"-t", name, name ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); // Get workspace info and parse mapping [self handleClientResponse:response]; // Set connections' workspaces [editConnection setWorkspace:workspace root:root]; [listConnection runBlock:^(P4ThreadOperation *operation) { dispatch_sync(dispatch_get_main_queue(), ^{ // Sync to force execution on queue [listConnection setWorkspace:workspace root:root]; }); }]; [syncConnection runBlock:^(P4ThreadOperation *operation) { dispatch_sync(dispatch_get_main_queue(), ^{ // Sync to force execution on queue [syncConnection setWorkspace:workspace root:root]; }); }]; // Get last file system event NSNumber *lastEventId = [NSUserDefaults objectForKey:kDefaultLastFileEventId]; // Set file events fileEventsP4 = nil; fileEventsP4 = [[PSFileEvents alloc] initWithRoot:root applicationEvents:NO // Ignore events from this app eventId:lastEventId.integerValue]; fileEventsP4.delegate = self; fileEventsUI = nil; fileEventsUI = [[PSFileEvents alloc] initWithRoot:root applicationEvents:YES // Watch all events eventId:0]; fileEventsUI.delegate = self; // Store last eventId for next run if (!lastEventId) [NSUserDefaults setObject:@([fileEventsP4 eventId]) forKey:kDefaultLastFileEventId]; // Refresh data changelist = nil; usersList = nil; unreadList = [NSMutableArray array]; shelvedChange = nil; shelvedList = nil; [self refreshPendingFiles]; [self refreshShelvedFiles]; [self refreshUsers]; responseBlock(operation, response); }]; } - (void)createWorkspace:(NSDictionary *)workspaceSpecs response:(P4ResponseBlock_t)responseBlock { [editConnection run:@"client" arguments:@[ @"-i" ] prompt:nil input:workspaceSpecs receive:nil response:responseBlock]; } #pragma mark Sync - (BOOL)isSynchronizing { return flags.synchronizing; } - (void)syncWorkspace:(P4ReceiveBlock_t)receiveBlock response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; if (flags.synchronizing) return; [autosyncTimer invalidate]; autosyncTimer = nil; flags.synchronizing = YES; [[NSNotificationCenter defaultCenter] postNotificationName:P4SyncStartedNotification object:self]; syncOperation = [syncConnection run:@"sync" arguments:nil prompt:nil input:nil receive:^(P4Operation *operation) { NSArray *response = operation.response; long total = [[response valueForKeyPath:@"@sum.totalFileSize"] longValue]; long completed = [[response valueForKeyPath:@"@sum.fileSize"] longValue]; if (!operation.total && total) { PSLog(@"Sync expected length : %.2f MB, files %@", total/1024.0f/1024.0f, [response valueForKeyPath:@"@sum.totalFileCount"]); } operation.total = total; operation.completed = completed; [[NSNotificationCenter defaultCenter] postNotificationName:P4SyncProgressNotification object:operation]; if (receiveBlock) receiveBlock(operation); } response:^(P4Operation *operation, NSArray *response) { flags.synchronizing = NO; PSLog(@"Sync finished"); [self handleChangelistResponse:response]; [self setAutosyncInterval:autosyncInterval]; [[NSNotificationCenter defaultCenter] postNotificationName:P4SyncFinishedNotification object:operation]; responseBlock(operation, response); }]; // Refresh [self refreshUsers]; [self refreshMapping]; } - (P4Operation *)syncOperation { return syncOperation; } - (void)setAutosyncInterval:(NSTimeInterval)interval { autosyncInterval = interval; [autosyncTimer invalidate]; autosyncTimer = nil; if (!autosyncInterval) return; autosyncTimer = [NSTimer scheduledTimerWithTimeInterval:autosyncInterval target:self selector:@selector(autosync) userInfo:nil repeats:NO]; } - (void)submitFiles:(NSArray *)paths message:(NSString *)message receive:(P4ReceiveBlock_t)receiveBlock response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSDictionary *changelistSpecs = @{ @"Change" : @"new", @"Client" : workspace, @"User" : username, @"Description" : message, @"Files" : paths, }; [editConnection run:@"reopen" arguments:@[ @"-c", @"default", @"//..." ] response:^(P4Operation *operation, NSArray *response) { [syncConnection run:@"submit" arguments:@[ @"-f", @"revertunchanged", @"-i" ] prompt:nil input:changelistSpecs receive:receiveBlock response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; // Check if there was no files to submit NSArray *noChanges = [operation errorsWithCode:P4ErrorNoFilesToSubmit]; [operation ignoreErrors:noChanges]; // Discard shelved versions of files that were submitted if (!operation.errors && shelvedChange) [self discardShelvedFiles:paths response:nil]; // Remove created changelist if no changes if (noChanges.count && response.count) { NSDictionary *info = [response objectAtIndex:0]; NSNumber *changelistId = [info objectForKey:@"change"]; [syncConnection run:@"change" arguments:@[ @"-d", changelistId ] response:nil]; } [[NSNotificationCenter defaultCenter] postNotificationName:P4SubmitFinishedNotification object:operation]; responseBlock(operation, response); }]; }]; } #pragma mark Mapping - (NSDictionary *)mapping { return [NSDictionary dictionaryWithObjects:clientMappings forKeys:clientPaths]; } - (NSString *)mappingForPath:(NSString *)path { return [self mappingForPath:path viewIndex:&(NSInteger){ 0 } mapped:&(BOOL){ NO } have:&(BOOL){ NO }]; } - (NSString *)mappingForPath:(NSString *)path viewIndex:(NSInteger *)viewIdx mapped:(BOOL *)mapped have:(BOOL *)have { // ensure that path is encoded if([path hasSpecialCharacters]) { path = [path encodePath]; } BOOL isDir = [path hasSuffix:@"/"]; BOOL isRemote = [path hasPrefix:@"//"]; *viewIdx = NSNotFound; *mapped = *have = NO; __block BOOL tracked; __block NSString *parentMapping, *resultMapping; [clientPaths enumerateObjectsUsingBlock:^(NSString *remote, NSUInteger idx, BOOL *stop) { BOOL excluded = [remote hasPrefix:@"-"] ? (remote = [remote substringFromIndex:1], YES) : NO; NSString *local = [clientMappings objectAtIndex:idx]; NSString *pathMapping = isRemote ? remote : local; if (pathMapping.length < parentMapping.length) return; if ([path isEqualCaseInsensitive:pathMapping] ? *mapped = YES : [path isSubpath:pathMapping]) { parentMapping = pathMapping; resultMapping = isRemote ? local : remote; tracked = !excluded; *viewIdx = idx; } else if (!*have && isDir && [pathMapping isSubpath:path]) { *have = YES; } }]; if (!tracked) return nil; if (*mapped) return resultMapping; NSString *relative = [path relativePath:parentMapping]; return [resultMapping stringByAppendingString:relative]; } - (void)mappingSet:(BOOL)set files:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(workspace.length, @"P4 Mapping without workspace set"); [editConnection run:@"client" arguments:@[ @"-o", workspace ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); // Get workspace info and parse mapping [self handleClientResponse:response]; [self mappingSet:set files:paths sync:nil response:^(P4Operation *operation, NSArray *mappingResponse) { // Refresh changelist [self refreshPendingFiles]; responseBlock(operation, response); }]; }]; } #pragma mark Users - (NSDictionary *)userInfo:(NSString *)user { if (!user.length) return nil; NSInteger loc = [user rangeOfString:@"@"].location; if (loc != NSNotFound) user = [user substringToIndex:loc]; return [usersList objectForKey:user]; } #pragma mark Commands - (void)listFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(paths.count, @"Listing empty paths"); [listConnection run:@"fstat" arguments:@[ @"-A", @"tags", // Filter attributes to tags-only @"-Oahl", @"-Dah", [self pathsWithDirectoryStarSuffix:paths] ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; responseBlock(operation, response); }]; } - (void)listDepotFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(paths.count, @"Listing empty depot paths"); [listConnection run:@"fstat" arguments:@[ @"-A", @"tags", // Filter attributes to tags-only @"-Oal", @"-Da", @"-F", [NSString stringWithFormat:@"^headAction=delete & ^headAction=move/delete | actionOwner=%@ | dir", username], // @"-F", @"^headAction=delete & ^headAction=move/delete & headRev | dir", [self pathsWithDirectoryStarSuffix:paths] ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; responseBlock(operation, response); }]; } - (void)listPendingFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; [listConnection run:@"opened" arguments:@[ [self pathsWithDirectoryDotsSuffix:paths] ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); [self handlePendingResponse:response]; NSArray *files = [response valueForKey:@"depotFile"]; if (!files.count) return responseBlock(operation, response); [listConnection run:@"fstat" arguments:@[ @"-A", @"tags", @"-Oa", @"-F", @"action", // Filter by files opened in the clients workspace files] response:responseBlock]; }]; } - (void)listOpenedFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { [editConnection run:@"opened" arguments:@[ @"-s", @"-a", [self pathsWithDirectoryDotsSuffix:paths] ] response:responseBlock]; } - (void)listVersions:(NSString *)path response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(path.length, @"Listing versions of empty path"); // ensure that path is encoded if([path hasSpecialCharacters]) { path = [path encodePath]; } [listConnection run:@"filelog" arguments:@[ @"-i", @"-l", path ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; NSMutableArray *iterations = [NSMutableArray arrayWithCapacity:response.count]; for (NSDictionary *version in response) { NSInteger numberOfVersions = [[version objectForKey:@"rev0"] integerValue]; NSMutableDictionary *iteration = [NSMutableDictionary dictionary]; NSMutableArray *versions = [NSMutableArray arrayWithCapacity:numberOfVersions]; [iteration setObject:versions forKey:@"versions"]; [iterations addObject:iteration]; for (NSInteger idx = 0; idx < numberOfVersions; idx++) [versions addObject:[NSMutableDictionary dictionary]]; for (NSString *key in version) { NSInteger location = [key rangeOfCharacterFromSet: [NSCharacterSet decimalDigitCharacterSet]].location; id object = [version objectForKey:key]; if (location != NSNotFound) { NSInteger versionNumber = [[key substringFromIndex:location] integerValue]; NSMutableDictionary *dict = [versions objectAtIndex:versionNumber]; NSString *dictKey = [key substringToIndex:location]; [dict setObject:object forKey:dictKey]; } else { [iteration setObject:object forKey:key]; } } } responseBlock(operation, iterations); }]; } - (void)listVersionsDetails:(NSArray *)versionPaths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(versionPaths.count, @"Listing version details of empty paths"); [listConnection run:@"fstat" arguments:@[ @"-Oaf", // @"-F", @"^headAction=delete & ^headAction=move/delete", versionPaths ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; responseBlock(operation, response); }]; } - (void)listFolderVersions:(NSString *)path response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(path.length, @"Listing folder versions of empty path"); // ensure that path is encoded if([path hasSpecialCharacters]) { path = [path encodePath]; } [listConnection run:@"changes" arguments:@[ @"-s", @"submitted", @"-l", [path stringByAppendingPath:@"..."] ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; responseBlock(operation, response); }]; } - (void)listChangelist:(NSNumber *)number response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert([number isKindOfClass:[NSNumber class]], @"Listing change of non-NSNumber class"); [listConnection run:@"describe" arguments:@[ @"-s", number ] response:^(P4Operation *operation, NSArray *response) { NSDictionary *dictionary = [response firstObject]; NSMutableDictionary *changelistDict = [NSMutableDictionary dictionary]; NSMutableDictionary *files = [NSMutableDictionary dictionary]; [changelistDict setObject:files forKey:@"files"]; for (NSString *key in dictionary) { id object = [dictionary objectForKey:key]; NSInteger location = [key rangeOfCharacterFromSet: [NSCharacterSet decimalDigitCharacterSet]].location; if (location != NSNotFound) { NSInteger versionNumber = [[key substringFromIndex:location] integerValue]; NSString *dictKey = [key substringToIndex:location]; NSMutableDictionary *dict = [files objectForKey:@( versionNumber )]; if (!dict) { dict = [NSMutableDictionary dictionary]; [files setObject:dict forKey:@( versionNumber )]; } [dict setObject:object forKey:dictKey]; } else { [changelistDict setObject:object forKey:key]; } } NSMutableArray *array = [NSMutableArray arrayWithCapacity:files.count]; for (NSUInteger idx = 0; idx < files.count; idx++) [array addObject:[files objectForKey:@(idx)]]; [changelistDict setObject:array forKey:@"files"]; responseBlock(operation, @[ changelistDict ]); }]; } - (void)listChangelist:(NSNumber *)number path:(NSString *)path response:(P4ResponseBlock_t)responseBlock { NSAssert(path.length, @"Listing change of empty path"); NSAssert([number isKindOfClass:[NSNumber class]], @"Listing change of non-NSNumber class"); // ensure that path is encoded if([path hasSpecialCharacters]) { path = [path encodePath]; } NSString *suffix = [NSString stringWithFormat:@"...@%1$@,@%1$@", number]; [listConnection run:@"files" arguments:@[ [path stringByAppendingPath:suffix] ] response:responseBlock]; } - (NSDictionary *)changelist { return changelist; } - (NSArray *)changesForPath:(NSString *)path { // ensure that path is encoded if([path hasSpecialCharacters]) { path = [path encodePath]; } if (changelist.count) { NSArray *changelistArray = [changelist.allValues valueForKeyPath:@"@unionOfArrays.self"]; if ([path hasSuffix:@"/"]) { // Directory NSMutableArray *change = [NSMutableArray array]; for (NSString *changePath in changelistArray) if ([changePath isSubpath:path]) [change addObject:changePath]; return change.count ? change : nil; } else { for (NSString *changePath in changelistArray) if ([changePath isEqualCaseInsensitive:path]) return @[ changePath ]; } } return nil; } #pragma mark File Management - (void)editFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(paths.count, @"Editing empty file list"); paths = [self pathsWithEncodingChecked:paths]; P4Operation *mappingOp = [self mappingSet:YES files:paths sync:[self pathsWithDirectoryDotsSuffix:paths] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); }]; mappingOp.name = @"edit"; P4Operation *editOp = [editConnection run:@"edit" arguments:[self pathsWithDirectoryDotsSuffix:paths] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorAlreadyEdited]; [operation ignoreErrorsWithCode:P4ErrorAlreadyOpened]; responseBlock(operation, response); }]; editOp.parentOperation = mappingOp; editOp.name = @"edit"; } - (void)addFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(paths.count, @"Adding empty file list"); // ALAN // // ensure that paths are encoded // paths = [self pathsWithEncodingChecked:paths]; paths = [self pathsDecoded:paths]; P4Operation *mappingOp = [self mappingSet:YES files:paths sync:nil response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); }]; mappingOp.name = @"add"; NSMutableArray *contents = [NSMutableArray array]; // add the "-f" attribute to allow for filenames with special %#@* characters [contents addObject:@"-f"]; P4Operation *directoryOp = [editConnection runBlock:^(P4ThreadOperation *operation) { // Get recursively all paths contents NSArray *dirContents = [self contentOfDirectories:paths]; [contents addObjectsFromArray:dirContents]; /* Alternative way using reconcile */ /* [self appendDirectorySuffixes:&paths]; */ }]; directoryOp.parentOperation = mappingOp; directoryOp.name = @"add"; P4Operation *addOp = [editConnection run:@"add" /* @"reconcile" */ arguments:contents /* @[ @"-a", paths ] */ response:^(P4Operation *addOperation, NSArray *response) { [self handleChangelistResponse:response]; // Revert already added files NSMutableArray *alreadyAdded = [NSMutableArray array]; [alreadyAdded addObjectsFromArray:[addOperation errorsWithCode:P4ErrorAddExistingFile]]; [alreadyAdded addObjectsFromArray:[addOperation errorsWithCode:P4ErrorAddExisting]]; if (!alreadyAdded.count) return responseBlock(addOperation, response); NSMutableArray *revertPaths = [NSMutableArray array]; for (NSError *error in alreadyAdded) [revertPaths addObject:error.localizedFailureReason]; [addOperation ignoreErrors:alreadyAdded]; // Reconcile already added paths [self reconcileFiles:revertPaths response:responseBlock]; }]; addOp.parentOperation = directoryOp; addOp.name = @"add"; } - (void)removeFiles:(NSArray *)removeFiles response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(removeFiles.count, @"Removing empty file list"); P4Operation *mappingOp = [self mappingSet:YES files:removeFiles sync:nil response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); }]; mappingOp.name = @"remove"; NSMutableArray *paths = [NSMutableArray array]; NSMutableArray *unmapPaths = [NSMutableArray array]; // Translate local paths and find mappings to remove [removeFiles enumerateObjectsUsingBlock:^(NSString *path, NSUInteger idx, BOOL *stop) { if (![path hasPrefix:@"//"]) path = [@"//" stringByAppendingString:[path relativePath:root]]; [clientPaths enumerateObjectsUsingBlock:^(NSString *clientPath, NSUInteger idx, BOOL *stop) { if ([clientPath hasPrefix:@"-"]) clientPath = [clientPath substringFromIndex:1]; if ([clientPath isEqualCaseInsensitive:path] ?: [path hasSuffix:@"/"] && [clientPath isSubpath:path]) [unmapPaths addObject:clientPath]; }]; [paths addObject:path]; }]; paths = [[self pathsWithDirectoryDotsSuffix:paths] mutableCopy]; paths = [[self pathsWithEncodingChecked:paths] mutableCopy]; // Discard shelved files if (shelvedList.count) [self discardShelvedFiles:paths response:nil]; // Remove all changes before deleting P4Operation *revertOp = [editConnection run:@"revert" arguments:@[ @"-k", paths ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorFileNotOpened]; if (operation.errors) return responseBlock(operation, response); // Get moved files that were deleted for (NSDictionary *item in response) { if ([[item objectForKey:@"oldAction"] isEqualToString:@"move/delete"]) { NSString *path = [item objectForKey:@"clientFile"]; if (![paths containsObject:path]) [paths addObject:path]; // Add moved paths to delete } } }]; revertOp.parentOperation = mappingOp; revertOp.name = @"remove"; P4Operation *deleteOp = [editConnection run:@"delete" arguments:@[ @"-v", paths ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorFileNotOnClient]; responseBlock(operation, response); if (!unmapPaths.count) return; // Revert temporary mapping P4Operation *revertMappingOp = [self mappingSet:NO files:unmapPaths sync:nil response:nil]; revertMappingOp.name = @"remove"; }]; deleteOp.parentOperation = revertOp; deleteOp.name = @"remove"; } - (void)moveFiles:(NSArray *)paths toPaths:(NSArray *)toPaths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(paths.count, @"Moving empty file list"); NSAssert(paths.count == toPaths.count, @"Move should have equal number of from-to paths"); P4ThreadOperation *finishOperation = [[P4ThreadOperation alloc] init]; finishOperation.name = @"move finished"; [finishOperation setResponseBlock:responseBlock]; NSArray *pathsWithToPaths = [NSArray array]; pathsWithToPaths = [paths arrayByAddingObjectsFromArray:[self pathsWithEncodingChecked:toPaths]]; P4Operation *mappingOp = [self mappingSet:YES files:pathsWithToPaths //[paths arrayByAddingObjectsFromArray:toPaths] sync:@[ @"-k", [self pathsWithDirectoryDotsSuffix:paths] ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) responseBlock(operation, response); }]; mappingOp.name = @"move"; paths = [self pathsWithDirectoryDotsSuffix:paths]; toPaths = [self pathsWithDirectoryDotsSuffix:toPaths]; paths = [self pathsWithEncodingChecked:paths]; toPaths = [self pathsWithEncodingChecked:toPaths]; // Open for edit NSArray *parthsForEditOperation = [self pathsWithEncodingChecked:paths]; P4Operation *editOp = [editConnection run:@"edit" arguments:@[ @"-k", parthsForEditOperation ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrors:operation.errors]; }]; editOp.parentOperation = mappingOp; editOp.name = @"move"; [finishOperation addDependency:editOp]; // // TODO: Add support for moving into locations marked for delete // // Move command performs only one move at once NSInteger idx = 0; for (NSString *path in paths) { NSString *fromFile = path; NSString *toFile = [toPaths objectAtIndex:idx++]; if([toFile hasSpecialCharacters]) { toFile = [toFile encodePath]; } NSAssert([fromFile hasSuffix:@"/..."] == [toFile hasSuffix:@"/..."], @"Moving folder to file"); P4Operation *op = [editConnection run:@"move" arguments:@[ @"-k", fromFile, toFile ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; NSArray *adds = ([operation errorsWithCode:P4ErrorFileNotInView] ?: [operation errorsWithCode:P4ErrorPathNotUnderRoot]); NSArray *deletes = [operation errorsWithCode:P4ErrorDestinationNotInView]; NSArray *overwrites = [operation errorsWithCode:P4ErrorMoveToExistingFile]; [operation ignoreErrors:adds]; [operation ignoreErrors:deletes]; [operation ignoreErrors:overwrites]; if (operation.errors) { // Pass errors to finish operation for (NSError *error in operation.errors) [finishOperation addError:error]; return; } // Adds if (adds.count) [self addFiles:@[ [toFile stringByRemovingSuffix:@"..."] ] response:nil]; // Deletes if (deletes.count) [self removeFiles:@[ fromFile ] response:nil]; // Overwrites if (overwrites.count && ![fromFile isEqualCaseInsensitive:toFile]) { // Ignore renaming character case [self reconcileFiles:@[ toFile ] response:nil]; [self removeFiles:@[ fromFile ] response:nil]; } [editConnection run:@"sync" arguments:@[ toPaths ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrors:operation.errors]; }].name = @"move"; }]; op.name = @"move"; op.parentOperation = editOp; [finishOperation addDependency:op]; }; [editConnection runOperation:finishOperation]; } - (void)revertFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; paths = [self pathsWithEncodingChecked:paths]; [editConnection run:@"revert" arguments:[self pathsWithDirectoryDotsSuffix:paths] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; responseBlock(operation, response); // TODO: Add error checking }].name = @"revert"; } - (void)revertUnchangedFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; // revert cannot contain special chars, paths need to be encoded paths = [self pathsWithEncodingChecked:paths]; [editConnection run:@"revert" arguments:@[ @"-a", [self pathsWithDirectoryDotsSuffix:paths] ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; responseBlock(operation, response); // TODO: Add error checking }].name = @"revert unchanged"; } - (void)revertToRevision:(NSString *)revisionPath path:(NSString *)path response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; P4Operation *mappingOp = [self mappingSet:YES files:@[ path ] sync:@[ @"-k", path ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); }]; mappingOp.name = @"revert revision"; // Sync previous revision P4Operation *syncOp = [editConnection run:@"sync" arguments:@[ @"-f", revisionPath ] response:^(P4Operation *mainOperation, NSArray *mainResponse) { // Revert by syncing latest revision if (mainOperation.errors) { // Sync previous revision [editConnection run:@"sync" arguments:@[ @"-f", path ] response:^(P4Operation *operation, NSArray *response) { responseBlock(mainOperation, mainResponse); }]; return; } // Parse paths NSArray *added, *updated, *deleted; NSDictionary *recordsPaths = @{ @"added" : added = [NSMutableArray array], @"updated" : updated = [NSMutableArray array], @"deleted" : deleted = [NSMutableArray array], }; for (NSDictionary *record in mainResponse) { NSString *recordPath = [record objectForKey:@"depotFile"]; NSString *recordAction = [record objectForKey:@"action"]; NSMutableArray *paths = [recordsPaths objectForKey:recordAction]; if (!paths) PSLog(@"Unknown revert to changelist response %@", recordAction); [paths addObject:recordPath]; } // Add files P4Operation *addOperation = nil; if(added.count) { NSMutableArray *args = [NSMutableArray array]; // add the "-f" attribute to allow for filenames with special %#@* characters [args addObject:@"-f"]; NSMutableArray *tmpPaths = [NSMutableArray array]; for (NSString *addedPath in added) { if([addedPath hasPrefix:@"//"]) { [tmpPaths addObject:[addedPath decodePath]]; // if add contains remote paths, we need to decode previously encoded path // and provide the -f argument as described in perforce documentation } else { [tmpPaths addObject:addedPath]; } } [args addObject:tmpPaths]; addOperation = [editConnection run:@"add" arguments:args response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorAddExistingFile]; [operation ignoreErrorsWithCode:P4ErrorAddExisting]; }]; } // Edit files P4Operation *editOperation = updated.count ? [editConnection run:@"edit" arguments:updated response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorMustSyncResolve]; }] : nil; // Sync newest revision P4Operation *syncNewestOperation = [editConnection run:@"sync" arguments:@[ @"-f", path ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorSyncFileOpened]; [operation ignoreErrorsWithCode:P4ErrorMustResolve]; if (editOperation.errors) return responseBlock(editOperation, editOperation.response); if (addOperation.errors) return responseBlock(addOperation, addOperation.response); // Delete files P4Operation *deleteOperation = deleted.count ? [editConnection run:@"delete" arguments:@[ deleted ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorFileNotOnClient]; }] : nil; P4Operation *resolveOperation = [editConnection run:@"resolve" arguments:@[ @"-ay", path ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; if (deleteOperation.errors) { responseBlock(deleteOperation, deleteOperation.response); return; } responseBlock(mainOperation, mainResponse); }]; [resolveOperation addDependency:deleteOperation]; }]; [syncNewestOperation addDependency:addOperation]; [syncNewestOperation addDependency:editOperation]; }]; syncOp.name = @"revert revision"; syncOp.parentOperation = mappingOp; } - (void)openRevision:(NSString *)revisionPath response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSRange range = [revisionPath rangeOfString:@"#" options:NSBackwardsSearch]; NSString *version = [revisionPath substringFromIndex:range.location]; NSString *filePath = [revisionPath substringToIndex:range.location]; NSString *filename = [[filePath lastPathComponent] stringByDeletingPathExtension]; filename = [filename stringByAppendingString:version]; filename = [filename stringByAppendingPathExtension:[filePath pathExtension]]; NSString *tmp = [NSString stringWithFormat:@"/tmp/p4/%@/%@", workspace, filename]; [listConnection run:@"print" arguments:@[ @"-o", tmp, revisionPath ] response:^(P4Operation *operation, NSArray *response) { responseBlock(operation, response); if (!operation.errors) [[NSWorkspace sharedWorkspace] openFile:tmp]; }].name = @"open revision"; } - (void)reconcileFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(paths.count, @"Reconciling empty paths"); paths = [self pathsWithDirectoryDotsSuffix:paths]; paths = [self pathsDecoded:paths]; NSArray *pathsForSyncAndRevert = [self pathsWithEncodingChecked:paths]; P4Operation *revertOp = [editConnection run:@"revert" arguments:@[ @"-k", pathsForSyncAndRevert ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; }]; revertOp.name = @"reconcile"; P4Operation *syncOp = [editConnection run:@"sync" arguments:@[ @"-k", pathsForSyncAndRevert ] response:nil]; [syncOp addDependency:revertOp]; syncOp.name = @"reconcile"; NSMutableArray *pathsForReconcile = [NSMutableArray array]; for (NSString *pat in pathsForSyncAndRevert) { [pathsForReconcile addObject:[pat decodePath]]; } P4Operation *reconcileOp = [editConnection run:@"reconcile" arguments:@[ @"-eadf", pathsForReconcile ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; [operation ignoreErrorsWithCode:P4ErrorReconcile]; responseBlock(operation, response); }]; [reconcileOp addDependency:syncOp]; reconcileOp.name = @"reconcile"; } - (void)resolveFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert(paths.count, @"Resolving empty paths"); [editConnection run:@"sync" arguments:@[ @"-f", paths ] response:^(P4Operation *operation, NSArray *response) { NSArray *resolveErrors = [operation errorsWithCode:P4ErrorMustResolve]; NSArray *resolvePaths = [resolveErrors valueForKeyPath:@"localizedFailureReason"]; [operation ignoreErrors:resolveErrors]; [operation ignoreErrorsWithCode:P4ErrorSyncFileOpened]; // Finish only if resolve path is not acquired if (!resolvePaths.count) return responseBlock(operation, response); [editConnection run:@"resolve" arguments:@[ @"-o", @"-ay", resolvePaths ] response:responseBlock]; }]; } - (void)setAttribute:(NSString *)name value:(NSString *)value paths:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; [editConnection run:@"edit" arguments:paths response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; NSArray *args = value ? @[ @"-n", name, @"-v", value, paths ] : @[ @"-n", name, paths ]; [editConnection run:@"attribute" arguments:args response:^(P4Operation *operation, NSArray *response) { responseBlock(operation, response); }]; }]; } - (P4Operation *)downloadPath:(NSString *)depotPath destination:(NSString *)path receive:(P4ReceiveBlock_t)receiveBlock response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; NSAssert([depotPath hasPrefix:@"//"], @"Expecting depot path"); NSAssert(![path hasPrefix:@"//"], @"Expecting local path"); // local path should be always decoded path = [path decodePath]; P4ThreadOperation *finishOperation = [[P4ThreadOperation alloc] init]; finishOperation.name = [NSString stringWithFormat:@"download finished %@", depotPath]; [finishOperation setResponseBlock:responseBlock]; // List files to download P4Operation *listFilesOp = [listConnection run:@"fstat" arguments: @[ @"-Ol", @"-F", @"^headAction=delete & ^headAction=move/delete & headRev", [depotPath hasSuffix:@"/"] ? [depotPath stringByAppendingString:@"..."] : depotPath ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; if (operation.errors) { // Pass errors to finish operation for (NSError *error in operation.errors) [finishOperation addError:error]; return; } // Queue file download operations for (NSDictionary *file in response) { NSString *depotFile = [file objectForKey:@"depotFile"]; NSString *relativePath = [depotFile relativePath:depotPath]; NSString *filepath = [path stringByAppendingPathComponent:[relativePath decodePath]]; NSString *directory = [filepath stringByDeletingLastPathComponent]; // Create file's parent directories if not exist if (![filemanager fileExistsAtPath:directory]) [filemanager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:NULL]; P4Operation *downloadFileOp = [listConnection run:@"print" arguments:@[ @"-o", filepath, depotFile ] prompt:nil input:nil receive:receiveBlock response:nil]; downloadFileOp.name = @"download"; [finishOperation addDependency:downloadFileOp]; // Add as dependency for finishOperation } }]; listFilesOp.name = @"download"; [finishOperation addDependency:listFilesOp]; // Add as dependency for finishOperation // Run finishOperation [listConnection runOperation:finishOperation]; return finishOperation; } - (P4Operation *)calculateSizeOfPaths:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { NSAssert(paths.count, @"Calculating size of empty paths"); P4Operation *operation = [listConnection run:@"sizes" arguments:@[ @"-s", [self pathsWithDirectoryDotsSuffix:paths] ] response:responseBlock]; return operation; } #pragma mark - Check opened files // this method looks at the opened files to determine if there are any files that have // been deleted from the local filesystem (e.g. if DESI was shut down and files were // removed) this will cause problems for files that are marked add, edit, or move/add - (void)checkOpenedFiles:(P4ResponseBlock_t)responseBlock { PSLog(@"Checking opened files"); P4ThreadOperation *finishOperation = [[P4ThreadOperation alloc] init]; finishOperation.name = @"finished checking opened files"; [finishOperation setResponseBlock:responseBlock]; [finishOperation addDependency:[listConnection run:@"opened" arguments:nil response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; NSArray *actions = [NSArray arrayWithObjects:@"edit", @"add", @"move/add", nil]; for (NSDictionary *dict in response) { // only worry about items with the actions edit, add or move/add NSString *action = [dict objectForKey:@"action"]; if(action && [actions containsObject:action]) { NSString *depotFile = [dict objectForKey:@"depotFile"]; if(depotFile) { P4Operation *fileOperation = nil; if([action caseInsensitiveCompare:@"edit"] == NSOrderedSame) { // EDIT ACTION fileOperation = [listConnection run:@"fstat" arguments:@[@"-T clientFile", depotFile] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; NSString *clientFile = nil; for (NSDictionary *dict in response) { clientFile = [dict objectForKey:@"clientFile"]; } if(![[NSFileManager defaultManager] fileExistsAtPath:clientFile]) { // file no longer exists locally -- reconcile (which will change it from edit to delete) PSLog(@"-- found file marked for edit that has been deleted. reconciling..."); [editConnection run:@"reconcile" arguments:@[ @"-eadf", depotFile ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; }]; } }]; } else if([action caseInsensitiveCompare:@"add"] == NSOrderedSame) { // ADD ACTION fileOperation = [listConnection run:@"fstat" arguments:@[@"-T clientFile", depotFile] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; NSString *clientFile = nil; for (NSDictionary *dict in response) { clientFile = [dict objectForKey:@"clientFile"]; } if(![[NSFileManager defaultManager] fileExistsAtPath:clientFile]) { // file no longer exists locally -- revert the item PSLog(@"-- found file marked for add that has been deleted. reverting..."); [editConnection run:@"revert" arguments:@[ @"-k", depotFile ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; }]; } }]; } else if([action caseInsensitiveCompare:@"move/add"] == NSOrderedSame) { // MOVE/ADD ACTION fileOperation = [listConnection run:@"fstat" arguments:@[@"-T clientFile,movedFile", depotFile] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; NSString *clientFile = nil; NSString *movedFile = nil; for (NSDictionary *dict in response) { clientFile = [dict objectForKey:@"clientFile"]; movedFile = [dict objectForKey:@"movedFile"]; } if(![[NSFileManager defaultManager] fileExistsAtPath:clientFile]) { // file no longer exists locally -- revert the item, then delete the original PSLog(@"-- found file marked for add that has been deleted. reverting..."); [editConnection run:@"revert" arguments:@[ @"-k", depotFile ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; [editConnection run:@"delete" arguments:@[ @"-k", movedFile ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; }]; }]; } }]; } if(fileOperation) { [finishOperation addDependency:fileOperation]; } } } } }]]; // Run finishOperation [listConnection runOperation:finishOperation]; } #pragma mark - Refresh - (void)refreshMapping { [listConnection run:@"client" arguments:@[ @"-o", workspace ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; [self handleClientResponse:response]; }].name = @"refresh mapping"; } - (void)refreshPendingFiles { [listConnection run:@"opened" arguments:nil response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; [self handlePendingResponse:response]; }].name = @"refresh pending"; } - (void)refreshShelvedFiles { [listConnection run:@"changes" arguments:@[ @"-u", username, @"-s", @"shelved" ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; NSNumber *change = nil; for (NSDictionary *dict in response) { NSString *client = [dict objectForKey:@"client"]; NSString *desc = [dict objectForKey:@"desc"]; if ([desc hasPrefix:P4ShelvedChangelistDescription]) { change = [dict objectForKey:@"change"]; if ([client isEqualToString:workspace]) break; } } if (![change isKindOfClass:[NSNumber class]]) { shelvedChange = nil; shelvedList = nil; return; } [listConnection run:@"fstat" arguments:@[ @"-Rs", @"-e", change, @"..." ] response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; shelvedList = [NSMutableArray arrayWithCapacity:response.count]; for (NSDictionary *dict in response) { NSString *depotPath = [dict objectForKey:@"depotFile"]; if (depotPath && ![shelvedList containsObject:depotPath]) [shelvedList addObject:depotPath]; } shelvedChange = change; }]; }].name = @"refresh shelved"; } - (void)refreshUsers { [listConnection run:@"users" arguments:nil response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return; NSMutableDictionary *users = [NSMutableDictionary dictionaryWithCapacity:response.count]; for (NSDictionary *user in response) { NSString *name = [user objectForKey:@"User"]; if (name.length) [users setObject:user forKey:name]; } usersList = users; }].name = @"refresh users"; } #pragma mark Shelving - (NSArray *)shelvedList { return shelvedList; } - (NSArray *)shelvedForPath:(NSString *)path { if (shelvedList.count) { if ([path hasSuffix:@"/"]) { // Directory NSMutableArray *shelved = [NSMutableArray array]; for (NSString *shelvedPath in shelvedList) if ([shelvedPath isSubpath:path]) [shelved addObject:shelvedPath]; return shelved.count ? shelved : nil; } else { for (NSString *shelvedPath in shelvedList) if ([shelvedPath isEqualCaseInsensitive:path]) return @[ shelvedPath ]; } } return nil; } - (void)listShelvedFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; if (!shelvedChange) return responseBlock(nil, nil); [listConnection run:@"fstat" arguments:@[ @"-Oa", @"-Rs", @"-e", shelvedChange, paths ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; responseBlock(operation, response); }]; } - (void)shelveFile:(NSString *)path response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; // Mapping local paths into depot if (![path hasPrefix:@"//"]) path = [self mappingForPath:path]; NSMutableArray *list = [shelvedList mutableCopy] ?: [NSMutableArray array]; if (![list containsObject:path]) [list addObject:path]; P4ResponseBlock_t shelveBlock = ^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorShelveExclusive]; if (!operation.errors) { NSDictionary *dict = [response lastObject]; NSNumber *change = [dict objectForKey:@"change"]; if ([change isKindOfClass:[NSNumber class]]) shelvedChange = change; shelvedList = list; // Notify observers for (id target in [self observers]) { if ([target respondsToSelector:@selector(file:shelved:)]) [target file:path shelved:dict]; } } responseBlock(operation, response); }; if (shelvedChange) { // Append shelved file to shelved changelist [editConnection run:@"reopen" arguments:@[ @"-c", shelvedChange, path ] response:^(P4Operation *operation, NSArray *response) { [editConnection run:@"shelve" arguments:@[ @"-f", @"-c", shelvedChange, path ] response:shelveBlock]; }]; } else { // Create new shelved changelist [editConnection run:@"shelve" arguments:@[ @"-i" ] prompt:nil input:@{ @"Change" : @"new", @"Client" : workspace, @"User" : username, @"Description" : P4ShelvedChangelistDescription, @"Files" : list, } receive:nil response:shelveBlock]; } } - (void)unshelveFile:(NSString *)path response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; // Mapping local paths into depot if (![path hasPrefix:@"//"]) path = [self mappingForPath:path]; [editConnection run:@"unshelve" arguments:@[ @"-s", shelvedChange, path ] response:^(P4Operation *operation, NSArray *response) { [self handleChangelistResponse:response]; NSArray *resolveErrors = [operation errorsWithCode:P4ErrorMustSubmitResolve]; NSString *resolvePath = [[resolveErrors lastObject] localizedFailureReason]; [operation ignoreErrors:resolveErrors]; if (!operation.errors && resolvePath.length) { // Resolve [editConnection run:@"resolve" arguments:@[ @"-o", @"-at", resolvePath ] response:^(P4Operation *operation, NSArray *response) { responseBlock(operation, response); }]; } else { // Errors or resolving not needed responseBlock(operation, response); } }]; } - (void)discardShelvedFiles:(NSArray *)shelvedPaths response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; if (!shelvedChange) return responseBlock(nil, nil); if (!shelvedPaths.count) return responseBlock(nil, nil); NSMutableArray *paths = [NSMutableArray array]; for (NSString *path in shelvedPaths) [paths addObject:[path hasPrefix:@"//"] ? path : [self mappingForPath:path]]; [editConnection run:@"shelve" arguments:@[ @"-c", shelvedChange, @"-d", paths ] response:^(P4Operation *operation, NSArray *response) { if (!operation.errors) { [shelvedList removeObjectsInArray:paths]; // Notify observers for (id target in [self observers]) { if ([target respondsToSelector:@selector(file:shelved:)]) for (NSString *path in paths) [target file:path shelved:nil]; } } if (shelvedList.count) return responseBlock(operation, response); // Move all files to default changelist [editConnection run:@"reopen" arguments:@[ @"-c", @"default", @"//..." ] response:^(P4Operation *operation, NSArray *response) { // Delete empty changelist [editConnection run:@"change" arguments:@[ @"-d", shelvedChange ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorChangeDeleted]; if (!operation.errors) { shelvedChange = nil; shelvedList = nil; } responseBlock(operation, response); }]; }]; }]; } - (void)openShelvedFile:(NSString *)path response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; if (!shelvedChange) return responseBlock(nil, nil); NSString *tmpPath = [NSString stringWithFormat:@"/tmp/p4/%@/%@", workspace, path.lastPathComponent]; [listConnection run:@"print" arguments:@[ @"-o", tmpPath, [path stringByAppendingFormat:@"@=%@", shelvedChange] ] response:^(P4Operation *operation, NSArray *response) { responseBlock(operation, response); if (!operation.errors) [[NSWorkspace sharedWorkspace] openFile:tmpPath]; }]; } #pragma mark Search - (P4NetworkOperation *)searchFiles:(NSString *)search path:(NSString *)path response:(P4ResponseBlock_t)responseBlock { // Make query NSString *query = search; PSLog(@"Search query : %@", query); PSLog(@"Search ticket : %@\nConnection ticket : %@", searchTicket, [editConnection ticket]); // Create payload NSString *ticket = searchTicket ?: [editConnection ticket]; NSString *method = @"POST"; NSDictionary *headers = @{ @"Content-Type" : @"application/json" }; NSDictionary *JSON = @{ @"userId" : username, @"ticket" : ticket, @"query" : query, @"paths" : path ? @[ path ] : @[ ], @"rowCount" : @(500), @"searchFields" : @[ // @{ @"field" : @"p4attr_tags", @"value" : tags ?: @"" } ], }; // Make body JSON NSError *error; NSData *body = !JSON ? nil : [NSJSONSerialization dataWithJSONObject:JSON options:NSJSONWritingPrettyPrinted error:&error]; P4NetworkOperation *operation; operation = [P4NetworkOperation operationWithUrl:searchUrl HTTPMethod:method HTTPHeaders:headers HTTPBody:body receive:nil completion:^(P4NetworkOperation *operation) { NSArray *payload = nil; if (operation.success && operation.data) { NSString *MIMEType = [operation.MIMEType lowercaseString]; if ([MIMEType isEqualToString:@"application/json"]) { NSError *error; NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:operation.data options:NSJSONReadingMutableContainers error:&error]; if (JSON) { payload = [JSON objectForKey:@"payload"]; NSDictionary *status = [JSON objectForKey:@"status"]; NSInteger code = [[status objectForKey:@"code"] integerValue]; if (code != 200) operation.error = [NSError errorWithFormat:@"%@", [status objectForKey:@"message"]]; } else { operation.error = error; } } else if ([MIMEType isEqualToString:@"text/plain"]) { operation.error = [NSError errorWithFormat:@"%@", [[NSString alloc] initWithData:operation.data encoding:NSUTF8StringEncoding]]; } else { operation.error = [NSError errorWithFormat:@"Server " "returned unrecognized MIME %@", operation.MIMEType]; PSLog(@"Response: %@", [[NSString alloc] initWithData:operation.data encoding:NSUTF8StringEncoding]); } } responseBlock((id)operation, payload); }]; // Simulate connection error when something's wrong with JSON if (JSON && !body) { operation.error = error; [operation cancel]; } else { [listConnection runOperation:operation]; } return operation; } #pragma mark Unread - (NSArray *)unreadlist { return unreadList; } - (NSArray *)unreadForPath:(NSString *)path { if (unreadList.count) { if ([path hasSuffix:@"/"]) { // Directory NSMutableArray *unread = [NSMutableArray array]; for (NSString *unreadPath in unreadList) if ([unreadPath isSubpath:path]) [unread addObject:unreadPath]; return unread.count ? unread : nil; } else { for (NSString *unreadPath in unreadList) if ([unreadPath isEqualCaseInsensitive:path]) return @[ unreadPath ]; } } return nil; } - (void)listUnreadFiles:(NSString *)path response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; if (!path) path = root; [listConnection runBlock:^(P4ThreadOperation *operation) { // Check if unread files still exists NSIndexSet *nonexistent = [unreadList indexesOfObjectsPassingTest:^BOOL(NSString *path, NSUInteger idx, BOOL *stop) { return ![filemanager fileExistsAtPath:path]; }]; // Remove non-existing records if (nonexistent.count) [unreadList removeObjectsAtIndexes:nonexistent]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // Post notification [[NSNotificationCenter defaultCenter] postNotificationName:P4UnreadUpdatedNotification object:self]; if (!unreadList.count) return responseBlock(nil, nil); NSArray *encodedUnreadList = [self pathsWithEncodingChecked:unreadList]; [listConnection run:@"fstat" arguments:@[ @"-A", @"tags", // Filter attributes to tags-only @"-Oah", encodedUnreadList ] response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; responseBlock(operation, response); }]; }]; }]; } - (void)markFilesRead:(NSArray *)paths { if (!unreadList.count || !paths.count) return; for (NSString *path in paths) { if ([path hasSuffix:@"/"]) { // Directory NSIndexSet *indexes; indexes = [unreadList indexesOfObjectsPassingTest: ^BOOL(NSString *unreadPath, NSUInteger idx, BOOL *stop) { return [unreadPath isSubpath:path]; }]; [unreadList removeObjectsAtIndexes:indexes]; } else { [unreadList removeObject:path]; } } [[NSNotificationCenter defaultCenter] postNotificationName:P4UnreadUpdatedNotification object:self]; } #pragma mark Generic - (void)runCommand:(NSString *)command response:(P4ResponseBlock_t)responseBlock { [listConnection run:command response:responseBlock]; } #pragma mark - Private - (void)autosync { PSLog(@"Starting auto-sync..."); [self syncWorkspace:nil response:nil]; } - (void)handleClientResponse:(NSArray *)response { clientSpecs = [[response lastObject] mutableCopy]; workspace = [clientSpecs objectForKey:@"Client"]; root = [[clientSpecs objectForKey:@"Root"] directoryPath]; NSString *clientPrefix = [NSString stringWithFormat:@"//%@/", workspace]; clientViews = [NSMutableArray array]; clientPaths = [NSMutableArray array]; clientMappings = [NSMutableArray array]; NSString *view, *key; for (NSInteger idx = 0; (key = [NSString stringWithFormat:@"View%ld", idx], view = [clientSpecs objectForKey:key]); idx++) { NSArray *args = [view arrayOfArguments]; NSAssert(args.count == 2, @"P4 Mapping has incorrect view value"); NSString *mappingPath = [args objectAtIndex:0]; mappingPath = [mappingPath stringByRemovingSuffix:@"..."]; mappingPath = [mappingPath stringByRemovingSuffix:@"*"]; NSString *localPath = [args objectAtIndex:1]; localPath = [localPath stringByRemovingSuffix:@"..."]; localPath = [localPath stringByRemovingSuffix:@"*"]; localPath = [localPath relativePath:clientPrefix]; localPath = [root stringByAppendingPath:localPath]; [clientViews addObject:view]; [clientPaths addObject:mappingPath]; [clientMappings addObject:localPath]; [clientSpecs removeObjectForKey:key]; // Remove View%d entry in specs } [[NSNotificationCenter defaultCenter] postNotificationName:P4MappingUpdatedNotification object:self]; } - (void)handleChangelistResponse:(NSArray *)response { if (!response) return; // Flush file events so they'll appear before p4 events [fileEventsUI flush]; BOOL submit = NO; NSDictionary *submitHeader = [response firstObject]; if ([submitHeader isKindOfClass:[NSDictionary class]] && [submitHeader objectForKey:@"openFiles"]) submit = YES; for (NSDictionary *dict in response) { if (![dict isKindOfClass:[NSDictionary class]]) continue; NSString *action = [dict objectForKey:@"action"]; if (!action) continue; // Check if reverted NSString *oldAction = [dict objectForKey:@"oldAction"]; BOOL revert = oldAction || submit; // Update changelist counters NSString *pendingAction = oldAction ?: action; NSMutableArray *changelistFiles = [changelist objectForKey:pendingAction]; if (revert) [changelistFiles removeObject:[dict objectForKey:@"depotFile"]]; else [changelistFiles addObject:[dict objectForKey:@"depotFile"]]; // Update changelist after move NSString *fromFile = [dict objectForKey:@"fromFile"]; if (fromFile) { [[changelist objectForKey:@"edit"] removeObject:fromFile]; [[changelist objectForKey:@"add"] removeObject:fromFile]; [[changelist objectForKey:@"move/add"] removeObject:fromFile]; if ([action isEqualToString:@"move/add"]) [[changelist objectForKey:@"move/delete"] addObject:fromFile]; } NSString *path = ([dict objectForKey:@"path"] ?: [dict objectForKey:@"clientFile"] ?: [dict objectForKey:@"depotFile"]); // Update unread list BOOL updated = [@[ @"added", @"updated" ] containsObject:action]; if (updated) { if (![unreadList containsObject:path]) [unreadList addObject:path]; } // Notify observers if (updated) { for (id target in [self observers]) if ([target respondsToSelector:@selector(file:updated:)]) [target file:path updated:dict]; } else if (revert) { for (id target in [self observers]) if ([target respondsToSelector:@selector(file:revertedAction:)]) [target file:path revertedAction:dict]; } else { for (id target in [self observers]) if ([target respondsToSelector:@selector(file:actionChanged:)]) [target file:path actionChanged:dict]; } } [[NSNotificationCenter defaultCenter] postNotificationName:P4ChangelistUpdatedNotification object:self]; [[NSNotificationCenter defaultCenter] postNotificationName:P4UnreadUpdatedNotification object:self]; } - (void)handlePendingResponse:(NSArray *)response { changelist = @{ @"edit" : [NSMutableArray array], @"add" : [NSMutableArray array], @"delete" : [NSMutableArray array], @"move/add" : [NSMutableArray array], @"move/delete" : [NSMutableArray array], }; for (NSDictionary *dict in response) { NSString *action = [dict objectForKey:@"action"]; NSMutableArray *changelistFiles = [changelist objectForKey:action]; [changelistFiles addObject:[dict objectForKey:@"depotFile"]]; } [[NSNotificationCenter defaultCenter] postNotificationName:P4ChangelistUpdatedNotification object:self]; } //- (NSArray *)mappingFiles:(NSArray *)paths onClient:(BOOL)mapped { // NSMutableArray *mappings = [NSMutableArray array]; // for (__strong NSString *path in paths) { // if (![path hasPrefix:@"//"]) { // NSString *relative = [path relativePath:root]; // if (!relative) // continue; // path = [@"//" stringByAppendingString:relative]; // } // if (![self mappingForPath:path] != mapped) // [mappings addObject:path]; // } // return mappings.count ? mappings : nil; //} - (P4Operation *)mappingSet:(BOOL)set files:(NSArray *)paths sync:(NSArray *)syncArgs response:(P4ResponseBlock_t)responseBlock { responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ }; // Find mapping changes NSMutableArray *changes = [NSMutableArray array]; for (__strong NSString *path in paths) { if (![path hasPrefix:@"//"]) { NSString *relative = [path relativePath:root]; NSAssert(relative, @"Mapping of path outside workspace root"); path = [@"//" stringByAppendingString:relative]; } path = [path encodePath]; BOOL isDir = [path hasSuffix:@"/"]; NSString *relativePath = [path relativePath:@"//"]; NSString *localPath = [[root stringByAppendingString:relativePath] decodePath]; NSString *clientPath = [NSString stringWithFormat:@"//%@/%@", workspace, relativePath]; BOOL mapped; NSInteger idx; NSString *mappingPath = [self mappingForPath:path viewIndex:&idx mapped:&mapped have:&(BOOL){ 0 }]; BOOL add = NO, remove = NO; if (mappingPath && mapped) { //PSLog(@"P4 Mapping already mapped:\n\t%@", path); remove = !set; // Remove mapping } else if (mappingPath) { // Parent already mapped //PSLog(@"P4 Mapping parent already mapped:\n\t%@\n\t%@", mappingPath, path); add = !set; // Add excluding mapping } else if (mapped) { //PSLog(@"P4 Mapping is excluded:\n\t%@", path); remove = set; // Remove excluded mapping } else { // Parents not mapped //PSLog(@"P4 Mapping is not tracked:\n\t%@", path); add = set; // Add new mapping } if (add) { NSString *depotPath = set ? path : [@"-" stringByAppendingString:path]; NSString *view = [NSString stringWithFormat:@"\"%1$@%3$@\" \"%2$@%3$@\"", depotPath, clientPath, isDir ? @"..." : @""]; PSLog(@"P4 Mapping (+) %@", view); [clientViews addObject:view]; [clientPaths addObject:depotPath]; [clientMappings addObject:localPath]; [changes addObject:@{ @"action" : set ? @"mapped" : @"ignored", @"depotFile" : path, @"clientFile" : localPath }]; } else if (remove) { PSLog(@"P4 Mapping (-) %@", [clientViews objectAtIndex:idx]); [clientViews removeObjectAtIndex:idx]; [clientPaths removeObjectAtIndex:idx]; [clientMappings removeObjectAtIndex:idx]; [changes addObject:@{ @"action" : @"removed", @"depotFile" : path, @"clientFile" : localPath }]; } } // Nothing to change if (!changes.count) return responseBlock(nil, nil), nil; // Update specs with new view [clientSpecs setObject:clientViews forKey:@"View"]; NSAssert(clientSpecs, @"Mapping without client specs"); P4Operation *clientOp = [editConnection run:@"client" arguments:@[ @"-i" ] prompt:nil input:clientSpecs receive:nil response:^(P4Operation *operation, NSArray *response) { if (operation.errors) return responseBlock(operation, response); // Notify observers for (id target in [self observers]) { if ([target respondsToSelector:@selector(file:mappingChanged:)]) for (NSDictionary *change in changes) [target file:[change objectForKey:@"depotFile"] mappingChanged:change]; } if (!syncArgs) responseBlock(operation, response); }]; if (!syncArgs) return clientOp; // Download newly mapped files // when syncing special characters we need to encode properly local file NSMutableArray *syncArgsEncoded = [NSMutableArray array]; for(id syncArg in syncArgs) { if ([syncArg isKindOfClass:[NSArray class]]) { NSMutableArray *tmpFilesArray = [NSMutableArray array]; for (NSString *filePath in syncArg) { if ([filePath hasPrefix:@"/"]) { if (![filePath hasPrefix:@"//"]) { [tmpFilesArray addObject:[filePath encodePath]]; // encode only local files that contain special characters } else { [tmpFilesArray addObject:filePath]; } } else { [tmpFilesArray addObject:syncArg]; } } [syncArgsEncoded addObject:tmpFilesArray]; } else { [syncArgsEncoded addObject:syncArg]; } } syncArgs = syncArgsEncoded; P4Operation *syncOp = [editConnection run:@"sync" arguments:syncArgs response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorFileUpToDate]; [operation ignoreErrorsWithCode:P4ErrorNoSuchFile]; responseBlock(operation, response); }]; syncOp.parentOperation = clientOp; return syncOp; } - (NSArray *)observers { return [NSArray arrayWithArray:observers]; // Copy observers to temporary retaining array } // Recursively list directory tree - (NSArray *)contentOfDirectory:(NSString *)path { NSDirectoryEnumerator *enumerator; enumerator = [[NSFileManager defaultManager] enumeratorAtURL:[NSURL fileURLWithPath:path isDirectory:YES] includingPropertiesForKeys:@[ NSURLIsDirectoryKey ] options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; NSMutableArray *array = enumerator ? [NSMutableArray arrayWithCapacity:512] : nil; for (NSURL *url in enumerator) { NSNumber *dir = nil; [url getResourceValue:&dir forKey:NSURLIsDirectoryKey error:NULL]; if (dir.boolValue) continue; [array addObject:url.path]; } return array; } // Recursively list multiple directory trees watching cycles - (NSArray *)contentOfDirectories:(NSArray *)paths { NSMutableArray *array = [NSMutableArray arrayWithCapacity:paths.count]; NSMutableArray *dirs = [NSMutableArray arrayWithCapacity:paths.count]; SEL comparator = @selector(localizedStandardCompare:); for (NSString *path in [paths sortedArrayUsingSelector:comparator]) { if (dirs.count && [path hasPrefix:[dirs lastObject]]) continue; // Ignore paths beneath included directories /* BOOL dir = [filemanager fileExistsAtPath:path isDirectory:&dir] && dir; */ BOOL dir = [path hasSuffix:@"/"]; [dir ? dirs : array addObject:path]; } for (NSString *dir in dirs) { NSDirectoryEnumerator *enumerator; enumerator = [[NSFileManager defaultManager] enumeratorAtURL:[NSURL fileURLWithPath:dir isDirectory:YES] includingPropertiesForKeys:@[ NSURLIsDirectoryKey ] options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; for (NSURL *url in enumerator) { NSNumber *dir = nil; [url getResourceValue:&dir forKey:NSURLIsDirectoryKey error:NULL]; if (dir.boolValue) continue; [array addObject:url.path]; } } return array; } - (NSArray *)pathsWithDirectoryStarSuffix:(NSArray *)paths { NSMutableArray *array = [NSMutableArray arrayWithCapacity:paths.count]; for (NSString *path in paths) { BOOL dir = [path hasSuffix:@"/"]; [array addObject:dir ? [path stringByAppendingString:@"*"] : path]; } return array; } - (NSArray *)pathsWithDirectoryDotsSuffix:(NSArray *)paths { NSMutableArray *array = [NSMutableArray arrayWithCapacity:paths.count]; NSMutableArray *dirs = [NSMutableArray arrayWithCapacity:paths.count]; SEL comparator = @selector(localizedStandardCompare:); for (NSString *path in [paths sortedArrayUsingSelector:comparator]) { if (dirs.count && [path hasPrefixCaseInsensitive:[dirs lastObject]]) continue; // Ignore paths beneath included directories /* BOOL dir = [filemanager fileExistsAtPath:path isDirectory:&dir] && dir; */ BOOL dir = [path hasSuffix:@"/"]; if (dir) [dirs addObject:path]; [array addObject:dir ? [path stringByAppendingString:@"..."] : path]; } return array; } - (NSArray *)pathsWithEncodingChecked:(NSArray *)paths { NSMutableArray *array = [NSMutableArray arrayWithCapacity:paths.count]; for(NSString *path in paths) { if([path hasSpecialCharacters]) { [array addObject:[path encodePath]]; } else { [array addObject:path]; } } return array; } - (NSArray *)pathsDecoded:(NSArray *)paths { NSMutableArray *array = [NSMutableArray arrayWithCapacity:paths.count]; for(NSString *path in paths) { if([path hasEncodedCharacters]) { [array addObject:[path decodePath]]; } else { [array addObject:path]; } } return array; } - (BOOL) pathsHaveSpecialCharacters:(NSArray *)paths { BOOL foundSpecial = false; for(NSString *path in paths) { if([path hasSpecialCharacters]) { foundSpecial = true; break; } } return foundSpecial; } #pragma mark - FileEventDelegate - (void)fileEvents:(PSFileEvents *)events created:(NSArray *)paths { if (events == fileEventsUI) { for (id target in [self observers]) { if ([target respondsToSelector:@selector(fileCreated:)]) for (NSString *path in paths) [target fileCreated:path]; } } else { PSLog(@"File > Created \n%@", [paths componentsJoinedByString:@"\n"]); [self addFiles:paths response:nil]; } } - (void)fileEvents:(PSFileEvents *)events removed:(NSArray *)paths { if (events == fileEventsUI) { for (id target in [self observers]) { if ([target respondsToSelector:@selector(fileRemoved:)]) for (NSString *path in paths) [target fileRemoved:path]; } [self markFilesRead:paths]; } else { PSLog(@"File > Removed \n%@", [paths componentsJoinedByString:@"\n"]); #warning Only mapped files can be removed via watchdog paths = [paths filteredArrayUsingBlock:^BOOL(NSString *path, NSUInteger idx) { return [self mappingForPath:path] != nil; }]; if (paths.count) [self removeFiles:paths response:nil]; } } - (void)fileEvents:(PSFileEvents *)events moved:(NSArray *)paths to:(NSArray *)newPaths { if (events == fileEventsUI) { for (id target in [self observers]) { if ([target respondsToSelector:@selector(fileMoved:toPath:)]) [paths enumerateObjectsUsingBlock:^(NSString *path, NSUInteger idx, BOOL *stop) { [target fileMoved:path toPath:[newPaths objectAtIndex:idx]]; }]; } [self markFilesRead:paths]; } else { #warning revise so only mapped files can be moved via watchdog NSMutableString *string = [NSMutableString string]; [paths enumerateObjectsUsingBlock:^(NSString *path, NSUInteger idx, BOOL *stop) { [string appendFormat:@"\n%@\n-> %@", path, [newPaths objectAtIndex:idx]]; }]; PSLog(@"File > Moved %@", string); [self moveFiles:paths toPaths:newPaths response:nil]; } } - (void)fileEvents:(PSFileEvents *)events renamed:(NSArray *)paths to:(NSArray *)newPaths { if (events == fileEventsUI) { for (id target in [self observers]) { if ([target respondsToSelector:@selector(fileRenamed:toPath:)]) [paths enumerateObjectsUsingBlock:^(NSString *path, NSUInteger idx, BOOL *stop) { [target fileRenamed:path toPath:[newPaths objectAtIndex:idx]]; }]; } [self markFilesRead:paths]; } else { NSMutableString *string = [NSMutableString string]; [paths enumerateObjectsUsingBlock:^(NSString *path, NSUInteger idx, BOOL *stop) { [string appendFormat:@"\n%@\n-> %@", path, [newPaths objectAtIndex:idx]]; }]; PSLog(@"File > Renamed %@", string); [self moveFiles:paths toPaths:newPaths response:nil]; } } - (void)fileEvents:(PSFileEvents *)events modified:(NSArray *)paths { if (events == fileEventsUI) { for (id target in [self observers]) { if ([target respondsToSelector:@selector(fileModified:)]) for (NSString *path in paths) [target fileModified:path]; } } else { PSLog(@"File > Modified %@", [paths componentsJoinedByString:@"\n"]); [self reconcileFiles:paths response:nil]; } } - (void)fileEventsWillChange:(PSFileEvents *)events { } - (void)fileEventsDidChange:(PSFileEvents *)events { NSUInteger eventId = events.eventId; // Store if event is latest if (eventId > [[NSUserDefaults objectForKey:kDefaultLastFileEventId] integerValue]) [NSUserDefaults setObject:@(eventId) forKey:kDefaultLastFileEventId]; } @end