Source

Murky / Source / HgLogOperation.m

//
//  HgLogOperation.m
//  Murky
//
//  Copyright 2008-2009 Jens Alfke. All rights reserved.
//

#import "HgLogOperation.h"
#import "HgDir.h"
#import "HgRepository.h"
#import "HgRevision.h"


@interface HgLogOperation ( )
- (BOOL) _parseFullXML;
- (BOOL) _parseRevNumbers;
@end


static NSString* stringForURL( NSURL *url )
{
    NSCParameterAssert(url);
    if( url.isFileURL )
        return url.path;
    else
        return url.absoluteString;
}


@implementation HgLogOperation


static void getFilenameAndDir( HgRepository *repo, HgFile *file, NSString **outFilename, id *outDir )
{
    if( file==nil || file.isRoot ) {
        *outDir = repo;
        *outFilename = nil;             // If given HgRepository, get all the revisions
    } else if( file.isDirectory ) {
        *outDir = file;
        *outFilename = @".";
    } else {
        *outDir = file.directory;
        *outFilename = file.name;
        // Make sure 'hg' command won't misinterpret filename as a flag or pattern!
        if( [*outFilename hasPrefix: @"-"] || [*outFilename hasPrefix: @"glob:"] || [*outFilename hasPrefix: @"re:"] )
            *outFilename = [@"./" stringByAppendingString: *outFilename];
    }
}


- (id) initWithRepository: (HgRepository*)repo 
                     file: (HgFile*)file
                     mode: (HgLogMode)mode
{
    id dir;
    NSString *filename;
    getFilenameAndDir(repo,file,&filename,&dir);
    
    self = [super initWithDirectory: dir
                            command: @"log", filename, nil];
    if( self ) {
        _repository = repo;
        _mode = mode;
    }
    return self;
}


- (id) initOutgoingWithRevision: (HgRevision*)revision
                      otherRepo: (NSURL*)otherRepo
{
    self = [super initWithDirectory: revision.root
                            command: @"outgoing", @"--quiet",
                                     stringForURL(otherRepo),
                                     nil];
    if( self ) {
        _repository = revision.repository;
        _mode = kHgLogModeListRevs;
    }
    return self;
}

- (id) initIncomingWithRepository: (HgRepository*)repo
                        otherRepo: (NSURL*)otherRepo
{
    self = [super initWithDirectory: repo.currentRevision.root
                            command: @"incoming", @"--quiet",
                                     stringForURL(otherRepo),
                                     nil];
    if( self ) {
        _repository = repo;
        _mode = kHgLogModeFull;
    }
    return self;
}


- (NSTask*) createTask
{
    if (_range.length > 0)
        [self prependArguments: @"--rev",
                                [NSString stringWithFormat: @"%u:%u",
                                    _range.location, _range.location+_range.length-1],
                                nil];
    
    switch (_mode) {
        case kHgLogModeListRevs:
            [self prependArguments: @"--template", @"{rev},",nil];
            break;
        case kHgLogModeMinimal: {
            NSString *stylePath = [[NSBundle bundleForClass: [self class]] pathForResource: @"xmlminimal" ofType: @"style"];
            Assert(stylePath,@"Missing xmlminimal.style");
            [self prependArguments: @"--style", stylePath,nil];
            break;
        }
        case kHgLogModeFull: {
            NSString *stylePath = [[NSBundle bundleForClass: [self class]] pathForResource: @"xml" ofType: @"style"];
            Assert(stylePath,@"Missing xml.style");
            [self prependArguments: @"--verbose", @"--style", stylePath,nil];
            break;
        }
    }
    return [super createTask];
}


- (void) finished
{
    if( ! self.error ) {
        LogTo(HgLog,@"Parsing...");
        CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
        if( _mode == kHgLogModeListRevs )
            [self _parseRevNumbers];
        else
            [self _parseFullXML];
        time = CFAbsoluteTimeGetCurrent() - time;
        LogTo(HgLog,@"took %.1f sec to parse %u revs", time, _revisions.count);
    }
}


@synthesize range=_range, revisions=_revisions, revisionNumbers=_revisionNumbers;


- (BOOL) _parseRevNumbers
{
    _revisionNumbers = [NSMutableIndexSet indexSet];
    for( NSString *str in [self.output componentsSeparatedByString: @","] )
        if( str.length > 0 )
            [_revisionNumbers addIndex: str.intValue];
    return YES;
}


#pragma mark -
#pragma mark XPATH UTILITIES:


/* Follow an XPath on an XML element, logging any error that occurs */
static NSArray* arrayForXPath( NSXMLElement *xml, NSString *xpath )
{
    NSError *error = nil;
    NSArray *nodes = [xml nodesForXPath: xpath error: &error];
    if( nodes == nil ) {
        Warn(@"HgLogOperation: Couldn't parse XPath '%@': %@",xpath,[error localizedDescription]);
    }
    return nodes;
}

/* Follow an XPath, returning the result as a single string */
static NSString* strForXPath( NSXMLElement *xml, NSString *xpath )
{
    NSArray *nodes = arrayForXPath(xml,xpath);
    if( nodes == nil )
        return nil;
    if( [nodes count] == 0 ) {
        //Log(@"No results for '%@'",xpath);
        return nil;
    }
    NSMutableString *str = [NSMutableString string];
    for( NSXMLNode *node in nodes )
        [str appendString: node.stringValue];
    return [str stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
}

/* Follow an XPath, returning the result as an array of strings */
static NSArray* stringsForXPath( NSXMLElement *xml, NSString *xpath )
{
    NSArray *nodes = arrayForXPath(xml,xpath);
    if( nodes == nil )
        return nil;
    NSMutableArray *strings = [NSMutableArray array];
    for( NSXMLNode *node in nodes ) {
        NSString *str = node.stringValue;
        str = [str stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
        [strings addObject: str];
    }
    return strings;
}

static BOOL identifierFromAttribute( NSXMLElement *xml, NSString *attr,
                                     HgRevisionID *outID )
{
    *outID = kZeroRevisionID;
    NSString *str = [[xml attributeForName: attr] stringValue];
    unsigned len = str.length;
    if( len != 40 ) {
        if( len>0 )
            Warn(@"Revision ID has wrong length (%u)",len);
        return NO;
    }
    const char *cStr = [str cStringUsingEncoding: NSASCIIStringEncoding];
    if( ! cStr ) {
        Warn(@"Invalid revision ID '%@'",str);
        return NO;
    }
    for( size_t i=0; i<20; i++ )
        outID->bytes[i] = (digittoint(cStr[2*i]) <<4) | digittoint(cStr[2*i+1]);
    return YES;
}


static NSDate* parseDate( NSString *hgDateStr )
{
    if (hgDateStr) {
        // 'hgdate' format contains two integers: seconds since 1970, space, then GMT offset in seconds.
        NSArray *bits = [hgDateStr componentsSeparatedByString: @" "];
        if( bits.count >= 2 ) {
        NSTimeInterval time = [[bits objectAtIndex: 0] doubleValue];
            if( time > 0.0 ) {
                //NSTimeInterval offset = [[bits objectAtIndex: 1] doubleValue];
                //FIX: Ignoring the offset since NSDate can't hold a timezone. Use a 2nd object for that?
                return [NSDate dateWithTimeIntervalSince1970: time];
            }
        }
        Warn(@"Failed to parse date '%@'",hgDateStr);
    }
    return nil;
}


#pragma mark -
#pragma mark XML PARSING:


- (HgRevision*) _revisionWithNumber: (int)revNo
{
    if( revNo < 0 )
        return nil;
    NSArray *revs = _repository.revisions;
    if( (unsigned)revNo < revs.count ) {
        HgRevision *rev = [revs objectAtIndex: revNo];
        if (!rev.isUncommitted)
            return rev;
    }
    return [_revsByNumber objectForKey: [NSNumber numberWithInt: revNo]];
}


- (void) _updateRevision: (HgRevision*)revision
           fromChangeset: (NSXMLElement*)changeset
{
    if (_mode == kHgLogModeFull) {
        revision.date = parseDate( [[changeset attributeForName: @"date"] stringValue] );
        revision.comment = strForXPath(changeset,@"description/child::text()");
        revision.author = strForXPath(changeset,@"author/@name");
        revision.email = strForXPath(changeset,@"author/@email");
        revision.branch = strForXPath(changeset,@"branch/@name");
    }
    
    // Link to its parent(s):
    if( revision.localNumber > 0 ) {
        NSArray *parents = stringsForXPath(changeset,@"parent/@rev");
        int parent;
        if( parents.count > 0 ) {
            parent = [[parents objectAtIndex: 0] intValue];
            if( parents.count > 1 ) {
                int parent2 = [[parents objectAtIndex: 1] intValue];
                revision.parent2 = [self _revisionWithNumber: parent2];
            }
        } else
            parent = revision.localNumber-1;
        
        revision.parent = [self _revisionWithNumber: parent];
    }
}


- (BOOL) _parseFullXML
{        
	NSXMLDocument *xml;
    // Parse XML output:
	// Assume 1.5 and working xml output
    NSError *error = nil;
    xml = [[NSXMLDocument alloc] initWithData: self.outputData
                                                     options: 0
													   error: &error];
	
    if( ! xml ) {		
		// workaround for xml.style not emitting its footer:
		NSMutableData *xmlData = [self.outputData mutableCopy];
		[xmlData appendData: [@"</repository>" dataUsingEncoding: NSUTF8StringEncoding]];

		NSXMLDocument *xml = [[NSXMLDocument alloc] initWithData: self.outputData
														 options: 0
														   error: &error];
		
		if ( ! xml) {
			[xmlData writeToFile: @"/tmp/Murky.out" options: 0 error: nil];
			return [self makeError: @"HgLogOperation couldn't parse XML: %@",error];
		}
    }
    NSXMLElement *root = xml.rootElement;
    
    // Build array of HgRevision from the <changeset> elements:
    NSArray *changesets = [root nodesForXPath: @"/repository/changeset" error: &error];
    if( ! changesets )
        return [self makeError: @"HgLogOperation couldn't get changesets from XML: %@",error];
    
    // Build the _revisions array from the XML changeset info:
    _revisions = [NSMutableArray array];
    _revsByNumber = [NSMutableDictionary dictionary];
    for (NSXMLElement *changeset in changesets) {
        int revNo = [[[changeset attributeForName: @"rev"] stringValue] intValue];
        HgRevisionID nodeID;
        if( ! identifierFromAttribute(changeset,@"node",&nodeID) )
            return [self makeError: @"HgLogOperation couldn't get changeset ID for rev #%i",revNo];
        HgRevision *revision = [_repository revisionWithID: nodeID];
        if( !revision ) {
            revision = [[HgRevision alloc] initWithRepository: _repository
                                                  localNumber: revNo
                                                   identifier: nodeID];
        }
        [_revisions addObject: revision];
        [_revsByNumber setObject: revision forKey: [NSNumber numberWithInt: revNo]];
    }
    
    // Now update the revisions' attributes (and parent links) from the XML:
    int i = 0;
    for (NSXMLElement *changeset in changesets) {
        HgRevision *revision = [_revisions objectAtIndex: i++];
        [self _updateRevision: revision fromChangeset: changeset];
    }
    
    // Sort revisions by increasing local rev number:
    [_revisions sortUsingSelector: @selector(compare:)];
    return YES;
}


@end