diff --git a/GitUp/Application/Document.m b/GitUp/Application/Document.m index ad326c7d..6d8f5ba7 100644 --- a/GitUp/Application/Document.m +++ b/GitUp/Application/Document.m @@ -21,6 +21,9 @@ #import "AuthenticationWindowController.h" #import +// Move to GitUpKit? +#import "QuickViewModel.h" + #import #define kWindowModeString_Map @"map" @@ -59,7 +62,20 @@ typedef NS_ENUM(NSInteger, NavigationAction) { kNavigationAction_Previous }; -@interface Document () +@interface Document () < +NSToolbarDelegate, +NSTextFieldDelegate, +GCLiveRepositoryDelegate, +GIWindowControllerDelegate, +GIMapViewControllerDelegate, +GISnapshotListViewControllerDelegate, +GIUnifiedReflogViewControllerDelegate, +GICommitListViewControllerDelegate, +GICommitRewriterViewControllerDelegate, +GICommitSplitterViewControllerDelegate, +GIConflictResolverViewControllerDelegate, +GIQuickViewControllerDelegate +> @property(nonatomic, strong) AuthenticationWindowController* authenticationWindowController; @property(nonatomic) IBOutlet GICustomToolbarItem* navigateItem; @property(nonatomic) IBOutlet GICustomToolbarItem* titleItem; @@ -108,7 +124,7 @@ @implementation Document { GIUnifiedReflogViewController* _unifiedReflogViewController; GICommitListViewController* _searchResultsViewController; GICommitListViewController* _ancestorsViewController; - GIQuickViewController* _quickViewController; + GIQuickViewControllerWithCommitsList* _quickViewController; GIDiffViewController* _diffViewController; GICommitRewriterViewController* _commitRewriterViewController; GICommitSplitterViewController* _commitSplitterViewController; @@ -121,10 +137,7 @@ @implementation Document { NSDateFormatter* _dateFormatter; CALayer* _fixedSnapshotLayer; CALayer* _animatingSnapshotLayer; - NSMutableArray* _quickViewCommits; - GCHistoryWalker* _quickViewAncestors; - GCHistoryWalker* _quickViewDescendants; - NSUInteger _quickViewIndex; + QuickViewModel* _quickViewModel; BOOL _searchReady; BOOL _preventSelectionLoopback; NSResponder* _savedFirstResponder; @@ -360,7 +373,9 @@ - (void)windowControllerDidLoadNib:(NSWindowController*)windowController { _searchResultsViewController.emptyLabel = NSLocalizedString(@"No Results", nil); [_searchControllerView replaceWithView:_searchResultsViewController.view]; - _quickViewController = [[GIQuickViewController alloc] initWithRepository:_repository]; + _quickViewModel = [[QuickViewModel alloc] initWithRepository:_repository]; + _quickViewController = [[GIQuickViewControllerWithCommitsList alloc] initWithRepository:_repository]; + _quickViewController.delegate = self; NSTabViewItem* quickItem = [_mainTabView tabViewItemAtIndex:[_mainTabView indexOfTabViewItemWithIdentifier:kWindowModeString_Map_QuickView]]; quickItem.view = _quickViewController.view; @@ -929,90 +944,47 @@ - (void)_hideHelp:(BOOL)open { #pragma mark - QuickView -- (void)_loadMoreAncestors { - if (![_quickViewAncestors iterateWithCommitBlock:^(GCHistoryCommit* commit, BOOL* stop) { - [_quickViewCommits addObject:commit]; - }]) { - _quickViewAncestors = nil; - } -} - -- (void)_loadMoreDescendants { - if (![_quickViewDescendants iterateWithCommitBlock:^(GCHistoryCommit* commit, BOOL* stop) { - [_quickViewCommits insertObject:commit atIndex:0]; - _quickViewIndex += 1; // We insert commits before the index too! - }]) { - _quickViewDescendants = nil; - } -} - - (void)_enterQuickViewWithHistoryCommit:(GCHistoryCommit*)commit commitList:(NSArray*)commitList { - [_repository suspendHistoryUpdates]; // We don't want the the history to change while in QuickView because of the walkers - - _quickViewCommits = [[NSMutableArray alloc] init]; - if (commitList) { - [_quickViewCommits addObjectsFromArray:commitList]; - _quickViewIndex = [_quickViewCommits indexOfObjectIdenticalTo:commit]; - XLOG_DEBUG_CHECK(_quickViewIndex != NSNotFound); - } else { - [_quickViewCommits addObject:commit]; - _quickViewIndex = 0; - _quickViewAncestors = [_repository.history walkerForAncestorsOfCommits:@[ commit ]]; - [self _loadMoreAncestors]; - _quickViewDescendants = [_repository.history walkerForDescendantsOfCommits:@[ commit ]]; - [self _loadMoreDescendants]; - } - - _quickViewController.commit = commit; - - [self _setWindowMode:kWindowModeString_Map_QuickView]; + __weak typeof(self) weakSelf = self; + [_quickViewModel enterWithHistoryCommit:commit commitList:commitList onResult:^(GCHistoryCommit * _Nonnull theCommit, NSArray * _Nullable theList) { + _quickViewController.commit = commit; + _quickViewController.list = commitList; + [weakSelf _updateToolBar]; + [weakSelf _setWindowMode:kWindowModeString_Map_QuickView]; + }]; } - (BOOL)_hasPreviousQuickView { - return (_quickViewIndex + 1 < _quickViewCommits.count); + return _quickViewModel.hasPrevious; } -- (void)_previousQuickView { - _quickViewIndex += 1; - GCHistoryCommit* commit = _quickViewCommits[_quickViewIndex]; +- (void)_selectQuickViewCommit:(GCHistoryCommit *)commit { _quickViewController.commit = commit; if (_searchView.superview) { _searchResultsViewController.selectedCommit = commit; } else { [_mapViewController selectCommit:commit]; } - if (_quickViewIndex == _quickViewCommits.count - 1) { - [self _loadMoreAncestors]; - } + [self _updateToolBar]; } +- (void)_previousQuickView { + [_quickViewModel moveBackward]; + [self _selectQuickViewCommit:_quickViewModel.currentCommit]; +} + - (BOOL)_hasNextQuickView { - return (_quickViewIndex > 0); + return _quickViewModel.hasNext; } - (void)_nextQuickView { - _quickViewIndex -= 1; - GCHistoryCommit* commit = _quickViewCommits[_quickViewIndex]; - _quickViewController.commit = commit; - if (_searchView.superview) { - _searchResultsViewController.selectedCommit = commit; - } else { - [_mapViewController selectCommit:commit]; - } - if (_quickViewIndex == 0) { - [self _loadMoreDescendants]; - } - [self _updateToolBar]; + [_quickViewModel moveForward]; + [self _selectQuickViewCommit:_quickViewModel.currentCommit]; } - (void)_exitQuickView { - _quickViewCommits = nil; - _quickViewAncestors = nil; - _quickViewDescendants = nil; - - [_repository resumeHistoryUpdates]; - + [_quickViewModel exit]; [self _setWindowMode:kWindowModeString_Map]; } @@ -1464,6 +1436,18 @@ - (void)unifiedReflogViewController:(GIUnifiedReflogViewController*)controller d [self toggleReflog:nil]; } +#pragma mark - GIQuickViewControllerDelegate +- (void)quickViewWantsToShowSelectedCommitsList:(NSArray *)commitsList selectedCommit:(GCHistoryCommit *)commit { + [self _enterQuickViewWithHistoryCommit:commit commitList:commitsList]; +} + +- (void)quickViewDidSelectCommit:(GCHistoryCommit *)commit commitsList:(NSArray *)commitsList { + // update quickViewModel. + // and toolbar. + _quickViewModel.selectedCommit = commit; + [self _selectQuickViewCommit:_quickViewModel.selectedCommit]; +} + #pragma mark - GICommitListViewControllerDelegate - (void)commitListViewControllerDidChangeSelection:(GICommitListViewController*)controller { diff --git a/GitUp/Application/QuickViewModel.h b/GitUp/Application/QuickViewModel.h new file mode 100644 index 00000000..9d3771fe --- /dev/null +++ b/GitUp/Application/QuickViewModel.h @@ -0,0 +1,32 @@ +// +// QuickViewModel.h +// Application +// +// Created by Dmitry Lobanov on 15/09/2019. +// + +#import +@import GitUpKit; + +NS_ASSUME_NONNULL_BEGIN + +@interface QuickViewModel : NSObject +- (instancetype)initWithRepository:(GCLiveRepository *)repository; + +// Checking +@property (assign, nonatomic, readonly) BOOL hasPrevious; +@property (assign, nonatomic, readonly) BOOL hasNext; +@property (assign, nonatomic, readonly) BOOL hasPaging; + +// Moving +- (void)moveBackward; +- (void)moveForward; + +// States +- (void)enterWithHistoryCommit:(GCHistoryCommit *)commit commitList:(NSArray *)commitList onResult:(void(^)(GCHistoryCommit *, NSArray * _Nullable))result; +- (void)exit; +@property (strong, nonatomic, readonly) GCHistoryCommit *currentCommit; +@property (strong, nonatomic, readwrite) GCHistoryCommit *selectedCommit; +@end + +NS_ASSUME_NONNULL_END diff --git a/GitUp/Application/QuickViewModel.m b/GitUp/Application/QuickViewModel.m new file mode 100644 index 00000000..b12f515e --- /dev/null +++ b/GitUp/Application/QuickViewModel.m @@ -0,0 +1,151 @@ +// +// QuickViewModel.m +// Application +// +// Created by Dmitry Lobanov on 15/09/2019. +// + +@import GitUpKit; +#import +#import "QuickViewModel.h" + +@interface QuickViewModel () + +@property (weak, nonatomic) GCLiveRepository *repository; + +@property (assign, nonatomic) NSUInteger index; +@property (strong, nonatomic) NSMutableArray *commits; + +@property (strong, nonatomic) GCHistoryWalker *ancestors; +@property (strong, nonatomic) GCHistoryWalker *descendants; + +#pragma mark - Protected +- (void)loadMoreAncestors; +- (void)loadMoreDescendants; +@end + +@implementation QuickViewModel + +#pragma mark - Initialization +- (instancetype)initWithRepository:(GCLiveRepository *)repository { + if ((self = [super init])) { + self.repository = repository; + } + return self; +} + +#pragma mark - Loading +- (void)loadMoreAncestors { + if (![_ancestors iterateWithCommitBlock:^(GCHistoryCommit* commit, BOOL* stop) { + [_commits addObject:commit]; + }]) { + _ancestors = nil; + } +} + +- (void)loadMoreDescendants { + if (![_descendants iterateWithCommitBlock:^(GCHistoryCommit* commit, BOOL* stop) { + [_commits insertObject:commit atIndex:0]; + _index += 1; // We insert commits before the index too! + }]) { + _descendants = nil; + } +} + +#pragma mark - Checking +- (BOOL)hasPrevious { + return _index + 1 < _commits.count; +} + +- (BOOL)hasNext { + return _index > 0; +} + +- (BOOL)hasPaging { + return _commits != nil; +} + +// TODO: Rename them appropriately. +// Blowing mind. +// Moving backward means moving to the end of array. ( or back to origin ) +// Moving forward means moving to the beginning of array. ( or to recent commits ) +// Also, these checks for end of array should be done __after__ increment or decrement of index. +#pragma mark - Moving +- (void)moveBackward { + _index += 1; + if (_index == _commits.count - 1) { + [self loadMoreAncestors]; + } +} + +- (void)moveForward { + _index -= 1; + if (_index == 0) { + [self loadMoreDescendants]; + } +} + +#pragma mark - State +- (void)enterWithHistoryCommit:(GCHistoryCommit *)commit commitList:(NSArray *)commitList onResult:(void(^)(GCHistoryCommit *, NSArray * _Nullable))result { + + // actually, we need to cleanup state if we reenter this function. + [self exit]; + + [_repository suspendHistoryUpdates]; // We don't want the the history to change while in QuickView because of the walkers + + _commits = [NSMutableArray new]; + if (commitList) { + [_commits addObjectsFromArray:commitList]; + _index = [_commits indexOfObjectIdenticalTo:commit]; + if (result) { + result(commit, commitList); + } + XLOG_DEBUG_CHECK(_index != NSNotFound); + } + else { + [_commits addObject:commit]; + _index = 0; + _ancestors = [_repository.history walkerForAncestorsOfCommits:@[ commit ]]; + [self loadMoreAncestors]; + _descendants = [_repository.history walkerForDescendantsOfCommits:@[ commit ]]; + [self loadMoreDescendants]; + if (result) { + result(commit, nil); + } + } +} + +- (void)cleanup { + _commits = nil; + _ancestors = nil; + _descendants = nil; +} + +- (void)exit { + [self cleanup]; + // resume history updates for repository. + if ([_repository areHistoryUpdatesSuspended]) { + [_repository resumeHistoryUpdates]; + } +} + +- (GCHistoryCommit *)currentCommit { + return _commits[_index]; +} + +- (void)setSelectedCommit:(GCHistoryCommit *)selectedCommit { + NSUInteger index = [_commits indexOfObjectIdenticalTo:selectedCommit]; + if (index == NSNotFound) { + // set index to zero. + _index = 0; + } + else { + _index = index; + } +} + +- (GCHistoryCommit *)selectedCommit { + return self.currentCommit; +} + +@end diff --git a/GitUp/GitUp.xcodeproj/project.pbxproj b/GitUp/GitUp.xcodeproj/project.pbxproj index 0249262d..42cce1d1 100644 --- a/GitUp/GitUp.xcodeproj/project.pbxproj +++ b/GitUp/GitUp.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 0A4881DC26C7B98D00289CF9 /* Libgit2Origin in Frameworks */ = {isa = PBXBuildFile; productRef = 0A4881DB26C7B98D00289CF9 /* Libgit2Origin */; }; 0A58CD77237B4F4B00C2BDD0 /* CloneWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A58CD73237B4F4B00C2BDD0 /* CloneWindowController.m */; }; 0A58CD7A237B4F9600C2BDD0 /* CloneWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0A58CD78237B4F9600C2BDD0 /* CloneWindowController.xib */; }; + 0AC3E01928B2E4F400309094 /* QuickViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AC3E01528B2E4F400309094 /* QuickViewModel.m */; }; 0AC8525123A11F2F00479160 /* PreferencesWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8525023A11F2F00479160 /* PreferencesWindowController.m */; }; 0AC8525423A11F3700479160 /* PreferencesWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0AC8525223A11F3700479160 /* PreferencesWindowController.xib */; }; 0AD4625B232711B000BE28D1 /* WelcomeWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AD4625A232711B000BE28D1 /* WelcomeWindowController.m */; }; @@ -124,6 +125,8 @@ 0A58CD73237B4F4B00C2BDD0 /* CloneWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CloneWindowController.m; sourceTree = ""; }; 0A58CD76237B4F4B00C2BDD0 /* CloneWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CloneWindowController.h; sourceTree = ""; }; 0A58CD79237B4F9600C2BDD0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CloneWindowController.xib; sourceTree = ""; }; + 0AC3E01528B2E4F400309094 /* QuickViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QuickViewModel.m; sourceTree = ""; }; + 0AC3E01828B2E4F400309094 /* QuickViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QuickViewModel.h; sourceTree = ""; }; 0AC8524D23A11F2F00479160 /* PreferencesWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreferencesWindowController.h; sourceTree = ""; }; 0AC8525023A11F2F00479160 /* PreferencesWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PreferencesWindowController.m; sourceTree = ""; }; 0AC8525323A11F3700479160 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreferencesWindowController.xib; sourceTree = ""; }; @@ -260,47 +263,49 @@ E2C338AD19F8562F00063D95 /* Application */ = { isa = PBXGroup; children = ( - 1D2DA2F923E9E99700691DEF /* GitUp.entitlements */, + 0A0D211A23579887003A2B5F /* AboutWindowController.h */, + 0A0D211723579887003A2B5F /* AboutWindowController.m */, + 0A2BBEC9235F9B2400912B65 /* AboutWindowController.xib */, E2C338B019F8562F00063D95 /* AppDelegate.h */, E2C338B119F8562F00063D95 /* AppDelegate.m */, E25EBCEA1AA3F8B700D3AF44 /* Application.xcassets */, + 0A2F488B23683DC90072C6FB /* AuthenticationWindowController.h */, + 0A2F488E23683DC90072C6FB /* AuthenticationWindowController.m */, + 0A2F489023683DD60072C6FB /* AuthenticationWindowController.xib */, + 0A58CD76237B4F4B00C2BDD0 /* CloneWindowController.h */, + 0A58CD73237B4F4B00C2BDD0 /* CloneWindowController.m */, + 0A58CD78237B4F9600C2BDD0 /* CloneWindowController.xib */, E2A5BE6C1A80E560008DD47F /* Common.h */, E2C338B519F8562F00063D95 /* Document.h */, E2C338B619F8562F00063D95 /* Document.m */, E2C338B819F8562F00063D95 /* Document.xib */, E21739F51A5080DD00EC6777 /* DocumentController.h */, E21739F61A5080DD00EC6777 /* DocumentController.m */, + A53C6D0A1E61A9CF0070387E /* FontSizeTransformer.h */, + A53C6D0B1E61A9CF0070387E /* FontSizeTransformer.m */, + 1D2DA2F923E9E99700691DEF /* GitUp.entitlements */, E2BDA0671AD47A2F00E69729 /* Help.plist */, E2C338AF19F8562F00063D95 /* Info.plist */, - E2A5BE6E1A814970008DD47F /* Localizable.strings */, 0AE7F5ED2312C1B000B06050 /* InfoPlist.strings */, + 0A0C5AC923720BAE000D84A1 /* KeychainAccessor.h */, + 0A0C5ACA23720BAE000D84A1 /* KeychainAccessor.m */, + E2A5BE6E1A814970008DD47F /* Localizable.strings */, E2C338B319F8562F00063D95 /* main.m */, E2C338BD19F8562F00063D95 /* MainMenu.xib */, - A53C6D0A1E61A9CF0070387E /* FontSizeTransformer.h */, - A53C6D0B1E61A9CF0070387E /* FontSizeTransformer.m */, - E2C5672B1A6D98BC00ECFE07 /* WindowController.h */, - E2C5672C1A6D98BC00ECFE07 /* WindowController.m */, - 31CD50E1203E2E2800360B3A /* ToolbarItemWrapperView.h */, - 31CD50E2203E2E2800360B3A /* ToolbarItemWrapperView.m */, - 0A0167682330CABD0069961E /* ServicesProvider.h */, - 0A0167692330CABD0069961E /* ServicesProvider.m */, - 0A0D211A23579887003A2B5F /* AboutWindowController.h */, - 0A0D211723579887003A2B5F /* AboutWindowController.m */, - 0A2BBEC9235F9B2400912B65 /* AboutWindowController.xib */, - 0A2F488B23683DC90072C6FB /* AuthenticationWindowController.h */, - 0A2F488E23683DC90072C6FB /* AuthenticationWindowController.m */, - 0A2F489023683DD60072C6FB /* AuthenticationWindowController.xib */, - 0A58CD76237B4F4B00C2BDD0 /* CloneWindowController.h */, - 0A58CD73237B4F4B00C2BDD0 /* CloneWindowController.m */, - 0A58CD78237B4F9600C2BDD0 /* CloneWindowController.xib */, 0AC8524D23A11F2F00479160 /* PreferencesWindowController.h */, 0AC8525023A11F2F00479160 /* PreferencesWindowController.m */, 0AC8525223A11F3700479160 /* PreferencesWindowController.xib */, + 0AC3E01828B2E4F400309094 /* QuickViewModel.h */, + 0AC3E01528B2E4F400309094 /* QuickViewModel.m */, + 0A0167682330CABD0069961E /* ServicesProvider.h */, + 0A0167692330CABD0069961E /* ServicesProvider.m */, + 31CD50E1203E2E2800360B3A /* ToolbarItemWrapperView.h */, + 31CD50E2203E2E2800360B3A /* ToolbarItemWrapperView.m */, 0AD46259232711B000BE28D1 /* WelcomeWindowController.h */, 0AD4625A232711B000BE28D1 /* WelcomeWindowController.m */, 0A3388BA2353BD630022528D /* WelcomeWindowController.xib */, - 0A0C5AC923720BAE000D84A1 /* KeychainAccessor.h */, - 0A0C5ACA23720BAE000D84A1 /* KeychainAccessor.m */, + E2C5672B1A6D98BC00ECFE07 /* WindowController.h */, + E2C5672C1A6D98BC00ECFE07 /* WindowController.m */, ); path = Application; sourceTree = ""; @@ -484,6 +489,7 @@ 0AC8525123A11F2F00479160 /* PreferencesWindowController.m in Sources */, 31CD50E3203E2E2800360B3A /* ToolbarItemWrapperView.m in Sources */, 0A2F488F23683DC90072C6FB /* AuthenticationWindowController.m in Sources */, + 0AC3E01928B2E4F400309094 /* QuickViewModel.m in Sources */, 0A0C5ACB23720BAE000D84A1 /* KeychainAccessor.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/GitUpKit/Core/GCHistory.h b/GitUpKit/Core/GCHistory.h index ca63be6b..bee61525 100644 --- a/GitUpKit/Core/GCHistory.h +++ b/GitUpKit/Core/GCHistory.h @@ -95,6 +95,18 @@ typedef NS_ENUM(NSUInteger, GCHistorySorting) { - (BOOL)reloadHistory:(GCHistory*)history referencesDidChange:(BOOL*)referencesDidChange addedCommits:(NSArray**)addedCommits removedCommits:(NSArray**)removedCommits error:(NSError**)error; - (GCHistory*)loadHistoryFromSnapshot:(GCSnapshot*)snapshot usingSorting:(GCHistorySorting)sorting error:(NSError**)error; +@end + +@interface GCRepositoryHistoryFileOptions : NSObject +@property (nonatomic, readonly) BOOL followRenames; +@property (nonatomic, readonly) BOOL includeMerges; +@property (nonatomic, readonly) BOOL shouldIteroverWhenObjectNotFoundInTreeEntryByPath; + +#pragma mark - Initialization +- (instancetype)initWithFollowRenames:(BOOL)followRenames includeMerges:(BOOL)includeMerges shouldIteroverWhenObjectNotFoundInTreeEntryByPath:(BOOL)shouldIteroverWhenObjectNotFoundInTreeEntryByPath; +@end +@interface GCRepository (GCHistoryFile) +- (NSArray*)lookupCommitsForFile:(NSString*)path options:(GCRepositoryHistoryFileOptions *)options error:(NSError**)error; - (NSArray*)lookupCommitsForFile:(NSString*)path followRenames:(BOOL)follow error:(NSError**)error; // git log {--follow} -p {file} @end diff --git a/GitUpKit/Core/GCHistory.m b/GitUpKit/Core/GCHistory.m index d88fcfa2..cadf557e 100644 --- a/GitUpKit/Core/GCHistory.m +++ b/GitUpKit/Core/GCHistory.m @@ -1202,12 +1202,34 @@ - (GCHistory*)loadHistoryFromSnapshot:(GCSnapshot*)snapshot usingSorting:(GCHist #pragma mark - File -- (NSArray*)lookupCommitsForFile:(NSString*)path followRenames:(BOOL)follow error:(NSError**)error { +@end + +@interface GCRepositoryHistoryFileOptions () +@property (nonatomic) BOOL followRenames; +@property (nonatomic) BOOL includeMerges; +@property (nonatomic) BOOL shouldIteroverWhenObjectNotFoundInTreeEntryByPath; +@end + +@implementation GCRepositoryHistoryFileOptions +#pragma mark - Initialization +- (instancetype)initWithFollowRenames:(BOOL)followRenames includeMerges:(BOOL)includeMerges shouldIteroverWhenObjectNotFoundInTreeEntryByPath:(BOOL)shouldIteroverWhenObjectNotFoundInTreeEntryByPath { + if ((self = [super init])) { + self.followRenames = followRenames; + self.includeMerges = includeMerges; + self.shouldIteroverWhenObjectNotFoundInTreeEntryByPath = shouldIteroverWhenObjectNotFoundInTreeEntryByPath; + } + return self; +} +@end + +@implementation GCRepository (GCHistoryFile) +- (NSArray*)lookupCommitsForFile:(NSString*)path options:(GCRepositoryHistoryFileOptions *)options error:(NSError**)error { + GCRepositoryHistoryFileOptions* theOptions = options ?: [[GCRepositoryHistoryFileOptions alloc] init]; NSMutableArray* commits = nil; char* fileName = strdup(GCGitPathFromFileSystemPath(path)); git_revwalk* walker = NULL; git_oid oid; - + CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_revwalk_new, &walker, self.private); CALL_LIBGIT2_FUNCTION_GOTO(cleanup, git_revwalk_push_head, walker); git_revwalk_sorting(walker, GIT_SORT_TOPOLOGICAL); @@ -1229,6 +1251,12 @@ - (NSArray*)lookupCommitsForFile:(NSString*)path followRenames:(BOOL)follow erro status = git_tree_entry_bypath(&entry, tree, fileName); if (status == GIT_OK) { for (unsigned int i = 0, count = git_commit_parentcount(commit); i < count; ++i) { + if (count > 1 && !theOptions.includeMerges) { + break; + } + if (commit == NULL) { + break; + } git_commit* parentCommit; status = git_commit_parent(&parentCommit, commit, i); if (status == GIT_OK) { @@ -1237,17 +1265,19 @@ - (NSArray*)lookupCommitsForFile:(NSString*)path followRenames:(BOOL)follow erro if (status == GIT_OK) { git_diff* diff = NULL; status = git_diff_tree_to_tree(&diff, self.private, parentTree, tree, &diffOptions); - if ((status == GIT_OK) && follow) { + if ((status == GIT_OK) && theOptions.followRenames) { status = git_diff_find_similar(diff, &findOptions); } if (status == GIT_OK) { for (size_t i2 = 0, count2 = git_diff_num_deltas(diff); i2 < count2; ++i2) { const git_diff_delta* delta = git_diff_get_delta(diff, i2); if (strcmp(delta->new_file.path, fileName) == 0) { - GCCommit* newCommit = [[GCCommit alloc] initWithRepository:self commit:commit]; + // here we need to retain commit. + git_commit *copy_commit; + git_commit_dup(©_commit, commit); + GCCommit* newCommit = [[GCCommit alloc] initWithRepository:self commit:copy_commit]; [commits addObject:newCommit]; [newCommit release]; - commit = NULL; if (delta->status == GIT_DELTA_RENAMED) { free(fileName); fileName = strdup(delta->old_file.path); @@ -1264,10 +1294,11 @@ - (NSArray*)lookupCommitsForFile:(NSString*)path followRenames:(BOOL)follow erro } git_commit_free(parentCommit); } - } + } // end of for-loop (count = git_commit_parentcount(commit)) git_tree_entry_free(entry); } else if (status == GIT_ENOTFOUND) { - status = GIT_ITEROVER; + // ignore error if option not set. + status = theOptions.shouldIteroverWhenObjectNotFoundInTreeEntryByPath ? GIT_ITEROVER : GIT_OK; } git_tree_free(tree); } @@ -1283,11 +1314,15 @@ - (NSArray*)lookupCommitsForFile:(NSString*)path followRenames:(BOOL)follow erro } CHECK_LIBGIT2_FUNCTION_CALL(goto cleanup, status, == GIT_OK); } - + cleanup: git_revwalk_free(walker); free(fileName); return [commits autorelease]; } +- (NSArray*)lookupCommitsForFile:(NSString*)path followRenames:(BOOL)follow error:(NSError**)error { + return [self lookupCommitsForFile:path options:[[GCRepositoryHistoryFileOptions alloc] initWithFollowRenames:follow includeMerges:NO shouldIteroverWhenObjectNotFoundInTreeEntryByPath:NO] error:error]; +} + @end diff --git a/GitUpKit/Extensions/NSView+Embedding.h b/GitUpKit/Extensions/NSView+Embedding.h new file mode 100644 index 00000000..6b2a76de --- /dev/null +++ b/GitUpKit/Extensions/NSView+Embedding.h @@ -0,0 +1,16 @@ +// +// NSView+Embedding.h +// GitUpKit (OSX) +// +// Created by Dmitry Lobanov on 06.04.2020. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSView (Embedding) +- (void)embedView:(NSView *)view; +@end + +NS_ASSUME_NONNULL_END diff --git a/GitUpKit/Extensions/NSView+Embedding.m b/GitUpKit/Extensions/NSView+Embedding.m new file mode 100644 index 00000000..95cbbece --- /dev/null +++ b/GitUpKit/Extensions/NSView+Embedding.m @@ -0,0 +1,37 @@ +// +// NSView+Embedding.m +// GitUpKit (OSX) +// +// Created by Dmitry Lobanov on 06.04.2020. +// + +#import "NSView+Embedding.h" + +@implementation NSView (Embedding) +- (void)embedView:(NSView *)view { + [self.class embedView:view inView:self]; +} + ++ (void)embedView:(NSView *)view inView:(NSView *)superview { + [superview addSubview:view]; + view.translatesAutoresizingMaskIntoConstraints = NO; + + if (superview != nil && view != nil) { + if (@available(macOS 10.11, *)) { + NSArray *constraints = @[ + [view.leftAnchor constraintEqualToAnchor:superview.leftAnchor], + [view.topAnchor constraintEqualToAnchor:superview.topAnchor], + [view.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor], + [view.rightAnchor constraintEqualToAnchor:superview.rightAnchor] + ]; + [NSLayoutConstraint activateConstraints:constraints]; + } else { + // Fallback on earlier versions + NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": view}]; + NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:@{@"view": view}]; + NSArray *constraints = [[NSArray arrayWithArray:verticalConstraints] arrayByAddingObjectsFromArray:horizontalConstraints]; + [NSLayoutConstraint activateConstraints:constraints]; + } + } +} +@end diff --git a/GitUpKit/GitUpKit.h b/GitUpKit/GitUpKit.h index 159c7218..671ad04a 100644 --- a/GitUpKit/GitUpKit.h +++ b/GitUpKit/GitUpKit.h @@ -55,6 +55,7 @@ #import #import #import +#import #import #import diff --git a/GitUpKit/GitUpKit.xcodeproj/project.pbxproj b/GitUpKit/GitUpKit.xcodeproj/project.pbxproj index 4dec92ca..738b26cf 100644 --- a/GitUpKit/GitUpKit.xcodeproj/project.pbxproj +++ b/GitUpKit/GitUpKit.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 0A1DA51E265806140041E737 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A1DA51D265806140041E737 /* Security.framework */; }; 0A4881D726C7B4CF00289CF9 /* Libgit2Origin in Frameworks */ = {isa = PBXBuildFile; productRef = 0A4881D626C7B4CF00289CF9 /* Libgit2Origin */; }; + 0AC3E00E28B2E49100309094 /* NSView+Embedding.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AC3E00C28B2E49100309094 /* NSView+Embedding.m */; }; + 0AC3E00F28B2E49100309094 /* NSView+Embedding.h in Headers */ = {isa = PBXBuildFile; fileRef = 0AC3E00D28B2E49100309094 /* NSView+Embedding.h */; }; + 0AC3E01228B2E49F00309094 /* GIQuickViewControllerWithCommitsList.h in Headers */ = {isa = PBXBuildFile; fileRef = 0AC3E01028B2E49F00309094 /* GIQuickViewControllerWithCommitsList.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0AC3E01328B2E49F00309094 /* GIQuickViewControllerWithCommitsList.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AC3E01128B2E49F00309094 /* GIQuickViewControllerWithCommitsList.m */; }; 0AC8525923A122C400479160 /* GILaunchServicesLocator.h in Headers */ = {isa = PBXBuildFile; fileRef = 0AC8525723A122C400479160 /* GILaunchServicesLocator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0AC8525A23A122C400479160 /* GILaunchServicesLocator.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8525823A122C400479160 /* GILaunchServicesLocator.m */; }; 1D615D81286BEDC600FFF7E7 /* XLFacilityMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 1D615D7E286BE79200FFF7E7 /* XLFacilityMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -343,6 +347,10 @@ /* Begin PBXFileReference section */ 0A1DA512265805C30041E737 /* libcommonCrypto.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcommonCrypto.tbd; path = usr/lib/system/libcommonCrypto.tbd; sourceTree = SDKROOT; }; 0A1DA51D265806140041E737 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 0AC3E00C28B2E49100309094 /* NSView+Embedding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSView+Embedding.m"; sourceTree = ""; }; + 0AC3E00D28B2E49100309094 /* NSView+Embedding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSView+Embedding.h"; sourceTree = ""; }; + 0AC3E01028B2E49F00309094 /* GIQuickViewControllerWithCommitsList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GIQuickViewControllerWithCommitsList.h; sourceTree = ""; }; + 0AC3E01128B2E49F00309094 /* GIQuickViewControllerWithCommitsList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GIQuickViewControllerWithCommitsList.m; sourceTree = ""; }; 0AC8525723A122C400479160 /* GILaunchServicesLocator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GILaunchServicesLocator.h; sourceTree = ""; }; 0AC8525823A122C400479160 /* GILaunchServicesLocator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GILaunchServicesLocator.m; sourceTree = ""; }; 0AD01C0B26D6A11C0068A02E /* Third-Party */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Third-Party"; sourceTree = ""; }; @@ -673,7 +681,6 @@ E22941E61AC11F56000A83AF /* Views */ = { isa = PBXGroup; children = ( - E229421A1AC11FF6000A83AF /* Views.xcassets */, E22941F71AC11F56000A83AF /* GIAdvancedCommitViewController.h */, E22941F81AC11F56000A83AF /* GIAdvancedCommitViewController.m */, E22941E71AC11F56000A83AF /* GIAdvancedCommitViewController.xib */, @@ -685,10 +692,10 @@ E229421E1AC120A8000A83AF /* GICommitSplitterViewController.xib */, E22941F91AC11F56000A83AF /* GICommitViewController.h */, E22941FA1AC11F56000A83AF /* GICommitViewController.m */, + E22941E91AC11F56000A83AF /* GIConfigViewController-Help.txt */, E22941FB1AC11F56000A83AF /* GIConfigViewController.h */, E22941FC1AC11F56000A83AF /* GIConfigViewController.m */, E22941EB1AC11F56000A83AF /* GIConfigViewController.xib */, - E22941E91AC11F56000A83AF /* GIConfigViewController-Help.txt */, E2BDA0701AD5B97B00E69729 /* GIConflictResolverViewController.h */, E2BDA0711AD5B97B00E69729 /* GIConflictResolverViewController.m */, E2BDA0731AD5B98A00E69729 /* GIConflictResolverViewController.xib */, @@ -697,18 +704,21 @@ E22941ED1AC11F56000A83AF /* GIDiffViewController.xib */, E22942011AC11F56000A83AF /* GIMapViewController.h */, E22942021AC11F56000A83AF /* GIMapViewController.m */, + E22941EF1AC11F56000A83AF /* GIMapViewController.xib */, E22941FF1AC11F56000A83AF /* GIMapViewController+Operations.h */, E22942001AC11F56000A83AF /* GIMapViewController+Operations.m */, - E22941EF1AC11F56000A83AF /* GIMapViewController.xib */, E22942031AC11F56000A83AF /* GIQuickViewController.h */, E22942041AC11F56000A83AF /* GIQuickViewController.m */, E22941F11AC11F56000A83AF /* GIQuickViewController.xib */, + 0AC3E01028B2E49F00309094 /* GIQuickViewControllerWithCommitsList.h */, + 0AC3E01128B2E49F00309094 /* GIQuickViewControllerWithCommitsList.m */, E22942051AC11F56000A83AF /* GISimpleCommitViewController.h */, E22942061AC11F56000A83AF /* GISimpleCommitViewController.m */, E22941F31AC11F56000A83AF /* GISimpleCommitViewController.xib */, E22942071AC11F56000A83AF /* GIStashListViewController.h */, E22942081AC11F56000A83AF /* GIStashListViewController.m */, E22941F51AC11F56000A83AF /* GIStashListViewController.xib */, + E229421A1AC11FF6000A83AF /* Views.xcassets */, ); path = Views; sourceTree = ""; @@ -719,14 +729,16 @@ E259C2D41A64FAA40079616B /* GCHistory+Rewrite-Tests.m */, E2D414891A02D68700B99634 /* GCHistory+Rewrite.h */, E2D4148A1A02D68700B99634 /* GCHistory+Rewrite.m */, + 1DF371CB22F5262300EF7BD9 /* GCLiveRepository+Utilities.h */, + 1DF371CC22F5262300EF7BD9 /* GCLiveRepository+Utilities.m */, E2790D441ACF12E200965A98 /* GCRepository+Index-Tests.m */, E2790D451ACF12E200965A98 /* GCRepository+Index.h */, E2790D461ACF12E200965A98 /* GCRepository+Index.m */, E259C2D61A64FAEA0079616B /* GCRepository+Utilities-Tests.m */, E218A58C1A56706600DFF1DF /* GCRepository+Utilities.h */, E218A58D1A56706600DFF1DF /* GCRepository+Utilities.m */, - 1DF371CB22F5262300EF7BD9 /* GCLiveRepository+Utilities.h */, - 1DF371CC22F5262300EF7BD9 /* GCLiveRepository+Utilities.m */, + 0AC3E00D28B2E49100309094 /* NSView+Embedding.h */, + 0AC3E00C28B2E49100309094 /* NSView+Embedding.m */, ); path = Extensions; sourceTree = ""; @@ -969,8 +981,10 @@ E267E1BD1B84D80500BAB377 /* GCCore.h in Headers */, E267E1BE1B84D80500BAB377 /* GCDiff.h in Headers */, E267E2A61B84F02A00BAB377 /* GCError.h in Headers */, + 0AC3E01228B2E49F00309094 /* GIQuickViewControllerWithCommitsList.h in Headers */, E267E1BF1B84D80500BAB377 /* GCFoundation.h in Headers */, E267E1C01B84D80500BAB377 /* GCFunctions.h in Headers */, + 0AC3E00F28B2E49100309094 /* NSView+Embedding.h in Headers */, E267E1C11B84D80500BAB377 /* GCHistory.h in Headers */, E267E1C21B84D80500BAB377 /* GCIndex.h in Headers */, E267E1C31B84D80500BAB377 /* GCLiveRepository.h in Headers */, @@ -1265,6 +1279,7 @@ E267E1D51B84D83100BAB377 /* GCFoundation.m in Sources */, E267E1D61B84D83100BAB377 /* GCFunctions.m in Sources */, E267E1D71B84D83100BAB377 /* GCHistory.m in Sources */, + 0AC3E00E28B2E49100309094 /* NSView+Embedding.m in Sources */, E267E1D81B84D83100BAB377 /* GCIndex.m in Sources */, E267E1D91B84D83100BAB377 /* GCLiveRepository.m in Sources */, DBDFBC1D22B61290003EEC6C /* NSColor+GINamedColors.m in Sources */, @@ -1330,6 +1345,7 @@ E267E2631B84DCA100BAB377 /* GISimpleCommitViewController.m in Sources */, E267E2641B84DCA100BAB377 /* GIStashListViewController.m in Sources */, 0AC8525A23A122C400479160 /* GILaunchServicesLocator.m in Sources */, + 0AC3E01328B2E49F00309094 /* GIQuickViewControllerWithCommitsList.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GitUpKit/Utilities/GIViewController+Utilities.h b/GitUpKit/Utilities/GIViewController+Utilities.h index e0f2e98e..2ca60e2c 100644 --- a/GitUpKit/Utilities/GIViewController+Utilities.h +++ b/GitUpKit/Utilities/GIViewController+Utilities.h @@ -27,6 +27,8 @@ - (void)stageAllFiles; - (void)unstageAllFiles; +- (void)getSelectedCommitsForFilesMatchingPaths:(NSArray *)paths result:(void(^)(NSArray *commits))result; + - (void)stageSubmoduleAtPath:(NSString*)path; - (void)unstageSubmoduleAtPath:(NSString*)path; - (BOOL)discardSubmoduleAtPath:(NSString*)path resetIndex:(BOOL)resetIndex error:(NSError**)error; diff --git a/GitUpKit/Utilities/GIViewController+Utilities.m b/GitUpKit/Utilities/GIViewController+Utilities.m index d13f2f97..3a99ba2a 100644 --- a/GitUpKit/Utilities/GIViewController+Utilities.m +++ b/GitUpKit/Utilities/GIViewController+Utilities.m @@ -23,7 +23,9 @@ #import "GCCore.h" #import "GCRepository+Index.h" #import "GCRepository+Utilities.h" +#import "GCHistory.h" #import "GIAppKit.h" +#import "GICommitListViewController.h" #import "XLFacilityMacros.h" #define kOpenDiffPath @"/usr/bin/opendiff" @@ -68,6 +70,34 @@ - (void)unstageAllFiles { } } +#pragma mark - Selection +- (NSArray *)selectedCommitsForFilesMatchingPaths:(NSArray *)paths error:(NSError *__autoreleasing*)error { + if (paths.count == 0) { + return @[]; + } + NSArray *commits = [self.repository lookupCommitsForFile:paths.firstObject followRenames:YES error:error]; + if (error && *error) { + return @[]; + } + return commits; +} + +- (void)getSelectedCommitsForFilesMatchingPaths:(NSArray *)paths result:(void(^)(NSArray *commits))result { + if (result == nil) { + return; + } + + NSError *error = nil; + NSArray * commits = [self selectedCommitsForFilesMatchingPaths:paths error:&error]; + if (error != nil) { + [self presentError:error]; + } + else { + result(commits); + } +} + +#pragma mark - Submodule - (void)stageSubmoduleAtPath:(NSString*)path { NSError* error; GCSubmodule* submodule = [self.repository lookupSubmoduleWithName:path error:&error]; @@ -107,6 +137,7 @@ - (void)discardSubmoduleAtPath:(NSString*)path resetIndex:(BOOL)resetIndex { }]; } +#pragma mark - Stage selected changes - (void)stageAllChangesForFile:(NSString*)path { return [self stageAllChangesForFiles:@[ path ]]; } @@ -271,6 +302,7 @@ - (void)discardSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldL }]; } +#pragma mark - Delete/Restore - (void)deleteUntrackedFile:(NSString*)path { [self confirmUserActionWithAlertType:kGIAlertType_Stop title:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the file \"%@\"?", nil), path.lastPathComponent] @@ -302,6 +334,7 @@ - (void)restoreFile:(NSString*)path toCommit:(GCCommit*)commit { }]; } +#pragma mark - Open/Show - (void)openFileWithDefaultEditor:(NSString*)path { [[NSWorkspace sharedWorkspace] openFile:[self.repository absolutePathForFile:path]]; // This will silently fail if the file doesn't exist in the working directory } @@ -333,6 +366,7 @@ - (void)openSubmoduleWithApp:(NSString*)path { } } +#pragma mark - Custom apps support - (void)_runTaskWithPath:(NSString*)path arguments:(NSArray*)arguments variables:(NSDictionary*)variables waitUntilExit:(BOOL)wait reportErrors:(BOOL)report { NSMutableDictionary* environment = [[NSMutableDictionary alloc] initWithDictionary:[[NSProcessInfo processInfo] environment]]; [environment addEntriesFromDictionary:variables]; @@ -614,6 +648,7 @@ - (void)resolveConflictInMergeTool:(GCIndexConflict*)conflict { } } +#pragma mark - Conflicts - (void)markConflictAsResolved:(GCIndexConflict*)conflict { NSError* error; if ([self.repository resolveConflictAtPath:conflict.path error:&error]) { @@ -693,6 +728,7 @@ - (GCCommit*)resolveConflictsWithResolver:(id)resolver return commit; } +#pragma mark - Contextual menu // Keep logic in sync with method below! - (NSMenu*)contextualMenuForDelta:(GCDiffDelta*)delta withConflict:(GCIndexConflict*)conflict allowOpen:(BOOL)allowOpen { NSMenu* menu = [[NSMenu alloc] initWithTitle:@""]; @@ -741,6 +777,7 @@ - (NSMenu*)contextualMenuForDelta:(GCDiffDelta*)delta withConflict:(GCIndexConfl return menu; } +#pragma mark - Keyboard events // Keep logic in sync with method above! - (BOOL)handleKeyDownEvent:(NSEvent*)event forSelectedDeltas:(NSArray*)deltas withConflicts:(NSDictionary*)conflicts allowOpen:(BOOL)allowOpen { if (deltas.count) { @@ -789,6 +826,7 @@ - (BOOL)handleKeyDownEvent:(NSEvent*)event forSelectedDeltas:(NSArray*)deltas wi return NO; } +#pragma mark - Diff tool // TODO: Use private app directory - (void)launchDiffToolWithCommit:(GCCommit*)commit otherCommit:(GCCommit*)otherCommit { NSString* identifier = [[NSUserDefaults standardUserDefaults] stringForKey:GIPreferences_DiffTool]; diff --git a/GitUpKit/Views/Base.lproj/GIQuickViewController.xib b/GitUpKit/Views/Base.lproj/GIQuickViewController.xib index ad228acf..e866140d 100644 --- a/GitUpKit/Views/Base.lproj/GIQuickViewController.xib +++ b/GitUpKit/Views/Base.lproj/GIQuickViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -25,101 +25,101 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -141,11 +141,11 @@ - + - + @@ -167,11 +167,11 @@ - + - + @@ -192,6 +192,7 @@ + diff --git a/GitUpKit/Views/GIQuickViewController.h b/GitUpKit/Views/GIQuickViewController.h index a8c08f79..cfda9b9d 100644 --- a/GitUpKit/Views/GIQuickViewController.h +++ b/GitUpKit/Views/GIQuickViewController.h @@ -16,7 +16,16 @@ #import "GIViewController.h" @class GCHistoryCommit; +@class GCCommit; +@class GCDiffDelta; +@class GCIndexConflict; +@protocol GIQuickViewControllerDelegate +- (void)quickViewWantsToShowSelectedCommitsList:(NSArray *)commitsList selectedCommit:(GCHistoryCommit *)commit; +- (void)quickViewDidSelectCommit:(GCHistoryCommit *)commit commitsList:(NSArray *)commitsList; +@end @interface GIQuickViewController : GIViewController @property(nonatomic, strong) GCHistoryCommit* commit; +@property(nonatomic, weak) id delegate; +@property(nonatomic, copy) void(^willShowContextualMenu)(NSMenu *menu, GCDiffDelta *delta, GCIndexConflict *conflict); @end diff --git a/GitUpKit/Views/GIQuickViewController.m b/GitUpKit/Views/GIQuickViewController.m index 9eca0e02..804f0059 100644 --- a/GitUpKit/Views/GIQuickViewController.m +++ b/GitUpKit/Views/GIQuickViewController.m @@ -21,6 +21,7 @@ #import "GIDiffContentsViewController.h" #import "GIDiffFilesViewController.h" #import "GIViewController+Utilities.h" +#import "NSView+Embedding.h" #import "GIInterface.h" #import "XLFacilityMacros.h" @@ -49,6 +50,7 @@ @implementation GIQuickViewController { GCDiff* _diff; } +#pragma mark - Initialization - (instancetype)initWithRepository:(GCLiveRepository*)repository { if ((self = [super initWithRepository:repository])) { _dateFormatter = [[NSDateFormatter alloc] init]; @@ -62,6 +64,7 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSSplitViewDidResizeSubviewsNotification object:nil]; } +#pragma mark - Compute sizes - (void)_recomputeInfoViewFrame { NSRect frame = _infoView.frame; NSSize size = [(NSTextFieldCell*)_messageTextField.cell cellSizeForBounds:NSMakeRect(0, 0, _messageTextField.frame.size.width, HUGE_VALF)]; @@ -75,26 +78,30 @@ - (void)_splitViewDidResizeSubviews:(NSNotification*)notification { } } +#pragma mark - View Lifecycle - (void)loadView { [super loadView]; _diffContentsViewController = [[GIDiffContentsViewController alloc] initWithRepository:self.repository]; _diffContentsViewController.delegate = self; _diffContentsViewController.emptyLabel = NSLocalizedString(@"No differences", nil); - [_contentsView replaceWithView:_diffContentsViewController.view]; _diffFilesViewController = [[GIDiffFilesViewController alloc] initWithRepository:self.repository]; _diffFilesViewController.delegate = self; - [_filesView replaceWithView:_diffFilesViewController.view]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_splitViewDidResizeSubviews:) name:NSSplitViewDidResizeSubviewsNotification object:_mainSplitView]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_splitViewDidResizeSubviews:) name:NSSplitViewDidResizeSubviewsNotification object:_infoSplitView]; + + [_contentsView embedView:_diffContentsViewController.view]; + [_filesView embedView:_diffFilesViewController.view]; } +#pragma mark - GIViewController - (void)viewDidFinishLiveResize { [self _recomputeInfoViewFrame]; } +#pragma mark - Message Utilities static inline void _AppendStringWithoutTrailingWhiteSpace(NSMutableString* string, NSString* append, NSRange range) { NSCharacterSet* set = [NSCharacterSet whitespaceCharacterSet]; while (range.length) { @@ -138,6 +145,7 @@ static inline void _AppendStringWithoutTrailingWhiteSpace(NSMutableString* strin return string; } +#pragma mark - Accessors - (void)setCommit:(GCHistoryCommit*)commit { if (commit != _commit) { _commit = commit; @@ -218,12 +226,15 @@ - (NSMenu*)diffContentsViewController:(GIDiffContentsViewController*)controller } else { [menu addItemWithTitle:NSLocalizedString(@"Restore File to This Version…", nil) block:NULL]; } - + + if (self.willShowContextualMenu) { + self.willShowContextualMenu(menu, delta, conflict); + } + return menu; } #pragma mark - GIDiffFilesViewControllerDelegate - - (void)diffFilesViewController:(GIDiffFilesViewController*)controller willSelectDelta:(GCDiffDelta*)delta { _disableFeedbackLoop = YES; [_diffContentsViewController setTopVisibleDelta:delta offset:0]; diff --git a/GitUpKit/Views/GIQuickViewControllerWithCommitsList.h b/GitUpKit/Views/GIQuickViewControllerWithCommitsList.h new file mode 100644 index 00000000..c9498271 --- /dev/null +++ b/GitUpKit/Views/GIQuickViewControllerWithCommitsList.h @@ -0,0 +1,19 @@ +// +// GIQuickViewControllerWithCommitsList.h +// GitUpKit (OSX) +// +// Created by Dmitry Lobanov on 31.08.2020. +// + +#import "GIViewController.h" +#import "GIQuickViewController.h" + +@class GCHistoryCommit; +@class GCCommit; + +@interface GIQuickViewControllerWithCommitsList : GIViewController +@property(nonatomic, strong) GCHistoryCommit* commit; +@property(weak, nonatomic) id delegate; +@property(nonatomic, strong) NSArray *list; +@end + diff --git a/GitUpKit/Views/GIQuickViewControllerWithCommitsList.m b/GitUpKit/Views/GIQuickViewControllerWithCommitsList.m new file mode 100644 index 00000000..009d4d61 --- /dev/null +++ b/GitUpKit/Views/GIQuickViewControllerWithCommitsList.m @@ -0,0 +1,209 @@ +// +// GIQuickViewControllerWithCommitsList.m +// GitUpKit (OSX) +// +// Created by Dmitry Lobanov on 31.08.2020. +// + +#import "GIQuickViewControllerWithCommitsList.h" + +#import "GIQuickViewController.h" +//#import "GIDiffContentsViewController.h" +//#import "GIDiffFilesViewController.h" +#import "GIViewController+Utilities.h" +#import "NSView+Embedding.h" + +#import "GIInterface.h" +#import "XLFacilityMacros.h" + +#import "GICommitListViewController.h" +@interface GIQuickViewControllerWithCommitsList () +@property (strong, nonatomic, readwrite) GICommitListViewController *commitListViewController; +@property (strong, nonatomic, readwrite) GIQuickViewController *quickViewController; + +@property (strong, nonatomic, readwrite) NSLayoutConstraint *hiddenConstraint; +@property (strong, nonatomic, readwrite) NSLayoutConstraint *revealedConstraint; +@end + +@implementation GIQuickViewControllerWithCommitsList + +#pragma mark - Check +- (BOOL)isHistoryShown { + return self.commitListViewController.results.count > 0; +} + +#pragma mark - Actions +- (void)toggleLeftView { + BOOL shouldReveal = self.isHistoryShown; + self.hiddenConstraint.active = !shouldReveal; + self.revealedConstraint.active = shouldReveal; + [self.view setNeedsLayout:YES]; + [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { + context.duration = 0.25; + context.allowsImplicitAnimation = YES; + [self.view layout]; + } completionHandler:^{ + }]; +} + +#pragma mark - Layout +- (void)addConstraints { + if (@available(macOS 10.11, *)) { + NSView *leftView = self.commitListViewController.view; + if (leftView.superview != nil) { + NSView *superview = leftView.superview; + NSArray *constraints = @[ + [leftView.leftAnchor constraintEqualToAnchor:superview.leftAnchor], + [leftView.topAnchor constraintEqualToAnchor:superview.topAnchor], + [leftView.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor], + [leftView.widthAnchor constraintEqualToAnchor:superview.widthAnchor multiplier:0.3] + ]; + [NSLayoutConstraint activateConstraints:constraints]; + } + + NSView *rightView = self.quickViewController.view; + if (rightView.superview != nil) { + NSView *superview = rightView.superview; + self.hiddenConstraint = [rightView.leftAnchor constraintEqualToAnchor:superview.leftAnchor]; + self.revealedConstraint = [rightView.leftAnchor constraintEqualToAnchor:leftView.rightAnchor]; + NSArray *constraints = @[ + [rightView.topAnchor constraintEqualToAnchor:superview.topAnchor], + [rightView.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor], + [rightView.rightAnchor constraintEqualToAnchor:superview.rightAnchor], + ]; + [NSLayoutConstraint activateConstraints:constraints]; + } + + [self toggleLeftView]; + } else { + NSView *leftView = self.commitListViewController.view; + if (leftView.superview != nil) { + NSView *superview = leftView.superview; + NSArray *constraints = @[ + [NSLayoutConstraint constraintWithItem:leftView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], + [NSLayoutConstraint constraintWithItem:leftView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeWidth multiplier:0.3 constant:0.0] + ]; + NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": leftView}]; + [NSLayoutConstraint activateConstraints:[constraints arrayByAddingObjectsFromArray:verticalConstraints]]; + } + NSView *rightView = self.quickViewController.view; + if (rightView.superview != nil) { + NSView *superview = rightView.superview; + self.hiddenConstraint = [NSLayoutConstraint constraintWithItem:rightView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0]; + self.revealedConstraint = [NSLayoutConstraint constraintWithItem:rightView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:leftView attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0]; + NSArray *constraints = @[ + [NSLayoutConstraint constraintWithItem:rightView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0], + ]; + NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": rightView}]; + [NSLayoutConstraint activateConstraints:[constraints arrayByAddingObjectsFromArray:verticalConstraints]]; + } + + [self toggleLeftView]; + } +} + +#pragma mark - View Lifecycle +- (void)loadView { + self.view = [[GIView alloc] initWithFrame:NSScreen.mainScreen.frame]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.commitListViewController = [[GICommitListViewController alloc] initWithRepository:self.repository]; + self.commitListViewController.delegate = self; + + self.quickViewController = [[GIQuickViewController alloc] initWithRepository:self.repository]; + __weak typeof(self) weakSelf = self; + self.quickViewController.willShowContextualMenu = ^(NSMenu *menu, GCDiffDelta *delta, GCIndexConflict *conflict) { + [weakSelf willShowContextualMenu:menu delta:delta conflict:conflict]; + }; + + [self.view addSubview:self.commitListViewController.view]; + [self.view addSubview:self.quickViewController.view]; + + self.commitListViewController.view.translatesAutoresizingMaskIntoConstraints = NO; + self.quickViewController.view.translatesAutoresizingMaskIntoConstraints = NO; + [self addConstraints]; +} + +- (void)setCommit:(GCHistoryCommit *)commit { + if (self.quickViewController.commit.autoIncrementID != commit.autoIncrementID || self.quickViewController.commit == nil) { + self.quickViewController.commit = commit; + } + if (self.commitListViewController.selectedCommit.autoIncrementID != commit.autoIncrementID || self.commitListViewController.selectedCommit == nil) { + self.commitListViewController.selectedCommit = commit; + } +} + +#pragma mark - Getters / Setters +- (GCHistoryCommit *)commit { + return self.quickViewController.commit; +} + +- (void)setDelegate:(id)delegate { + self.quickViewController.delegate = delegate; +} + +- (id)delegate { + return self.quickViewController.delegate; +} + +- (void)setList:(NSArray *)list { + self.commitListViewController.results = list; + [self toggleLeftView]; +} + +- (NSArray *)list { + NSMutableArray* result = [NSMutableArray new]; + for (GCHistoryCommit* element in self.commitListViewController.results) { + if ([element isKindOfClass:GCHistoryCommit.class]) { + [result addObject:element]; + } + } + return [result copy]; +} + +#pragma mark - CommitListControllerDelegate +- (void)commitListViewControllerDidChangeSelection:(GICommitListViewController *)controller { + // we should reload data in quickview. + self.quickViewController.commit = controller.selectedCommit; + [self.quickViewController.delegate quickViewDidSelectCommit:self.quickViewController.commit commitsList:nil]; + // TODO: add quick view model. + // also we should update QuickViewModel to be in touch with toolbar... +} + +#pragma mark - GIViewController +- (void)viewDidFinishLiveResize { + [self.commitListViewController viewDidFinishLiveResize]; + [self.quickViewController viewDidFinishLiveResize]; +} + +#pragma mark - Contextual Menu Handling +- (void)willShowContextualMenu:(NSMenu *)menu delta:(GCDiffDelta *)delta conflict:(GCIndexConflict *)conflict { + if (GC_FILE_MODE_IS_FILE(delta.newFile.mode)) { + if (self.isHistoryShown) { + __weak typeof(self) weakSelf = self; + [menu addItemWithTitle:NSLocalizedString(@"Hide file history...", nil) block:^{ + [weakSelf.delegate quickViewWantsToShowSelectedCommitsList:nil selectedCommit:weakSelf.commit]; + }]; + } + else { + __weak typeof(self) weakSelf = self; + [menu addItemWithTitle:NSLocalizedString(@"Show file history...", nil) block:^{ + // git log + // show selected files history. + [weakSelf getSelectedCommitsForFilesMatchingPaths:@[delta.canonicalPath] result:^(NSArray *commits) { + NSMutableArray *result = [NSMutableArray new]; + for (GCCommit *commit in commits) { + GCHistoryCommit *historyCommit = [weakSelf.repository.history historyCommitForCommit:commit]; + [result addObject:historyCommit]; + } + [weakSelf.delegate quickViewWantsToShowSelectedCommitsList:[result copy] selectedCommit:result.firstObject]; + }]; + }]; + } + } +} + +@end