CommitView: Don't keep rearranging when iterating over files
[GitX.git] / PBGitRepository.m
blob821cd2a5ba58b4e3e15e7138b79af7a5a9c0be74
1 //
2 //  PBGitRepository.m
3 //  GitTest
4 //
5 //  Created by Pieter de Bie on 13-06-08.
6 //  Copyright 2008 __MyCompanyName__. All rights reserved.
7 //
9 #import "PBGitRepository.h"
10 #import "PBGitCommit.h"
11 #import "PBGitWindowController.h"
12 #import "PBGitBinary.h"
14 #import "NSFileHandleExt.h"
15 #import "PBEasyPipe.h"
16 #import "PBGitRef.h"
17 #import "PBGitRevSpecifier.h"
19 NSString* PBGitRepositoryErrorDomain = @"GitXErrorDomain";
21 @implementation PBGitRepository
23 @synthesize revisionList, branches, currentBranch, refs, hasChanged, config;
25 - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
27         if (outError) {
28                 *outError = [NSError errorWithDomain:PBGitRepositoryErrorDomain
29                                       code:0
30                                   userInfo:[NSDictionary dictionaryWithObject:@"Reading files is not supported." forKey:NSLocalizedFailureReasonErrorKey]];
31         }
32         return NO;
35 + (BOOL) isBareRepository: (NSString*) path
37         return [[PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:[NSArray arrayWithObjects:@"rev-parse", @"--is-bare-repository", nil] inDir:path] isEqualToString:@"true"];
40 + (NSURL*)gitDirForURL:(NSURL*)repositoryURL;
42         if (![PBGitBinary path])
43                 return nil;
45         NSString* repositoryPath = [repositoryURL path];
47         if ([self isBareRepository:repositoryPath])
48                 return repositoryURL;
51         // Use rev-parse to find the .git dir for the repository being opened
52         NSString* newPath = [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:[NSArray arrayWithObjects:@"rev-parse", @"--git-dir", nil] inDir:repositoryPath];
53         if ([newPath isEqualToString:@".git"])
54                 return [NSURL fileURLWithPath:[repositoryPath stringByAppendingPathComponent:@".git"]];
55         if ([newPath length] > 0)
56                 return [NSURL fileURLWithPath:newPath];
58         return nil;
61 // For a given path inside a repository, return either the .git dir
62 // (for a bare repo) or the directory above the .git dir otherwise
63 + (NSURL*)baseDirForURL:(NSURL*)repositoryURL;
65         NSURL* gitDirURL         = [self gitDirForURL:repositoryURL];
66         NSString* repositoryPath = [gitDirURL path];
68         if (![self isBareRepository:repositoryPath]) {
69                 repositoryURL = [NSURL fileURLWithPath:[[repositoryURL path] stringByDeletingLastPathComponent]];
70         }
72         return repositoryURL;
75 // NSFileWrapper is broken and doesn't work when called on a directory containing a large number of directories and files.
76 //because of this it is safer to implement readFromURL than readFromFileWrapper.
77 //Because NSFileManager does not attempt to recursively open all directories and file when fileExistsAtPath is called
78 //this works much better.
79 - (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
81         if (![PBGitBinary path])
82         {
83                 if (outError) {
84                         NSDictionary* userInfo = [NSDictionary dictionaryWithObject:[PBGitBinary notFoundError]
85                                                                                                                                  forKey:NSLocalizedRecoverySuggestionErrorKey];
86                         *outError = [NSError errorWithDomain:PBGitRepositoryErrorDomain code:0 userInfo:userInfo];
87                 }
88                 return NO;
89         }
91         BOOL isDirectory = FALSE;
92         [[NSFileManager defaultManager] fileExistsAtPath:[absoluteURL path] isDirectory:&isDirectory];
93         if (!isDirectory) {
94                 if (outError) {
95                         NSDictionary* userInfo = [NSDictionary dictionaryWithObject:@"Reading files is not supported."
96                                                                                                                                  forKey:NSLocalizedRecoverySuggestionErrorKey];
97                         *outError = [NSError errorWithDomain:PBGitRepositoryErrorDomain code:0 userInfo:userInfo];
98                 }
99                 return NO;
100         }
103         NSURL* gitDirURL = [PBGitRepository gitDirForURL:[self fileURL]];
104         if (!gitDirURL) {
105                 if (outError) {
106                         NSDictionary* userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"%@ does not appear to be a git repository.", [self fileName]]
107                                                                                                                                  forKey:NSLocalizedRecoverySuggestionErrorKey];
108                         *outError = [NSError errorWithDomain:PBGitRepositoryErrorDomain code:0 userInfo:userInfo];
109                 }
110                 return NO;
111         }
113         [self setFileURL:gitDirURL];
114         [self setup];
115         [self readCurrentBranch];
116         return YES;
119 - (void) setup
121         config = [[PBGitConfig alloc] initWithRepository:self.fileURL.path];
122         self.branches = [NSMutableArray array];
123         [self reloadRefs];
124         revisionList = [[PBGitRevList alloc] initWithRepository:self];
127 - (id) initWithURL: (NSURL*) path
129         if (![PBGitBinary path])
130                 return nil;
132         NSURL* gitDirURL = [PBGitRepository gitDirForURL:path];
133         if (!gitDirURL)
134                 return nil;
136         self = [self init];
137         [self setFileURL: gitDirURL];
139         [self setup];
140         
141         // We don't want the window controller to display anything yet..
142         // We'll leave that to the caller of this method.
143 #ifndef CLI
144         [self addWindowController:[[PBGitWindowController alloc] initWithRepository:self displayDefault:NO]];
145 #endif
147         [self showWindows];
149         return self;
152 // The fileURL the document keeps is to the .git dir, but that’s pretty
153 // useless for display in the window title bar, so we show the directory above
154 - (NSString*)displayName
156         NSString* dirName = self.fileURL.path.lastPathComponent;
157         if ([dirName isEqualToString:@".git"])
158                 dirName = [self.fileURL.path stringByDeletingLastPathComponent].lastPathComponent;
159         NSString* displayName;
160         if (![[PBGitRef refFromString:[[self headRef] simpleRef]] type]) {
161                 displayName = [NSString stringWithFormat:@"%@ (detached HEAD)", dirName];
162         } else {
163                 displayName = [NSString stringWithFormat:@"%@ (branch: %@)", dirName,
164                                          [[self headRef] description]];
165         }
167         return displayName;
170 // Get the .gitignore file at the root of the repository
171 - (NSString*)gitIgnoreFilename
173         return [[self workingDirectory] stringByAppendingPathComponent:@".gitignore"];
176 - (BOOL)isBareRepository
178         if([self workingDirectory]) {
179                 return [PBGitRepository isBareRepository:[self workingDirectory]];
180         } else {
181                 return true;
182         }
185 // Overridden to create our custom window controller
186 - (void)makeWindowControllers
188 #ifndef CLI
189         [self addWindowController: [[PBGitWindowController alloc] initWithRepository:self displayDefault:YES]];
190 #endif
193 - (PBGitWindowController *)windowController
195         if ([[self windowControllers] count] == 0)
196                 return NULL;
197         
198         return [[self windowControllers] objectAtIndex:0];
201 - (void) addRef: (PBGitRef *) ref fromParameters: (NSArray *) components
203         NSString* type = [components objectAtIndex:1];
205         NSString* sha;
206         if ([type isEqualToString:@"tag"] && [components count] == 4)
207                 sha = [components objectAtIndex:3];
208         else
209                 sha = [components objectAtIndex:2];
211         NSMutableArray* curRefs;
212         if (curRefs = [refs objectForKey:sha])
213                 [curRefs addObject:ref];
214         else
215                 [refs setObject:[NSMutableArray arrayWithObject:ref] forKey:sha];
218 // reloadRefs: reload all refs in the repository, like in readRefs
219 // To stay compatible, this does not remove a ref from the branches list
220 // even after it has been deleted.
221 // returns YES when a ref was changed
222 - (BOOL) reloadRefs
224         _headRef = nil;
225         BOOL ret = NO;
227         refs = [NSMutableDictionary dictionary];
229         NSString* output = [PBEasyPipe outputForCommand:[PBGitBinary path]
230                                                                                    withArgs:[NSArray arrayWithObjects:@"for-each-ref", @"--format=%(refname) %(objecttype) %(objectname)"
231                                                                                                          " %(*objectname)", @"refs", nil]
232                                                                                           inDir: self.fileURL.path];
233         NSArray* lines = [output componentsSeparatedByString:@"\n"];
235         for (NSString* line in lines) {
236                 // If its an empty line, skip it (e.g. with empty repositories)
237                 if ([line length] == 0)
238                         continue;
240                 NSArray* components = [line componentsSeparatedByString:@" "];
242                 // First do the ref matching. If this ref is new, add it to our ref list
243                 PBGitRef *newRef = [PBGitRef refFromString:[components objectAtIndex:0]];
244                 PBGitRevSpecifier* revSpec = [[PBGitRevSpecifier alloc] initWithRef:newRef];
245                 if ([self addBranch:revSpec] != revSpec)
246                         ret = YES;
248                 // Also add this ref to the refs list
249                 [self addRef:newRef fromParameters:components];
250         }
252         // Add an "All branches" option in the branches list
253         [self addBranch:[PBGitRevSpecifier allBranchesRevSpec]];
254         [self addBranch:[PBGitRevSpecifier localBranchesRevSpec]];
256         [[[self windowController] window] setTitle:[self displayName]];
258         return ret;
261 - (void) lazyReload
263         if (!hasChanged)
264                 return;
266         [self reloadRefs];
267         [self.revisionList reload];
268         hasChanged = NO;
271 - (PBGitRevSpecifier *)headRef
273         if (_headRef)
274                 return _headRef;
276         NSString* branch = [self parseSymbolicReference: @"HEAD"];
277         if (branch && [branch hasPrefix:@"refs/heads/"])
278                 _headRef = [[PBGitRevSpecifier alloc] initWithRef:[PBGitRef refFromString:branch]];
279         else
280                 _headRef = [[PBGitRevSpecifier alloc] initWithRef:[PBGitRef refFromString:@"HEAD"]];
282         return _headRef;
284                 
285 // Returns either this object, or an existing, equal object
286 - (PBGitRevSpecifier*) addBranch: (PBGitRevSpecifier*) rev
288         if ([[rev parameters] count] == 0)
289                 rev = [self headRef];
291         // First check if the branch doesn't exist already
292         for (PBGitRevSpecifier* r in branches)
293                 if ([rev isEqualTo: r])
294                         return r;
296         [self willChangeValueForKey:@"branches"];
297         [branches addObject: rev];
298         [self didChangeValueForKey:@"branches"];
299         return rev;
302 - (BOOL)removeBranch:(PBGitRevSpecifier *)rev
304         for (PBGitRevSpecifier *r in branches) {
305                 if ([rev isEqualTo:r]) {
306                         [self willChangeValueForKey:@"branches"];
307                         [branches removeObject:r];
308                         [self didChangeValueForKey:@"branches"];
309                         return TRUE;
310                 }
311         }
312         return FALSE;
314         
315 - (void) readCurrentBranch
317                 self.currentBranch = [self addBranch: [self headRef]];
320 - (NSString *) workingDirectory
322         if ([self.fileURL.path hasSuffix:@"/.git"])
323                 return [self.fileURL.path substringToIndex:[self.fileURL.path length] - 5];
324         else if ([[self outputForCommand:@"rev-parse --is-inside-work-tree"] isEqualToString:@"true"])
325                 return [PBGitBinary path];
326         
327         return nil;
328 }               
330 - (int) returnValueForCommand:(NSString *)cmd
332         int i;
333         [self outputForCommand:cmd retValue: &i];
334         return i;
337 - (NSFileHandle*) handleForArguments:(NSArray *)args
339         NSString* gitDirArg = [@"--git-dir=" stringByAppendingString:self.fileURL.path];
340         NSMutableArray* arguments =  [NSMutableArray arrayWithObject: gitDirArg];
341         [arguments addObjectsFromArray: args];
342         return [PBEasyPipe handleForCommand:[PBGitBinary path] withArgs:arguments];
345 - (NSFileHandle*) handleInWorkDirForArguments:(NSArray *)args
347         NSString* gitDirArg = [@"--git-dir=" stringByAppendingString:self.fileURL.path];
348         NSMutableArray* arguments =  [NSMutableArray arrayWithObject: gitDirArg];
349         [arguments addObjectsFromArray: args];
350         return [PBEasyPipe handleForCommand:[PBGitBinary path] withArgs:arguments inDir:[self workingDirectory]];
353 - (NSFileHandle*) handleForCommand:(NSString *)cmd
355         NSArray* arguments = [cmd componentsSeparatedByString:@" "];
356         return [self handleForArguments:arguments];
359 - (NSString*) outputForCommand:(NSString *)cmd
361         NSArray* arguments = [cmd componentsSeparatedByString:@" "];
362         return [self outputForArguments: arguments];
365 - (NSString*) outputForCommand:(NSString *)str retValue:(int *)ret;
367         NSArray* arguments = [str componentsSeparatedByString:@" "];
368         return [self outputForArguments: arguments retValue: ret];
371 - (NSString*) outputForArguments:(NSArray*) arguments
373         return [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir: self.fileURL.path];
376 - (NSString*) outputInWorkdirForArguments:(NSArray*) arguments
378         return [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir: [self workingDirectory]];
381 - (NSString*) outputInWorkdirForArguments:(NSArray *)arguments retValue:(int *)ret
383         return [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir:[self workingDirectory] retValue: ret];
386 - (NSString*) outputForArguments:(NSArray *)arguments retValue:(int *)ret
388         return [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir: self.fileURL.path retValue: ret];
391 - (NSString*) outputForArguments:(NSArray *)arguments inputString:(NSString *)input retValue:(int *)ret
393         return [PBEasyPipe outputForCommand:[PBGitBinary path]
394                                                            withArgs:arguments
395                                                                   inDir:[self workingDirectory]
396                                                         inputString:input
397                                                            retValue: ret];
400 - (NSString *)outputForArguments:(NSArray *)arguments inputString:(NSString *)input byExtendingEnvironment:(NSDictionary *)dict retValue:(int *)ret
402         return [PBEasyPipe outputForCommand:[PBGitBinary path]
403                                                            withArgs:arguments
404                                                                   inDir:[self workingDirectory]
405                                  byExtendingEnvironment:dict
406                                                         inputString:input
407                                                            retValue: ret];
410 - (BOOL)executeHook:(NSString *)name output:(NSString **)output
412         return [self executeHook:name withArgs:[NSArray array] output:output];
415 - (BOOL)executeHook:(NSString *)name withArgs:(NSArray *)arguments output:(NSString **)output
417         NSString *hookPath = [[[[self fileURL] path] stringByAppendingPathComponent:@"hooks"] stringByAppendingPathComponent:name];
418         if (![[NSFileManager defaultManager] isExecutableFileAtPath:hookPath])
419                 return TRUE;
421         NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
422                 [self fileURL].path, @"GIT_DIR",
423                 [[self fileURL].path stringByAppendingPathComponent:@"index"], @"GIT_INDEX_FILE",
424                 nil
425         ];
427         int ret = 1;
428         NSString *_output =     [PBEasyPipe outputForCommand:hookPath withArgs:arguments inDir:[self workingDirectory] byExtendingEnvironment:info inputString:nil retValue:&ret];
430         if (output)
431                 *output = _output;
433         return ret == 0;
436 - (NSString *)parseReference:(NSString *)reference
438         int ret = 1;
439         NSString *ref = [self outputForArguments:[NSArray arrayWithObjects: @"rev-parse", @"--verify", reference, nil] retValue: &ret];
440         if (ret)
441                 return nil;
443         return ref;
446 - (NSString*) parseSymbolicReference:(NSString*) reference
448         NSString* ref = [self outputForArguments:[NSArray arrayWithObjects: @"symbolic-ref", @"-q", reference, nil]];
449         if ([ref hasPrefix:@"refs/"])
450                 return ref;
452         return nil;
455 - (void) finalize
457         NSLog(@"Dealloc of repository");
458         [super finalize];
460 @end