mirror of
https://github.com/kennethreitz-archive/gitx.git
synced 2026-06-05 23:40:18 +00:00
ffae6a8564
This is probably the most common non-branch GitX option, so let's add it by default
406 lines
12 KiB
Objective-C
406 lines
12 KiB
Objective-C
//
|
||
// PBGitRepository.m
|
||
// GitTest
|
||
//
|
||
// Created by Pieter de Bie on 13-06-08.
|
||
// Copyright 2008 __MyCompanyName__. All rights reserved.
|
||
//
|
||
|
||
#import "PBGitRepository.h"
|
||
#import "PBGitCommit.h"
|
||
#import "PBGitWindowController.h"
|
||
|
||
#import "NSFileHandleExt.h"
|
||
#import "PBEasyPipe.h"
|
||
#import "PBGitRef.h"
|
||
#import "PBGitRevSpecifier.h"
|
||
|
||
NSString* PBGitRepositoryErrorDomain = @"GitXErrorDomain";
|
||
|
||
@implementation PBGitRepository
|
||
|
||
@synthesize revisionList, branches, currentBranch, refs, hasChanged;
|
||
static NSString* gitPath;
|
||
|
||
+ (void) initialize
|
||
{
|
||
// Try to find the path of the Git binary
|
||
char* path = getenv("GIT_PATH");
|
||
if (path != nil) {
|
||
gitPath = [NSString stringWithCString:path];
|
||
return;
|
||
}
|
||
|
||
// No explicit path. Try it with "which"
|
||
gitPath = [PBEasyPipe outputForCommand:@"/usr/bin/which" withArgs:[NSArray arrayWithObject:@"git"]];
|
||
if (gitPath.length > 0)
|
||
return;
|
||
|
||
// Still no path. Let's try some default locations.
|
||
NSArray* locations = [NSArray arrayWithObjects:@"/opt/local/bin/git",
|
||
@"/sw/bin/git",
|
||
@"/opt/git/bin/git",
|
||
@"/usr/local/bin/git",
|
||
nil];
|
||
for (NSString* location in locations) {
|
||
if ([[NSFileManager defaultManager] fileExistsAtPath:location]) {
|
||
gitPath = location;
|
||
return;
|
||
}
|
||
}
|
||
|
||
NSLog(@"Could not find a git binary!");
|
||
}
|
||
|
||
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
|
||
{
|
||
if (outError) {
|
||
*outError = [NSError errorWithDomain:PBGitRepositoryErrorDomain
|
||
code:0
|
||
userInfo:[NSDictionary dictionaryWithObject:@"Reading files is not supported." forKey:NSLocalizedFailureReasonErrorKey]];
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
+ (BOOL) isBareRepository: (NSString*) path
|
||
{
|
||
return [[PBEasyPipe outputForCommand:gitPath withArgs:[NSArray arrayWithObjects:@"rev-parse", @"--is-bare-repository", nil] inDir:path] isEqualToString:@"true"];
|
||
}
|
||
|
||
+ (NSURL*)gitDirForURL:(NSURL*)repositoryURL;
|
||
{
|
||
NSString* repositoryPath = [repositoryURL path];
|
||
|
||
if ([self isBareRepository:repositoryPath])
|
||
return repositoryURL;
|
||
|
||
|
||
// Use rev-parse to find the .git dir for the repository being opened
|
||
NSString* newPath = [PBEasyPipe outputForCommand:gitPath withArgs:[NSArray arrayWithObjects:@"rev-parse", @"--git-dir", nil] inDir:repositoryPath];
|
||
if ([newPath isEqualToString:@".git"])
|
||
return [NSURL fileURLWithPath:[repositoryPath stringByAppendingPathComponent:@".git"]];
|
||
if ([newPath length] > 0)
|
||
return [NSURL fileURLWithPath:newPath];
|
||
|
||
return nil;
|
||
}
|
||
|
||
// For a given path inside a repository, return either the .git dir
|
||
// (for a bare repo) or the directory above the .git dir otherwise
|
||
+ (NSURL*)baseDirForURL:(NSURL*)repositoryURL;
|
||
{
|
||
NSURL* gitDirURL = [self gitDirForURL:repositoryURL];
|
||
NSString* repositoryPath = [gitDirURL path];
|
||
|
||
if (![self isBareRepository:repositoryPath]) {
|
||
repositoryURL = [NSURL fileURLWithPath:[[repositoryURL path] stringByDeletingLastPathComponent]];
|
||
}
|
||
|
||
return repositoryURL;
|
||
}
|
||
|
||
- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError
|
||
{
|
||
BOOL success = NO;
|
||
|
||
if (![fileWrapper isDirectory]) {
|
||
if (outError) {
|
||
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Reading files is not supported.", [fileWrapper filename]]
|
||
forKey:NSLocalizedRecoverySuggestionErrorKey];
|
||
*outError = [NSError errorWithDomain:PBGitRepositoryErrorDomain code:0 userInfo:userInfo];
|
||
}
|
||
} else {
|
||
NSURL* gitDirURL = [PBGitRepository gitDirForURL:[self fileURL]];
|
||
if (gitDirURL) {
|
||
[self setFileURL:gitDirURL];
|
||
success = YES;
|
||
} else if (outError) {
|
||
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"%@ does not appear to be a git repository.", [fileWrapper filename]]
|
||
forKey:NSLocalizedRecoverySuggestionErrorKey];
|
||
*outError = [NSError errorWithDomain:PBGitRepositoryErrorDomain code:0 userInfo:userInfo];
|
||
}
|
||
|
||
if (success) {
|
||
[self setup];
|
||
[self readCurrentBranch];
|
||
}
|
||
}
|
||
|
||
return success;
|
||
}
|
||
|
||
- (void) setup
|
||
{
|
||
self.branches = [NSMutableArray array];
|
||
[self reloadRefs];
|
||
revisionList = [[PBGitRevList alloc] initWithRepository:self];
|
||
}
|
||
|
||
- (id) initWithURL: (NSURL*) path
|
||
{
|
||
NSURL* gitDirURL = [PBGitRepository gitDirForURL:path];
|
||
if (!gitDirURL)
|
||
return nil;
|
||
|
||
self = [self init];
|
||
[self setFileURL: gitDirURL];
|
||
|
||
[self setup];
|
||
|
||
// We don't want the window controller to display anything yet..
|
||
// We'll leave that to the caller of this method.
|
||
[self addWindowController:[[PBGitWindowController alloc] initWithRepository:self displayDefault:NO]];
|
||
[self showWindows];
|
||
|
||
return self;
|
||
}
|
||
|
||
// The fileURL the document keeps is to the .git dir, but that’s pretty
|
||
// useless for display in the window title bar, so we show the directory above
|
||
- (NSString*)displayName
|
||
{
|
||
NSString* displayName = self.fileURL.path.lastPathComponent;
|
||
if ([displayName isEqualToString:@".git"])
|
||
displayName = [self.fileURL.path stringByDeletingLastPathComponent].lastPathComponent;
|
||
return displayName;
|
||
}
|
||
|
||
// Overridden to create our custom window controller
|
||
- (void)makeWindowControllers
|
||
{
|
||
[self addWindowController: [[PBGitWindowController alloc] initWithRepository:self displayDefault:YES]];
|
||
}
|
||
|
||
- (NSWindowController *)windowController
|
||
{
|
||
if ([[self windowControllers] count] == 0)
|
||
return NULL;
|
||
|
||
return [[self windowControllers] objectAtIndex:0];
|
||
}
|
||
|
||
- (void) addRef: (PBGitRef *) ref fromParameters: (NSArray *) components
|
||
{
|
||
NSString* type = [components objectAtIndex:1];
|
||
|
||
NSString* sha;
|
||
if ([type isEqualToString:@"tag"] && [components count] == 4)
|
||
sha = [components objectAtIndex:3];
|
||
else
|
||
sha = [components objectAtIndex:2];
|
||
|
||
NSMutableArray* curRefs;
|
||
if (curRefs = [refs objectForKey:sha])
|
||
[curRefs addObject:ref];
|
||
else
|
||
[refs setObject:[NSMutableArray arrayWithObject:ref] forKey:sha];
|
||
}
|
||
|
||
// reloadRefs: reload all refs in the repository, like in readRefs
|
||
// To stay compatible, this does not remove a ref from the branches list
|
||
// even after it has been deleted.
|
||
// returns YES when a ref was changed
|
||
- (BOOL) reloadRefs
|
||
{
|
||
BOOL ret = NO;
|
||
|
||
refs = [NSMutableDictionary dictionary];
|
||
|
||
NSString* output = [PBEasyPipe outputForCommand:gitPath
|
||
withArgs:[NSArray arrayWithObjects:@"for-each-ref", @"--format=%(refname) %(objecttype) %(objectname)"
|
||
" %(*objectname)", @"refs", nil]
|
||
inDir: self.fileURL.path];
|
||
NSArray* lines = [output componentsSeparatedByString:@"\n"];
|
||
|
||
for (NSString* line in lines) {
|
||
// If its an empty line, skip it (e.g. with empty repositories)
|
||
if ([line length] == 0)
|
||
continue;
|
||
|
||
NSArray* components = [line componentsSeparatedByString:@" "];
|
||
|
||
// First do the ref matching. If this ref is new, add it to our ref list
|
||
PBGitRef *newRef = [PBGitRef refFromString:[components objectAtIndex:0]];
|
||
PBGitRevSpecifier* revSpec = [[PBGitRevSpecifier alloc] initWithRef:newRef];
|
||
if ([[newRef type] isEqualToString:@"head"] || [[newRef type] isEqualToString:@"remote"])
|
||
if ([self addBranch:revSpec] != revSpec)
|
||
ret = YES;
|
||
|
||
// Also add this ref to the refs list
|
||
[self addRef:newRef fromParameters:components];
|
||
}
|
||
|
||
self.refs = refs;
|
||
|
||
// Add an "All branches" option in the branches list
|
||
[self addBranch:[PBGitRevSpecifier allBranchesRevSpec]];
|
||
return ret;
|
||
}
|
||
|
||
- (void) lazyReload
|
||
{
|
||
if (!hasChanged)
|
||
return;
|
||
|
||
[self reloadRefs];
|
||
[self.revisionList reload];
|
||
hasChanged = NO;
|
||
}
|
||
|
||
- (PBGitRevSpecifier*) headRef
|
||
{
|
||
NSString* branch = [self parseSymbolicReference: @"HEAD"];
|
||
if (branch && [branch hasPrefix:@"refs/heads/"])
|
||
return [[PBGitRevSpecifier alloc] initWithRef:[PBGitRef refFromString:branch]];
|
||
|
||
return [[PBGitRevSpecifier alloc] initWithRef:[PBGitRef refFromString:@"HEAD"]];
|
||
}
|
||
|
||
// Returns either this object, or an existing, equal object
|
||
- (PBGitRevSpecifier*) addBranch: (PBGitRevSpecifier*) rev
|
||
{
|
||
if ([[rev parameters] count] == 0)
|
||
rev = [self headRef];
|
||
|
||
// First check if the branch doesn't exist already
|
||
for (PBGitRevSpecifier* r in branches)
|
||
if ([rev isEqualTo: r])
|
||
return r;
|
||
|
||
[self willChangeValueForKey:@"branches"];
|
||
[branches addObject: rev];
|
||
[self didChangeValueForKey:@"branches"];
|
||
return rev;
|
||
}
|
||
|
||
- (void) showHistoryView
|
||
{
|
||
if (!self.windowController)
|
||
return;
|
||
|
||
[((PBGitWindowController *)self.windowController) showHistoryView:self];
|
||
}
|
||
|
||
- (void) selectBranch: (PBGitRevSpecifier*) rev
|
||
{
|
||
int i;
|
||
for (i = 0; i < [branches count]; i++) {
|
||
PBGitRevSpecifier* aRev = [branches objectAtIndex:i];
|
||
if (rev == aRev) {
|
||
self.currentBranch = [NSIndexSet indexSetWithIndex:i];
|
||
[self showHistoryView];
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void) readCurrentBranch
|
||
{
|
||
[self selectBranch: [self addBranch: [self headRef]]];
|
||
}
|
||
|
||
- (NSString *) workingDirectory
|
||
{
|
||
if ([self.fileURL.path hasSuffix:@"/.git"])
|
||
return [self.fileURL.path substringToIndex:[self.fileURL.path length] - 5];
|
||
else if ([[self outputForCommand:@"rev-parse --is-inside-work-tree"] isEqualToString:@"true"])
|
||
return gitPath;
|
||
|
||
return nil;
|
||
}
|
||
|
||
- (int) returnValueForCommand:(NSString *)cmd
|
||
{
|
||
int i;
|
||
[self outputForCommand:cmd retValue: &i];
|
||
return i;
|
||
}
|
||
|
||
- (NSFileHandle*) handleForArguments:(NSArray *)args
|
||
{
|
||
NSString* gitDirArg = [@"--git-dir=" stringByAppendingString:self.fileURL.path];
|
||
NSMutableArray* arguments = [NSMutableArray arrayWithObject: gitDirArg];
|
||
[arguments addObjectsFromArray: args];
|
||
return [PBEasyPipe handleForCommand:gitPath withArgs:arguments];
|
||
}
|
||
|
||
- (NSFileHandle*) handleInWorkDirForArguments:(NSArray *)args
|
||
{
|
||
NSString* gitDirArg = [@"--git-dir=" stringByAppendingString:self.fileURL.path];
|
||
NSMutableArray* arguments = [NSMutableArray arrayWithObject: gitDirArg];
|
||
[arguments addObjectsFromArray: args];
|
||
return [PBEasyPipe handleForCommand:gitPath withArgs:arguments inDir:[self workingDirectory]];
|
||
}
|
||
|
||
- (NSFileHandle*) handleForCommand:(NSString *)cmd
|
||
{
|
||
NSArray* arguments = [cmd componentsSeparatedByString:@" "];
|
||
return [self handleForArguments:arguments];
|
||
}
|
||
|
||
- (NSString*) outputForCommand:(NSString *)cmd
|
||
{
|
||
NSArray* arguments = [cmd componentsSeparatedByString:@" "];
|
||
return [self outputForArguments: arguments];
|
||
}
|
||
|
||
- (NSString*) outputForCommand:(NSString *)str retValue:(int *)ret;
|
||
{
|
||
NSArray* arguments = [str componentsSeparatedByString:@" "];
|
||
return [self outputForArguments: arguments retValue: ret];
|
||
}
|
||
|
||
- (NSString*) outputForArguments:(NSArray*) arguments
|
||
{
|
||
return [PBEasyPipe outputForCommand:gitPath withArgs:arguments inDir: self.fileURL.path];
|
||
}
|
||
|
||
- (NSString*) outputInWorkdirForArguments:(NSArray*) arguments
|
||
{
|
||
return [PBEasyPipe outputForCommand:gitPath withArgs:arguments inDir: [self workingDirectory]];
|
||
}
|
||
|
||
|
||
- (NSString*) outputForArguments:(NSArray *)arguments retValue:(int *)ret;
|
||
{
|
||
return [PBEasyPipe outputForCommand:gitPath withArgs:arguments inDir: self.fileURL.path retValue: ret];
|
||
}
|
||
|
||
- (NSString*) outputForArguments:(NSArray *)arguments inputString:(NSString *)input retValue:(int *)ret;
|
||
{
|
||
return [PBEasyPipe outputForCommand:gitPath
|
||
withArgs:arguments
|
||
inDir: self.fileURL.path
|
||
inputString:input
|
||
retValue: ret];
|
||
}
|
||
|
||
- (NSString*) parseReference:(NSString *)reference
|
||
{
|
||
return [self outputForArguments:[NSArray arrayWithObjects: @"rev-parse", reference, nil]];
|
||
}
|
||
|
||
- (NSString*) parseSymbolicReference:(NSString*) reference
|
||
{
|
||
NSString* ref = [self outputForArguments:[NSArray arrayWithObjects: @"symbolic-ref", reference, nil]];
|
||
if ([ref hasPrefix:@"refs/"])
|
||
return ref;
|
||
|
||
return nil;
|
||
}
|
||
|
||
- (BOOL) removeRef:(NSString *)ref
|
||
{
|
||
int i;
|
||
[self outputForArguments:[NSArray arrayWithObjects:@"branch", @"-D", ref, nil] retValue:&i];
|
||
|
||
if (i == 0) {
|
||
// Todo: We can do better than this!
|
||
[self reloadRefs];
|
||
[revisionList reload];
|
||
return YES;
|
||
}
|
||
return NO;
|
||
}
|
||
@end
|