Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle submodule conflicts #996

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
166 changes: 126 additions & 40 deletions GitUpKit/Components/Base.lproj/GIDiffContentsViewController.xib

Large diffs are not rendered by default.

69 changes: 63 additions & 6 deletions GitUpKit/Components/GIDiffContentsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ @interface GIConflictDiffCellView : NSTableCellView
@property(nonatomic, weak) IBOutlet NSButton* resolveButton;
@end

@interface GISubmoduleConflictDiffCellView : NSTableCellView
@property(nonatomic, weak) IBOutlet NSTextField* statusTextField;
@property(nonatomic, weak) IBOutlet NSTextField* oursTextField;
@property(nonatomic, weak) IBOutlet NSTextField* theirsTextField;
@property(nonatomic, weak) IBOutlet NSButton* chooseOursButton;
@property(nonatomic, weak) IBOutlet NSButton* chooseTheirsButton;
@end

@interface GISubmoduleDiffCellView : NSTableCellView
@property(nonatomic, weak) IBOutlet NSView* contentView;
@property(nonatomic, weak) IBOutlet NSTextField* oldSHA1TextField;
Expand Down Expand Up @@ -158,6 +166,9 @@ @implementation GIBinaryDiffCellView
@implementation GIConflictDiffCellView
@end

@implementation GISubmoduleConflictDiffCellView
@end

@implementation GISubmoduleDiffCellView
@end

Expand All @@ -183,6 +194,7 @@ @implementation GIDiffContentsViewController {
CGFloat _headerViewHeight;
CGFloat _emptyViewHeight;
CGFloat _conflictViewHeight;
CGFloat _submoduleConflictViewHeight;
CGFloat _submoduleViewHeight;
CGFloat _binaryViewHeight;
}
Expand Down Expand Up @@ -227,6 +239,7 @@ - (void)loadView {
_headerViewHeight = [[_tableView makeViewWithIdentifier:@"header" owner:self] frame].size.height;
_emptyViewHeight = [[_tableView makeViewWithIdentifier:@"empty" owner:self] frame].size.height;
_conflictViewHeight = [[_tableView makeViewWithIdentifier:@"conflict" owner:self] frame].size.height;
_submoduleConflictViewHeight = [[_tableView makeViewWithIdentifier:@"submodule_conflict" owner:self] frame].size.height;
_submoduleViewHeight = [[_tableView makeViewWithIdentifier:@"submodule" owner:self] frame].size.height;
_binaryViewHeight = [[_tableView makeViewWithIdentifier:@"binary" owner:self] frame].size.height;

Expand Down Expand Up @@ -537,12 +550,23 @@ - (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:(NSTableColumn*)
status = NSLocalizedString(@"deleted by them", nil);
break;
}
GIConflictDiffCellView* view = [_tableView makeViewWithIdentifier:@"conflict" owner:self];
view.statusTextField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"This file has conflicts (%@)", nil), status];
view.openButton.tag = (uintptr_t)data;
view.mergeButton.tag = (uintptr_t)data;
view.resolveButton.tag = (uintptr_t)data;
return view;
if (data.conflict.ancestorFileMode == kGCFileMode_Commit) {
// Submodule conflict
GISubmoduleConflictDiffCellView* view = [_tableView makeViewWithIdentifier:@"submodule_conflict" owner:self];
view.statusTextField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"This submodule has conflicts (%@)", nil), status];
view.oursTextField.stringValue = data.conflict.ourBlobSHA1;
view.theirsTextField.stringValue = data.conflict.theirBlobSHA1;
view.chooseOursButton.tag = (uintptr_t)data;
view.chooseTheirsButton.tag = (uintptr_t)data;
return view;
} else {
GIConflictDiffCellView* view = [_tableView makeViewWithIdentifier:@"conflict" owner:self];
view.statusTextField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"This file has conflicts (%@)", nil), status];
view.openButton.tag = (uintptr_t)data;
view.mergeButton.tag = (uintptr_t)data;
view.resolveButton.tag = (uintptr_t)data;
return view;
}
} else if (GC_FILE_MODE_IS_SUBMODULE(delta.oldFile.mode) || GC_FILE_MODE_IS_SUBMODULE(delta.newFile.mode)) {
GISubmoduleDiffCellView* view = [_tableView makeViewWithIdentifier:@"submodule" owner:self];
NSString* oldSHA1 = delta.oldFile ? delta.oldFile.SHA1 : nil;
Expand Down Expand Up @@ -660,6 +684,8 @@ - (CGFloat)tableView:(NSTableView*)tableView heightOfRow:(NSInteger)row {
return [data.imageDiffView desiredHeightForWidth:[_tableView.tableColumns[0] width]];
} else if (data.empty) {
return _emptyViewHeight;
} else if (data.conflict && data.conflict.ancestorFileMode == kGCFileMode_Commit) {
return _submoduleConflictViewHeight;
} else if (data.conflict) {
return _conflictViewHeight;
} else if (GC_FILE_MODE_IS_SUBMODULE(delta.oldFile.mode) || GC_FILE_MODE_IS_SUBMODULE(delta.newFile.mode)) {
Expand Down Expand Up @@ -730,4 +756,35 @@ - (IBAction)markAsResolved:(id)sender {
[self markConflictAsResolved:data.conflict];
}

- (IBAction)chooseOurs:(id)sender {
GIDiffContentData* data = (__bridge GIDiffContentData*)(void*)[(NSButton*)sender tag];
NSError *error;

[self.repository updateSubmoduleReferenceAtPath:data.conflict.path toCommitSHA1:data.conflict.ourBlobSHA1 error:&error];

if (!error) {
[self markConflictAsResolved:data.conflict];
} else {
[self presentError:error];
}

[self.repository notifyWorkingDirectoryChanged];
}

- (IBAction)chooseTheirs:(id)sender {
GIDiffContentData* data = (__bridge GIDiffContentData*)(void*)[(NSButton*)sender tag];
NSError *error;


[self.repository updateSubmoduleReferenceAtPath:data.conflict.path toCommitSHA1:data.conflict.theirBlobSHA1 error:&error];

if (!error) {
[self markConflictAsResolved:data.conflict];
} else {
[self presentError:error];
}

[self.repository notifyWorkingDirectoryChanged];
}

@end
23 changes: 22 additions & 1 deletion GitUpKit/Core/GCDiff.m
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,14 @@ - (BOOL)isSubmodule {
case kGCFileDiffChange_Ignored:
case kGCFileDiffChange_Untracked:
case kGCFileDiffChange_Unreadable:
case kGCFileDiffChange_Conflicted:
return GC_FILE_MODE_IS_SUBMODULE(_oldFile.mode);

case kGCFileDiffChange_Added:
case kGCFileDiffChange_Modified:
case kGCFileDiffChange_Renamed:
case kGCFileDiffChange_Copied:
case kGCFileDiffChange_TypeChanged:
case kGCFileDiffChange_Conflicted:
return GC_FILE_MODE_IS_SUBMODULE(_newFile.mode);
}
XLOG_DEBUG_UNREACHABLE();
Expand Down Expand Up @@ -354,6 +354,27 @@ - (void)_cacheDeltasIfNeeded {
XLOG_DEBUG_UNREACHABLE();
}
}

// Remove superfluous "untracked" deltas for conflicting submodules
// Needed when the input _deltas looks like this:
// 1: [Conflicted] "submodule"
// 2: [Untracked] "submodule/"
// Which happens every time there's a submodule entry that's a conflict
NSMutableArray<GCDiffDelta *> *deltasToFilterOut = [NSMutableArray array];
for (GCDiffDelta* delta in _deltas) {
if (delta.change == kGCFileDiffChange_Conflicted && delta.isSubmodule) {
// see if there's a superfluous untracked diff for that submodule and remove it
NSString *pathWithTrailingSlash = [NSString stringWithFormat:@"%@/", delta.canonicalPath];
for (GCDiffDelta* delta in _deltas) {
if (delta.isSubmodule && [delta.canonicalPath isEqualToString:pathWithTrailingSlash]) {
[deltasToFilterOut addObject:delta];
break; // there's only one so we can break out early if we've found it
}
}
}
}

[_deltas removeObjectsInArray:deltasToFilterOut];
}
}

Expand Down
43 changes: 39 additions & 4 deletions GitUpKit/Core/GCIndex.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ - (id)initWithAncestor:(const git_index_entry*)ancestor our:(const git_index_ent
_status = ancestor ? kGCIndexConflictStatus_BothModified : kGCIndexConflictStatus_BothAdded;

git_oid_cpy(&_ourOID, &our->id);
XLOG_DEBUG_CHECK((our->mode == GIT_FILEMODE_BLOB) || (our->mode == GIT_FILEMODE_BLOB_EXECUTABLE) || (our->mode == GIT_FILEMODE_LINK));
XLOG_DEBUG_CHECK((our->mode == GIT_FILEMODE_BLOB) || (our->mode == GIT_FILEMODE_BLOB_EXECUTABLE) || (our->mode == GIT_FILEMODE_LINK) || (our->mode == GIT_FILEMODE_COMMIT));
_ourFileMode = GCFileModeFromMode(our->mode);

git_oid_cpy(&_theirOID, &their->id);
XLOG_DEBUG_CHECK((their->mode == GIT_FILEMODE_BLOB) || (their->mode == GIT_FILEMODE_BLOB_EXECUTABLE) || (their->mode == GIT_FILEMODE_LINK));
XLOG_DEBUG_CHECK((their->mode == GIT_FILEMODE_BLOB) || (their->mode == GIT_FILEMODE_BLOB_EXECUTABLE) || (their->mode == GIT_FILEMODE_LINK) || (their->mode == GIT_FILEMODE_COMMIT));
_theirFileMode = GCFileModeFromMode(their->mode);
} else if (our) {
XLOG_DEBUG_CHECK(!strcmp(our->path, ancestor->path));
Expand All @@ -64,7 +64,7 @@ - (id)initWithAncestor:(const git_index_entry*)ancestor our:(const git_index_ent
}
if (ancestor) {
git_oid_cpy(&_ancestorOID, &ancestor->id);
XLOG_DEBUG_CHECK((ancestor->mode == GIT_FILEMODE_BLOB) || (ancestor->mode == GIT_FILEMODE_BLOB_EXECUTABLE) || (ancestor->mode == GIT_FILEMODE_LINK));
XLOG_DEBUG_CHECK((ancestor->mode == GIT_FILEMODE_BLOB) || (ancestor->mode == GIT_FILEMODE_BLOB_EXECUTABLE) || (ancestor->mode == GIT_FILEMODE_LINK) || (ancestor->mode == GIT_FILEMODE_COMMIT));
_ancestorFileMode = GCFileModeFromMode(ancestor->mode);
}
if (our) {
Expand Down Expand Up @@ -301,6 +301,16 @@ - (BOOL)_addEntry:(const git_index_entry*)entry toIndex:(git_index*)index error:
return YES;
}

// This function adapts to handle submodules by directly using the commit OID and setting the correct file mode for submodules.
- (BOOL)_addSubmoduleEntry:(const git_index_entry*)entry toIndex:(git_index*)index withCommitOid:(const git_oid *)commitOid error:(NSError**)error {
git_index_entry copyEntry;
bcopy(entry, &copyEntry, sizeof(git_index_entry));
git_oid_cpy(&copyEntry.id, commitOid);
copyEntry.mode = GIT_FILEMODE_COMMIT;
CALL_LIBGIT2_FUNCTION_RETURN(NO, git_index_add, index, &copyEntry);
return YES;
}

- (BOOL)addFile:(NSString*)path withContents:(NSData*)contents toIndex:(GCIndex*)index error:(NSError**)error {
git_index_entry entry;
bzero(&entry, sizeof(git_index_entry));
Expand All @@ -316,7 +326,32 @@ - (BOOL)addFileInWorkingDirectory:(NSString*)path toIndex:(GCIndex*)index error:
bzero(&entry, sizeof(git_index_entry));
entry.path = GCGitPathFromFileSystemPath(path);
git_index_entry__init_from_stat(&entry, &info, true);
return [self _addEntry:&entry toIndex:index.private error:error];

if (entry.mode == GIT_FILEMODE_COMMIT) {
GCSubmodule *submodule = [self lookupSubmoduleWithName:path error:error];
if (!submodule) {
return NO;
}

GCRepository *submoduleRepository = [[GCRepository alloc] initWithSubmodule:submodule error:error];
if (!submoduleRepository) {
return NO;
}

GCCommit *headCommit;
if (![submoduleRepository lookupHEADCurrentCommit:&headCommit branch:NULL error:error]) {
return NO;
}

git_oid oid;
if (!GCGitOIDFromSHA1(headCommit.SHA1, &oid, error)) {
return NO;
}

return [self _addSubmoduleEntry:&entry toIndex:index.private withCommitOid:&oid error:error];
} else {
return [self _addEntry:&entry toIndex:index.private error:error];
}
}

- (BOOL)addLinesInWorkingDirectoryFile:(NSString*)path toIndex:(GCIndex*)index error:(NSError**)error usingFilter:(GCIndexLineFilter)filter {
Expand Down
1 change: 1 addition & 0 deletions GitUpKit/Core/GCRepository+HEAD.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ typedef NS_OPTIONS(NSUInteger, GCCheckoutOptions) {
- (BOOL)setDetachedHEADToCommit:(GCCommit*)commit error:(NSError**)error; // git update-ref HEAD {commit}

- (BOOL)moveHEADToCommit:(GCCommit*)commit reflogMessage:(NSString*)message error:(NSError**)error; // git reset --soft {commit} (but with custom reflog message)
- (BOOL)updateSubmoduleReferenceAtPath:(NSString *)submodulePath toCommitSHA1:(NSString *)commitSHA1 error:(NSError **)error;

- (BOOL)checkoutCommit:(GCCommit*)commit options:(GCCheckoutOptions)options error:(NSError**)error; // git checkout {commit}
- (BOOL)checkoutLocalBranch:(GCLocalBranch*)branch options:(GCCheckoutOptions)options error:(NSError**)error; // git checkout {branch}
Expand Down
19 changes: 19 additions & 0 deletions GitUpKit/Core/GCRepository+HEAD.m
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,25 @@ - (BOOL)checkoutCommit:(GCCommit*)commit options:(GCCheckoutOptions)options erro
return YES;
}

- (BOOL)updateSubmoduleReferenceAtPath:(NSString*)submodulePath toCommitSHA1:(NSString*)commitSHA1 error:(NSError**)error {
GCSubmodule *submodule = [self lookupSubmoduleWithName:submodulePath error:error];
if (!submodule) {
return NO;
}

GCRepository *submoduleRepository = [[GCRepository alloc] initWithSubmodule:submodule error:error];
if (!submoduleRepository) {
return NO;
}

GCCommit *targetCommit = [submoduleRepository findCommitWithSHA1:commitSHA1 error:error];
if (!targetCommit) {
return NO;
}

return [submoduleRepository checkoutCommit:targetCommit options:kGCCheckoutOption_UpdateSubmodulesRecursively error:error];
}

// Because by default git_checkout_tree() assumes the baseline (i.e. expected content of workdir) is HEAD we must checkout first, then update HEAD
- (BOOL)checkoutLocalBranch:(GCLocalBranch*)branch options:(GCCheckoutOptions)options error:(NSError**)error {
GCCommit* tipCommit = [self lookupTipCommitForBranch:branch error:error];
Expand Down
16 changes: 16 additions & 0 deletions GitUpKit/Core/GCSubmodule.m
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,23 @@ - (BOOL)updateAllSubmodulesResursively:(BOOL)force error:(NSError**)error {
if (submodules == nil) {
return NO;
}

NSArray<NSString *> *conflictPaths = @[];

git_index* index = [self reloadRepositoryIndex:error];

if (index && git_index_has_conflicts(index)) {
conflictPaths = [self checkConflicts:nil].allKeys;
}

git_index_free(index);

for (GCSubmodule* submodule in submodules) {
if ([conflictPaths containsObject:submodule.path]) {
// conflict needs to be resolved first, will be handled elsewhere but we shouldn't return an error
continue;
}

NSError* localError;
if (![self updateSubmodule:submodule force:force error:&localError]) {
if ([localError.domain isEqualToString:GCErrorDomain] && (localError.code == kGCErrorCode_NotFound)) {
Expand Down