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.

  • Participants
  • Parent commits 0f74316

Comments (0)

Files changed (11)

File 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";

File 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>

File Localize.json

 {
     "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", 

File 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;
 		};

File 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

File 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

File 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>

File Source/HgRevision.h

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

File 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
 {

File 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;
 

File 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;