// // P4Item.m // Perforce // // Created by Adam Czubernat on 25.07.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "P4Item.h" #import @interface P4Item () { NSArray *tags, *lowercaseTags; } @property (nonatomic, retain) P4ItemAction *lockedAction; + (NSMutableArray *)targets; - (NSArray *)insertFiles:(NSArray *)paths destination:(NSString *)path copy:(BOOL)copy; - (void)loadingAction:(P4ItemAction *)action message:(NSString *)message; - (void)finishAction:(P4ItemAction *)action response:(NSArray *)response errors:(NSArray *)errors; - (BOOL)promptAction:(P4ItemAction *)action message:(NSString *)message; @end @implementation P4Item @synthesize lockedAction; @synthesize localPath = _localPath, remotePath = _remotePath; - (id)init { NSAssert(0, @"Wrong P4Item initializer"); return nil; } - (instancetype)initWithParent:(P4Item *)parentItem { if (self = [super init]) { parent = parentItem; if (!parent) { // Root flags.directory = YES; name = @"Root"; self.remotePath = @"//"; self.localPath = [[P4Workspace sharedInstance] root]; [[P4Workspace sharedInstance] addObserver:self]; } PSInstanceCreated([self class]); } return self; } - (void)dealloc { if (!parent) [[P4Workspace sharedInstance] removeObserver:self]; PSInstanceDeallocated([self class]); } - (id)debugQuickLookObject { NSString *string = self.path; NSFont *font = [NSFont systemFontOfSize:12.0]; CGRect rect = { 0, 0, [string sizeWithFont:font] }; rect.size.width += rect.size.height + 5.0; rect = CGRectIntegral(rect); NSImage *image = [NSImage imageWithSize:rect.size flipped:NO drawingHandler:^BOOL(NSRect dstRect) { [self.icon drawInRect:(CGRect) { 0, 0, rect.size.height, rect.size.height } fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil]; CGRect frame = rect; frame.origin.x += rect.size.height + 5.0; [string drawInRect:frame withAttributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : [NSColor blackColor] }]; return YES; }]; return image; } - (NSString *)description { return [NSString stringWithFormat:@"%@ : %@", NSStringFromClass([self class]), self.remotePath ?: self.localPath]; } #pragma mark - Public - (instancetype)parent { return parent; } - (NSString *)name { return name; } - (NSString *)path { return self.localPath ?: self.remotePath; } - (NSString *)localPath { return [_localPath decodePath]; } - (NSString *)remotePath { return [_remotePath encodePath]; } - (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 if (!metadata) file = @"IconFolderLocal"; 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 (!metadata) file = @"IconFileLocal"; 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 overlay; } - (NSImage *)previewWithSize:(CGSize)size { CGFloat minSize = fmin(size.width, size.height); if (self.localPath && [[NSFileManager defaultManager] fileExistsAtPath:self.localPath]) { // Generate real preview NSImage *image = [NSImage imageWithFilePreview:self.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 = self.remotePath.pathExtension; NSImage *image = [[NSWorkspace sharedWorkspace] iconForFileType:fileType]; [image setSize:CGSizeMake(minSize, minSize)]; return image; } #pragma mark - Actions - (void)open { if (flags.directory) return; // check to see if the file exists locally // if it does, then open the local file // otherwise, open the file from the server if([[NSFileManager defaultManager] fileExistsAtPath:self.localPath]) { // Open using default app [[NSWorkspace sharedWorkspace] openFile:self.localPath]; } else { // Open from the depot [self openFromDepot]; } // Mark as read if (flags.unread) [self markAsRead]; } - (void)openFromDepot { if (flags.directory) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening file from server..."]; NSString *tmpPath = [[NSString stringWithFormat:@"/tmp/p4/%@/%@", [[P4Workspace sharedInstance] workspace], name] decodePath]; [[P4Workspace sharedInstance] downloadPath:self.remotePath destination:tmpPath receive:nil response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; if (!operation.errors) [[NSWorkspace sharedWorkspace] openFile:tmpPath]; }]; } - (void)openWithCheckout { if ([status isEqualToString:@"edit"] || [status hasSuffix:@"add"]) { if (flags.unread) [self markAsRead]; [[NSWorkspace sharedWorkspace] openFile:self.localPath]; return; } P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening for edit..."]; [[P4Workspace sharedInstance] editFiles:@[ self.remotePath ] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; [[NSWorkspace sharedWorkspace] openFile:self.localPath]; }]; } - (void)showInFinder { if (self.localPath.length) [[NSWorkspace sharedWorkspace] selectFile:self.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 edit \"%@\" " "directory with all of its contents?", name]] == NO) return; [self checkoutFiles:@[ self.remotePath ]]; } - (void)checkoutFiles:(NSArray *)paths { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening for edit..."]; [[P4Workspace sharedInstance] editFiles:paths response:^(P4Operation *operation, NSArray *response) { // Replace not-in-workspace errors with user friendly message NSArray *errors = operation.errors; if (errors) { [operation ignoreErrorsWithCode:P4ErrorPathNotUnderRoot]; [operation ignoreErrorsWithCode:P4ErrorFileNotOnClient]; [operation ignoreErrorsWithCode:P4ErrorFileNotInView]; errors = operation.errors ?: @[ [NSError errorWithFormat:@"This file is not in DESI"] ]; } [self finishAction:action response:response errors:errors]; }]; } - (void)checkoutAllFiles:(NSArray *)paths { // Prompt for checking out folder if ([self promptAction:lockedAction message: [NSString stringWithFormat:@"Would you like to edit \n\"%@\"\n" "with all of its contents?", [paths componentsJoinedByString:@"\"\n\""]]] == NO) return; [self checkoutFiles:paths]; } - (void)addItem { [self addFiles:@[ self.localPath ]]; } - (void)addFiles:(NSArray *)paths { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Adding files..."]; [[P4Workspace sharedInstance] addFiles:paths response:^(P4Operation *operation, NSArray *response) { response = [response filteredArrayUsingClass:[NSDictionary class]]; response = [response valueForKey:@"depotFile"]; [self finishAction:action response:response errors:operation.errors]; }]; } - (void)reconcile { // Prompt for reconciling whole folder if (flags.directory && [self promptAction:lockedAction message: [NSString stringWithFormat:@"Would you like to reconcile all files in \"%@\" " "directory?\n\nThis operation can take some time.\n", name]] == NO) return; [self reconcileFiles:@[ self.localPath ]]; } - (void)reconcileFiles:(NSArray *)paths { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Reconciling..."]; [[P4Workspace sharedInstance] reconcileFiles:paths response:^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorOpenedBy]; response = [response filteredArrayUsingClass:[NSDictionary class]]; response = [response valueForKey:@"depotFile"]; NSArray *errors = operation.errors; if (!errors && !response.count) errors = @[ [NSError errorWithFormat:@"No files to reconcile"] ]; [self finishAction:action response:response errors:errors]; }]; } - (void)checkIn { [self checkInFiles:@[ self.remotePath ]]; } - (void)checkInFiles:(NSArray *)paths { P4ItemAction *action = lockedAction; [self finishAction:action response:paths errors:nil]; } - (void)checkInAll { if (!flags.directory) return; [self checkInAllFiles:@[ self.localPath ]]; } - (void)checkInAllFiles:(NSArray *)paths { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Submitting all files..."]; [[P4Workspace sharedInstance] listPendingFiles:paths response:^(P4Operation *operation, NSArray *response) { NSArray *paths = [response valueForKey:@"depotFile"]; NSArray *errors = operation.errors; if (!errors && !paths.count) errors = @[ [NSError errorWithFormat:@"No files to submit"] ]; [self finishAction:action response:paths errors:errors]; }]; } - (void)deleteItem { [self deleteItems:@[ self ]]; } - (void)deleteItems:(NSArray *)items { P4ItemAction *action = lockedAction; NSString *prompt = items.count > 4 ? [NSString stringWithFormat:@"Would you like to delete %ld items?", items.count] : [NSString stringWithFormat:@"Would you like to delete\n\"%@\"?", [[items valueForKey:@"name"] componentsJoinedByString:@"\"\n\""]]; if (![self promptAction:action message:prompt]) return; [self loadingAction:action message:@"Deleting files..."]; NSMutableArray *remotePaths = [NSMutableArray array]; // Remove files on local filesystem for (P4Item *item in items) { // Get local for remote path NSString *local = item.localPath; if (item.metadata) // Mark for delete only on tracked files [remotePaths addObject:item.remotePath]; // Remove local file if ([[NSFileManager defaultManager] fileExistsAtPath:local] && ![[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[local stringByDeletingLastPathComponent] destination:nil files:@[ [local lastPathComponent] ] tag:NULL]) { [self finishAction:action response:nil errors: @[ [NSError errorWithFormat:@"Couldn't delete file \"%@\"", item.name] ]]; return; } } if (!remotePaths.count) return [self finishAction:action response:nil errors:nil]; // Mark remote files as deleted [[P4Workspace sharedInstance] removeFiles:remotePaths response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (void)revert { // Reverting a file if (!flags.directory) { [self revertFiles:@[ self.localPath ]]; } else { // Prompt for reverting folder P4ItemAction *action = lockedAction; if (![self promptAction:lockedAction message: [NSString stringWithFormat:@"Would you like to revert \"%@\" " "directory with all of its contents?", name]]) return; [self loadingAction:action message:@"Reverting folder content..."]; [[P4Workspace sharedInstance] revertFiles:@[ [self.path encodePath ] ] response:^(P4Operation *operation, NSArray *response) { NSArray *errors = operation.errors; if (errors.count == 1 && [errors.firstObject code] == P4ErrorFileNotOpened) errors = @[ [NSError errorWithFormat:@"Nothing to revert"] ]; [self finishAction:action response:response errors:errors]; }]; } } - (void)revertFiles:(NSArray *)paths { P4ItemAction *action = lockedAction; NSString *prompt = paths.count > 4 ? [NSString stringWithFormat:@"Would you like to revert %ld items?", paths.count] : [NSString stringWithFormat:@"Would you like to revert\n\"%@\"?", [[paths valueForKey:@"lastPathComponent"] 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 errors:operation.errors]; }]; } - (void)revertIfUnchanged { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Cancelling edit..."]; [[P4Workspace sharedInstance] revertUnchangedFiles:@[ self.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 isEqualCaseInsensitive:self.localPath]) { [self finishAction:action response:response errors:operation.errors]; } else { [self finishAction:action response:response errors: @[ [NSError errorWithFormat: @"Action couldn't complete - file was changed.\n" "Please revert or submit pending changes"] ]]; } }]; } - (void)resolve { P4ItemAction *action = lockedAction; NSString *prompt = [NSString stringWithFormat:@"Would you like to resolve conflict" " using your version\n%@ ?", self.localPath]; if (![self promptAction:action message:prompt]) return; [self loadingAction:action message:@"Resolving file..."]; [[P4Workspace sharedInstance] resolveFiles:@[ self.localPath ] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (void)mapToWorkspace { P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Setting paths to sync..."]; [[P4Workspace sharedInstance] mappingSet:YES files:@[ self.remotePath ] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (void)unmapFromWorkspace { P4ItemAction *action = lockedAction; // Prompt for unmapping existing folder if ([[NSFileManager defaultManager] fileExistsAtPath:self.localPath] && [self promptAction:lockedAction message: [NSString stringWithFormat:@"Would you like to unsync \"%@\"%@", name, self.isDirectory ? @" directory with all of its contents?" : @""]] == NO) return; [self loadingAction:action message:@"Setting paths to unsync..."]; [[P4Workspace sharedInstance] mappingSet:NO files:@[ self.remotePath ] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (void)markAsRead { if (flags.directory || !flags.unread) return; [[P4Workspace sharedInstance] markFilesRead:@[ self.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)markFilesAsRead:(NSArray *)paths { for (NSString *path in paths) { [[self itemAtPath:path] markAsRead]; } } - (void)markAllAsRead { if (!flags.directory || !flags.unread) return; [[P4Workspace sharedInstance] markFilesRead:@[ self.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:self.remotePath ?: self.localPath response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (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:self.remotePath ?: self.localPath response:^(P4Operation *operation, NSArray *response) { [self finishLoading]; [self finishAction:action response:response errors:operation.errors]; }]; } - (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:@[ self.remotePath ?: self.localPath ] response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (void)openShelve { if (flags.directory) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening shelved version..."]; [[P4Workspace sharedInstance] openShelvedFile:self.remotePath response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (void)showVersions { P4ItemAction *action = lockedAction; [self finishAction:action response:[self valueForKey:@"path"] errors:nil]; } - (void)showFolderHistory { P4ItemAction *action = lockedAction; [self finishAction:action response:[self valueForKey:@"path"] errors:nil]; } - (void)copyShareLink { NSString *path = [@"p4:" stringByAppendingString:self.remotePath]; path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; // NSURL *url = [[NSURL alloc] initWithString:path]; NSPasteboard *pboard = [NSPasteboard generalPasteboard]; [pboard declareTypes:@[ NSPasteboardTypeString, NSPasteboardTypeHTML ] owner:self]; [pboard setString:path forType:NSPasteboardTypeString]; [pboard setString:[NSString stringWithFormat:@"%1$@", path] forType:NSPasteboardTypeHTML]; // [pboard writeObjects:@[ path ]]; // Didn't work on apps like MS Outlook } - (void)openVersion:(NSString *)versionPath { if (flags.directory) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Opening previous file version..."]; [[P4Workspace sharedInstance] openRevision:versionPath response:^(P4Operation *operation, NSArray *response) { [self finishAction:action response:response errors:operation.errors]; }]; } - (void)revertToVersion:(NSString *)revisionPath { P4ItemAction *action = lockedAction; NSString *prompt = [NSString stringWithFormat:@"Would you like to promote " "version to latest?\n%@", revisionPath]; if (![self promptAction:action message:prompt]) return; [self loadingAction:action message:@"Promoting file version..."]; // Sync file with specified revision [[P4Workspace sharedInstance] revertToRevision:revisionPath path:[self.path encodePath] response:^(P4Operation *operation, NSArray *response) { response = [response valueForKey:@"depotFile"]; [self finishAction:action response:response errors:operation.errors]; }]; } - (void)revertToChangelist:(NSNumber *)number { P4ItemAction *action = lockedAction; NSString *prompt = [NSString stringWithFormat:@"Would you like to revert " "directory to changelist %@?", number]; if (![self promptAction:action message:prompt]) return; [self loadingAction:action message:[NSString stringWithFormat: @"Reverting to changelist %@...", number]]; NSString *path = [NSString stringWithFormat:@"%@...", self.remotePath]; NSString *revisionPath = [NSString stringWithFormat:@"%@...@%@", self.remotePath, number]; // Check if opened [[P4Workspace sharedInstance] listOpenedFiles:@[ path ] response:^(P4Operation *operation, NSArray *response) { // Fail if opened if (response) { [self finishAction:action response:nil errors: @[ [NSError errorWithFormat:@"Looks like there are locked files in the folder. Make sure that files are not locked by you or others before reverting."] ]]; return; } // Sync file with specified revision [[P4Workspace sharedInstance] revertToRevision:revisionPath path:path response:^(P4Operation *operation, NSArray *response) { response = [response valueForKey:@"depotFile"]; [self finishAction:action response:response errors:operation.errors]; }]; }]; } - (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 { if ((flags.directory && !parent) || // Don't edit root directory self.isDeleted) { // Don't edit deleted return NO; } return YES; } - (BOOL)isDeleted { return [status hasSuffix:@"delete"]; } - (void)createDirectory { P4ItemAction *action = lockedAction; if (!action) action = [P4ItemAction actionForItem:self name:@"Create directory" selector:@selector(createDirectory)]; if (!self.localPath) { [self finishAction:action response:nil errors: @[ [NSError errorWithFormat:@"Couldn't create directory in item without local path"] ]]; return; } NSFileManager *filemanager = [NSFileManager defaultManager]; NSString *untitledPath = [self.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:YES attributes:nil error:&error]) { [self finishAction:action response:nil errors:@[ error ]]; return; } [self fileCreated:destinationPath]; [self finishAction:action response:@[ destinationPath ] errors:nil]; } - (void)rename:(NSString *)newName { if ([name isEqualToString:newName]) return; P4ItemAction *action = lockedAction; [self loadingAction:action message:@"Renaming..."]; NSFileManager *filemanager = [NSFileManager defaultManager]; BOOL exists = [filemanager fileExistsAtPath:self.localPath]; BOOL isCasingRename = [name isEqualCaseInsensitive:newName]; NSString *newRemotePath = [[self.remotePath stringByDeletingPath] stringByAppendingPath:newName]; NSString *newLocalPath = [[self.localPath stringByDeletingPath] stringByAppendingPath:[newName decodePath]]; if (flags.directory) { newRemotePath = [[newRemotePath directoryPath] encodePath]; newLocalPath = [newLocalPath directoryPath]; } // Check if new filename isn't already taken on remote if (!isCasingRename && [parent itemAtPath:newRemotePath]) { [self finishAction:action response:nil errors: @[ [NSError errorWithFormat:@"Name is already taken: %@", newRemotePath] ]]; return; } // Rename local file if (exists) { NSError *error; NSString *src = self.localPath; NSString *dst = newLocalPath; // Rename character case atomically if (isCasingRename) { if (rename([src fileSystemRepresentation], [dst fileSystemRepresentation])) { [self finishAction:action response:nil errors: @[ [NSError errorWithFormat:@"Couldn't rename file %@", src] ]]; return; } // Normal rename } else if (![filemanager moveItemAtPath:src toPath:dst error:&error]) { [self finishAction:action response:nil errors:@[ error ]]; return; } } if (!metadata) { // Mark for move only on tracked files [self finishAction:action response:nil errors:nil]; return; } [[P4Workspace sharedInstance] moveFiles:@[ self.remotePath ] toPaths:@[ newRemotePath ] response:^(P4Operation *operation, NSArray *response) { if (!flags.tracked) [operation ignoreErrorsWithCode:P4ErrorFileNotOpened]; if (operation.errors || exists) return [self finishAction:action response:response errors:operation.errors]; // Copy from depot if doesn't exist on local filesystem [[P4Workspace sharedInstance] downloadPath:self.remotePath destination:self.localPath receive:^(P4Operation *operation) { [self loadingAction:action message: [NSString stringWithFormat:@"Downloading %@", self.remotePath]]; } response:^(P4Operation *operation, NSArray *response) { [filemanager moveItemAtPath:self.localPath toPath:newLocalPath error:NULL]; [self finishAction:action response:response errors:operation.errors]; }]; }]; } - (void)insertFiles:(NSArray *)paths { [self loadingAction:lockedAction message:@"Adding files..."]; [self insertFiles:paths destination:self.localPath copy:YES]; } - (void)copyFiles:(NSArray *)paths { [self loadingAction:lockedAction message:@"Copying files..."]; [self insertFiles:paths destination:self.localPath copy:YES]; } - (void)moveFiles:(NSArray *)paths { [self loadingAction:lockedAction message:@"Moving files..."]; [self insertFiles:paths destination:self.localPath copy:NO]; } - (void)pasteFiles:(NSArray *)paths { [self loadingAction:lockedAction message:@"Pasting files..."]; [self insertFiles:paths destination:self.localPath copy:YES]; } - (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:@[ self.remotePath ?: self.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 errors:operation.errors]; [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 errors: @[ [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:@[ self.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 errors:operation.errors]; [self finishLoading]; }]; } - (NSArray *)downloadItems:(NSArray *)items destination:(NSString *)path { [self loadingAction:lockedAction message:@"Downloading files..."]; return [self insertFiles:[items valueForKeyPath:@"path"] destination:path copy:YES]; } #pragma mark - Contextual actions - (NSArray *)actions { BOOL tracked = metadata != nil; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:self.localPath]; BOOL deleted = [self isDeleted]; NSMutableArray *actions = [NSMutableArray array]; void (^add)(NSString *, SEL) = ^(NSString *actionName, SEL selector) { [actions addObject:[P4ItemAction actionForItem:self name:actionName selector:selector]]; }; if (flags.directory) { if ([[P4Workspace sharedInstance] changesForPath:self.remotePath].count) add(@"Submit all files", @selector(checkInAll)); if (tracked) add(@"Edit all files", @selector(checkout)); if (exists) add(@"Reconcile all files", @selector(reconcile)); } else { if (tracked && !deleted) add(@"Open", @selector(openWithCheckout)); if (self.localPath.length) add(@"Open read only", @selector(open)); if (status) add(@"Submit", @selector(checkIn)); else if (tracked) add(@"Edit file", @selector(checkout)); else add(@"Mark for add", @selector(addItem)); } // Mappings if (self.isIgnored) add(@"Stop ignoring", @selector(mapToWorkspace)); else if (self.isMapped && parent) add(@"Unsync from computer", @selector(unmapFromWorkspace)); else if (flags.directory && self.isTracked && parent) add(@"Ignore folder", @selector(unmapFromWorkspace)); else if (flags.directory && tracked) add(@"Sync with computer", @selector(mapToWorkspace)); // Unread if (flags.unread) { if (flags.directory) add(@"Mark all as read", @selector(markAllAsRead)); else add(@"Mark as read", @selector(markAsRead)); } if (self.localPath.length) { add(@"Show in Finder", @selector(showInFinder)); [actions.lastObject setDisabled:!exists]; } // History if (tracked) { if (flags.directory) add(@"Show folder history", @selector(showFolderHistory)); else add(@"Show Versions", @selector(showVersions)); } // Favorites if (flags.directory && parent) { if ([self isFavoriteFolder]) add(@"Remove from favorites", @selector(removeFavoriteFolder)); else add(@"Add to favorites", @selector(addFavoriteFolder)); } if (tracked && parent) add(@"Copy link", @selector(copyShareLink)); if (flags.directory && self.localPath && exists) add(@"New directory", @selector(createDirectory)); // Shelving if ([status isEqualToString:@"edit"]) { // Shelve only when edited if (flags.shelved) add(@"Shelve new version", @selector(shelve)); else add(@"Shelve", @selector(shelve)); } else if (flags.shelved) { add(@"Unshelve", @selector(unshelve)); } if (flags.shelved) { add(@"Open shelved version", @selector(openShelve)); add(@"Discard shelved version", @selector(discardShelve)); } // Reverting if (flags.directory && parent && (flags.hasMapped || flags.tracked)) add(@"Revert all files", @selector(revert)); if ([status isEqualToString:@"edit"]) add(@"Cancel edit", @selector(revertIfUnchanged)); if ([status isEqualToString:@"delete"]) add(@"Undo delete", @selector(revert)); else if (status) add(@"Revert changes", @selector(revert)); if ([metadata objectForKey:@"unresolved"]) add(@"Resolve using mine...", @selector(resolve)); // Deleting if (flags.directory && parent) add(@"Trash all files", @selector(deleteItem)); else if (tracked || status) add(@"Trash", @selector(deleteItem)); else add(@"Trash local file", @selector(deleteItem)); return actions; } - (NSArray *)actionsForItems:(NSArray *)items { // Default actions return [NSMutableArray array]; } - (id)defaultAction { if (flags.directory) return nil; if ([self isDeleted]) return nil; return [P4ItemAction actionForItem:self name:@"Open" selector:@selector(openWithCheckout)]; } - (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]; } #pragma mark - Utils + (void)addObserver:(id)observer { NSMutableArray *targets = [[self class] targets]; if (![targets containsObject:observer]) [targets addObject:observer]; } + (void)removeObserver:(id )observer { NSMutableArray *targets = [[self class] targets]; if ([targets containsObject:observer]) [targets removeObject:observer]; } - (void)loadPath:(NSString *)path PS_ABSTRACT_METHOD - (instancetype)itemAtPath:(NSString *)cachedPath { if (!cachedPath.length) // ??? return self; // ??? BOOL remote = [cachedPath hasPrefix:@"//"]; NSString *parentPath = remote ? self.remotePath : self.localPath; if ([cachedPath isEqualCaseInsensitive:parentPath]) // Check if self return self; NSArray *items = self->children; for (NSUInteger i = 0, count = items.count; ichildren; for (NSUInteger i = 0, count = items.count; ichildren, i = 0, count = items.count; } } } - (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 NSComparisonResultMake(obj1.name, obj2.name, localizedStandardCompare:); }]; } - (void)finishLoading { flags.loading = NO; flags.failure = NO; for (id 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 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 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; } - (NSArray *)insertFiles:(NSArray *)paths destination:(NSString *)destination copy:(BOOL)copy { P4ItemAction *action = lockedAction; P4ThreadOperation *finishOperation = [[P4ThreadOperation alloc] init]; destination = [destination directoryPath]; NSString *root = [P4Workspace sharedInstance].root; BOOL srcOutsideRoot = NO; BOOL dstOutsideRoot = ![destination isSubpath:root]; NSFileManager *filemanager = [NSFileManager defaultManager]; NSMutableArray *sources = [NSMutableArray arrayWithCapacity:paths.count]; NSMutableArray *destinations = [NSMutableArray arrayWithCapacity:paths.count]; for (__strong NSString *src in paths) { BOOL depot = [src hasPrefix:@"//"]; BOOL dir = (depot ? [src hasSuffix:@"/"] : [filemanager fileExistsAtPath:src isDirectory:&dir] && dir); NSString *filename = src.lastPathComponent; NSString *dest = [destination stringByAppendingPath:filename]; if (dir) { src = [src directoryPath]; dest = [dest directoryPath]; } NSString *localSrc = depot ? [root stringByAppendingString:[src relativePath:@"//"]] : src; // Inserting into the same location if ([localSrc isEqualCaseInsensitive:dest]) { if (copy) { // Make duplicate name NSString *ext = [dest pathExtension]; NSString *copyPath = [[dest stringByDeletingPathExtension] stringByAppendingString:@" copy"]; dest = [copyPath stringByAppendingPathExtension:ext]; for (NSInteger i = 1; [filemanager fileExistsAtPath:dest]; i++) dest = [copyPath stringByAppendingFormat:@" %ld.%@", i, ext]; } else { // Can't move into same location [self finishAction:action response:nil errors: @[ [NSError errorWithFormat:@"Can't move \"%@\" into the same location", filename] ]]; break; } } // Inserting directory into self if (dir && [dest isSubpath:localSrc]) { [self finishAction:action response:nil errors: @[ [NSError errorWithFormat:@"Can't %@ directory \"%@\" into self", copy ? @"copy" : @"move", filename] ]]; break; } // Overwriting existing file if ([filemanager fileExistsAtPath:dest]) { 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]) // Ask if overwrite continue; // Skip this file // Overwrite [filemanager removeItemAtPath:dest error:NULL]; } // Copying from depot if (depot) { P4Operation *copyOperation = [[P4Workspace sharedInstance] downloadPath:src destination:dest receive:^(P4Operation *operation) { [self loadingAction:action message: [NSString stringWithFormat:@"Downloading %@", filename]]; } response:^(P4Operation *operation, NSArray *response) { for (NSError *error in operation.errors) [finishOperation addError:error]; // Pass errors to finish operation }]; // Queue operations [finishOperation addDependency:copyOperation]; // Move / copy local file } else { NSError *error; if (![filemanager fileExistsAtPath:dest.stringByDeletingLastPathComponent]) [filemanager createDirectoryAtPath:dest.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:NULL]; if ([filemanager fileExistsAtPath:localSrc] && (copy ? ![filemanager copyItemAtPath:localSrc toPath:dest error:&error] : ![filemanager moveItemAtPath:localSrc toPath:dest error:&error])) { // Filemanager error [self finishAction:action response:nil errors:@[ error ]]; break; } if (![localSrc isSubpath:root]) srcOutsideRoot = YES; } // Add file for workspace action [sources addObject:src]; [destinations addObject:dest]; } if (!destinations.count) { // Nothing to move [self finishAction:action response:nil errors:nil]; return nil; } // Run finishOperation NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [finishOperation setBlock:^(P4ThreadOperation *finishOperation) { P4ResponseBlock_t block = ^(P4Operation *operation, NSArray *response) { [operation ignoreErrorsWithCode:P4ErrorPathNotUnderRoot]; // Ignore error when moving outside root [self loadingAction:action message:@"Completed"]; [self finishAction:action response:response errors:operation.errors]; }; if (finishOperation.errors) block(finishOperation, finishOperation.response); else if (!copy && !dstOutsideRoot && !srcOutsideRoot) [[P4Workspace sharedInstance] moveFiles:sources toPaths:destinations response:block]; else if (!dstOutsideRoot) [[P4Workspace sharedInstance] addFiles:destinations response:block]; else if (!copy && !srcOutsideRoot) [[P4Workspace sharedInstance] removeFiles:sources response:block]; else block(nil, nil); }]; [queue addOperation:finishOperation]; return [destinations valueForKeyPath:@"lastPathComponent"]; } - (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 errors:(NSArray *)errors { if ([action.delegate respondsToSelector:@selector(action:didFinish:errors:)]) [action.delegate action:action didFinish:response errors:errors]; } - (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 itemAtPath:filePath]; [item finishLoading]; } - (void)fileRemoved:(NSString *)filePath { P4Item *item = [self itemAtPath:filePath]; [item finishLoading]; } - (void)fileMoved:(NSString *)oldPath toPath:(NSString *)newPath { P4Item *item = [self itemAtPath:oldPath]; [item finishLoading]; item = [self itemAtPath:newPath]; [item finishLoading]; } - (void)fileRenamed:(NSString *)oldPath toPath:(NSString *)newPath { P4Item *item = [self itemAtPath:oldPath]; [item finishLoading]; item = [self itemAtPath:newPath]; [item finishLoading]; } - (void)fileModified:(NSString *)filePath { P4Item *item = [self itemAtPath:filePath]; [item finishLoading]; } #pragma mark P4 Events - (void)file:(NSString *)filePath actionChanged:(NSDictionary *)info { NSString *action = [info objectForKey:@"action"]; P4Item *item = [self itemAtPath: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 itemAtPath:filePath]; if (item) { NSString *action = [info objectForKey:@"action"]; NSNumber *revision = [info objectForKey:@"rev"]; BOOL reverted = ([action isEqualToString:@"reverted"] || [action isEqualToString:@"cleared"]); 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 itemAtPath: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 itemAtPath:filePath]; if (!item) return; item->flags.shelved = info != nil; [item finishLoading]; } #pragma mark - QuickLook Preview Item - (NSURL *)previewItemURL { return self.localPath ? [NSURL fileURLWithPath:self.localPath] : nil; } - (NSString *)previewItemTitle { return name; } @end #pragma mark - P4ItemAction implementation @implementation P4ItemAction @synthesize delegate, item, name, selector, object, disabled; + (id)actionForItem:(P4Item *)item name:(NSString *)name selector:(SEL)selector { return [self actionForItem:item object:nil name:name selector:selector]; } + (id)actionForItem:(P4Item *)item object:(id)object name:(NSString *)name selector:(SEL)selector { P4ItemAction *action = [[P4ItemAction alloc] init]; action.item = item; action.object = object; action.name = name; action.selector = selector; return action; } - (void)performAction { NSAssert(item, @"P4ItemAction should have an item"); @synchronized(item) { item.lockedAction = self; if (![item respondsToSelector:selector]) return; if (object) objc_msgSend(item, selector, object); else if (item) objc_msgSend(item, selector); item.lockedAction = nil; } } @end