//
// 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"
NSString * const P4ChangelistUpdatedNotification = @"P4ChangelistUpdatedNotification";
NSString * const P4UnreadUpdatedNotification = @"P4UnreadUpdatedNotification";
NSString * const P4SyncStartedNotification = @"P4SyncStartedNotification";
NSString * const P4SyncFinishedNotification = @"P4SyncFinishedNotification";
NSString * const P4SubmitFinishedNotification = @"P4SubmitFinishedNotification";
NSString * const P4ShelvedChangelistDescription = @"P4 Shelved progress";
@interface P4Workspace () <PSFileEventsDelegate> {
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;
NSOperationQueue *queue;
NSTimer *autosyncTimer;
NSTimeInterval autosyncInterval;
NSMutableDictionary *mapping;
NSMutableArray *mappingIgnored;
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;
- (NSMutableArray *)observers;
- (NSString *)mappingPath:(NSString *)path;
- (NSArray *)mappingPaths:(NSArray *)paths;
- (NSArray *)contentOfDirectory:(NSString *)path;
- (NSArray *)contentOfDirectories:(NSArray *)paths;
- (void)appendDirectorySuffixes:(NSArray **)paths;
- (void)appendDirectorySuffixes:(NSArray **)paths movedPaths:(NSArray **)movedPaths;
@end
@implementation P4Credentials
@synthesize username, password, address, resumeSession;
@end
@implementation P4Workspace
@synthesize address, username, workspace, root, syncDescription;
+ (P4Workspace *)sharedInstance {
static P4Workspace *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[P4Workspace alloc] init];
});
return sharedInstance;
}
- (void)addObserver:(id <P4WorkspaceDelegate>)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 <P4WorkspaceDelegate>)observer {
if ([observers containsObject:observer])
[observers removeObject:observer];
}
- (id)init {
self = [super init];
if (self) {
queue = [[NSOperationQueue alloc] init];
queue.name = @"com.perforce.P4Workspace";
queue.maxConcurrentOperationCount = 1;
filemanager = [NSFileManager defaultManager];
}
return self;
}
- (NSString *)ticket {
return [editConnection ticket];
}
- (NSInteger)openedFilesCount {
return [[changelist objectForKey:@"edit"] count];
}
- (NSInteger)addedFilesCount {
return [[changelist objectForKey:@"add"] count] +
[[changelist objectForKey:@"move/add"] count];
}
- (NSInteger)deletedFilesCount {
return [[changelist objectForKey:@"delete"] count] +
[[changelist objectForKey:@"move/delete"] count];
}
- (NSInteger)unreadCount {
return unreadList.count;
}
#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;
if (flags.connected) {
PSLog(@"Connected %@@%@", username, address);
// 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 = [NSUserDefaults stringForKey:kDefaultSearchTicket];
} else {
// Retrieve initial ticket as p4search
searchTicket = [editConnection ticket];
[NSUserDefaults setString:searchTicket forKey:kDefaultSearchTicket];
}
// 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 = [NSUserDefaults stringForKey:kDefaultSearchUrl];
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];
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];
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;
[NSUserDefaults setString:searchTicket forKey:kDefaultSearchTicket];
// 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;
mapping = nil;
mappingIgnored = 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 && (responseBlock(operation, response), 1))
return;
[self handleClientResponse:response];
NSDictionary *client = [response lastObject];
root = [client objectForKey:@"Root"];
root = [root directoryPath];
workspace = [client objectForKey:@"Client"];
// 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];
});
}];
// Set file events
fileEventsP4 = [[PSFileEvents alloc] initWithRoot:root ignoreSelf:YES];
fileEventsP4.delegate = self;
fileEventsUI = [[PSFileEvents alloc] initWithRoot:root];
fileEventsUI.delegate = self;
// Refresh data
changelist = nil;
usersList = nil;
unreadList = [NSMutableArray array];
[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];
[syncConnection
run:@"sync"
arguments:@[ @"//..." ]
prompt:nil
input:nil
receive:receiveBlock
response:^(P4Operation *operation, NSArray *response) {
flags.synchronizing = NO;
syncDescription = nil;
[self handleChangelistResponse:response];
[self setAutosyncInterval:autosyncInterval];
[[NSNotificationCenter defaultCenter]
postNotificationName:P4SyncFinishedNotification object:operation];
responseBlock(operation, response);
}];
// Refresh
[self refreshUsers];
}
- (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
- (NSArray *)mappingForPaths:(NSArray *)paths {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:paths.count];
BOOL depot = [[paths lastObject] hasPrefix:@"//"];
for (NSString *path in paths) {
__block NSString *localMapping, *depotMapping;
__block BOOL have, tracked, mapped;
have = tracked = mapped = NO;
[mapping enumerateKeysAndObjectsUsingBlock:^(NSString *remote, NSString *local, BOOL *stop) {
NSString *mappingPath = depot ? remote : local;
if ([path hasPrefix:mappingPath]) {
localMapping = local;
depotMapping = remote;
tracked = ![mappingIgnored containsObject:remote];
if ((mapped = (path.length == mappingPath.length)))
*stop = YES;
} else if ([path hasSuffix:@"/"] && [mappingPath hasPrefix:path])
have = YES;
}];
NSString *action;
if (mapped)
action = tracked ? @"mapped" : @"ignored";
else if (tracked) {
NSString *relative = [path substringFromIndex:depot ?
depotMapping.length : localMapping.length];
localMapping = [localMapping stringByAppendingString:relative];
depotMapping = [depotMapping stringByAppendingString:relative];
action = @"tracked";
} else if (have)
action = @"have";
else
action = @"untracked";
[result addObject:[NSDictionary dictionaryWithObjectsAndKeys:
path, @"path",
action, @"action",
localMapping, @"clientFile",
depotMapping, @"depotFile", nil]];
}
return result;
}
- (void)mappingSet:(BOOL)map path:(NSString *)path response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
NSAssert(workspace.length, @"P4 Mapping without workspace set");
NSAssert([path hasPrefix:@"//"], @"P4 Mapping path should start with //");
NSAssert([path hasSuffix:@"/"], @"P4 Mapping path should end with /");
// Mapping
[editConnection
run:@"client"
arguments:@[ @"-o", workspace ]
response:^(P4Operation *operation, NSArray *response) {
if (operation.errors && (responseBlock(operation, response), 1))
return;
NSMutableDictionary *specs = [response lastObject];
NSString *client = [specs objectForKey:@"Client"];
NSString *clientPrefix = [NSString stringWithFormat:@"//%@/", client];
NSString *rootPrefix = [specs objectForKey:@"Root"];
NSMutableDictionary *mapped = [NSMutableDictionary dictionaryWithCapacity:specs.count];
NSMutableArray *ignored = [NSMutableArray array];
NSString *parent = nil;
BOOL ignoring = NO;
NSString *view, *key;
NSMutableArray *views = [NSMutableArray array];
for (NSInteger idx=0;
(key = [NSString stringWithFormat:@"View%ld", idx], view = [specs objectForKey:key]);
idx++, [specs removeObjectForKey:key]) {
NSArray *args = [view arrayOfArguments];
NSAssert(args.count == 2, @"P4 Mapping has incorrect view value");
NSString *localPath = [args objectAtIndex:1];
NSString *depotPath = [args objectAtIndex:0];
BOOL ignorePath = [depotPath hasPrefix:@"-"];
depotPath = [depotPath stringByRemovingSuffix:@"..."];
depotPath = [depotPath stringByRemovingSuffix:@"*"];
if (ignorePath)
[ignored addObject:depotPath = [depotPath substringFromIndex:1]];
localPath = [localPath stringByRemovingSuffix:@"..."];
localPath = [localPath stringByRemovingSuffix:@"*"];
localPath = [localPath stringByRemovingPrefix:clientPrefix];
localPath = [rootPrefix stringByAppendingPath:localPath];
[mapped setObject:localPath forKey:depotPath];
if ([path hasPrefix:depotPath] && ![parent hasPrefix:depotPath]) {
ignoring = ignorePath;
parent = depotPath;
if ([depotPath isEqualToString:path]) {
if (ignoring == map) // Re-add to remove ignore
continue;
break; // Already added
}
}
[views addObject:view];
}
NSString *relativePath = [path stringByRemovingPrefix:@"//"];
NSString *localPath = [rootPrefix stringByAppendingString:relativePath];
NSString *newView = [NSString stringWithFormat:@"\"//%1$@...\" \"%2$@%1$@...\"",
relativePath, clientPrefix];
if (map) { // Map
if (parent && !ignoring) { // Parent already added
response = @[
[NSString stringWithFormat: @"Already mapped: %@ to %@", path, parent] ];
responseBlock(operation, response);
return;
} else if ([path isEqualToString:parent]) {
response = @[ [NSString stringWithFormat:@"Remove ignoring view: %@", newView] ];
[mapped removeObjectForKey:path];
[ignored removeObject:path];
} else {
response = @[ [NSString stringWithFormat:@"Mapping new view: %@", newView] ];
[mapped setObject:localPath forKey:path];
[views addObject:newView];
}
} else {
if (!parent || ignoring) { // Parent already ignored
response = @[ ignoring ?
[NSString stringWithFormat:@"Already ignored: %@ in %@", path, parent] :
[NSString stringWithFormat:@"Already unmapped: %@", path] ];
responseBlock(operation, response);
return;
} else if ([path isEqualToString:parent]) {
response = @[ [NSString stringWithFormat:@"Removing view: %@", newView] ];
[mapped removeObjectForKey:path];
} else {
newView = [@"\"-" stringByAppendingString:[newView substringFromIndex:1]];
response = @[ [NSString stringWithFormat:@"Ignoring view: %@", newView] ];
[mapped setObject:localPath forKey:path];
[ignored addObject:path];
[views addObject:newView];
}
}
// Create updated specs
[specs setObject:views forKey:@"View"];
// Update client
[editConnection
run:@"client"
arguments:@[ @"-i" ]
prompt:nil
input:specs
receive:nil
response:^(P4Operation *operation, NSArray *updateResponse) {
if (operation.errors && (responseBlock(operation, response), 1))
return;
// Apply mapping changes
mapping = mapped;
mappingIgnored = ignored;
NSDictionary *changeInfo = nil;
if ([views containsObject:newView]) {
NSString *action = [ignored containsObject:path] ? @"ignored" : @"mapped";
changeInfo = @{
@"action" : action,
@"clientFile" : localPath,
@"depotFile" : path,
};
}
// Notify observers
for (id <P4WorkspaceDelegate> target in [self observers]) {
if ([target respondsToSelector:@selector(file:mappingChanged:)])
[target file:path mappingChanged:changeInfo];
}
responseBlock(operation, response);
// Refresh changelist
[self refreshPendingFiles];
}];
}];
}
- (void)listMappings:(NSString *)path response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
NSAssert(path.length, @"Listing map of empty folder");
NSAssert([path hasSuffix:@"/"], @"Listing mapping of non-directory");
[listConnection
run:@"where"
arguments:@[ [path stringByAppendingPath:@"..."] ]
response:^(P4Operation *operation, NSArray *response) {
[operation ignoreErrorsWithCode:P4ErrorNoSuchFile];
[operation ignoreErrorsWithCode:P4ErrorFileNotInView];
NSMutableDictionary *have = [NSMutableDictionary dictionary];
PSLog(@"MAPPING : %@", path);
for (NSDictionary *responseMapping in response) {
NSString *depotPath = [responseMapping objectForKey:@"depotFile"];
NSString *clientFile = [responseMapping objectForKey:@"path"];
NSInteger loc = [depotPath
rangeOfString:@"/" options:0
range:(NSRange) {
path.length,
depotPath.length - path.length
}].location;
loc = loc == NSNotFound ? path.length : loc+1;
NSString *pathExtension = [depotPath substringFromIndex:loc];
depotPath = [depotPath substringToIndex:loc];
BOOL wholeMap = [pathExtension isEqualToString:@"..."];
if (!wholeMap && [have objectForKey:depotPath])
continue;
if (!wholeMap) {
if ([have objectForKey:depotPath])
continue;
clientFile = [clientFile substringToIndex:clientFile.length - pathExtension.length];
}
if ([responseMapping objectForKey:@"unmap"]) {
if (!wholeMap)
continue;
clientFile = [@"-" stringByAppendingString:clientFile];
}
[have setObject:clientFile forKey:depotPath];
};
responseBlock(operation, @[ have ]);
}];
}
#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");
NSMutableArray *array = [NSMutableArray arrayWithCapacity:paths.count];
for (NSString *path in paths) {
BOOL dir = [path hasSuffix:@"/"];
[array addObject:dir ? [path stringByAppendingString:@"*"] : path];
}
paths = array;
[listConnection
run:@"fstat"
arguments:@[
@"-A", @"tags", // Filter attributes to tags-only
@"-Oah", @"-Dah",
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");
NSMutableArray *array = [NSMutableArray arrayWithCapacity:paths.count];
for (NSString *path in paths) {
BOOL dir = [path hasSuffix:@"/"];
[array addObject:dir ? [path stringByAppendingString:@"*"] : path];
}
paths = array;
[listConnection
run:@"fstat"
arguments:@[
@"-A", @"tags", // Filter attributes to tags-only
@"-Oa", @"-Da",
@"-F", @"^headAction=delete & ^headAction=move/delete & headRev | dir",
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)^{ };
[self appendDirectorySuffixes:&paths];
[listConnection
run:@"fstat"
arguments:@[
@"-A", @"tags",
@"-Oa",
@"-F", @"action", // Filter by files opened in the clients workspace
paths.count ? paths : @"..." ]
response:^(P4Operation *operation, NSArray *response) {
if (!operation.errors) {
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];
}
responseBlock(operation, response);
}];
}
- (void)listVersions:(NSString *)path response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
NSAssert(path.length, @"Listing versions of empty path");
[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);
}];
}
#pragma mark File Management
- (void)editFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
NSAssert(paths.count, @"Editing empty file list");
[self appendDirectorySuffixes:&paths];
[editConnection
run:@"edit"
arguments:paths
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse:response];
//
// TODO: Add error checking and adding untracked files
//
responseBlock(operation, response);
}];
}
- (void)addFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
NSAssert(paths.count, @"Adding empty file list");
[queue addOperationWithBlock:^{
// Get recursively all paths contents
NSArray *content = [self contentOfDirectories:paths];
/* Alternative way using reconcile */
/* [self appendDirectorySuffixes:&paths]; */
[editConnection
run:@"add" /* @"reconcile" */
arguments:content /* @[ @"-a", paths ] */
response:^(P4Operation *addOperation, NSArray *response) {
[self handleChangelistResponse:response];
// Revert already added files
NSArray *alreadyAdded = [addOperation errorsWithCode:P4ErrorAddExistingFile];
NSArray *existing = [addOperation errorsWithCode:P4ErrorAddExisting];
if (existing) {
alreadyAdded = [[NSArray arrayWithArray:alreadyAdded] arrayByAddingObjectsFromArray:existing];
}
if (!alreadyAdded.count && (responseBlock(addOperation, response), 1))
return;
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];
}];
}];
}
- (void)removeFiles:(NSMutableArray *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
NSAssert(paths.count, @"Removing empty file list");
[self appendDirectorySuffixes:&paths];
// Discard shelved files
if (shelvedList.count)
[self discardShelvedFiles:paths response:nil];
[editConnection
run:@"revert"
arguments:@[ @"-k", paths ]
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse: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];
}
}
[editConnection setNextOperation:
[editConnection
run:@"delete"
arguments:@[ @"-k", paths ]
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse:response];
// Get deleted files from response
for (NSDictionary *item in response) {
// Files successfully deleted
NSString *path = [item objectForKey:@"clientFile"];
[paths removeObject:path];
}
// Remove untracked files directly
NSArray *untracked = [operation errorsWithCode:P4ErrorFileNotOnClient];
for (NSError *error in untracked) {
NSString *path = error.localizedFailureReason;
[paths removeObject:path];
path = [path stringByRemovingSuffix:@"/..."];
if ([filemanager fileExistsAtPath:path]) {
[filemanager removeItemAtPath:path error:NULL];
PSLog(@"Warning > Removing file manually %@", path);
}
}
[operation ignoreErrors:untracked];
// NSAssert(!paths.count || operation.errors, @"Deleted count doesn't match");
responseBlock(operation, response);
}]
];
}];
}
- (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");
[self appendDirectorySuffixes:&paths movedPaths:&toPaths];
// Open for edit
[editConnection
run:@"edit"
arguments:paths
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse:response];
//
// TODO: Add support for moving into locations marked for delete
//
// Move command performs only one move at once, enumerate
[paths enumerateObjectsUsingBlock:^(NSString *path, NSUInteger idx, BOOL *stop) {
NSString *fromFile = path;
NSString *toFile = [toPaths objectAtIndex:idx];
[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];
// Adds
if (adds.count)
[self addFiles:@[ [toFile stringByRemovingSuffix:@"..."] ] response:nil];
// Deletes
if (deletes.count)
[self removeFiles:@[ fromFile ] response:nil];
// Overwrites
if (overwrites.count) {
[self reconcileFiles:@[ toFile ] response:nil];
[self removeFiles:@[ fromFile ] response:nil];
}
responseBlock(operation, response);
}];
}];
}];
}
- (void)revertFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
[self appendDirectorySuffixes:&paths];
[editConnection
run:@"revert"
arguments:paths
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse:response];
//
// TODO: Add error checking
//
responseBlock(operation, response);
}];
}
- (void)revertUnchangedFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
[self appendDirectorySuffixes:&paths];
[editConnection
run:@"revert"
arguments:@[ @"-a", paths ]
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse:response];
//
// TODO: Add error checking
//
responseBlock(operation, response);
}];
}
- (void)reconcileFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
NSAssert(paths.count, @"Reconciling empty paths");
[self appendDirectorySuffixes:&paths];
[editConnection
run:@"revert"
arguments:@[ @"-k", paths ]
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse:response];
[operation ignoreErrorsWithCode:P4ErrorFileNotOpened];
[editConnection setNextOperation:
[editConnection
run:@"reconcile"
arguments:@[ @"-ead", paths ]
response:^(P4Operation *operation, NSArray *response) {
[self handleChangelistResponse:response];
[operation ignoreErrorsWithCode:P4ErrorReconcile];
responseBlock(operation, response);
}]
];
}];
}
- (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 && (responseBlock(operation, response), 1))
return;
[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);
}];
}];
}
#pragma mark - Refresh
- (void)refreshMapping {
[listConnection
run:@"client"
arguments:@[ @"-o", workspace ]
response:^(P4Operation *operation, NSArray *response) {
if (operation.errors)
return;
[self handleClientResponse:response];
}];
}
- (void)refreshPendingFiles {
[self listPendingFiles:nil response:nil];
}
- (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;
}];
}];
}
- (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;
}];
}
#pragma mark Shelving
- (NSArray *)shelvedForPaths:(NSArray *)paths {
if (!shelvedList.count)
return nil;
NSMutableArray *result = [NSMutableArray array];
for (NSString *path in paths) {
if ([path hasSuffix:@"/"]) {
// Directory
for (NSString *shelvedPath in shelvedList) {
if ([shelvedPath hasPrefix:path]) {
[result addObject:shelvedPath];
}
}
} else if ([shelvedList containsObject:path]) {
[result addObject:path];
}
}
return result;
}
- (void)shelveFile:(NSString *)path response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
// Mapping local paths into depot
if (![path hasPrefix:@"//"])
path = [self mappingPath: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 <P4WorkspaceDelegate> 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 mappingPath: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 *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
if (!shelvedChange && (responseBlock(nil, nil), 1))
return;
if (paths.count && ![[paths lastObject] hasPrefix:@"//"])
paths = [self mappingPaths:paths]; // Mapping local paths into depot
if (!paths.count && (responseBlock(nil, nil), 1))
return;
[editConnection
run:@"shelve"
arguments:@[ @"-c", shelvedChange, @"-d", paths ]
response:^(P4Operation *operation, NSArray *response) {
if (!operation.errors) {
[shelvedList removeObjectsInArray:paths];
// Notify observers
for (id <P4WorkspaceDelegate> target in [self observers]) {
if ([target respondsToSelector:@selector(file:shelved:)])
for (NSString *path in paths)
[target file:path shelved:nil];
}
}
if (shelvedList.count && (responseBlock(operation, response), 1))
return;
// 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 && (responseBlock(nil, nil), 1))
return;
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];
}];
}
- (void)listShelvedFiles:(NSArray *)paths response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
if (!shelvedChange && (responseBlock(nil, nil), 1))
return;
[listConnection
run:@"fstat"
arguments:@[ @"-Oa", @"-Rs", @"-e", shelvedChange, paths ]
response:^(P4Operation *operation, NSArray *response) {
[operation ignoreErrorsWithCode:P4ErrorNoSuchFile];
[operation ignoreErrorsWithCode:P4ErrorFileNotInView];
responseBlock(operation, response);
}];
}
#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
- (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 hasPrefix:path];
}];
[unreadList removeObjectsAtIndexes:indexes];
} else {
[unreadList removeObject:path];
}
}
[[NSNotificationCenter defaultCenter]
postNotificationName:P4UnreadUpdatedNotification
object:self];
}
- (NSArray *)unreadForPaths:(NSArray *)paths {
if (!unreadList.count)
return nil;
NSMutableArray *result = [NSMutableArray array];
for (NSString *path in paths) {
if ([path hasSuffix:@"/"]) {
// Directory
for (NSString *unreadPath in unreadList) {
if ([unreadPath hasPrefix:path]) {
[result addObject:path];
break;
}
}
} else if ([unreadList containsObject:path]) {
[result addObject:path];
}
}
return result;
}
- (void)listUnreadFiles:(NSString *)path response:(P4ResponseBlock_t)responseBlock {
responseBlock = responseBlock ?: (P4ResponseBlock_t)^{ };
if (!path)
path = root;
[queue addOperationWithBlock:^{
// Check if unread files still exists
NSMutableIndexSet *nonexistent = [NSMutableIndexSet indexSet];
NSMutableIndexSet *unreadPaths = [NSMutableIndexSet indexSet];
[unreadList enumerateObjectsUsingBlock:^(NSString *unreadPath, NSUInteger idx, BOOL *stop) {
if ([filemanager fileExistsAtPath:unreadPath]) {
if ([unreadPath hasPrefix:path])
[unreadPaths addIndex:idx];
} else {
[nonexistent addIndex:idx];
}
}];
// Remove non-existing records
if (nonexistent.count)
[unreadList removeObjectsAtIndexes:nonexistent];
// Post notification
[[NSNotificationCenter defaultCenter]
postNotificationName:P4UnreadUpdatedNotification
object:self];
if (!unreadPaths.count) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
responseBlock(nil, nil);
}];
return;
}
[listConnection
run:@"fstat"
arguments:@[
@"-A", @"tags", // Filter attributes to tags-only
@"-Oah",
[unreadList objectsAtIndexes:unreadPaths] ]
response:^(P4Operation *operation, NSArray *response) {
[operation ignoreErrorsWithCode:P4ErrorNoSuchFile];
[operation ignoreErrorsWithCode:P4ErrorFileNotInView];
responseBlock(operation, response);
}];
}];
}
#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...");
syncDescription = @"Auto-sync in progress...";
[self syncWorkspace:nil response:nil];
}
- (void)handleClientResponse:(NSArray *)response {
NSDictionary *specs = [response lastObject];
NSString *client = [specs objectForKey:@"Client"];
NSString *clientPrefix = [NSString stringWithFormat:@"//%@/", client];
NSString *rootPrefix = [specs objectForKey:@"Root"];
NSMutableDictionary *mapped = [NSMutableDictionary dictionaryWithCapacity:specs.count];
NSMutableArray *ignored = [NSMutableArray array];
NSString *view, *key;
for (NSInteger idx=0;
(key = [NSString stringWithFormat:@"View%ld", idx], view = [specs objectForKey:key]);
idx++) {
NSArray *args = [view arrayOfArguments];
NSAssert(args.count == 2, @"P4 Mapping has incorrect view value");
NSString *localPath = [args objectAtIndex:1];
NSString *depotPath = [args objectAtIndex:0];
BOOL ignorePath = [depotPath hasPrefix:@"-"];
depotPath = [depotPath stringByRemovingSuffix:@"..."];
depotPath = [depotPath stringByRemovingSuffix:@"*"];
if (ignorePath)
[ignored addObject:depotPath = [depotPath substringFromIndex:1]];
localPath = [localPath stringByRemovingSuffix:@"..."];
localPath = [localPath stringByRemovingSuffix:@"*"];
localPath = [localPath stringByRemovingPrefix:clientPrefix];
localPath = [rootPrefix stringByAppendingPath:localPath];
[mapped setObject:localPath forKey:depotPath];
}
mapping = mapped;
mappingIgnored = ignored;
}
- (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.count ? [response objectAtIndex:0] : nil;
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 <P4WorkspaceDelegate> target in [self observers])
if ([target respondsToSelector:@selector(file:updated:)])
[target file:path updated:dict];
} else if (revert) {
for (id <P4WorkspaceDelegate> target in [self observers])
if ([target respondsToSelector:@selector(file:revertedAction:)])
[target file:path revertedAction:dict];
} else {
for (id <P4WorkspaceDelegate> 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];
}
- (NSArray *)observers {
return [observers copy];
}
- (NSString *)mappingPath:(NSString *)path {
BOOL depot = [path hasPrefix:@"//"];
__block NSString *localMapping, *depotMapping;
__block BOOL tracked, mapped;
tracked = mapped = NO;
[mapping enumerateKeysAndObjectsUsingBlock:^(NSString *remote, NSString *local, BOOL *stop) {
NSString *mappingPath = depot ? remote : local;
if ([path hasPrefix:mappingPath]) {
localMapping = local;
depotMapping = remote;
tracked = ![mappingIgnored containsObject:remote];
if ((mapped = (path.length == mappingPath.length)))
*stop = YES;
}
}];
if (!mapped && tracked) {
NSString *relative = [path substringFromIndex:depot ?
depotMapping.length : localMapping.length];
localMapping = [localMapping stringByAppendingString:relative];
depotMapping = [depotMapping stringByAppendingString:relative];
}
return depot ? localMapping : depotMapping;
}
- (NSArray *)mappingPaths:(NSArray *)paths {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:paths.count];
for (NSString *path in paths) {
NSString *mappedPath = [self mappingPath:path];
if (mappedPath)
[result addObject:mappedPath];
}
return result;
}
// Recursively list directory tree
- (NSArray *)contentOfDirectory:(NSString *)path {
NSDirectoryEnumerator *enumerator;
enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:path]
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]
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;
}
// Appends '...' suffixes into directories watching cycles
- (void)appendDirectorySuffixes:(NSArray *__autoreleasing *)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:@"/"];
[array addObject:dir ?
([dirs addObject:path], [path stringByAppendingString:@"..."]) : path];
}
*paths = array;
}
// Appends '...' suffixes to both source or destination if any of them is a directory
- (void)appendDirectorySuffixes:(NSArray *__autoreleasing *)paths movedPaths:(NSArray *__autoreleasing *)movedPaths {
NSMutableArray *fromPaths = [NSMutableArray arrayWithCapacity:(*paths).count];
NSMutableArray *toPaths = [NSMutableArray arrayWithCapacity:(*paths).count];
[*paths enumerateObjectsUsingBlock:^(NSString *path, NSUInteger idx, BOOL *stop) {
NSString *from = path;
NSString *to = [*movedPaths objectAtIndex:idx];
/* BOOL dir = ([filemanager fileExistsAtPath:to isDirectory:&dir] ?:
[filemanager fileExistsAtPath:from isDirectory:&dir]) && dir; */
BOOL dir = [to hasSuffix:@"/"] || [from hasSuffix:@"/"];
[fromPaths addObject:dir ? [from stringByAppendingString:@"..."] : from];
[toPaths addObject:dir ? [to stringByAppendingString:@"..."] : to];
}];
*paths = fromPaths;
*movedPaths = toPaths;
}
#pragma mark - FileEventDelegate
- (void)fileEvents:(PSFileEvents *)events created:(NSArray *)paths {
if (events == fileEventsUI) {
for (id <P4WorkspaceDelegate> 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 <P4WorkspaceDelegate> 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"]);
[self removeFiles:paths response:nil];
}
}
- (void)fileEvents:(PSFileEvents *)events moved:(NSArray *)paths to:(NSArray *)newPaths {
if (events == fileEventsUI) {
for (id <P4WorkspaceDelegate> 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 {
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 <P4WorkspaceDelegate> 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 <P4WorkspaceDelegate> 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];
}
}
@end