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

Basic file merging #672

Open
wants to merge 2 commits 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ObjectiveGit/GTIndex.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
@class GTIndexEntry;
@class GTRepository;
@class GTTree;
@class GTMergeResult;

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -234,6 +235,23 @@ NS_ASSUME_NONNULL_BEGIN
- (GTIndexEntry * _Nullable)entryWithName:(NSString *)name error:(NSError **)error __deprecated_msg("use entryWithPath:error: instead.");


@end

@interface GTIndex (FileMerging)

/// Gets the result of a merge with the given file entries
///
/// The parameters taked are the ones received from `enumerateConflictedFiles`.
///
/// ancestor - The ancestor entry
/// ours - The index entry of our side
/// theirs - The index entry of their side
/// options - The merge options to use. Can be nil.
/// error - The error if one occurred. Can be NULL.
///
/// Returns The results of the merge or nil on error
- (GTMergeResult * _Nullable)resultOfMergingAncestorEntry:(GTIndexEntry *)ancestor ourEntry:(GTIndexEntry *)ours theirEntry:(GTIndexEntry *)theirs options:(NSDictionary * _Nullable)options error:(NSError * _Nullable __autoreleasing *)error;

@end

NS_ASSUME_NONNULL_END
27 changes: 27 additions & 0 deletions ObjectiveGit/GTIndex.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
#import "GTBlob.h"
#import "NSArray+StringArray.h"
#import "NSError+Git.h"
#import "GTMerge+Private.h"

#import "git2/errors.h"
#import "git2/merge.h"

// The block synonymous with libgit2's `git_index_matched_path_cb` callback.
typedef BOOL (^GTIndexPathspecMatchedBlock)(NSString *matchedPathspec, NSString *path, BOOL *stop);
Expand Down Expand Up @@ -406,4 +408,29 @@ - (GTIndexEntry *)entryWithName:(NSString *)name {
- (GTIndexEntry *)entryWithName:(NSString *)name error:(NSError **)error {
return [self entryWithPath:name error:error];
}

@end

@implementation GTIndex (FileMerging)

- (GTMergeResult *)resultOfMergingAncestorEntry:(GTIndexEntry *)ancestorEntry ourEntry:(GTIndexEntry *)ourEntry theirEntry:(GTIndexEntry *)theirEntry options:(NSDictionary *)options error:(NSError * _Nullable __autoreleasing *)error {
NSParameterAssert(ourEntry);
NSParameterAssert(theirEntry);
NSParameterAssert(ancestorEntry);

git_merge_file_result gitResult;
git_merge_file_options opts;

BOOL success = [GTMergeFile handleMergeFileOptions:&opts optionsDict:options error:error];
if (!success) return nil;

int gitError = git_merge_file_from_index(&gitResult, self.repository.git_repository, ancestorEntry.git_index_entry, ourEntry.git_index_entry, theirEntry.git_index_entry, &opts);
if (gitError != 0) {
if (error) *error = [NSError git_errorFor:gitError description:@"Merging entries failed"];
return nil;
}

return [[GTMergeResult alloc] initWithGitMergeFileResult:&gitResult];
}

@end
15 changes: 15 additions & 0 deletions ObjectiveGit/GTMerge+Private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// GTMerge+Private.h
// ObjectiveGitFramework
//
// Created by Etienne on 27/10/2018.
// Copyright © 2018 GitHub, Inc. All rights reserved.
//

#import "GTMerge.h"

@interface GTMergeFile (Private)

+ (BOOL)handleMergeFileOptions:(git_merge_file_options *)opts optionsDict:(NSDictionary *)dict error:(NSError **)error;

@end
72 changes: 72 additions & 0 deletions ObjectiveGit/GTMerge.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// GTMerge.h
// ObjectiveGitFramework
//
// Created by Etienne on 26/10/2018.
// Copyright © 2018 GitHub, Inc. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "git2/merge.h"

NS_ASSUME_NONNULL_BEGIN

/// Represents the result of a merge
@interface GTMergeResult : NSObject

/// Was the merge automerable ?
@property (readonly,getter=isAutomergeable) BOOL automergeable;

/// The path of the resulting merged file, nil in case of conflicts
@property (readonly) NSString * _Nullable path;

/// The resulting mode of the merged file
@property (readonly) unsigned int mode;

/// The contents of the resulting merged file
@property (readonly) NSData *data;

/// Initialize the merge result from a libgit2 struct.
/// Ownership of the memory will be transferred to the receiver.
- (instancetype)initWithGitMergeFileResult:(git_merge_file_result *)result;

- (instancetype)init NS_UNAVAILABLE;

@end

/// Represents inputs for a tentative merge
@interface GTMergeFile : NSObject

/// The file data
@property (readonly) NSData *data;

/// The file path. Can be nil to not merge paths.
@property (readonly) NSString * _Nullable path;

/// The file mode. Can be 0 to not merge modes.
@property (readonly) unsigned int mode;

/// Perform a merge between files
///
/// ancestorFile - The file to consider the ancestor
/// ourFile - The file to consider as our version
/// theirFile - The file to consider as the incoming version
/// options - The options of the merge. Can be nil.
/// error - A pointer to an error object. Can be NULL.
///
/// Returns the result of the merge, or nil if an error occurred.
+ (GTMergeResult * _Nullable)performMergeWithAncestor:(GTMergeFile *)ancestorFile ourFile:(GTMergeFile *)ourFile theirFile:(GTMergeFile *)theirFile options:(NSDictionary * _Nullable)options error:(NSError **)error;

+ (instancetype)fileWithString:(NSString *)string path:(NSString * _Nullable)path mode:(unsigned int)mode;

/// Initialize an input file for a merge
- (instancetype)initWithData:(NSData *)data path:(NSString * _Nullable)path mode:(unsigned int)mode NS_DESIGNATED_INITIALIZER;

- (instancetype)init NS_UNAVAILABLE;

/// Inner pointer to a libgit2-compatible git_merge_file_input struct.
- (git_merge_file_input *)git_merge_file_input __attribute__((objc_returns_inner_pointer));

@end

NS_ASSUME_NONNULL_END
150 changes: 150 additions & 0 deletions ObjectiveGit/GTMerge.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// GTMergeFile.m
// ObjectiveGitFramework
//
// Created by Etienne on 26/10/2018.
// Copyright © 2018 GitHub, Inc. All rights reserved.
//

#import "GTMerge.h"
#import "GTOID.h"
#import "GTObjectDatabase.h"
#import "GTOdbObject.h"
#import "GTRepository.h"
#import "GTIndex.h"
#import "GTIndexEntry.h"
#import "NSError+Git.h"

@interface GTMergeResult ()

@property (assign) git_merge_file_result result;

@end

@implementation GTMergeResult

- (instancetype)initWithGitMergeFileResult:(git_merge_file_result *)result {
self = [super init];
if (!self) return nil;

memcpy(&_result, result, sizeof(_result));

return self;
}

- (void)dealloc {
git_merge_file_result_free(&_result);
}

- (BOOL)isAutomergeable {
return !!_result.automergeable;
}

- (NSString *)path {
return (_result.path ? [NSString stringWithUTF8String:_result.path] : nil);
}

- (unsigned int)mode {
return _result.mode;
}

- (NSData *)data {
return [[NSData alloc] initWithBytesNoCopy:(void *)_result.ptr length:_result.len freeWhenDone:NO];
}

@end

@interface GTMergeFile ()

@property (copy) NSData *data;
@property (copy) NSString *path;
@property (assign) unsigned int mode;
@property (assign) git_merge_file_input file;

@end

@implementation GTMergeFile

+ (instancetype)fileWithString:(NSString *)string path:(NSString * _Nullable)path mode:(unsigned int)mode {
NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];

NSAssert(stringData != nil, @"String couldn't be converted to UTF-8");

return [[self alloc] initWithData:stringData path:path mode:mode];
}

+ (instancetype)fileWithIndexEntry:(GTIndexEntry *)entry error:(NSError **)error {
NSParameterAssert(entry);

const git_index_entry *git_entry = entry.git_index_entry;
GTOID *ancestorOID = [[GTOID alloc] initWithGitOid:&git_entry->id];
GTRepository *repository = entry.index.repository;
GTObjectDatabase *database = [repository objectDatabaseWithError:error];
NSData *contents = [[database objectWithOID:ancestorOID error:error] data];
if (contents == nil) {
return nil;
}

return [[self alloc] initWithData:contents path:entry.path mode:git_entry->mode];
}

- (instancetype)initWithData:(NSData *)data path:(NSString *)path mode:(unsigned int)mode {
NSParameterAssert(data);
self = [super init];
if (!self) return nil;

_data = data;
_path = path;
_mode = mode;

git_merge_file_init_input(&_file, GIT_MERGE_FILE_INPUT_VERSION);

_file.ptr = self.data.bytes;
_file.size = self.data.length;
_file.path = [self.path UTF8String];
_file.mode = self.mode;

return self;
}

- (git_merge_file_input *)git_merge_file_input {
return &_file;
}

+ (BOOL)handleMergeFileOptions:(git_merge_file_options *)opts optionsDict:(NSDictionary *)dict error:(NSError **)error {
NSParameterAssert(opts);

int gitError = git_merge_file_init_options(opts, GIT_MERGE_FILE_OPTIONS_VERSION);
if (gitError != 0) {
if (error) *error = [NSError git_errorFor:gitError description:@"Invalid option initialization"];
return NO;
}

if (dict.count != 0) {
if (error) *error = [NSError git_errorFor:-1 description:@"No options handled"];
return NO;
}
return YES;
}

+ (GTMergeResult *)performMergeWithAncestor:(GTMergeFile *)ancestorFile ourFile:(GTMergeFile *)ourFile theirFile:(GTMergeFile *)theirFile options:(NSDictionary *)options error:(NSError **)error {
NSParameterAssert(ourFile);
NSParameterAssert(theirFile);
NSParameterAssert(ancestorFile);

git_merge_file_result gitResult;
git_merge_file_options opts;

BOOL success = [GTMergeFile handleMergeFileOptions:&opts optionsDict:options error:error];
if (!success) return nil;

int gitError = git_merge_file(&gitResult, ancestorFile.git_merge_file_input, ourFile.git_merge_file_input, theirFile.git_merge_file_input, &opts);
if (gitError != 0) {
if (error) *error = [NSError git_errorFor:gitError description:@"Merge file failed"];
return nil;
}

return [[GTMergeResult alloc] initWithGitMergeFileResult:&gitResult];
}

@end
Loading