//
// P4Item.m
// Perforce
//
// Created by Adam Czubernat on 25.07.2013.
// Copyright (c) 2013 Perforce Software, Inc. All rights reserved.
//
#import "P4Item.h"
#import <objc/objc-runtime.h>
@interface P4Item () <P4WorkspaceDelegate> {
NSArray *tags, *lowercaseTags;
}
@property (nonatomic, retain) P4ItemAction *lockedAction;
+ (NSMutableArray *)targets;
- (void)insertFiles:(NSArray *)paths copy:(BOOL)copy;
- (void)loadingAction:(P4ItemAction *)action message:(NSString *)message;
- (void)finishAction:(P4ItemAction *)action response:(NSArray *)response error:(NSError *)error;
- (BOOL)promptAction:(P4ItemAction *)action message:(NSString *)message;
@end
@implementation P4Item
@synthesize lockedAction;
- (id)init {
self = [super init];
PSInstanceCreated([self class]);
return self;
}
- (void)dealloc {
if (!parent)
[[P4Workspace sharedInstance] removeObserver:self];
PSInstanceDeallocated([self class]);
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ : %@", NSStringFromClass([self class]), remotePath ?: localPath];
}
#pragma mark - Public
- (P4Item *)parent { return parent; }
- (NSString *)name { return name; }
- (NSString *)path { return localPath ?: remotePath; }
- (NSString *)localPath { return localPath; }
- (NSString *)remotePath { return remotePath; }
- (BOOL)isDirectory { return flags.directory; }
- (NSArray *)children {
if (!children && !flags.loading && !flags.failure) {
flags.loading = YES;
[self loadPath:self.path];
}
return children;
}
- (BOOL)isLoading { return flags.loading; }
- (BOOL)hasError { return flags.failure; }
- (BOOL)isTracked { return flags.tracked; }
- (BOOL)hasMapped { return flags.hasMapped; }
- (BOOL)isMapped { return flags.mapped; }
- (BOOL)isIgnored { return flags.ignored; }
- (BOOL)isUnread { return flags.unread; }
- (BOOL)isShelved { return flags.shelved; }
- (NSString *)status { return status; }
- (NSString *)statusOwner { return statusOwner; }
- (NSArray *)tags { return tags; };
- (BOOL)hasTag:(NSString *)tag {
return [lowercaseTags containsObject:[tag lowercaseString]];
}
- (NSDictionary *)metadata { return metadata; }
- (NSString *)iconName {
NSString *file;
if (flags.directory) {
if (flags.ignored)
file = @"IconFolderIgnored";
else if (flags.mapped)
file = @"IconFolderMapped";
else if (flags.tracked || !parent)
file = @"IconFolder";
else if (flags.hasMapped)
file = @"IconFolderMixed";
else
file = @"IconFolderUntracked";
} else {
NSString *action = status ?: [metadata objectForKey:@"otherAction0"];
if (action) {
if ([action isEqualToString:@"add"])
file = @"IconFileAdd";
else if ([action isEqualToString:@"edit"])
file = @"IconFileCheckout";
else if ([action isEqualToString:@"delete"])
file = @"IconFileDelete";
else if ([action isEqualToString:@"move/add"])
file = @"IconFileMoveAdd";
else if ([action isEqualToString:@"move/delete"])
file = @"IconFileMoveDelete";
if (!status && statusOwner)
file = [file stringByAppendingString:@"Other"];
}
else if (flags.tracked)
file = @"IconFile";
else
file = @"IconFileUntracked";
}
return file;
}
- (NSImage *)icon {
return [NSImage imageNamed:[[self iconName] stringByAppendingString:@".png"]];
}
- (NSImage *)iconHighlighted {
return [NSImage imageNamed:[[self iconName] stringByAppendingString:@"_alt.png"]];
}
- (NSColor *)overlay {
return nil; // UNUSED
CGFloat COLOR_H = 0.9f;
CGFloat COLOR_L = 0.6f;
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:localPath];
if (flags.ignored)
return [NSColor colorWithDeviceRed:COLOR_H green:COLOR_L blue:COLOR_L alpha:1.0f];
if (flags.tracked || flags.mapped) {
if (exists)
return [NSColor colorWithDeviceRed:COLOR_L green:COLOR_H blue:COLOR_L alpha:1.0f];
return [NSColor colorWithDeviceRed:COLOR_L green:0.7 blue:COLOR_L alpha:1.0f];
}
return [NSColor colorWithDeviceRed:COLOR_L green:COLOR_L blue:COLOR_L alpha:1.0f];
}
- (NSImage *)previewWithSize:(CGSize)size {
CGFloat minSize = fminf(size.width, size.height);
if (localPath) {
// Generate real preview
NSImage *image = [NSImage
imageWithFilePreview:localPath
size:CGSizeMake(minSize, minSize)
icon:YES];
if (image)
return image;
}
// File not found generate preview from generic icon
NSString *fileType;
if (flags.directory)
fileType = NSFileTypeForHFSTypeCode(kGenericFolderIcon);
else
fileType = remotePath.pathExtension;
NSImage *image = [[NSWorkspace sharedWorkspace] iconForFileType:fileType];
[image setSize:CGSizeMake(minSize, minSize)];
return image;
}
#pragma mark - Actions
- (void)open {
if (flags.directory)
return;
// Open using default app
[[NSWorkspace sharedWorkspace] openFile:localPath];
// Mark as read
if (flags.unread)
[self markAsRead];
}
- (void)openFromDepot {
if (flags.directory)
return;
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Opening file from depot..."];
NSString *tmpPath = [NSString stringWithFormat:@"/tmp/p4/%@/%@",
[[P4Workspace sharedInstance] workspace], name];
[[P4Workspace sharedInstance]
runCommand:[NSString stringWithFormat:@"print -o \"%@\" \"%@\"", tmpPath, remotePath]
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
if (!operation.errors)
[[NSWorkspace sharedWorkspace] openFile:tmpPath];
}];
}
- (void)openWithCheckout {
[self checkout];
[self open];
}
- (void)showInFinder {
if (localPath.length)
[[NSWorkspace sharedWorkspace] selectFile:localPath
inFileViewerRootedAtPath:@""];
}
- (void)checkout {
if (status)
return;
// Prompt for checking out folder
if (flags.directory &&
[self promptAction:lockedAction message:
[NSString stringWithFormat:@"Would you like to checkout '%@' "
"directory with all of its contents?", name]] == NO)
return;
[self checkoutItems:@[ self ]];
}
- (void)checkoutItems:(NSArray *)items {
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Checking out..."];
NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count];
for (P4Item *item in items)
[paths addObject:item.localPath];
[[P4Workspace sharedInstance]
editFiles:paths
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
}
- (void)addItem {
[self addItems:@[ self ]];
}
- (void)addItems:(NSArray *)items {
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Adding files..."];
NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count];
for (P4Item *item in items)
[paths addObject:item.localPath];
[[P4Workspace sharedInstance]
addFiles:paths
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
}
- (void)checkIn {
[self checkInItems:@[ self ]];
}
- (void)checkInItems:(NSArray *)items {
P4ItemAction *action = lockedAction;
[self finishAction:action response:[items valueForKey:@"localPath"] error:nil];
}
- (void)checkInAll {
if (!flags.directory)
return;
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Check-in all files..."];
[[P4Workspace sharedInstance]
listPendingFiles:@[ localPath ]
response:^(P4Operation *operation, NSArray *response) {
NSArray *paths = [response valueForKey:@"depotFile"];
NSError *error = (operation.error ?: paths.count ? nil :
[NSError errorWithFormat:@"No files to check-in"]);
[self finishAction:action response:paths error:error];
}];
}
- (void)deleteItem {
[self deleteItems:@[ self ]];
}
- (void)deleteItems:(NSArray *)items {
P4ItemAction *action = lockedAction;
NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count];
for (P4Item *item in items)
[paths addObject:item.localPath];
NSString *prompt = paths.count > 4 ?
[NSString stringWithFormat:@"Would you like to delete %ld items?", paths.count] :
[NSString stringWithFormat:@"Would you like to delete\n%@ ?",
[[items valueForKey:@"path"] componentsJoinedByString:@"\n"]];
if (![self promptAction:action message:prompt])
return;
[self loadingAction:action message:@"Deleting files..."];
// Remove files
NSError *error;
NSFileManager *filemanager = [NSFileManager defaultManager];
for (NSString *path in paths) {
if (![filemanager removeItemAtPath:path error:&error]) {
[self finishAction:action response:nil error:error];
return;
}
}
[[P4Workspace sharedInstance]
removeFiles:paths
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
}
- (void)revert {
[self revertItems:@[ self ]];
}
- (void)revertItems:(NSArray *)items {
P4ItemAction *action = lockedAction;
NSMutableArray *paths = [NSMutableArray arrayWithCapacity:items.count];
for (P4Item *item in items)
[paths addObject:item.localPath];
NSString *prompt = paths.count > 4 ?
[NSString stringWithFormat:@"Would you like to revert %ld items?", paths.count] :
[NSString stringWithFormat:@"Would you like to revert\n%@ ?",
[[items valueForKey:@"path"] componentsJoinedByString:@"\n"]];
if (![self promptAction:action message:prompt])
return;
[self loadingAction:action message:@"Reverting files..."];
[[P4Workspace sharedInstance]
revertFiles:paths
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
}
- (void)revertIfUnchanged {
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Undo checkout..."];
[[P4Workspace sharedInstance]
revertUnchangedFiles:@[ localPath ]
response:^(P4Operation *operation, NSArray *response) {
NSDictionary *revert = [response lastObject];
NSString *clientAction = [revert objectForKey:@"action"];
NSString *clientFile = [revert objectForKey:@"clientFile"];
if ([clientAction isEqualToString:@"reverted"] &&
[clientFile isEqualToString:localPath]) {
[self finishAction:action response:response error:operation.error];
} else {
NSError *error = [NSError errorWithFormat:
@"Action couldn't complete - file was changed.\n"
"Please revert or submit pending changes"];
[self finishAction:action response:response error:error];
}
}];
}
- (void)mapToWorkspace {
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Setting workspace mapping..."];
[[P4Workspace sharedInstance]
mappingSet:YES
path:remotePath
response:^(P4Operation *operation, NSArray *response) {
PSLog(@"Map response %@", response);
[self finishAction:action response:response error:operation.error];
}];
}
- (void)unmapFromWorkspace {
P4ItemAction *action = lockedAction;
// Prompt for unmapping existing folder
if ([[NSFileManager defaultManager] fileExistsAtPath:localPath] &&
[self promptAction:lockedAction message:
[NSString stringWithFormat:@"Would you like to unmap '%@' "
"directory with all of its contents?", name]] == NO)
return;
[self loadingAction:action message:@"Setting workspace mapping..."];
[[P4Workspace sharedInstance]
mappingSet:NO
path:remotePath
response:^(P4Operation *operation, NSArray *response) {
PSLog(@"Unmap response %@", response);
[self finishAction:action response:response error:operation.error];
}];
}
- (void)markAsRead {
if (flags.directory || !flags.unread)
return;
[[P4Workspace sharedInstance] markFilesRead:@[ localPath ]];
flags.unread = NO;
[self finishLoading];
// Propagate read change through parents
P4Item *parentItem = self;
while ((parentItem = parentItem->parent) && parentItem->parent) {
BOOL hasUpdated = NO;
for (P4Item *parentChild in parentItem->children)
if (parentChild->flags.unread) {
hasUpdated = YES;
break;
}
if (hasUpdated)
break;
parentItem->flags.unread = NO;
[parentItem finishLoading];
}
}
- (void)markItemsAsRead:(NSArray *)items {
for (P4Item *item in items) {
[item markAsRead];
}
}
- (void)markAllAsRead {
if (!flags.directory || !flags.unread)
return;
[[P4Workspace sharedInstance] markFilesRead:@[ localPath ]];
flags.unread = NO;
[self finishLoading];
// Propagate read change through parents
P4Item *parentItem = self;
while ((parentItem = parentItem->parent) && parentItem->parent) {
BOOL hasUpdated = NO;
for (P4Item *parentChild in parentItem->children)
if (parentChild->flags.unread) {
hasUpdated = YES;
break;
}
if (hasUpdated)
break;
parentItem->flags.unread = NO;
[parentItem finishLoading];
}
// Propagate read change through children
if (!children) // Not loaded yet
return;
NSMutableArray *queue = [NSMutableArray array];
if (children.count)
[queue addObjectsFromArray:children];
while (queue.count) {
P4Item *child = [queue lastObject];
[queue removeLastObject];
if (!child->flags.unread)
continue; // Has updated items
child->flags.unread = NO;
if (child->children.count)
[queue addObjectsFromArray:child->children];
[child finishLoading];
}
}
- (void)shelve {
if (flags.directory || !status)
return;
if (flags.shelved && [self promptAction:lockedAction message:
[NSString stringWithFormat:
@"Would you like to overwrite currently shelved file version "
"of '%@' ?", name]] == NO)
return;
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Shelving file..."];
[[P4Workspace sharedInstance]
shelveFile:remotePath ?: localPath
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
}
- (void)unshelve {
P4ItemAction *action = lockedAction;
// Prompt for overwriting
if (![self promptAction:lockedAction message:
[NSString stringWithFormat:
@"Would you like to unshelve and overwrite current"
" workspace file '%@' ?", name]])
return;
[self loadingAction:action message:@"Unshelving..."];
[[P4Workspace sharedInstance]
unshelveFile:remotePath ?: localPath
response:^(P4Operation *operation, NSArray *response) {
[self finishLoading];
[self finishAction:action response:response error:operation.error];
}];
}
- (void)discardShelve {
P4ItemAction *action = lockedAction;
// Prompt for discarding
if (![self promptAction:lockedAction message:
[NSString stringWithFormat:
@"Would you like to discard shelved version of"
" workspace file '%@' ?", name]])
return;
[self loadingAction:action message:@"Discarding shelved version..."];
[[P4Workspace sharedInstance]
discardShelvedFiles:@[ remotePath ?: localPath ]
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
}
- (void)openShelve {
if (flags.directory)
return;
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Opening shelved version..."];
[[P4Workspace sharedInstance]
openShelvedFile:remotePath
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
}
- (void)showVersions {
P4ItemAction *action = lockedAction;
[self finishAction:action response:[self valueForKey:@"path"] error:nil];
}
- (void)copyShareLink {
NSString *path = [@"p4:" stringByAppendingString:remotePath];
path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// NSURL *url = [[NSURL alloc] initWithString:path];
NSPasteboard *pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
[pboard setString:path forType:NSPasteboardTypeString];
// [pboard writeObjects:@[ path ]]; // Didn't work on apps like MS Outlook
}
- (void)openVersion:(NSString *)versionPath {
if (flags.directory)
return;
NSRange range = [versionPath rangeOfString:@"#" options:NSBackwardsSearch];
NSString *version = [versionPath substringFromIndex:range.location];
NSString *filePath = [versionPath substringToIndex:range.location];
NSString *filename = [[filePath lastPathComponent] stringByDeletingPathExtension];
filename = [filename stringByAppendingString:version];
filename = [filename stringByAppendingPathExtension:[filePath pathExtension]];
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Opening previous file version..."];
NSString *tmp = [NSString stringWithFormat:@"/tmp/p4/%@/%@",
[[P4Workspace sharedInstance] workspace], filename];
[[P4Workspace sharedInstance]
runCommand:[NSString stringWithFormat:@"print -o \"%@\" \"%@\"", tmp, versionPath]
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
if (!operation.errors)
[[NSWorkspace sharedWorkspace] openFile:tmp];
}];
}
- (void)revertToVersion:(NSString *)versionPath {
P4ItemAction *action = lockedAction;
NSString *prompt = [NSString stringWithFormat:@"Would you like to promote "
"version to latest?\n%@", versionPath];
if (![self promptAction:action message:prompt])
return;
[self loadingAction:action message:@"Promoting file version..."];
// Sync file with specified revision
[[P4Workspace sharedInstance]
runCommand:[NSString stringWithFormat:@"sync -f \"%@\"", versionPath]
response:^(P4Operation *operation, NSArray *response) {
if (operation.errors) {
NSArray *openedErrors = [operation errorsWithCode:P4ErrorOpenedLaterRevision];
NSString *openedPath = [[openedErrors lastObject] localizedFailureReason];
// Revert by syncing latest revision
[[P4Workspace sharedInstance]
runCommand:[NSString stringWithFormat:@"sync \"%@\"", openedPath]
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
return;
}
NSDictionary *record = response.count ? [response objectAtIndex:0] : nil;
NSString *syncPath = [record objectForKey:@"depotFile"];
NSString *syncAction = [record objectForKey:@"action"];
PSLog(@"Reverting %@\n\tPath %@\n\tAction %@", versionPath, syncPath, syncAction);
if ([syncAction isEqualToString:@"added"]) {
[[P4Workspace sharedInstance]
addFiles:@[ syncPath ]
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
}];
} else if ([syncAction isEqualToString:@"updated"]) {
[[P4Workspace sharedInstance]
editFiles:@[ syncPath ]
response:^(P4Operation *operation, NSArray *response) {
NSArray *resolveErrors = [operation errorsWithCode:P4ErrorMustSyncResolve];
NSArray *resolvePaths = [resolveErrors valueForKeyPath:@"localizedFailureReason"];
[operation ignoreErrors:resolveErrors];
// Finish if there are errors or there's no path to resolve
if (operation.errors || !resolvePaths.count) {
[self finishAction:action response:response error:operation.error];
return;
}
[[P4Workspace sharedInstance]
resolveFiles:resolvePaths
response:^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
PSLog(@"Resolved %@", response);
}];
}];
} else {
[self finishAction:action response:response error:
[NSError errorWithFormat:@"Unknown promote response %@", syncAction]];
}
}];
}
- (BOOL)isFavoriteFolder {
return [[[P4WorkspaceDefaults sharedInstance] favoriteFolders]
containsObject:self.path];
}
- (void)addFavoriteFolder {
if (flags.directory && !self.isFavoriteFolder)
[[P4WorkspaceDefaults sharedInstance] addFavoriteFolder:self.path];
}
- (void)removeFavoriteFolder {
if (flags.directory && self.isFavoriteFolder)
[[P4WorkspaceDefaults sharedInstance] removeFavoriteFolder:self.path];
}
#pragma mark - Edit Actions
- (BOOL)isEditable { return NO; }
- (void)createDirectory {
P4ItemAction *action = lockedAction;
if (!action)
action = [P4ItemAction
actionForItem:self name:@"Create directory"
selector:@selector(createDirectory)];
if (!localPath) {
[self finishAction:action response:nil error:
[NSError errorWithFormat: @"Couldn't create directory in item without local path"]];
return;
}
NSFileManager *filemanager = [NSFileManager defaultManager];
NSString *untitledPath = [localPath stringByAppendingPath:@"untitled folder"];
NSString *destinationPath = untitledPath;
NSInteger number = 1;
while ([filemanager fileExistsAtPath:destinationPath])
destinationPath = [untitledPath stringByAppendingFormat:@" %ld", number++];
destinationPath = [destinationPath stringByAppendingString:@"/"];
NSError *error;
if (![filemanager createDirectoryAtPath:destinationPath
withIntermediateDirectories:NO
attributes:nil
error:&error]) {
[self finishAction:action response:nil error:error];
return;
}
[self fileCreated:destinationPath];
[self finishAction:action response:@[ destinationPath ] error:error];
}
- (void)rename:(NSString *)newName {
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Renaming..."];
NSFileManager *filemanager = [NSFileManager defaultManager];
NSString *src = localPath;
NSString *dst = [[localPath stringByDeletingPath] stringByAppendingPath:newName];
// Append '/' slash to directories
BOOL dir = [filemanager fileExistsAtPath:src isDirectory:&dir] && dir;
dst = dir ? [dst stringByAppendingString:@"/"] : dst;
// Move a file
NSError *error;
if (![filemanager moveItemAtPath:src toPath:dst error:&error]) {
[self finishAction:action response:nil error:error];
return;
}
[[P4Workspace sharedInstance]
moveFiles:@[ src ]
toPaths:@[ dst ]
response:^(P4Operation *operation, NSArray *response) {
if (!flags.tracked)
[operation ignoreErrorsWithCode:P4ErrorFileNotOpened];
//rename isn't fully tested
[self finishAction:action response:response error:operation.error];
}];
}
- (void)addFiles:(NSArray *)paths {
[self loadingAction:lockedAction message:@"Adding files..."];
[self insertFiles:paths copy:YES];
}
- (void)copyFiles:(NSArray *)paths {
[self loadingAction:lockedAction message:@"Copying files..."];
[self insertFiles:paths copy:YES];
}
- (void)moveFiles:(NSArray *)paths {
[self loadingAction:lockedAction message:@"Moving files..."];
[self insertFiles:paths copy:NO];
}
- (void)addTag:(NSString *)tag {
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Adding tag..."];
tag = [tag stringByReplacingOccurrencesOfString:@"," withString:@"-"];
tag = [[tag componentsSeparatedByCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]] componentsJoinedByString:@"-"];
NSString *attr = [metadata objectForKey:@"openattr-tags"];
attr = attr.length ? [attr stringByAppendingFormat:@",%@", tag] : tag;
[[P4Workspace sharedInstance]
setAttribute:@"tags"
value:attr
paths:@[ remotePath ?: localPath ]
response:^(P4Operation *operation, NSArray *response) {
NSDictionary *dict = [response lastObject];
if ([[dict objectForKey:@"status"] isEqualToString:@"set"]) {
NSMutableDictionary *dict = [NSMutableDictionary
dictionaryWithDictionary:metadata];
[dict setObject:attr forKey:@"openattr-tags"];
metadata = dict;
[self refreshTags];
}
[self finishAction:action response:response error:operation.error];
[self finishLoading];
}];
}
- (void)removeTag:(NSString *)tag {
P4ItemAction *action = lockedAction;
[self loadingAction:action message:@"Removing tag..."];
NSString *attr = [metadata objectForKey:@"openattr-tags"];
if (!attr.length) {
[self finishAction:action response:nil
error:[NSError errorWithFormat:@"There's no pending tags to remove"]];
return;
}
NSMutableArray *tagsArr = [[attr componentsSeparatedByString:@","] mutableCopy];
[tagsArr removeObject:tag];
attr = tagsArr.count ? [tagsArr componentsJoinedByString:@","] : nil;
[[P4Workspace sharedInstance]
setAttribute:@"tags"
value:attr
paths:@[ remotePath ]
response:^(P4Operation *operation, NSArray *response) {
if (!operation.errors) {
NSMutableDictionary *dict = [NSMutableDictionary
dictionaryWithDictionary:metadata];
if (attr)
[dict setObject:attr forKey:@"openattr-tags"];
else
[dict removeObjectForKey:@"openattr-tags"];
metadata = dict;
[self refreshTags];
}
[self finishAction:action response:response error:operation.error];
[self finishLoading];
}];
}
#pragma mark - Contextual actions
- (NSArray *)actions {
// Default actions
P4ItemAction *action;
NSMutableArray *actions = [NSMutableArray array];
BOOL dir = NO;
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:localPath
isDirectory:&dir];
if (localPath.length && !flags.directory) {
action = [P4ItemAction
actionForItem:self name:@"Open"
selector:@selector(openWithCheckout)];
action.disabled = !exists || dir;
[actions addObject:action];
action = [P4ItemAction
actionForItem:self name:@"Open read only"
selector:@selector(open)];
action.disabled = !exists || dir;
[actions addObject:action];
}
if (flags.directory) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Check out all files"
selector:@selector(checkout)]];
[actions addObject:[P4ItemAction
actionForItem:self name:@"Check-in all files..."
selector:@selector(checkInAll)]];
}
if (flags.directory && parent) {
if ([self isFavoriteFolder])
[actions addObject:[P4ItemAction
actionForItem:self name:@"Remove from favorites"
selector:@selector(removeFavoriteFolder)]];
else
[actions addObject:[P4ItemAction
actionForItem:self name:@"Add to favorites"
selector:@selector(addFavoriteFolder)]];
}
if (!flags.directory && !status) {
if (flags.tracked)
[actions addObject:[P4ItemAction
actionForItem:self name:@"Check out"
selector:@selector(checkout)]];
else
[actions addObject:[P4ItemAction
actionForItem:self name:@"Mark for add"
selector:@selector(addItem)]];
}
if (status) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Check-in..."
selector:@selector(checkIn)]];
}
if (flags.unread) {
if (flags.directory)
[actions addObject:[P4ItemAction
actionForItem:self name:@"Mark all as read"
selector:@selector(markAllAsRead)]];
else
[actions addObject:[P4ItemAction
actionForItem:self name:@"Mark as read"
selector:@selector(markAsRead)]];
}
if (localPath.length) {
action = [P4ItemAction
actionForItem:self name:@"Show in Finder"
selector:@selector(showInFinder)];
action.disabled = !exists || dir != flags.directory;
[actions addObject:action];
}
if (!flags.directory && flags.tracked) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Show Versions"
selector:@selector(showVersions)]];
}
if (flags.tracked && parent)
[actions addObject:[P4ItemAction
actionForItem:self name:@"Copy link"
selector:@selector(copyShareLink)]];
if (status &&
// ![status isEqualToString:@"delete"] && // Shelve only edited but not deleted files
// ![status isEqualToString:@"move/delete"]
[status isEqualToString:@"edit"]) { // Shelve only when edited
if (flags.shelved)
[actions addObject:[P4ItemAction
actionForItem:self name:@"Shelve new version..."
selector:@selector(shelve)]];
else
[actions addObject:[P4ItemAction
actionForItem:self name:@"Shelve"
selector:@selector(shelve)]];
} else if (flags.shelved) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Unshelve..."
selector:@selector(unshelve)]];
}
// Manage shelved version
if (flags.shelved) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Open shelved version"
selector:@selector(openShelve)]];
[actions addObject:[P4ItemAction
actionForItem:self name:@"Discard shelved version"
selector:@selector(discardShelve)]];
}
if (status) {
if ([status isEqualToString:@"edit"])
[actions addObject:[P4ItemAction
actionForItem:self name:@"Undo checkout"
selector:@selector(revertIfUnchanged)]];
if ([status isEqualToString:@"delete"])
[actions addObject:[P4ItemAction
actionForItem:self name:@"Undo delete"
selector:@selector(revert)]];
else
[actions addObject:[P4ItemAction
actionForItem:self name:@"Revert changes"
selector:@selector(revert)]];
}
if (flags.directory && self.isEditable) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"New directory"
selector:@selector(createDirectory)]];
}
if (flags.directory && parent && flags.tracked) {
NSDictionary *mapping = [[[P4Workspace sharedInstance]
mappingForPaths:@[ localPath ]] lastObject];
NSString *map = [mapping objectForKey:@"action"];
action = [P4ItemAction
actionForItem:self name:nil
selector:@selector(unmapFromWorkspace)];
if ([map isEqualToString:@"tracked"]) {
action.name = @"Ignore folder in Workspace";
} else if ([map isEqualToString:@"mapped"]) {
action.name = @"Unmap from Workspace";
} else {
action.name = @"Not mapped into Workspace";
action.disabled = YES;
}
[actions addObject:action];
}
if (flags.directory && parent) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Delete all files"
selector:@selector(deleteItem)]];
} else if (flags.directory) { // Don't delete root directory
} else if ([status isEqualToString:@"delete"]) { // Don't delete
} else if ([status isEqualToString:@"move/delete"]) { // Don't delete
} else if (flags.tracked || status) {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Mark for delete"
selector:@selector(deleteItem)]];
} else {
[actions addObject:[P4ItemAction
actionForItem:self name:@"Delete local file"
selector:@selector(deleteItem)]];
}
return actions;
}
- (NSArray *)actionsForItems:(NSArray *)items {
// Default actions
return [NSMutableArray array];
}
- (id)defaultAction {
return nil;
}
- (void)performAction:(SEL)selector object:(id)object delegate:(id)delegate {
P4ItemAction *action = [P4ItemAction actionForItem:self name:nil selector:selector];
action.object = object;
action.delegate = delegate;
[action performAction];
}
- (void)performAction:(SEL)selector items:(NSArray *)items delegate:(id)delegate {
P4ItemAction *action = [P4ItemAction actionForItems:items name:nil selector:selector];
action.delegate = delegate;
[action performAction];
}
#pragma mark - Utils
+ (void)addObserver:(id<P4ItemDelegate>)observer {
NSMutableArray *targets = [[self class] targets];
if (![targets containsObject:observer])
[targets addObject:observer];
}
+ (void)removeObserver:(id <P4ItemDelegate>)observer {
NSMutableArray *targets = [[self class] targets];
if ([targets containsObject:observer])
[targets removeObject:observer];
}
- (void)loadPath:(NSString *)path PS_ABSTRACT_METHOD
- (P4Item *)cachedItemForPath:(NSString *)filePath {
BOOL remote = [filePath hasPrefix:@"//"];
NSString *parentPath = remote ? remotePath : localPath;
if (!filePath.length)
return self;
if ([filePath isEqualToString:parentPath])
return self;
// Children for whole path because it is not under parent's path
if (!parentPath || ![filePath hasPrefix:parentPath]) {
for (P4Item *child in self->children)
if ([remote ? child->remotePath : child->localPath isEqualToString:filePath])
return child;
return nil;
}
NSString *relative = [filePath substringFromIndex:parentPath.length];
relative = [relative stringByRemovingSuffix:@"/"];
NSArray *components = [relative pathComponents];
// Traversing
P4Item *item = self;
for (NSString *component in components) {
if (!item->children.count) // Not loaded
return nil;
for (P4Item *child in item->children) {
if ([child->name isEqualToString:component]) {
item = child;
break;
}
}
}
NSString *itemPath = remote ? item->remotePath : item->localPath;
if (itemPath && ![itemPath isEqualToString:filePath])
return nil;
return item;
}
- (void)reload {
self->children = nil;
self->metadata = nil;
[self refreshTags];
[self finishLoading];
self->flags.loading = YES;
}
#pragma mark - Protected
- (void)sortChildren {
// Sort files alphanumerically
children = [children sortedArrayUsingComparator:
^NSComparisonResult(P4Item *obj1, P4Item *obj2) {
return [obj1.name localizedStandardCompare:obj2.name];
}];
}
- (void)finishLoading {
flags.loading = NO;
flags.failure = NO;
for (id <P4ItemDelegate> target in [[self class] targets]) {
if ([target respondsToSelector:@selector(itemDidLoad:)])
[target itemDidLoad:self];
}
}
- (void)failWithError:(NSError *)error {
flags.loading = NO;
flags.failure = YES;
for (id <P4ItemDelegate> target in [[self class] targets]) {
if ([target respondsToSelector:@selector(item:didFailWithError:)])
[target item:self didFailWithError:error];
}
}
- (void)invalidate {
flags.loading = NO;
flags.failure = NO;
for (id <P4ItemDelegate> target in [[self class] targets]) {
if ([target respondsToSelector:@selector(itemDidInvalidate:)])
[target itemDidInvalidate:self];
}
}
- (void)refreshTags {
NSString *attr = ([metadata objectForKey:@"openattr-tags"] ?:
[metadata objectForKey:@"attr-tags"]);
tags = [attr componentsSeparatedByString:@","];
lowercaseTags = [tags valueForKeyPath:@"lowercaseString"];
}
#pragma mark - Private
+ (NSMutableArray *)targets {
static char targetsKey;
NSMutableArray *array = objc_getAssociatedObject(self, &targetsKey);
if (!array) {
CFArrayCallBacks callbacks = { 0, NULL, NULL, CFCopyDescription, CFEqual };
array = CFBridgingRelease(CFArrayCreateMutable(NULL, 0, &callbacks));
objc_setAssociatedObject(self, &targetsKey, array, OBJC_ASSOCIATION_RETAIN);
}
return array;
}
- (void)insertFiles:(NSArray *)paths copy:(BOOL)copy {
P4ItemAction *action = lockedAction;
NSFileManager *filemanager = [NSFileManager defaultManager];
NSMutableArray *sources = [NSMutableArray arrayWithCapacity:paths.count];
NSMutableArray *destinations = [NSMutableArray arrayWithCapacity:paths.count];
for (NSString *src in paths) {
// Remove files to overwrite
NSString *filename = src.lastPathComponent;
NSString *dest = [localPath stringByAppendingPath:filename];
// Inserting into same location
if ([src isEqualToString:dest]) {
if (copy) {
// Make duplicate
NSString *ext = [dest pathExtension];
NSString *copyPath = [[dest stringByDeletingPathExtension]
stringByAppendingString:@" copy"];
dest = [copyPath stringByAppendingPathExtension:ext];
NSInteger number = 1;
while ([filemanager fileExistsAtPath:dest])
dest = [copyPath stringByAppendingFormat:@" %ld.%@", number++, ext];
} else {
// Can't move into same location
[self finishAction:action response:nil error:
[NSError errorWithFormat:@"Can't move '%@' into the same location", filename]];
return;
}
}
if ([filemanager fileExistsAtPath:dest]) {
// Ask if overwrite
NSString *prompt = [NSString stringWithFormat:
@"An item named '%@' already exists in this location. "
"Do you want to replace it with the one you're moving ?",
filename];
if (![self promptAction:action message:prompt])
continue;
// Overwrite
[filemanager removeItemAtPath:dest error:NULL];
}
// Append '/' slash to directories
BOOL dir = [filemanager fileExistsAtPath:src isDirectory:&dir] && dir;
[destinations addObject:dir ? [dest directoryPath] : dest];
[sources addObject:dir ? [src directoryPath] : src];
// Move / copy a file
NSError *error;
if (copy ?
![filemanager copyItemAtPath:src toPath:dest error:&error] :
![filemanager moveItemAtPath:src toPath:dest error:&error]) {
// Filemanager error
[self finishAction:action response:nil error:error];
return;
}
}
if (!destinations.count) { // Nothing to move
[self finishAction:action response:nil error:nil];
return;
}
P4ResponseBlock_t block = ^(P4Operation *operation, NSArray *response) {
[self finishAction:action response:response error:operation.error];
};
if (copy)
[[P4Workspace sharedInstance] addFiles:destinations response:block];
else
[[P4Workspace sharedInstance] moveFiles:sources toPaths:destinations response:block];
}
- (void)loadingAction:(P4ItemAction *)action message:(NSString *)message {
if ([action.delegate respondsToSelector:@selector(action:loadingMessage:)])
[action.delegate action:action loadingMessage:message];
}
- (void)finishAction:(P4ItemAction *)action response:(NSArray *)response error:(NSError *)error {
if ([action.delegate respondsToSelector:@selector(action:didFinish:error:)])
[action.delegate action:action didFinish:response error:error];
}
- (BOOL)promptAction:(P4ItemAction *)action message:(NSString *)message {
if ([action.delegate respondsToSelector:@selector(action:prompt:)])
return [action.delegate action:action prompt:message];
return YES;
}
#pragma mark - P4Workspace delegate
#pragma mark Filesystem Events
- (void)fileCreated:(NSString *)filePath {
P4Item *item = [self cachedItemForPath:filePath];
[item finishLoading];
}
- (void)fileRemoved:(NSString *)filePath {
P4Item *item = [self cachedItemForPath:filePath];
[item finishLoading];
}
- (void)fileMoved:(NSString *)oldPath toPath:(NSString *)newPath {
P4Item *item = [self cachedItemForPath:oldPath];
[item finishLoading];
item = [self cachedItemForPath:newPath];
[item finishLoading];
}
- (void)fileRenamed:(NSString *)oldPath toPath:(NSString *)newPath {
P4Item *item = [self cachedItemForPath:oldPath];
[item finishLoading];
item = [self cachedItemForPath:newPath];
[item finishLoading];
}
- (void)fileModified:(NSString *)filePath {
P4Item *item = [self cachedItemForPath:filePath];
[item finishLoading];
}
#pragma mark P4 Events
- (void)file:(NSString *)filePath actionChanged:(NSDictionary *)info {
NSString *action = [info objectForKey:@"action"];
P4Item *item = [self cachedItemForPath:filePath];
if (item) {
item->remotePath = [info objectForKey:@"depotFile"];
item->status = action;
item->flags.tracked = YES;
}
[item finishLoading];
}
- (void)file:(NSString *)filePath revertedAction:(NSDictionary *)info {
// NSString *oldAction = [info objectForKey:@"oldAction"];
P4Item *item = [self cachedItemForPath:filePath];
if (item) {
NSString *action = [info objectForKey:@"action"];
NSNumber *revision = [info objectForKey:@"rev"];
BOOL reverted = [action isEqualToString:@"reverted"];
item->status = nil;
item->flags.tracked = reverted || revision;
NSString *pendingTags = [item->metadata objectForKey:@"openattr-tags"];
if (pendingTags) {
NSMutableDictionary *dict = [NSMutableDictionary
dictionaryWithDictionary:item->metadata];
[dict removeObjectForKey:@"openattr-tags"];
if (!reverted)
[dict setObject:pendingTags forKey:@"attr-tags"];
item->metadata = dict;
[item refreshTags];
}
}
[item finishLoading];
}
- (void)file:(NSString *)filePath updated:(NSDictionary *)info {
// Empty
}
- (void)file:(NSString *)filePath mappingChanged:(NSDictionary *)info {
P4Item *item = [self cachedItemForPath:filePath];
if (!item)
return; // Isn't loaded
NSString *action = [info objectForKey:@"action"];
BOOL ignored = [action isEqualToString:@"ignored"];
BOOL mapped = [action isEqualToString:@"mapped"];
if (info &&
item->flags.ignored == ignored &&
item->flags.mapped == mapped)
return; // No change
if (info) {
item->flags.ignored = ignored;
item->flags.tracked = !ignored;
item->flags.mapped = mapped;
} else {
// Take into account inherited mapping from parent
item->flags.ignored = NO;
item->flags.mapped = NO;
if (!item->parent)
item->flags.tracked = NO;
else if (item->parent->flags.mapped || item->parent->flags.tracked)
item->flags.tracked = YES;
else
item->flags.tracked = NO;
}
// Propagate mapping change through parents
P4Item *parentItem = item;
while ((parentItem = parentItem->parent) && parentItem->parent) {
if (item->flags.mapped || item->flags.hasMapped) {
// Mapping
if (parentItem->flags.hasMapped)
break;
parentItem->flags.hasMapped = YES;
} else {
// Unmapping
BOOL hasMapped = NO;
for (P4Item *parentChild in parentItem->children)
if (parentChild->flags.mapped || parentChild->flags.hasMapped) {
hasMapped = YES;
break;
}
if (hasMapped)
break;
parentItem->flags.hasMapped = NO;
}
[parentItem finishLoading];
}
[item finishLoading];
// Propagate mapping change through children
if (!item->children) // Not loaded yet
return;
NSMutableArray *queue = [NSMutableArray array];
if (item->children.count)
[queue addObjectsFromArray:item->children];
while (queue.count) {
P4Item *child = [queue lastObject];
[queue removeLastObject];
if (child->flags.mapped || child->flags.ignored)
continue; // Has own mapping
child->flags.tracked = item->flags.tracked;
if (child->children.count)
[queue addObjectsFromArray:child->children];
[child finishLoading];
}
}
- (void)file:(NSString *)filePath shelved:(NSDictionary *)info {
P4Item *item = [self cachedItemForPath:filePath];
if (!item)
return;
item->flags.shelved = info != nil;
[item finishLoading];
}
#pragma mark - QuickLook Preview Item
- (NSURL *)previewItemURL {
return localPath ? [NSURL fileURLWithPath:localPath] : nil;
}
- (NSString *)previewItemTitle {
return name;
}
@end
#pragma mark - P4ItemAction implementation
@implementation P4ItemAction
@synthesize delegate, item, items, name, selector, object, disabled;
- (void)setItems:(NSArray *)anItems {
// Create non retaining array of items
CFArrayCallBacks callbacks = { 0, NULL, NULL, CFCopyDescription, CFEqual };
items = CFBridgingRelease(CFArrayCreateMutable(NULL, 0, &callbacks));
[(NSMutableArray *)items addObjectsFromArray:anItems];
}
+ (id)actionForItem:(P4Item *)item name:(NSString *)name selector:(SEL)selector {
P4ItemAction *action = [[P4ItemAction alloc] init];
action.item = item;
action.name = name;
action.selector = selector;
return action;
}
+ (id)actionForItems:(NSArray *)items name:(NSString *)name selector:(SEL)selector {
P4ItemAction *action = [[P4ItemAction alloc] init];
action.items = items;
action.item = [items lastObject];
action.name = name;
action.selector = selector;
return action;
}
- (void)performAction {
NSAssert(item, @"P4ItemAction should have an item");
NSAssert(!(items && object), @"Received Action with object and items");
@synchronized(item) {
item.lockedAction = self;
if (![item respondsToSelector:selector])
return;
if (items)
objc_msgSend(item, selector, items);
else if (object)
objc_msgSend(item, selector, object);
else if (item)
objc_msgSend(item, selector);
item.lockedAction = nil;
}
}
@end