// // P4SpecManager.m // SCMMenuExtra // // Created by Michael Bishop on 1/29/10. // Copyright 2010 Perforce Software. All rights reserved. // #import "P4SpecManager.h" #import "P4SpecDescription.h" #import "P4Port.h" #import "P4Connection.h" #import "P4ConnectionPool.h" #import "P4Response.h" #import "P4ClientApi.h" #import "P4ErrorCodes.h" #import "NGAUtilities.h" #import "P4TaggedDataInflaterTransformer.h" #import "P4TaggedDataInflaterTransformer.h" #import "P4SpecManager_p.h" #import "P4SpecEntityDescriptionAdditions.h" #define COREDATA_STORE_TYPE NSInMemoryStoreType //NSString * const kP4SerializedPropertyKeys = @"P4SerializedPropertyKeys"; //NSString * const kP4DataBackedPropertyNameKey = @"P4DataBackedPropertyNameKey"; NSString * const kSpecPropertyNameDateRefreshed = @"lastRefreshDatesByProperty"; NSString * const kSpecPropertyNameType = @"type"; NSString * const kSpecPropertyNameIdentifier = @"identifier"; NSString * const kSpecPropertyNameBaseFieldValues = @"baseFields"; NSString * const kSpecPropertyNameTheirsFieldValues = @"theirsFields"; // 'Theirs' refers to Perforce terminology when performing a merge. NSString * const kEntityPropertyNameSpecSummarizedPropertyKeys = @"summarizedPropertyKeys"; NSString * const kP4LastAccessTimeKey = @"isLastAccessTime"; //NSString * const kSerializedDataKeyPostfix = @"AsSerializedData"; // MetaData strings NSString * const kP4FetchedRelationships = @"P4FetchedRelationships"; NSString * const kP4FetchedRelationshipDestinationSpecType = @"P4FetchedRelationshipDestinationSpecType"; NSString * const kP4FetchedRelationshipDestinationInverseName = @"P4FetchedRelationshipDestinationInverseName"; NSString * const kP4FetchedRelationshipName = @"P4FetchedRelationshipName"; NSString * const kP4InverseFetchedPropertyRelationshipName = @"P4InverseFetchedPropertyRelationshipName"; NSString * const kP4InverseFetchedPropertyRelationshipSpecType = @"P4InverseFetchedPropertyRelationshipSpecType"; NSString * const kNGAFetchedPropertyFetchDate = @"NGAFetchedPropertyFetchDate"; NSString * const kNGAFetchedPropertyFetchData = @"NGAFetchedPropertyFetchData"; @interface P4SpecManager () -(id)initWithP4Port:(NSString*)p username:(NSString*)u; -(NSString*)specCacheDirectory; -(NSManagedObjectModel *)managedObjectModel; -(NSManagedObjectContext *) managedObjectContext; -(NSString*)entityNameForTypeName:(NSString*)typeName; -(NSEntityDescription*)newEntityDescriptionForType:(NSString*)typeName error:(NSError**)error; -(NSArray*)filterArgumentsForListingSpecType:(NSString*)command fromPredicate:(NSPredicate*)predicate; @end struct defaultspec { NSString * const type; NSString * const spec; } speclist[] = { { @"branch", @"Branch;code:301;rq;ro;fmt:L;len:32;;" "Update;code:302;type:date;ro;fmt:L;len:20;;" "Access;code:303;type:date;ro;fmt:L;len:20;;" "Owner;code:304;fmt:R;len:32;;" "Description;code:306;type:text;len:128;;" "Options;code:309;type:line;len:32;val:" "unlocked/locked;;" "View;code:311;type:wlist;words:2;len:64;;" }, { @"change", @"Change;code:201;rq;ro;fmt:L;seq:1;len:10;;" "Date;code:202;type:date;ro;fmt:R;seq:3;len:20;;" "Client;code:203;ro;fmt:L;seq:2;len:32;;" "User;code:204;ro;fmt:L;seq:4;len:32;;" "Status;code:205;ro;fmt:R;seq:5;len:10;;" "Description;code:206;type:text;rq;seq:6;;" "JobStatus;code:207;fmt:I;type:select;seq:8;;" "Jobs;code:208;type:wlist;seq:7;len:32;;" "Files;code:210;type:llist;len:64;;" }, { @"client", @"Client;code:301;rq;ro;seq:1;len:32;;" "Update;code:302;type:date;ro;seq:2;fmt:L;len:20;;" "Access;code:303;type:date;ro;seq:4;fmt:L;len:20;;" "Owner;code:304;seq:3;fmt:R;len:32;;" "Host;code:305;seq:5;fmt:R;len:32;;" "Description;code:306;type:text;len:128;;" "Root;code:307;rq;type:line;len:64;;" "AltRoots;code:308;type:llist;len:64;;" "Options;code:309;type:line;len:64;val:" "noallwrite/allwrite,noclobber/clobber,nocompress/compress," "unlocked/locked,nomodtime/modtime,normdir/rmdir;;" "SubmitOptions;code:313;type:select;fmt:L;len:25;val:" "submitunchanged/submitunchanged+reopen/revertunchanged/" "revertunchanged+reopen/leaveunchanged/leaveunchanged+reopen;;" "LineEnd;code:310;type:select;fmt:L;len:12;val:" "local/unix/mac/win/share;;" "View;code:311;type:wlist;words:2;len:64;;" }, { @"depot", @"Depot;code:251;rq;ro;len:32;;" "Owner;code:252;len:32;;" "Date;code:253;type:date;ro;len:20;;" "Description;code:254;type:text;len:128;;" "Type;code:255;rq;len:10;;" "Address;code:256;len:64;;" "Suffix;code:258;len:64;;" "Map;code:257;rq;len:64;;" }, { @"group", @"Group;code:401;rq;ro;len:32;;" "MaxResults;code:402;type:word;len:12;;" "MaxScanRows;code:403;type:word;len:12;;" "MaxLockTime;code:407;type:word;len:12;;" "Timeout;code:406;type:word;len:12;;" "Subgroups;code:404;type:wlist;len:32;opt:default;;" "Owners;code:408;type:wlist;len:32;opt:default;;" "Users;code:405;type:wlist;len:32;opt:default;;" }, { @"job", @"Job;code:101;rq;len:32;;" "Status;code:102;type:select;rq;len:10;" "pre:open;val:open/suspended/closed;;" "User;code:103;rq;len:32;pre:$user;;" "Date;code:104;type:date;ro;len:20;pre:$now;;" "Description;code:105;type:text;rq;pre:$blank;;" }, { @"label", @"Label;code:301;rq;ro;fmt:L;len:32;;" "Update;code:302;type:date;ro;fmt:L;len:20;;" "Access;code:303;type:date;ro;fmt:L;len:20;;" "Owner;code:304;fmt:R;len:32;;" "Description;code:306;type:text;len:128;;" "Options;code:309;type:line;len:64;val:" "unlocked/locked;;" "Revision;code:312;type:word;words:1;len:64;;" "View;code:311;type:wlist;len:64;;" }, { @"license", @"License;code:451;len:32;;" "License-Expires;code:452;len:10;;" "Support-Expires;code:453;len:10;;" "Customer;code:454;type:line;len:128;;" "Application;code:455;len:32;;" "IPaddress;code:456;len:24;;" "Platform;code:457;len:32;;" "Clients;code:458;len:8;;" "Users;code:459;len:8;;" }, { @"protect", @"Protections;code:501;type:wlist;words:5;opt:default;len:64;;" }, { @"spec", @"Fields;code:351;type:wlist;words:5;rq;;" "Words;code:352;type:wlist;words:2;;" "Formats;code:353;type:wlist;words:3;;" "Values;code:354;type:wlist;words:2;;" "Presets;code:355;type:wlist;words:2;;" "Comments;code:356;type:text;;" }, { @"triggers", @"Triggers;code:551;type:wlist;words:4;len:64;opt:default;" }, { @"typemap", @"TypeMap;code:601;type:wlist;words:2;len:64;opt:default;" }, { @"user", @"User;code:651;rq;ro;seq:1;len:32;;" "Email;code:652;fmt:R;rq;seq:3;len:32;;" "Update;code:653;fmt:L;type:date;ro;seq:2;len:20;;" "Access;code:654;fmt:L;type:date;ro;len:20;;" "FullName;code:655;fmt:R;type:line;rq;len:32;;" "JobView;code:656;type:line;len:64;;" "Password;code:657;len:32;;" "Reviews;code:658;type:wlist;len:64;;" }, { nil, nil } }; // P4SpecManagers are retained by the application as a singleton static NSMutableDictionary * SpecManagerInstances = nil; @implementation P4SpecManager @synthesize connection; +(id)auxiliarySpecMetadata { static dispatch_once_t pred; static NSDictionary *sharedInstance = nil; dispatch_once(&pred, ^{ // IDEA: Add additional description information that a property of a spec // refers to a name (or names) of other specs. We can use this when // building our entity descriptions. NSString * pathToMetadataPropertyList = [[NSBundle bundleForClass:[self class]] pathForResource:@"P4AuxiliarySpecMetadata" ofType:@"plist"]; sharedInstance = [[NSDictionary dictionaryWithContentsOfFile:pathToMetadataPropertyList] retain]; if ( !sharedInstance ) LOG_CRITICAL(@"%@ not found! Fetched properties will not work!", pathToMetadataPropertyList); }); return sharedInstance; } +(NSArray*)allOpenManagers { return [SpecManagerInstances allValues]; } +(P4SpecManager*)managerWithPortString:(NSString*)p4port username:(NSString*)username { if ( !SpecManagerInstances ) SpecManagerInstances = [NSMutableDictionary new]; NSString * key = [NSString stringWithFormat:@"%@/%@", p4port, username]; P4SpecManager * specManager = [SpecManagerInstances objectForKey:key]; if ( !specManager ) { specManager = [[[P4SpecManager alloc] initWithP4Port:p4port username:username] autorelease]; [SpecManagerInstances setObject:specManager forKey:key]; } return specManager; } -(id)initWithP4Port:(NSString*)p username:(NSString*)u { if ( (self = [super init]) == nil ) return nil; connection = [[P4Connection connectionWithPortString:p user:u] retain]; specDescriptions = [NSMutableDictionary new]; [self reset]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp]; return self; } -(id)init { return [self initWithP4Port:nil username:nil]; } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [specDescriptions release]; [connection release]; [managedObjectContext release]; [persistentStoreCoordinator release]; [managedObjectModel release]; [super dealloc]; } -(void)applicationWillTerminate:(NSNotification*)notification { [self flushCache:nil]; } -(void)refreshSpecDefinitionsWithCompletion:(SpecRefreshCompletionBlock)completion { __block int oustandingUpdateCount = 0; __block NSMutableArray * errors = [NSMutableArray array]; // There's probably a better way to do this, but I need to run a block with a // shared variable for each item in the spec list and then at the end, clean up // Thought: one way might be to assemble the blocks first and then to for( struct defaultspec *sp = &speclist[ 0 ]; sp->type; sp++ ) { NSString * typename = sp->type; NSArray * arguments = [NSArray arrayWithObjects:@"spec", @"-o", sp->type, nil]; [errors retain]; oustandingUpdateCount++; [self runArguments:arguments updateBlock:nil completionBlock:^(P4Response*response) { oustandingUpdateCount--; // These all happen on the main thread so we are ok changing this if ( response.error ) { [errors addObject:response.error]; [errors release]; return; } NSString * specdef = [self specDefStringFromSpecDefinition:response.result]; [self setSpecDefinitionAsString:specdef forTypeNamed:typename]; // lastly call the completion since we have fetched all the spec // definitions if (oustandingUpdateCount) { [errors release]; return; } NSError * error = nil; if ( [errors count] ) error = [NSError errorWithDomain:P4MEErrorDomain code:kP4MESpecDefinitionRefreshError userInfo:[NSDictionary dictionaryWithObject:errors forKey:kP4MEErrorUnderlyingErrorsKey] ]; completion( error ); [errors release]; }]; } } -(BOOL)flushCache:(NSError**)error { if ( ![[self managedObjectContext] hasChanges] ) return YES; return [[self managedObjectContext] save:error]; } -(void)save:(NSTimer*)timer { // LOG_DEBUG(@"saving..."); [savingTimer release]; savingTimer = nil; NSError * error = nil; if ( ![self flushCache:&error] ) { LOG_ERROR(@"%@", error); [NSApp presentError:error]; } // LOG_DEBUG(@"...saved"); } -(void)requestSave { if ( ![[self managedObjectContext] hasChanges] ) return; // saves after a delay. The clock will restart if another request comes in const NSTimeInterval kSaveThrottle = 0.5; // If the save has been delayed longer than this, it will save regardless const NSTimeInterval kForcedSaveTimeout = 5.0; if ( !savingTimer ) { // LOG_DEBUG(@"...save requested..."); savingTimer = [[NSTimer scheduledTimerWithTimeInterval:kSaveThrottle target:self selector:@selector(save:) userInfo:[NSDate date] repeats:NO] retain]; } else { if ( [[NSDate date] timeIntervalSinceDate:[savingTimer userInfo]] > kForcedSaveTimeout ) { // LOG_DEBUG( @"Seconds since first save request = %f. Forcing Save.", [[NSDate date] timeIntervalSinceDate:[savingTimer userInfo]] ); [savingTimer fire]; } else { [savingTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:kSaveThrottle]]; } } } -(void)reset { [specDescriptions removeAllObjects]; for( struct defaultspec *sp = &speclist[ 0 ]; sp->type; sp++ ) [self setSpecDefinitionAsString:sp->spec forTypeNamed:sp->type]; } -(BOOL)saveSpec:(P4Spec*)spec completionBlock:(void(^)(P4Response * response))completion { if ( !spec.isDirty ) { // LOG_DEBUG( @"Spec (%@:'%@') not dirty. Ignoring save request.", spec.type, spec.identifier ); return YES; } NSString * form = [spec form]; // LOG_DEBUG( @"Saving Form: %@", form ); return [self.connection runArguments:[NSArray arrayWithObjects:spec.type, @"-i", nil] withContext:nil content:form updateBlock:nil completionBlock:completion]; } -(NSString*)entityNameForTypeName:(NSString*)typeName { return [typeName capitalizedString]; } -(NSString*)propertyNameForIdentifierPropertyForSpecType:(NSString*)typeName { NSEntityDescription * entityDescription = [NSEntityDescription entityForName:[self entityNameForTypeName:typeName ] inManagedObjectContext:[self managedObjectContext]]; NSString * propertyName = [[entityDescription userInfo] objectForKey:kP4IdentifierPropertyName]; return propertyName; } -(NSFetchRequest*)fetchRequestForType:(NSString*)typeName { NSFetchRequest * fetchRequest = [[[NSFetchRequest alloc] init] autorelease]; NSEntityDescription * entityDescription = [NSEntityDescription entityForName:[self entityNameForTypeName:typeName] inManagedObjectContext:[self managedObjectContext]]; [fetchRequest setEntity:entityDescription]; return fetchRequest; } -(NSFetchRequest*)fetchRequestForType:(NSString*)typeName identifier:(NSString*)identifier { NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName]; [fetchRequest setFetchLimit:1]; NSString * identifierProperty = [self propertyNameForIdentifierPropertyForSpecType:typeName]; NSPredicate * predicate = [NSPredicate predicateWithFormat:@"%K == %@", identifierProperty, identifier]; [fetchRequest setPredicate:predicate]; return fetchRequest; } -(P4Spec*)newSpecWithType:(NSString*)typeName identifier:(NSString*)identifier { // Since it didn't exist, you can add it here // The returned object doesn't have anything loaded but when its values // are accessed, it will load from the server P4Spec * spec = [NSEntityDescription insertNewObjectForEntityForName:[self entityNameForTypeName:typeName ] inManagedObjectContext:[self managedObjectContext]]; // Here, we want to set the identifier without firing a change // Alternatively, we could construct and init one ourself [spec setPrimitiveValue:identifier forKey:[[spec entity] identifierPropertyName]]; return [spec retain]; } -(P4Spec*)specOfType:(NSString*)typeName identifier:(NSString*)identifier createIfNotFound:(BOOL)create { // First try to retrieve it from the cache if ( identifier == nil ) return nil; NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName identifier:identifier]; if ( !fetchRequest ) return nil; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; if ( !fetchResults ) { // LOG_DEBUG(@"%@", error); [NSApp presentError:error]; return nil; } if ( [fetchResults count] ) return [fetchResults objectAtIndex:0]; if ( !create ) return nil; return [[self newSpecWithType:typeName identifier:identifier] autorelease]; } -(NSArray*)specListOfType:(NSString*)typeName completion:(SpeclistRefreshCompletionBlock)completion { NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName]; [fetchRequest setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:[self propertyNameForIdentifierPropertyForSpecType:typeName] ascending:YES]]]; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; // NSDictionary * organizedResults = [fetchResults NGA_dictionaryByKeyingOnKey:kSpecPropertyNameIdentifier]; DECLARE_UNRETAINED_SELF(P4SpecManager*); [self runArguments:[NSArray arrayWithObject:[self listCommandForSpecType:typeName]] updateBlock: ^(NSDictionary * specData) { NSString * identifier = [specData objectForKey:typeName]; if ( !identifier ) identifier = [specData objectForKey:[typeName capitalizedString]]; // Skip this one since there is nothing to identify it if ( !identifier ) return; // This adds a lot of overhead by seeing if the spec already exists. Yet, if two requests for the same spec type come in at the same time, we have to make sure that we don't create a spec since the other request may have just created it. P4Spec * spec = [unretainedSelf specOfType:typeName identifier:identifier createIfNotFound:NO]; if ( !spec ) { // LOG_DEBUG(@"%@: Creating a '%@' with ID '%@'", self.portString, typeName, identifier); spec = [[self newSpecWithType:typeName identifier:identifier] autorelease]; } [spec updateWithRawData:specData]; } completionBlock:^(P4Response * response) { // First we organize the results so we may use them without // having to re-fetch the specs we are going to update // We organize them into a dictionary so we can reference them with // a fast lookup. I can only think there is a better way to do this // LOG_DEBUG(@"%lu results from fetch", (unsigned long)[organizedResults count]); NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; if ( !fetchResults ) { // LOG_DEBUG(@"%@", error); [NSApp presentError:error]; } completion( fetchResults, response.error ); // LOG_DEBUG(@"Results from command: %@", fetchResults); }]; return fetchResults; } -(NSString*)listCommandForSpecType:(NSString*)specType { if ( [specType isEqualToString:@"branch"]) return [specType stringByAppendingString:@"es"]; return [specType stringByAppendingString:@"s"]; } -(NSArray*)filterArgumentsWhenListingType:(NSString*)typeName forComparisonPredicate:(NSComparisonPredicate*)compoundPredicate { NSExpression * leftExpression = [compoundPredicate leftExpression]; NSExpression * rightExpression = [compoundPredicate rightExpression]; if ( [leftExpression expressionType] != NSKeyPathExpressionType ) return nil; if ( [rightExpression expressionType] != NSConstantValueExpressionType ) return nil; NSString * keyPath = [leftExpression keyPath]; id value = [rightExpression constantValue]; if ( ![value isKindOfClass:[NSString class]] ) value = [value stringValue]; if ( !keyPath ) return nil; NSMutableArray * arguments = [NSMutableArray array]; BOOL hasNameFilter = NO; // It would be nice to replace this with a table if ([typeName isEqualToString:@"branch"]) { hasNameFilter = YES; if ( [keyPath isEqualToString:@"owner"] ) { [arguments addObject:@"-u"]; [arguments addObject:value]; } } else if ([typeName isEqualToString:@"change"]) { if ( [keyPath isEqualToString:@"user"] ) { [arguments addObject:@"-u"]; [arguments addObject:value]; } else if ( [keyPath isEqualToString:@"client"] ) { [arguments addObject:@"-c"]; [arguments addObject:value]; } } else if ([typeName isEqualToString:@"client"]) { hasNameFilter = YES; if ( [keyPath isEqualToString:@"owner"] && value ) { [arguments addObject:@"-u"]; [arguments addObject:value]; } } else if ([typeName isEqualToString:@"label"]) { hasNameFilter = YES; if ( [keyPath isEqualToString:@"owner"] ) { [arguments addObject:@"-u"]; [arguments addObject:[value stringValue]]; } } if ( hasNameFilter && ([keyPath isEqualToString:@"identifier"] || [keyPath isEqualToString:typeName] ) ) { [arguments addObject:@"-e"]; NSString * identifierSearchString = value; if ( NSEqualToPredicateOperatorType != [compoundPredicate predicateOperatorType] ) { if ( NSBeginsWithPredicateOperatorType == [compoundPredicate predicateOperatorType] ) identifierSearchString = [NSString stringWithFormat:@"%@*",identifierSearchString ]; if ( NSEndsWithPredicateOperatorType == [compoundPredicate predicateOperatorType] ) identifierSearchString = [NSString stringWithFormat:@"*%@",identifierSearchString ]; if ( NSContainsPredicateOperatorType == [compoundPredicate predicateOperatorType] ) identifierSearchString = [NSString stringWithFormat:@"*%@*",identifierSearchString ]; } [arguments addObject:identifierSearchString]; } return arguments; } -(NSString*)jobViewOperatorforPredicateOperatorType:(NSPredicateOperatorType)predicateOperatorType { switch (predicateOperatorType) { case NSLessThanPredicateOperatorType: return @"<"; case NSLessThanOrEqualToPredicateOperatorType: return @"<="; case NSGreaterThanPredicateOperatorType: return @">"; case NSGreaterThanOrEqualToPredicateOperatorType: return @">"; case NSEqualToPredicateOperatorType: return @"="; } return nil; } -(NSString*)jobViewStringfromPredicate:(NSPredicate*)predicate { // Our strategy must be to take this predicate and wherever we can, create // a filter that will return a superset of items this predicate matches. // They will be re-filtered later with [NSPredicate evaluateWithObject:] #if 0 Perforce job views: A job view is an expression that selects jobs according to word and date matches. Job views are used by the 'p4 jobs' -e flag to select which jobs to display. Also, the 'p4 user' form contains a JobView field which selects which jobs are to be presented during changelist creation for automatic closing upon changelist submission. 'p4 job' indexes all whitespace-separated words, and then any punctuation-separated words within those words. So 'sub-par' is entered into the index as 'sub', 'par', and 'sub-par'. Case is not considered. 'p4 job' separately indexes all date fields in a way that allows searching for a range of dates. In its simplest form, a job view can contain a list of words, separated by spaces, that must appear in a job for it to be selected. For a match to occur all words must appear somewhere in the job, not including date fields: JobView: GUI redrawing bug p4 jobs -e 'GUI redrawing bug' To select a particular field, the 'field=word' syntax may be used: JobView: GUI redrawing status=open Logical operators & (and), | (or), ^ (not), and () (grouping) may also be used; spaces remain a low-precedence 'and' operator: JobView: redrawing (type=bug|type=sir) status=open The ^ (not) operator cannot be used alone or with | (or), only in conjuction with & (and) or space (and): JobView: type=bug & ^status=closed Comparative operators >, >=, <, <=, and = are permitted. Because they succeed if any word in the field matches, only the = operator is useful against fields containing blocks of text: JobView: priority<=b description=gui Text searches may embed the wildcard *, which matches anything: JobView: redraw* type=bug To match operator characters, you can escape them with \. Date fields may be searched using comparative operators. Dates are of the form yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss. If a specific time is not given, the equality operators (=, <=, >=) match the whole day: JobView: reported_date>=1998/01/01 status=closed Text field comparisons are done alphabetically. Date field comparisons are done chronologically. #endif NSMutableString * jobView = [NSMutableString string]; if ( [predicate isKindOfClass:[NSComparisonPredicate class]] ) { NSComparisonPredicate * comparisonPredicate = (NSComparisonPredicate*)predicate; NSExpression * leftExpression = [comparisonPredicate leftExpression]; if ( [leftExpression expressionType] != NSKeyPathExpressionType ) return nil; NSExpression * rightExpression = [comparisonPredicate rightExpression]; if ( [rightExpression expressionType] != NSConstantValueExpressionType ) return nil; NSString * keyPath = [leftExpression keyPath]; if ( !keyPath ) return nil; id value = [rightExpression constantValue]; if ( ![value isKindOfClass:[NSString class]] ) value = [value stringValue]; if ( !value ) return nil; NSString * operatorString = [self jobViewOperatorforPredicateOperatorType:[comparisonPredicate predicateOperatorType]]; if ( NSNotEqualToPredicateOperatorType == [comparisonPredicate predicateOperatorType] ) { [jobView appendString:@"^"]; operatorString = @"="; } [jobView appendString:keyPath]; [jobView appendString:operatorString]; if ( NSEndsWithPredicateOperatorType == [comparisonPredicate predicateOperatorType] || NSContainsPredicateOperatorType == [comparisonPredicate predicateOperatorType] ) [jobView appendString:@"*"]; [jobView appendString:value]; if ( NSBeginsWithPredicateOperatorType == [comparisonPredicate predicateOperatorType] || NSContainsPredicateOperatorType == [comparisonPredicate predicateOperatorType] ) [jobView appendString:@"*"]; return jobView; } // The predicate must be either compound or comparison if ( ![predicate isKindOfClass:[NSCompoundPredicate class]] ) return jobView; NSCompoundPredicate * compoundPredicate = (NSCompoundPredicate*)predicate; BOOL atBeginning = YES; for (NSPredicate * subPredicate in [compoundPredicate subpredicates]) { if ( NSNotPredicateType == [compoundPredicate compoundPredicateType] ) [jobView appendString:@"^"]; [jobView appendFormat:@"(%@)", [self jobViewStringfromPredicate:subPredicate]]; if (atBeginning) { atBeginning = NO; if ( NSOrPredicateType == [compoundPredicate compoundPredicateType] ) [jobView appendString:@" | "]; else if ( NSAndPredicateType == [compoundPredicate compoundPredicateType] ) [jobView appendString:@" "]; } } return jobView; } -(NSArray*)filterArgumentsForListingSpecType:(NSString*)typeName fromPredicate:(NSPredicate*)predicate { NSMutableArray * arguments = nil; // Note that for jobs queries, we can specify a precise server-side search if ( [typeName isEqualToString:@"job"] ) { arguments = [NSMutableArray arrayWithObject:@"-e"]; NSString * jobView = [self jobViewStringfromPredicate:predicate]; [arguments addObject:jobView]; return arguments; } // NON-JOB SPECS if ( [predicate isKindOfClass:[NSComparisonPredicate class]] ) { return [self filterArgumentsWhenListingType:typeName forComparisonPredicate:(NSComparisonPredicate*)predicate]; } // COMPOUND PREDICATES if ( ![predicate isKindOfClass:[NSCompoundPredicate class]] ) return nil; NSCompoundPredicate * compoundPredicate = (NSCompoundPredicate*)predicate; // We make sure all the top-level // items are ANDs and after that, create filters for only the top-level // items. So, the server will fetch more than this predicate requests, but // that's okay because the results will be post-processed through the // predicate before being returned to the client if ( NSAndPredicateType != [compoundPredicate compoundPredicateType] ) return nil; for (NSPredicate * subPredicate in [compoundPredicate subpredicates]) { if (!arguments) arguments = [NSMutableArray array]; if ( [subPredicate isKindOfClass:[NSComparisonPredicate class]] ) [arguments addObjectsFromArray:[self filterArgumentsWhenListingType:typeName forComparisonPredicate:(NSComparisonPredicate*)predicate]]; } return arguments; } -(NSArray*)executeFetchRequest:(NSFetchRequest*)fetchRequest incrementalUpdateBlock:(void(^)(NSArray * speclist))updateBlock completionBlock:(void(^)(NSArray * speclist, NSError *))completionBlock { NSString * typeName = [[fetchRequest entity] type]; if ( !typeName ) return nil; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; NSDictionary * organizedResults = [fetchResults NGA_dictionaryByKeyingOnKey:kSpecPropertyNameIdentifier]; NSString * listCommand = [self listCommandForSpecType:typeName]; NSArray * filterArguments = [self filterArgumentsForListingSpecType:typeName fromPredicate:[fetchRequest predicate]]; NSArray * arguments = [[NSArray arrayWithObject:listCommand] arrayByAddingObjectsFromArray:filterArguments]; // LOG_DEBUG(@"arguments from fetchRequest: %@", arguments); [self runArguments:arguments updateBlock:^(NSDictionary * specData) { NSString * identifier = [specData objectForKey:typeName]; if ( !identifier ) identifier = [specData objectForKey:[typeName capitalizedString]]; // Skip this one since there is nothing to identify it if ( !identifier ) return; P4Spec * spec = [organizedResults objectForKey:identifier]; if ( !spec ) spec = [[self newSpecWithType:typeName identifier:identifier] autorelease]; [spec updateWithRawData:specData]; if ( updateBlock && [[fetchRequest predicate] evaluateWithObject:spec] ) { updateBlock( [NSArray arrayWithObject:spec] ); } } completionBlock:^(P4Response * response) { // First we organize the results so we may use them without // having to re-fetch the specs we are going to update // We organize them into a dictionary so we can reference them with // a fast lookup. I can only think there is a better way to do this // LOG_DEBUG(@"%lu results from fetch", (unsigned long)[organizedResults count]); if (!completionBlock) return; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; if ( !fetchResults ) { // LOG_DEBUG(@"%@", error); [NSApp presentError:error]; } completionBlock( fetchResults, response.error ); // LOG_DEBUG(@"Results from command: %@", fetchResults); }]; return fetchResults; } -(NSString*)specCacheDirectory { NSArray * paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); if ( !paths || [paths count] == 0 ) return nil; NSString * cachePath = [paths objectAtIndex:0]; cachePath = [cachePath stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]; cachePath = [cachePath stringByAppendingPathComponent:self.portString]; cachePath = [cachePath stringByAppendingPathComponent:self.username]; return cachePath; } -(NSString*)classNameForSpecTypeName:(NSString*)specTypeName { if ( [specTypeName isEqualToString:@"client"] ) return @"P4Client"; if ( [specTypeName isEqualToString:@"user"] ) return @"P4User"; return NSStringFromClass([P4Spec class]); } -(NSDictionary*)entitiesWithFetchedPropertiesFromEntities:(NSDictionary*)entities { NSMutableDictionary * entitiesWithFetchedProperties = [[entities mutableCopy] autorelease]; for (NSString * entityName in [entitiesWithFetchedProperties allKeys]) { NSEntityDescription * entity = [entities objectForKey:entityName]; NSString * typeName = [[entity userInfo] objectForKey:kP4SpecTypeKey]; NSString * identifierPropertyName = [[entity userInfo] objectForKey:kP4IdentifierPropertyName]; NSArray * fetchedProperties = [[[[self class] auxiliarySpecMetadata] objectForKey:typeName] objectForKey:kP4FetchedRelationships]; for (NSDictionary * fetchedProperty in fetchedProperties) { NSString * fetchedRelationshipName = [fetchedProperty objectForKey:kP4FetchedRelationshipName]; if ( !fetchedRelationshipName ) { LOG_ERROR(@"No entry of %@ in the %@ for %@", kP4FetchedRelationshipName, kP4FetchedRelationships, typeName); continue; } NSString * fetchedSpecType = [fetchedProperty objectForKey:kP4FetchedRelationshipDestinationSpecType]; if ( !fetchedRelationshipName ) { LOG_ERROR(@"No entry of %@ in the %@ for %@", kP4FetchedRelationshipDestinationSpecType, kP4FetchedRelationships, typeName); continue; } NSString * inverseRelationshipName = [fetchedProperty objectForKey:kP4FetchedRelationshipDestinationInverseName]; if ( !fetchedRelationshipName ) { LOG_ERROR(@"No entry of %@ in the %@ for %@", kP4FetchedRelationshipDestinationInverseName, kP4FetchedRelationships, typeName); continue; } NSFetchedPropertyDescription * clientsPropertyDescription = [[[NSFetchedPropertyDescription alloc] init] autorelease]; [clientsPropertyDescription setName:fetchedRelationshipName]; NSFetchRequest * clientsFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; [clientsFetchRequest setEntity:[entities objectForKey:[self entityNameForTypeName:fetchedSpecType]]]; [clientsFetchRequest setPredicate:[NSPredicate predicateWithFormat:@"%K=$FETCH_SOURCE.%K", inverseRelationshipName, identifierPropertyName]]; [clientsPropertyDescription setFetchRequest:clientsFetchRequest]; NSMutableDictionary * fetchedPropertyDescriptions = [[[entity fetchedPropertyDescriptions] mutableCopy] autorelease]; if ( !fetchedPropertyDescriptions ) fetchedPropertyDescriptions = [NSMutableDictionary dictionary]; [fetchedPropertyDescriptions setObject:clientsPropertyDescription forKey:[clientsPropertyDescription name]]; [entity setFetchedPropertyDescriptions:fetchedPropertyDescriptions]; // Now make a note of the inverse relationship for // notification when loading the inverse spec { NSMutableDictionary * inverseFetchProperty = [NSMutableDictionary dictionaryWithCapacity:2]; [inverseFetchProperty setObject:inverseRelationshipName forKey:kP4InverseFetchedPropertyRelationshipName]; [inverseFetchProperty setObject:typeName forKey:kP4InverseFetchedPropertyRelationshipSpecType]; NSEntityDescription * inverseEntity = [entitiesWithFetchedProperties objectForKey:[self entityNameForTypeName:fetchedSpecType]]; // Let's make sure that property is indexed for faster lookups NSPropertyDescription * inverseProperty = [[inverseEntity propertiesByName] objectForKey:inverseRelationshipName]; [inverseProperty setIndexed:YES]; NSMutableArray * inverseFetchedRelationships = [[[inverseEntity inverseFetchedProperties] mutableCopy] autorelease]; if (!inverseFetchedRelationships) inverseFetchedRelationships = [NSMutableArray array]; [inverseFetchedRelationships addObject:inverseFetchProperty]; [inverseEntity setInverseFetchedProperties:inverseFetchedRelationships]; } } [entitiesWithFetchedProperties setObject:entity forKey:entityName]; } return entitiesWithFetchedProperties; } -(NSManagedObjectModel *)managedObjectModel { if (managedObjectModel) return managedObjectModel; managedObjectModel = [[NSManagedObjectModel alloc] init]; NSMutableDictionary * specEntitiesByName = [NSMutableDictionary dictionaryWithCapacity:[specDescriptions count]]; for ( NSString * specTypeKey in specDescriptions ) { NSError * error = nil; NSEntityDescription * entityDescription = [self newEntityDescriptionForType:specTypeKey error:&error]; if ( !entityDescription ) { LOG_ERROR( @"Skipping processing specdef for type: %@ because of error: %@", specTypeKey, error ); } else { // LOG_DEBUG( @"Created entity named '%@' for type: '%@' -- %@", [entityDescription name], specTypeKey, entityDescription ); [specEntitiesByName setObject:entityDescription forKey:[entityDescription name]]; [entityDescription release]; } } NSDictionary * specEntitiesWithFetchedPropertiesByName = [self entitiesWithFetchedPropertiesFromEntities:specEntitiesByName]; [managedObjectModel setEntities:[specEntitiesWithFetchedPropertiesByName allValues]]; // Make each spectype in its own configuration so it can be saved into separate cache files for ( NSString * specTypeKey in specDescriptions ) { NSEntityDescription * entityDescription = [specEntitiesWithFetchedPropertiesByName objectForKey:[self entityNameForTypeName:specTypeKey]]; [managedObjectModel setEntities:[NSArray arrayWithObject:entityDescription] forConfiguration:specTypeKey]; } return managedObjectModel; } /** Returns the persistent store coordinator for the application. This implementation will create and return a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) */ - (NSPersistentStoreCoordinator *) persistentStoreCoordinator { if (persistentStoreCoordinator) return persistentStoreCoordinator; NSManagedObjectModel *mom = [self managedObjectModel]; if (!mom) { NSAssert(NO, @"Managed object model is nil"); LOG_CRITICAL(@"%@:%@ No model to generate a store from", [self class], NSStringFromSelector(_cmd)); return nil; } NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *specCacheDirectory = [self specCacheDirectory]; NSError *error = nil; if ( ![fileManager fileExistsAtPath:specCacheDirectory isDirectory:NULL] ) { if (![fileManager createDirectoryAtPath:specCacheDirectory withIntermediateDirectories:YES attributes:nil error:&error]) { NSAssert2(NO, @"Failed to create Spec Cache directory %@ : %@", specCacheDirectory, error); LOG_CRITICAL(@"Error creating application support directory at %@ : %@",specCacheDirectory,error); return nil; } } persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; for ( struct defaultspec *sp = &speclist[ 0 ]; sp->type; sp++ ) { NSString * baseStoreName = sp->type; NSString * storeType = COREDATA_STORE_TYPE; NSString * storeName = nil; if ( [storeType isEqualToString:NSXMLStoreType] ) storeName = [baseStoreName stringByAppendingString:@".xml"]; else if ( [storeType isEqualToString:NSSQLiteStoreType] ) storeName = [baseStoreName stringByAppendingString:@".sql"]; else if ( [storeType isEqualToString:NSBinaryStoreType] ) storeName = [baseStoreName stringByAppendingString:@".bin"]; else if ( [storeType isEqualToString:NSInMemoryStoreType] ) storeName = [baseStoreName stringByAppendingString:@".ram"]; NSURL *url = [NSURL fileURLWithPath: [specCacheDirectory stringByAppendingPathComponent:storeName]]; if (![persistentStoreCoordinator addPersistentStoreWithType:storeType configuration:sp->type URL:url options:nil error:&error]){ LOG_ERROR(@"%@", error); [[NSApplication sharedApplication] presentError:error]; [persistentStoreCoordinator release], persistentStoreCoordinator = nil; return nil; } } return persistentStoreCoordinator; } /** Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) */ - (NSManagedObjectContext *) managedObjectContext { if (managedObjectContext) return managedObjectContext; NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (!coordinator) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; [dict setValue:@"Failed to initialize the store" forKey:NSLocalizedDescriptionKey]; [dict setValue:@"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey]; NSError *error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict]; LOG_ERROR(@"%@", error); [[NSApplication sharedApplication] presentError:error]; return nil; } managedObjectContext = [[NSManagedObjectContext alloc] init]; [managedObjectContext setPersistentStoreCoordinator: coordinator]; return managedObjectContext; } -(void)setSpecDefinitionAsFieldDefinitions:(NSDictionary*)fieldDefinitions forTypeNamed:(NSString*)typeName { [self setSpecDefinitionAsString:[self specDefStringFromSpecDefinition:fieldDefinitions] forTypeNamed:typeName]; } -(void)setSpecDefinitionAsString:(NSString*)string forTypeNamed:(NSString*)typeName { [specDescriptions setObject:[P4SpecDescription specDescriptionWithEncodedDefinition:string] forKey:typeName]; } -(BOOL)runArguments:(NSArray*)arguments updateBlock:(UpdateBlock)update completionBlock:(void(^)(P4Response*))completion { return [self.connection runArguments:arguments withContext:nil updateBlock:update completionBlock:completion]; } -(NSPropertyDescription*)propertyDescriptionForSpecFieldDescription:(P4SpecFieldDescription*)fieldDescription { NSString * tag = fieldDescription.tag; NSAttributeDescription * propertyDescription = [[[NSAttributeDescription alloc] init] autorelease]; NSMutableDictionary * userInfo = [NSMutableDictionary dictionary]; [userInfo setObject:tag forKey:kP4PropertyDescriptionSpecTag]; // Check if element is a "last access time" element if ( [fieldDescription isDate] && [fieldDescription isAlwaysSet] ) [userInfo setObject:[NSNumber numberWithBool:YES] forKey:kP4LastAccessTimeKey]; NSString * propertyName = nil; if ( [tag compare:@"description" options:NSCaseInsensitiveSearch] == NSOrderedSame) propertyName = @"comment"; else propertyName = [tag NGA_stringByConvertingFirstWordToLowerCase]; NSCharacterSet * disallowedSQLCharacters = [NSCharacterSet characterSetWithCharactersInString:@"-"]; // remove all invalid SQL characters propertyName = [[propertyName componentsSeparatedByCharactersInSet:disallowedSQLCharacters] componentsJoinedByString:@""]; // TYPE if ( [fieldDescription isDate] ) { [propertyDescription setAttributeType:NSDateAttributeType]; } else if ( [fieldDescription isList] ) { [propertyDescription setAttributeType:NSTransformableAttributeType]; } else { [propertyDescription setAttributeType:NSStringAttributeType]; } // NAME [propertyDescription setName:propertyName]; [propertyDescription setUserInfo:userInfo]; // VALUES // Not sure if I need to implement this since the server is really the // system that sets all the values. Our cache simply reads the values from // that system and places them in our objects. This is the data model // for our *cache* objects which we modify. not a direct translation from // the server // switch (specElem->opt) // { // case SDO_REQUIRED: // [propertyDescription setOptional:NO]; // Not sure if I need to set a default here. We are receiving the defaults // from the server and I need to be able to set them client-side // case SDO_DEFAULT: // [propertyDescription setDefaultValue:StringFromUtf8StrPtr(specElem->GetPreset()) ]; // break; // } return propertyDescription; } -(NSEntityDescription*)newEntityDescriptionForType:(NSString*)typeName error:(NSError**)error { NSEntityDescription * entityDescription = [[NSEntityDescription alloc] init]; P4SpecDescription * specDefinition = [specDescriptions objectForKey:typeName]; [entityDescription setName:[self entityNameForTypeName:typeName]]; NSSet * specCodes = [specDefinition allCodes]; NSMutableArray * properties = [NSMutableArray arrayWithCapacity:[specCodes count]]; NSMutableDictionary * specToMOPropertiesDict = [NSMutableDictionary dictionaryWithCapacity:[specCodes count]]; NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithCapacity:5]; for (NSNumber * code in [specDefinition allCodes]) { P4SpecFieldDescription * fieldDescription = [specDefinition fieldForCode:[code intValue]]; if ( !fieldDescription ) { LOG_ERROR( @"No field description loaded for code %d.", [code intValue] ); continue; } NSString * tag = fieldDescription.tag; NSPropertyDescription * propertyDescription = [self propertyDescriptionForSpecFieldDescription:fieldDescription]; // determine if this is the property which stores the last time this // spec was accessed on the server if ( [[propertyDescription userInfo] objectForKey:kP4LastAccessTimeKey] ) [userInfo setObject:[propertyDescription name] forKey:kP4LastAcccessPropertyName]; [specToMOPropertiesDict setObject:[propertyDescription name] forKey:tag]; // The very first property is always the ID property if ( [fieldDescription isIdentifier] ) { [userInfo setObject:[propertyDescription name] forKey:kP4IdentifierPropertyName]; [propertyDescription setIndexed:YES]; } [properties addObject:propertyDescription]; } // Some properties that come back in the dictionary data are summaries // of other properties. These are handled specially in updateFromRawData: // 'desc' is a summary property for 'Description' when loading data for // a changelist. The data for the summary property is used when the full // data is not available. Asking for the property value when there is only // summary data loaded will cause the full data to be loaded. NSDictionary * summaryProperties = [[[[self class] auxiliarySpecMetadata] objectForKey:typeName] objectForKey:kP4SpecSummaryProperties]; if ( summaryProperties ) { NSAttributeDescription * summarizedKeysProperty = [[[NSAttributeDescription alloc] init] autorelease]; [summarizedKeysProperty setName:kEntityPropertyNameSpecSummarizedPropertyKeys]; [properties addObject:summarizedKeysProperty]; [summarizedKeysProperty setAttributeType:NSTransformableAttributeType]; [userInfo setObject:summaryProperties forKey:kP4SpecSummaryProperties]; } // Some properties are aliases for other properties. For example, "time" // and "date" are interchangeable when loading change summaries NSDictionary * aliases = [[[[self class] auxiliarySpecMetadata] objectForKey:typeName] objectForKey:kP4SpecPropertyAliases]; if (aliases) [specToMOPropertiesDict addEntriesFromDictionary:aliases]; // --- SPEC MANAGEMENT PROPERTIES --- // Date refreshed. This is stored into the cache so when we reload the // spec, we won't refresh it until is older than the refresh rate. NSAttributeDescription * dateRefreshedProperty = [[[NSAttributeDescription alloc] init] autorelease]; [dateRefreshedProperty setName:kSpecPropertyNameDateRefreshed]; [dateRefreshedProperty setAttributeType:NSTransformableAttributeType]; [properties addObject:dateRefreshedProperty]; // Base fields. This stores the base versions of fields that have been edited. If // this property has contents, it means this spec is "dirty" and needs // to be sent to the server or reverted.. NSAttributeDescription * baseFieldsProperty = [[[NSAttributeDescription alloc] init] autorelease]; [baseFieldsProperty setName:kSpecPropertyNameBaseFieldValues]; [baseFieldsProperty setAttributeType:NSTransformableAttributeType]; [properties addObject:baseFieldsProperty]; // 'Theirs' fields. If we have a 'base' value (see above) that is subsequently // modified, we store the modification in the "theirs" field. Should we ever // need to merge the two, we have all three legs and can merge appropriately NSAttributeDescription * theirsFieldsProperty = [[[NSAttributeDescription alloc] init] autorelease]; [theirsFieldsProperty setName:kSpecPropertyNameTheirsFieldValues]; [theirsFieldsProperty setAttributeType:NSTransformableAttributeType]; [properties addObject:theirsFieldsProperty]; [entityDescription setProperties:properties]; [userInfo setObject:specToMOPropertiesDict forKey:kP4SpecKeyToMOKeyMapping]; [userInfo setObject:self forKey:kP4SpecManager]; [userInfo setObject:typeName forKey:kP4SpecTypeKey]; [userInfo setObject:specDefinition forKey:kP4SpecDescriptionKey]; [entityDescription setManagedObjectClassName:[self classNameForSpecTypeName:typeName]]; [entityDescription setUserInfo:userInfo]; return entityDescription; } -(NSString*)formOfTypeNamed:(NSString*)typeName fromDictionary:(NSDictionary*)dictionary error:(NSError**)error { P4SpecDescription * specDefinition = [specDescriptions objectForKey:typeName]; if ( !specDefinition ) { if ( error ) { NSString * localizedString = NSLocalizedString(@"Could not find spec description for type %@", @"Spec Description not found error description"); localizedString = [NSString stringWithFormat:localizedString, typeName]; NSDictionary * userInfo = [NSDictionary dictionaryWithObjectsAndKeys:localizedString, NSLocalizedDescriptionKey, nil]; *error = [NSError errorWithDomain:P4MEErrorDomain code:kP4MESpecDefinitionNotFound userInfo:userInfo]; } return nil; } return [specDefinition formFromSpecProperties:dictionary]; } - (NSString*)portString { return self.connection.portString; } - (P4Port*)p4Port { return self.connection.p4port; } - (NSString*)username { return self.connection.username; } -(NSString*)specDefStringFromSpecDefinition:(NSDictionary*)dict { P4SpecDescription * specDescription = [P4SpecDescription specDescriptionWithSpecDefProperties:dict]; return [specDescription encodedDefinition]; } @end
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 20722 | jdputsch | initial branch, prep for -Zapp= support | ||
//guest/michael_bishop/MacMenu/src/P4ObjectLayer/P4SpecManager.m | |||||
#1 | 8331 | Matt Attaway |
Adding initial version of MacMenu for Perforce MacMenu is a helpful Perforce client that sits in your toolbar. It allows you to run standard Perforce operations on the document that is open the currently active editor/viewer. |