Commits

Jens Alfke committed 8dc6ef3

Improved GUI file comparison:
* Code is abstracted into a new CompareTool class.
* Multiple tools (not just opendiff / FileMerge) can be configured by editing CompareTools.plist. This is part of #26, but the app code is still hardwired to use opendiff, though (see -[RepoController showComparisonFromRevision:...].)
* When comparing an uncommitted change, the Save command in FileMerge is activated, and will save the changes back to the working tree.
* New "Compare Conflicts" menu command that opens a 3-way merge of a conflicted uncommitted file. That is, it tells opendiff to show both conflicting versions and uses -merge to pass the common ancestor. Again, saving will save to the working tree. You can use the hidden bottom pane in FileMerge to make changes by hand.

Comments (0)

Files changed (11)

English.lproj/MainMenu.strings

 
 /* Class = "NSMenuItem"; title = "Mark As Resolved"; ObjectID = "607"; */
 "607.title" = "Mark As Resolved";
+
+/* Class = "NSMenuItem"; title = "Compare Conflicts"; ObjectID = "610"; */
+"610.title" = "Compare Conflicts";

English.lproj/MainMenu.xib

 									<reference key="NSOnImage" ref="664567982"/>
 									<reference key="NSMixedImage" ref="578101116"/>
 								</object>
+								<object class="NSMenuItem" id="1059191347">
+									<reference key="NSMenu" ref="502084290"/>
+									<string key="NSTitle">Compare Conflicts</string>
+									<string key="NSKeyEquiv"/>
+									<int key="NSKeyEquivModMask">1048576</int>
+									<int key="NSMnemonicLoc">2147483647</int>
+									<reference key="NSOnImage" ref="664567982"/>
+									<reference key="NSMixedImage" ref="578101116"/>
+								</object>
 								<object class="NSMenuItem" id="649192194">
 									<reference key="NSMenu" ref="502084290"/>
 									<bool key="NSIsDisabled">YES</bool>
 					</object>
 					<int key="connectionID">609</int>
 				</object>
+				<object class="IBConnectionRecord">
+					<object class="IBActionConnection" key="connection">
+						<string key="label">remergeFiles:</string>
+						<reference key="source" ref="1014"/>
+						<reference key="destination" ref="1059191347"/>
+					</object>
+					<int key="connectionID">612</int>
+				</object>
 			</object>
 			<object class="IBMutableOrderedSet" key="objectRecords">
 				<object class="NSArray" key="orderedObjects">
 							<reference ref="743461433"/>
 							<reference ref="312225896"/>
 							<reference ref="3375322"/>
+							<reference ref="1059191347"/>
 						</object>
 						<reference key="parent" ref="626404410"/>
 					</object>
 						<reference key="object" ref="3375322"/>
 						<reference key="parent" ref="502084290"/>
 					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">610</int>
+						<reference key="object" ref="1059191347"/>
+						<reference key="parent" ref="502084290"/>
+					</object>
 				</object>
 			</object>
 			<object class="NSMutableDictionary" key="flattenedProperties">
 					<string>600.IBPluginDependency</string>
 					<string>603.IBPluginDependency</string>
 					<string>607.IBPluginDependency</string>
+					<string>610.IBPluginDependency</string>
 					<string>72.IBPluginDependency</string>
 					<string>72.ImportedFromIB2</string>
 					<string>73.IBPluginDependency</string>
 					<string>{74, 862}</string>
 					<string>{{71, 836}, {426, 20}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
-					<string>{{536, 763}, {238, 73}}</string>
+					<string>{{461, 763}, {238, 73}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>{{315, 763}, {241, 73}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
-					<string>{{511, 633}, {219, 203}}</string>
+					<string>{{511, 613}, {222, 223}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>{{233, 693}, {296, 143}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<integer value="1"/>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<integer value="1"/>
 				</object>
 			</object>
 			<nil key="sourceID"/>
-			<int key="maxID">609</int>
+			<int key="maxID">612</int>
 		</object>
 		<object class="IBClassDescriber" key="IBDocument.Classes">
 			<object class="NSMutableArray" key="referencedPartialClassDescriptions">
 							<string>openSelectedFileInWindow:</string>
 							<string>pushPullRevisions:</string>
 							<string>refreshStatus:</string>
+							<string>remergeFiles:</string>
 							<string>removeFromRepository:</string>
 							<string>revealInFinder:</string>
 							<string>showComparison:</string>
 							<string>id</string>
 							<string>id</string>
 							<string>id</string>
+							<string>id</string>
 						</object>
 					</object>
 					<object class="NSMutableDictionary" key="actionInfosByName">
 							<string>openSelectedFileInWindow:</string>
 							<string>pushPullRevisions:</string>
 							<string>refreshStatus:</string>
+							<string>remergeFiles:</string>
 							<string>removeFromRepository:</string>
 							<string>revealInFinder:</string>
 							<string>showComparison:</string>
 								<string key="candidateClassName">id</string>
 							</object>
 							<object class="IBActionInfo">
+								<string key="name">remergeFiles:</string>
+								<string key="candidateClassName">id</string>
+							</object>
+							<object class="IBActionInfo">
 								<string key="name">removeFromRepository:</string>
 								<string key="candidateClassName">id</string>
 							</object>
 {
     "English.lproj/FileViewer.xib": "89b3ce19501369084592522616927d3e", 
-    "English.lproj/MainMenu.xib": "567d3e20cd0077aeabeaaf1447bce385", 
+    "English.lproj/MainMenu.xib": "8a7e4fcada9f543995c542e3e6d07bca", 
     "English.lproj/Projects.xib": "770fab372e314ce3a66f4c501d11d8ee", 
     "English.lproj/Repo.xib": "5e5a74986e025440b9b7ed1220451e61", 
     "French.lproj/FileViewer.strings": "3180f066f393da424f8f32f60384cedf", 

Murky.xcodeproj/project.pbxproj

 		27E772930FB09EA7006504EF /* MYWindowUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 27E772920FB09EA7006504EF /* MYWindowUtils.m */; };
 		27F3DD2E116FD0C5002FC27D /* status_conflict.png in Resources */ = {isa = PBXBuildFile; fileRef = 27F3DD2D116FD0C5002FC27D /* status_conflict.png */; };
 		27F3DD30116FD3A2002FC27D /* status_resolved.png in Resources */ = {isa = PBXBuildFile; fileRef = 27F3DD2F116FD3A2002FC27D /* status_resolved.png */; };
+		27F3DF42117247A4002FC27D /* CompareTool.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F3DF41117247A4002FC27D /* CompareTool.m */; };
+		27F3DF5C11724E94002FC27D /* CompareTools.plist in Resources */ = {isa = PBXBuildFile; fileRef = 27F3DF5B11724E94002FC27D /* CompareTools.plist */; };
 		27F909AC10E96E0900892C41 /* FileViewer.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27F909AA10E96E0900892C41 /* FileViewer.xib */; };
 		27F909B010E96E4900892C41 /* FileViewer.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F909AF10E96E4900892C41 /* FileViewer.m */; };
 		27FEB46D0FBB200600290049 /* toolbar_add.png in Resources */ = {isa = PBXBuildFile; fileRef = 27FEB4640FBB200600290049 /* toolbar_add.png */; };
 		27E772920FB09EA7006504EF /* MYWindowUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MYWindowUtils.m; sourceTree = "<group>"; };
 		27F3DD2D116FD0C5002FC27D /* status_conflict.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = status_conflict.png; sourceTree = "<group>"; };
 		27F3DD2F116FD3A2002FC27D /* status_resolved.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = status_resolved.png; sourceTree = "<group>"; };
+		27F3DF40117247A4002FC27D /* CompareTool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CompareTool.h; sourceTree = "<group>"; };
+		27F3DF41117247A4002FC27D /* CompareTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CompareTool.m; sourceTree = "<group>"; };
+		27F3DF5B11724E94002FC27D /* CompareTools.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CompareTools.plist; sourceTree = "<group>"; };
 		27F909AB10E96E0900892C41 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = English.lproj/FileViewer.xib; sourceTree = "<group>"; };
 		27F909AE10E96E4900892C41 /* FileViewer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileViewer.h; sourceTree = "<group>"; };
 		27F909AF10E96E4900892C41 /* FileViewer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileViewer.m; sourceTree = "<group>"; };
 				27107D1D0C887D3700ED7715 /* Predicate.m */,
 				272AB4C80C8DD2600068C695 /* HgConfigFile.h */,
 				272AB4C90C8DD2600068C695 /* HgConfigFile.m */,
+				27F3DF40117247A4002FC27D /* CompareTool.h */,
+				27F3DF41117247A4002FC27D /* CompareTool.m */,
+				27F3DF5B11724E94002FC27D /* CompareTools.plist */,
 				27C1C934104EF9F400781C99 /* SourceHighlighting.h */,
 				27C1C935104EF9F400781C99 /* SourceHighlighting.m */,
 				73CA824710135EC90081F0D8 /* Terminal.h */,
 				73ACF87910F715E4003304B2 /* no_file.png in Resources */,
 				27F3DD2E116FD0C5002FC27D /* status_conflict.png in Resources */,
 				27F3DD30116FD3A2002FC27D /* status_resolved.png in Resources */,
+				27F3DF5C11724E94002FC27D /* CompareTools.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 				27C1C936104EF9F400781C99 /* SourceHighlighting.m in Sources */,
 				27F909B010E96E4900892C41 /* FileViewer.m in Sources */,
 				7341D0BF10F470E000076123 /* IconTextCell.m in Sources */,
+				27F3DF42117247A4002FC27D /* CompareTool.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

Source/CompareTool.h

+//
+//  CompareTool.h
+//  Murky
+//
+//  Created by Jens Alfke on 4/11/10.
+//  Copyright 2010 Jens Alfke. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+@interface CompareTool : NSObject {
+    NSString *_toolID;
+    NSString *_leftPath, *_rightPath, *_basePath, *_savePath;
+}
+
++ (NSDictionary*) toolMap;
+
++ (CompareTool*) toolWithID: (NSString*)toolID;
+
+@property (copy,nonatomic) NSString *leftPath, *rightPath, *basePath, *savePath;
+
+- (BOOL) open: (NSError**)outError;
+
+@end

Source/CompareTool.m

+//
+//  CompareTool.m
+//  Murky
+//
+//  Created by Jens Alfke on 4/11/10.
+//  Copyright 2010 Jens Alfke. All rights reserved.
+//
+
+#import "CompareTool.h"
+#import "MYTask.h"
+
+
+@implementation CompareTool
+
+// Maps tool ID --> command argument array:
+static NSDictionary* sToolMap;
+
++ (void) initialize {
+    if (!sToolMap) {
+        NSString *path = [[NSBundle bundleForClass:self] pathForResource: @"CompareTools" ofType:@"plist"];
+        if (path)
+            sToolMap = [NSDictionary dictionaryWithContentsOfFile: path];
+        if (!sToolMap) {
+            Warn(@"Could not read CompareTools.plist from path %@", path);
+            sToolMap = [NSDictionary dictionary];
+        }
+    }
+}
+
++ (NSDictionary*) toolMap {
+    return sToolMap;
+}
+
+- (id) initWithToolID: (NSString*)toolID {
+    Assert(toolID);
+    if (![sToolMap objectForKey: toolID]) {
+        Warn(@"Unknown CompareTool ID '%@'", toolID);
+        return nil;
+    }
+    self = [super init];
+    if (self != nil) {
+        _toolID = toolID;
+    }
+    return self;
+}
+
++ (CompareTool*) toolWithID: (NSString*)toolID {
+    return [[self alloc] initWithToolID: toolID];
+}
+
+@synthesize leftPath=_leftPath, rightPath=_rightPath, basePath=_basePath, savePath=_savePath;
+
+- (BOOL) open: (NSError**)outError {
+    Assert(_leftPath && _rightPath);
+    NSDictionary *settings = [sToolMap objectForKey: _toolID];
+    Assert(settings);
+    NSString *cmd = [settings objectForKey: @"cmd"];
+    Assert(cmd);
+    NSMutableArray *args = [[settings objectForKey: @"argv"] mutableCopy];
+    Assert(args);
+    
+    NSArray *baseArgs = [settings objectForKey: @"argv_base"];
+    if (baseArgs && _basePath)
+        [args addObjectsFromArray: baseArgs];
+    NSArray *saveArgs = [settings objectForKey: @"argv_save"];
+    if (saveArgs && _savePath)
+        [args addObjectsFromArray: saveArgs];
+
+    NSUInteger indexL = [args indexOfObject: @"$left"];
+    NSUInteger indexR = [args indexOfObject: @"$right"];
+    NSUInteger indexB = [args indexOfObject: @"$base"];
+    NSUInteger indexS = [args indexOfObject: @"$save"];
+    [args replaceObjectAtIndex: indexL withObject: _leftPath];
+    [args replaceObjectAtIndex: indexR withObject: _rightPath];
+    if (_basePath && indexB != NSNotFound)
+        [args replaceObjectAtIndex: indexB withObject: _basePath];
+    if (_savePath && indexS != NSNotFound)
+        [args replaceObjectAtIndex: indexS withObject: _savePath];
+    
+    MYTask *task = [[MYTask alloc] initWithCommand: cmd arguments: args];
+    [task ignoreOutput];
+    BOOL ok = [task start];
+    if (outError) *outError = task.error;
+    return ok;
+}
+
+
+@end

Source/CompareTools.plist

+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>FileMerge</key>
+	<dict>
+		<key>cmd</key>
+		<string>/usr/bin/opendiff</string>
+		<key>argv_base</key>
+		<array>
+			<string>-ancestor</string>
+			<string>$base</string>
+		</array>
+		<key>argv_save</key>
+		<array>
+			<string>-merge</string>
+			<string>$save</string>
+		</array>
+		<key>argv</key>
+		<array>
+			<string>$left</string>
+			<string>$right</string>
+		</array>
+	</dict>
+</dict>
+</plist>

Source/HgRevision.h

 @property (readonly)            NSArray *uncleanFiles;
 
 - (BOOL) descendsFrom: (HgRevision*)ancestor;
+- (HgRevision*) commonAncestorWith: (HgRevision*)otherRevision;
 
 - (HgFile*) fileAtPath: (NSString*)path;
 

Source/HgRevision.m

     return NO;
 }
 
+- (HgRevision*) commonAncestorWith: (HgRevision*)otherRevision {
+    Assert(otherRevision);
+    for (HgRevision *ancestor=self; ancestor; ancestor=ancestor->_parent) {
+        if ([otherRevision descendsFrom: ancestor])
+            return ancestor;
+    }
+    return nil;
+}
+
 
 - (NSString*) absolutePath
 {

Source/RepoController.h

 - (IBAction) discardChanges: (id)sender;
 - (IBAction) markResolved: (id)sender;
 
-- (IBAction) merge: (id)sender;
 - (IBAction) commitChanges: (id)sender;
 - (IBAction) commitAllChanges: (id)sender;
 - (IBAction) pushPullRevisions: (id)sender;         // sender.tag is a HgTransferOp
 
+- (IBAction) merge: (id)sender;
+- (IBAction) remergeFiles: (id)sender;
+
 - (IBAction) endCommitSheet: (id)sender;
 - (IBAction) beginPushSheetFilePicker: (id)sender;
 

Source/RepoController_Actions.m

 #import "HgConfigFile.h"
 #import "URLFormatter.h"
 #import "Terminal.h"
+#import "CompareTool.h"
 
 #define kPrefRevertWithoutBackup @"RevertWithoutBackup"
 
     NSString *pathNew = [revNew getPathToFileContents: file inTempDir: self.tempDir error: outError];
     if( ! pathNew ) return NO;
     
-    MYTask *task = [[MYTask alloc] initWithCommand: @"/usr/bin/opendiff", pathOld,pathNew, nil];
-    [task ignoreOutput];
-    BOOL ok = [task start];
-    if (outError) *outError = task.error;
-    return ok;
+    CompareTool *tool = [CompareTool toolWithID: @"FileMerge"];  //FIX: Make this a pref
+    tool.leftPath = pathOld;
+    tool.rightPath = pathNew;
+    
+    if (file.revision.isUncommitted)
+        tool.savePath = file.absolutePath;
+    
+    HgRevision *revBase = [revOld commonAncestorWith: revNew];
+    if (revBase && revBase != revOld && revBase != revNew) {
+        NSString *pathBase = [revBase getPathToFileContents: file inTempDir: self.tempDir error: outError];
+        tool.basePath = pathBase;
+    }
+    
+    return [tool open: outError];
 }
 
 
 
 - (IBAction) showComparison: (id)sender
 {
+    BOOL ok;
     NSError *error = nil;
     NSArray *files = self.selectedFiles;
     if( files.count == 1 && [[files objectAtIndex: 0] isFile] ) {
             NSBeep();
             return;
         }
-        [self showComparisonFromRevision: revOld 
-                              toRevision: revNew 
-                                  ofFile: [files objectAtIndex: 0]
-                                   error: &error];      
+        ok = [self showComparisonFromRevision: revOld 
+                                   toRevision: revNew 
+                                       ofFile: [files objectAtIndex: 0]
+                                        error: &error];      
     } else {
         // Multiple selection: Compare each against base:
-        [self showComparisonOfFiles: files error: &error];
+        ok = [self showComparisonOfFiles: files error: &error];
     }
-    if( error )
+    if( !ok )
         [self presentError: error];
 }
 
     }
 }
 
+// Redo a merge in a graphical compare/merge tool.
+- (IBAction) remergeFiles: (id)sender {
+    HgRevision *rev = self.selectedRevision;
+    if( !rev.isUncommitted || !rev.parent2 ) {
+        NSBeep();
+        return;
+    }
+    BOOL any = NO;
+    for (HgFile *file in [_tree selectedObjects]) {
+        if (file.isFile && file.mergeStatus == kConflict) {
+            any = YES;
+            NSError *error;
+            if (![self showComparisonFromRevision: rev.parent 
+                                       toRevision: rev.parent2 
+                                           ofFile: file
+                                            error: &error]) {
+                [self presentError: error];
+                return;
+            }
+        }
+    }
+    if (!any)
+        NSBeep();
+}
+
 - (HgMergeStatus) mergeStatusOfSelection {
     HgRevision *rev = self.selectedRevision;
     if( ! rev.isUncommitted || !rev.parent2 )
         return rev!=nil && ! rev.isUncommitted;
     } else if (action == @selector(merge:)) {
         return [_repo canMergeWithRevision: nil];
+    } else if (action == @selector(remergeFiles:)) {
+        return self.mergeStatusOfSelection == kConflict;
     } else if (action == @selector(markResolved:)) {
         HgMergeStatus status = self.mergeStatusOfSelection;
         NSString *title;