// Copyright (c) 2013 GitHub Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#include "repository.h"
#include <string.h>
#include <map>
#include <utility>

void Repository::Init(Local<Object> target) {
  Nan::HandleScope scope;
  git_libgit2_init();

  Local<FunctionTemplate> newTemplate = Nan::New<FunctionTemplate>(
      Repository::New);
  newTemplate->SetClassName(Nan::New<String>("Repository").ToLocalChecked());
  newTemplate->InstanceTemplate()->SetInternalFieldCount(1);

  Local<ObjectTemplate> proto = newTemplate->PrototypeTemplate();
  Nan::SetMethod(proto, "getPath", Repository::GetPath);
  Nan::SetMethod(proto, "_getWorkingDirectory",
                  Repository::GetWorkingDirectory);
  Nan::SetMethod(proto, "exists", Repository::Exists);
  Nan::SetMethod(proto, "getSubmodulePaths", Repository::GetSubmodulePaths);
  Nan::SetMethod(proto, "getHead", Repository::GetHead);
  Nan::SetMethod(proto, "getHeadAsync", Repository::GetHeadAsync);
  Nan::SetMethod(proto, "refreshIndex", Repository::RefreshIndex);
  Nan::SetMethod(proto, "isIgnored", Repository::IsIgnored);
  Nan::SetMethod(proto, "isSubmodule", Repository::IsSubmodule);
  Nan::SetMethod(proto, "getConfigValue", Repository::GetConfigValue);
  Nan::SetMethod(proto, "setConfigValue", Repository::SetConfigValue);
  Nan::SetMethod(proto, "getStatus", Repository::GetStatus);
  Nan::SetMethod(proto, "getStatusForPath", Repository::GetStatusForPath);
  Nan::SetMethod(proto, "getStatusAsync", Repository::GetStatusAsync);
  Nan::SetMethod(proto, "checkoutHead", Repository::CheckoutHead);
  Nan::SetMethod(proto, "getReferenceTarget", Repository::GetReferenceTarget);
  Nan::SetMethod(proto, "getDiffStats", Repository::GetDiffStats);
  Nan::SetMethod(proto, "getIndexBlob", Repository::GetIndexBlob);
  Nan::SetMethod(proto, "getHeadBlob", Repository::GetHeadBlob);
  Nan::SetMethod(proto, "compareCommits", Repository::CompareCommits);
  Nan::SetMethod(proto, "compareCommitsAsync", Repository::CompareCommitsAsync);
  Nan::SetMethod(proto, "_release", Repository::Release);
  Nan::SetMethod(proto, "getLineDiffs", Repository::GetLineDiffs);
  Nan::SetMethod(proto, "getLineDiffDetails", Repository::GetLineDiffDetails);
  Nan::SetMethod(proto, "getReferences", Repository::GetReferences);
  Nan::SetMethod(proto, "checkoutRef", Repository::CheckoutReference);
  Nan::SetMethod(proto, "add", Repository::Add);

  Nan::Set(target,
            Nan::New<String>("Repository").ToLocalChecked(),
            Nan::GetFunction(newTemplate).ToLocalChecked());
}

NODE_MODULE(git, Repository::Init)

NAN_METHOD(Repository::New) {
  Nan::HandleScope scope;
  Repository* repository = new Repository(
    Local<String>::Cast(info[0]), Local<Boolean>::Cast(info[1]));
  repository->Wrap(info.This());
  info.GetReturnValue().SetUndefined();
}

git_repository* Repository::GetRepository(Nan::NAN_METHOD_ARGS_TYPE args) {
  return Nan::ObjectWrap::Unwrap<Repository>(args.This())->repository;
}

git_repository* Repository::GetAsyncRepository(Nan::NAN_METHOD_ARGS_TYPE args) {
  return Nan::ObjectWrap::Unwrap<Repository>(args.This())->async_repository;
}

int Repository::GetBlob(Nan::NAN_METHOD_ARGS_TYPE args,
                        git_repository* repo, git_blob*& blob) {
  std::string path(*Nan::Utf8String(args[0]));

  int useIndex = false;
  if (args.Length() >= 3) {
    Local<Object> optionsArg(Local<Object>::Cast(args[2]));
    if (Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("useIndex").ToLocalChecked()).ToLocalChecked()).FromJust())
      useIndex = true;
  }

  if (useIndex) {
    git_index* index;
    if (git_repository_index(&index, repo) != GIT_OK)
      return -1;

    git_index_read(index, 0);
    const git_index_entry* entry = git_index_get_bypath(index, path.data(), 0);
    if (entry == NULL) {
      git_index_free(index);
      return -1;
    }

    const git_oid* blobSha = &entry->id;
    if (blobSha != NULL && git_blob_lookup(&blob, repo, blobSha) != GIT_OK)
      blob = NULL;
  } else {
    git_reference* head;
    if (git_repository_head(&head, repo) != GIT_OK)
      return -1;

    const git_oid* sha = git_reference_target(head);
    git_commit* commit;
    int commitStatus = git_commit_lookup(&commit, repo, sha);
    git_reference_free(head);
    if (commitStatus != GIT_OK)
      return -1;

    git_tree* tree;
    int treeStatus = git_commit_tree(&tree, commit);
    git_commit_free(commit);
    if (treeStatus != GIT_OK)
      return -1;

    git_tree_entry* treeEntry;
    if (git_tree_entry_bypath(&treeEntry, tree, path.c_str()) != GIT_OK) {
      git_tree_free(tree);
      return -1;
    }

    const git_oid* blobSha = git_tree_entry_id(treeEntry);
    if (blobSha != NULL && git_blob_lookup(&blob, repo, blobSha) != GIT_OK)
      blob = NULL;
    git_tree_entry_free(treeEntry);
    git_tree_free(tree);
  }

  if (blob == NULL)
    return -1;

  return 0;
}

// C++ equivalent to GIT_DIFF_OPTIONS_INIT, we can not use it directly because
// of C++'s strong typing.
git_diff_options Repository::CreateDefaultGitDiffOptions() {
  git_diff_options options = { 0 };
  options.version = GIT_DIFF_OPTIONS_VERSION;
  options.context_lines = 3;
  return options;
}

NAN_METHOD(Repository::Exists) {
  Nan::HandleScope scope;

  info.GetReturnValue().Set(Nan::New<Boolean>(GetRepository(info) != NULL));
}

NAN_METHOD(Repository::GetPath) {
  Nan::HandleScope scope;
  git_repository* repository = GetRepository(info);
  const char* path = git_repository_path(repository);

  info.GetReturnValue().Set(Nan::New<String>(path).ToLocalChecked());
}

NAN_METHOD(Repository::GetWorkingDirectory) {
  Nan::HandleScope scope;
  git_repository* repository = GetRepository(info);
  const char* path = git_repository_workdir(repository);
  info.GetReturnValue().Set(Nan::New<String>(path).ToLocalChecked());
}

NAN_METHOD(Repository::GetSubmodulePaths) {
  Nan::HandleScope scope;
  git_repository* repository = GetRepository(info);
  std::vector<std::string> paths;
  git_submodule_foreach(repository, SubmoduleCallback, &paths);
  Local<Object> v8Paths = Nan::New<Array>(paths.size());
  for (size_t i = 0; i < paths.size(); i++)
    Nan::Set(v8Paths, i, Nan::New<String>(paths[i].data()).ToLocalChecked());
  info.GetReturnValue().Set(v8Paths);
}

class HeadWorker {
  git_repository *repository;
  std::string result;

 public:
  void Execute() {
    git_reference *head;
    if (git_repository_head(&head, repository) != GIT_OK) return;

    if (git_repository_head_detached(repository) == 1) {
      const git_oid *oid = git_reference_target(head);
      if (oid) {
        result.resize(GIT_OID_HEXSZ);
        git_oid_tostr(&result[0], GIT_OID_HEXSZ + 1, oid);
      }
    } else {
      result = git_reference_name(head);
    }

    git_reference_free(head);
  }

  std::pair<Local<Value>, Local<Value>> Finish() {
    if (result.empty()) {
      return {Nan::Null(), Nan::Null()};
    } else {
      return {Nan::Null(), Nan::New(result).ToLocalChecked()};
    }
  }

  HeadWorker(git_repository *repository) : repository(repository) {}
};

NAN_METHOD(Repository::GetHead) {
  HeadWorker worker(GetRepository(info));
  worker.Execute();
  info.GetReturnValue().Set(worker.Finish().second);
}

NAN_METHOD(Repository::GetHeadAsync) {
  class HeadAsyncWorker : public Nan::AsyncWorker {
    HeadWorker worker;

   public:
    void Execute () { worker.Execute(); }

    void HandleOKCallback() {
      auto result = worker.Finish();
      Local<Value> argv[] = {result.first, result.second};
      callback->Call(2, argv);
    }

    HeadAsyncWorker(Nan::Callback *callback, git_repository *repository) : Nan::AsyncWorker(callback), worker(repository) {}
  };

  auto callback = new Nan::Callback(Local<Function>::Cast(info[0]));
  Nan::AsyncQueueWorker(new HeadAsyncWorker(callback, GetAsyncRepository(info)));
}

NAN_METHOD(Repository::RefreshIndex) {
  Nan::HandleScope scope;
  git_repository* repository = GetRepository(info);
  git_index* index;
  if (git_repository_index(&index, repository) == GIT_OK) {
    git_index_read(index, 0);
    git_index_free(index);
  }
  info.GetReturnValue().SetUndefined();
}

NAN_METHOD(Repository::IsIgnored) {
  Nan::HandleScope scope;
  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));

  git_repository* repository = GetRepository(info);
  std::string path(*Nan::Utf8String(info[0]));
  int ignored;
  if (git_ignore_path_is_ignored(&ignored,
                                 repository,
                                 path.c_str()) == GIT_OK)
    return info.GetReturnValue().Set(Nan::New<Boolean>(ignored == 1));
  else
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));
}

NAN_METHOD(Repository::IsSubmodule) {
  Nan::HandleScope scope;
  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));

  git_index* index;
  git_repository* repository = GetRepository(info);
  if (git_repository_index(&index, repository) == GIT_OK) {
    std::string path(*Nan::Utf8String(info[0]));
    const git_index_entry* entry = git_index_get_bypath(index, path.c_str(), 0);
    Local<Boolean> isSubmodule = Nan::New<Boolean>(
        entry != NULL && (entry->mode & S_IFMT) == GIT_FILEMODE_COMMIT);
    git_index_free(index);
    return info.GetReturnValue().Set(isSubmodule);
  } else {
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));
  }
}

NAN_METHOD(Repository::GetConfigValue) {
  Nan::HandleScope scope;
  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::Null());

  git_config* config;
  git_repository* repository = GetRepository(info);
  if (git_repository_config_snapshot(&config, repository) != GIT_OK)
    return info.GetReturnValue().Set(Nan::Null());

  std::string configKey(*Nan::Utf8String(info[0]));
  const char* configValue;
  if (git_config_get_string(
        &configValue, config, configKey.c_str()) == GIT_OK) {
    git_config_free(config);
    return info.GetReturnValue().Set(Nan::New<String>(configValue)
                                      .ToLocalChecked());
  } else {
    git_config_free(config);
    return info.GetReturnValue().Set(Nan::Null());
  }
}

NAN_METHOD(Repository::SetConfigValue) {
  Nan::HandleScope scope;
  if (info.Length() != 2)
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));

  git_config* config;
  git_repository* repository = GetRepository(info);
  if (git_repository_config(&config, repository) != GIT_OK)
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));

  std::string configKey(*Nan::Utf8String(info[0]));
  std::string configValue(*Nan::Utf8String(info[1]));

  int errorCode = git_config_set_string(
      config, configKey.c_str(), configValue.c_str());
  git_config_free(config);
  return info.GetReturnValue().Set(Nan::New<Boolean>(errorCode == GIT_OK));
}

static int StatusCallback(const char* path, unsigned int status, void* payload) {
  auto statuses = static_cast<std::map<std::string, unsigned int> *>(payload);
  statuses->insert(std::make_pair(std::string(path), status));
  return GIT_OK;
}

class StatusWorker {
  git_repository *repository;
  std::map<std::string, unsigned int> statuses;
  char **paths;
  unsigned path_count;
  int code;

 public:
  void Execute() {
    git_status_options options = GIT_STATUS_OPTIONS_INIT;
    options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
    if (paths) {
      options.pathspec.count = path_count;
      options.pathspec.strings = paths;
    }
    code = git_status_foreach_ext(repository, &options, StatusCallback, &statuses);
    if (paths) {
      git_strarray_free(&options.pathspec);
    }
  }

  std::pair<Local<Value>, Local<Value>> Finish() {
    if (code == GIT_OK) {
      Local<Object> result = Nan::New<Object>();
      for (auto iter = statuses.begin(), end = statuses.end(); iter != end; ++iter) {
        Nan::Set(
          result,
          Nan::New<String>(iter->first.c_str()).ToLocalChecked(),
          Nan::New<Number>(iter->second)
        );
      }
      return {Nan::Null(), result};
    } else {
      return {Nan::Error("Git status failed"), Nan::Null()};
    }
  }

  StatusWorker(git_repository *repository, Local<Value> path_filter) : repository{repository} {
    if (path_filter->IsArray()) {
      Local<Array> js_paths = Local<Array>::Cast(path_filter);
      path_count = js_paths->Length();
      paths = reinterpret_cast<char **>(malloc(path_count * sizeof(char *)));
      for (unsigned i = 0; i < path_count; i++) {
        Nan::Utf8String js_path(Nan::Get(js_paths, i).ToLocalChecked());
        paths[i] = reinterpret_cast<char *>(malloc(js_path.length() + 1));
        strcpy(paths[i], *js_path);
      }
    } else {
      paths = NULL;
      path_count = 0;
    }
  }
};

NAN_METHOD(Repository::GetStatusAsync) {
  class StatusAsyncWorker : public Nan::AsyncWorker {
    StatusWorker worker;

   public:
    void Execute() {
      worker.Execute();
    }

    void HandleOKCallback() {
      auto result = worker.Finish();
      Local<Value> argv[] = {result.first, result.second};
      callback->Call(2, argv);
    }

    StatusAsyncWorker(Nan::Callback *callback, git_repository *repository, Local<Value> path_filter)
      : Nan::AsyncWorker(callback), worker(repository, path_filter) {}
  };

  auto callback = new Nan::Callback(Local<Function>::Cast(info[0]));
  Local<Value> path_filter = info.Length() > 1 ? info[1] : Local<Value>::Cast(Nan::Null());
  Nan::AsyncQueueWorker(new StatusAsyncWorker(callback, GetAsyncRepository(info), path_filter));
}

NAN_METHOD(Repository::GetStatus) {
  Local<Value> path_filter = info.Length() > 0 ? info[0] : Local<Value>::Cast(Nan::Null());
  StatusWorker worker(GetRepository(info), path_filter);
  worker.Execute();
  auto result = worker.Finish();
  if (result.first->IsNull()) {
    info.GetReturnValue().Set(worker.Finish().second);
  } else {
    info.GetReturnValue().Set(Nan::New<Object>());
  }
}

NAN_METHOD(Repository::GetStatusForPath) {
  git_repository* repository = GetRepository(info);
  Nan::Utf8String path(info[0]);
  unsigned int status = 0;
  if (git_status_file(&status, repository, *path) == GIT_OK)
    return info.GetReturnValue().Set(Nan::New<Number>(status));
  else
    return info.GetReturnValue().Set(Nan::New<Number>(0));
}

NAN_METHOD(Repository::CheckoutHead) {
  Nan::HandleScope scope;
  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));

  Nan::Utf8String utf8Path(info[0]);
  char* path = *utf8Path;

  git_checkout_options options = GIT_CHECKOUT_OPTIONS_INIT;
  options.checkout_strategy = GIT_CHECKOUT_FORCE |
                              GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH;
  git_strarray paths;
  paths.count = 1;
  paths.strings = &path;
  options.paths = paths;

  int result = git_checkout_head(GetRepository(info), &options);
  return info.GetReturnValue().Set(Nan::New<Boolean>(result == GIT_OK));
}

NAN_METHOD(Repository::GetReferenceTarget) {
  Nan::HandleScope scope;
  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::Null());

  std::string refName(*Nan::Utf8String(info[0]));
  git_oid sha;
  if (git_reference_name_to_id(
        &sha, GetRepository(info), refName.c_str()) == GIT_OK) {
    char oid[GIT_OID_HEXSZ + 1];
    git_oid_tostr(oid, GIT_OID_HEXSZ + 1, &sha);
    return info.GetReturnValue().Set(Nan::New<String>(oid, -1)
                                  .ToLocalChecked());
  } else {
    return info.GetReturnValue().Set(Nan::Null());
  }
}

NAN_METHOD(Repository::GetDiffStats) {
  Nan::HandleScope scope;

  int added = 0;
  int deleted = 0;
  Local<Object> result = Nan::New<Object>();
  Nan::Set(result,
            Nan::New<String>("added").ToLocalChecked(),
            Nan::New<Number>(added));
  Nan::Set(result,
            Nan::New<String>("deleted").ToLocalChecked(),
            Nan::New<Number>(deleted));

  if (info.Length() < 1)
    return info.GetReturnValue().Set(result);

  git_repository* repository = GetRepository(info);
  git_reference* head;
  if (git_repository_head(&head, repository) != GIT_OK)
    return info.GetReturnValue().Set(result);

  const git_oid* sha = git_reference_target(head);
  git_commit* commit;
  int commitStatus = git_commit_lookup(&commit, repository, sha);
  git_reference_free(head);
  if (commitStatus != GIT_OK)
    return info.GetReturnValue().Set(result);

  git_tree* tree;
  int treeStatus = git_commit_tree(&tree, commit);
  git_commit_free(commit);
  if (treeStatus != GIT_OK)
    return info.GetReturnValue().Set(result);

  Nan::Utf8String utf8Path(info[0]);
  char* path = *utf8Path;

  git_diff_options options = CreateDefaultGitDiffOptions();
  git_strarray paths;
  paths.count = 1;
  paths.strings = &path;
  options.pathspec = paths;
  options.context_lines = 0;
  options.flags = GIT_DIFF_DISABLE_PATHSPEC_MATCH;

  git_diff* diffs;
  int diffStatus = git_diff_tree_to_workdir(&diffs, repository, tree, &options);
  git_tree_free(tree);
  if (diffStatus != GIT_OK)
    return info.GetReturnValue().Set(result);

  int deltas = git_diff_num_deltas(diffs);
  if (deltas != 1) {
    git_diff_free(diffs);
    return info.GetReturnValue().Set(result);
  }

  git_patch* patch;
  int patchStatus = git_patch_from_diff(&patch, diffs, 0);
  git_diff_free(diffs);
  if (patchStatus != GIT_OK)
    return info.GetReturnValue().Set(result);

  int hunks = git_patch_num_hunks(patch);
  for (int i = 0; i < hunks; i++) {
    int lines = git_patch_num_lines_in_hunk(patch, i);
    for (int j = 0; j < lines; j++) {
      const git_diff_line* line;
      if (git_patch_get_line_in_hunk(&line, patch, i, j) == GIT_OK) {
        switch (line->origin) {
          case GIT_DIFF_LINE_ADDITION:
            added++;
            break;
          case GIT_DIFF_LINE_DELETION:
            deleted++;
            break;
        }
      }
    }
  }
  git_patch_free(patch);

  Nan::Set(result,
            Nan::New<String>("added").ToLocalChecked(),
            Nan::New<Number>(added));
  Nan::Set(result,
            Nan::New<String>("deleted").ToLocalChecked(),
            Nan::New<Number>(deleted));

  return info.GetReturnValue().Set(result);
}

NAN_METHOD(Repository::GetHeadBlob) {
  Nan::HandleScope scope;
  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::Null());

  std::string path(*Nan::Utf8String(info[0]));

  git_repository* repo = GetRepository(info);
  git_reference* head;
  if (git_repository_head(&head, repo) != GIT_OK)
    return info.GetReturnValue().Set(Nan::Null());

  const git_oid* sha = git_reference_target(head);
  git_commit* commit;
  int commitStatus = git_commit_lookup(&commit, repo, sha);
  git_reference_free(head);
  if (commitStatus != GIT_OK)
    return info.GetReturnValue().Set(Nan::Null());

  git_tree* tree;
  int treeStatus = git_commit_tree(&tree, commit);
  git_commit_free(commit);
  if (treeStatus != GIT_OK)
    return info.GetReturnValue().Set(Nan::Null());

  git_tree_entry* treeEntry;
  if (git_tree_entry_bypath(&treeEntry, tree, path.c_str()) != GIT_OK) {
    git_tree_free(tree);
    return info.GetReturnValue().Set(Nan::Null());
  }

  git_blob* blob = NULL;
  const git_oid* blobSha = git_tree_entry_id(treeEntry);
  if (blobSha != NULL && git_blob_lookup(&blob, repo, blobSha) != GIT_OK)
    blob = NULL;
  git_tree_entry_free(treeEntry);
  git_tree_free(tree);
  if (blob == NULL)
    return info.GetReturnValue().Set(Nan::Null());

  const char* content = static_cast<const char*>(git_blob_rawcontent(blob));
  Local<Value> value = Nan::New<String>(content).ToLocalChecked();
  git_blob_free(blob);
  return info.GetReturnValue().Set(value);
}

NAN_METHOD(Repository::GetIndexBlob) {
  Nan::HandleScope scope;
  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::Null());

  std::string path(*Nan::Utf8String(info[0]));

  git_repository* repo = GetRepository(info);
  git_index* index;
  if (git_repository_index(&index, repo) != GIT_OK)
    return info.GetReturnValue().Set(Nan::Null());

  git_index_read(index, 0);
  const git_index_entry* entry = git_index_get_bypath(index, path.data(), 0);
  if (entry == NULL) {
    git_index_free(index);
    return info.GetReturnValue().Set(Nan::Null());
  }

  git_blob* blob = NULL;
  const git_oid* blobSha = &entry->id;
  if (blobSha != NULL && git_blob_lookup(&blob, repo, blobSha) != GIT_OK)
    blob = NULL;
  git_index_free(index);
  if (blob == NULL)
    return info.GetReturnValue().Set(Nan::Null());

  const char* content = static_cast<const char*>(git_blob_rawcontent(blob));
  Local<Value> value = Nan::New<String>(content).ToLocalChecked();
  git_blob_free(blob);
  return info.GetReturnValue().Set(value);
}

int Repository::SubmoduleCallback(
    git_submodule* submodule, const char* name, void* payload) {
  std::vector<std::string>* submodules =
      static_cast<std::vector<std::string>*>(payload);
  const char* submodulePath = git_submodule_path(submodule);
  if (submodulePath != NULL)
    submodules->push_back(submodulePath);
  return GIT_OK;
}

NAN_METHOD(Repository::Release) {
  Nan::HandleScope scope;
  Repository* repo = Nan::ObjectWrap::Unwrap<Repository>(info.This());
  if (repo->repository != NULL) {
    git_repository_free(repo->repository);
    repo->repository = NULL;
  }
  info.GetReturnValue().SetUndefined();
}

unsigned GetCommitCount(git_repository *repository, git_oid *left_oid, git_oid *right_oid) {
  git_revwalk *revwalk;
  if (git_revwalk_new(&revwalk, repository) != GIT_OK) return 0;

  git_revwalk_push(revwalk, left_oid);
  git_revwalk_hide(revwalk, right_oid);

  unsigned result = 0;
  git_oid current_commit;
  while (git_revwalk_next(&current_commit, revwalk) == GIT_OK) result++;
  git_revwalk_free(revwalk);

  return result;
}

class CompareCommitsWorker {
  git_repository *repository;
  std::string left_id;
  std::string right_id;
  unsigned ahead_count;
  unsigned behind_count;

 public:
  void Execute() {
    git_oid left_oid;
    if (git_oid_fromstr(&left_oid, left_id.c_str()) != GIT_OK) return;

    git_oid right_oid;
    if (git_oid_fromstr(&right_oid, right_id.c_str()) != GIT_OK) return;

    git_oid merge_base;
    if (git_merge_base(&merge_base, repository, &left_oid, &right_oid) != GIT_OK) return;

    ahead_count = GetCommitCount(repository, &left_oid, &merge_base);
    behind_count = GetCommitCount(repository, &right_oid, &merge_base);
  }

  std::pair<Local<Value>, Local<Value>> Finish() {
    Local<Object> result = Nan::New<Object>();
    Nan::Set(result, Nan::New("ahead").ToLocalChecked(), Nan::New<Integer>(ahead_count));
    Nan::Set(result, Nan::New("behind").ToLocalChecked(), Nan::New<Integer>(behind_count));
    return {Nan::Null(), result};
  }

  CompareCommitsWorker(git_repository *repository, Local<Value> js_left_id,
                       Local<Value> js_right_id) : repository(repository) {
    left_id = *Nan::Utf8String(js_left_id);
    right_id = *Nan::Utf8String(js_right_id);
  }
};

NAN_METHOD(Repository::CompareCommits) {
  if (info.Length() < 2) {
    info.GetReturnValue().Set(Nan::Null());
    return;
  }

  CompareCommitsWorker worker(GetRepository(info), info[0], info[1]);
  worker.Execute();
  info.GetReturnValue().Set(worker.Finish().second);
}

NAN_METHOD(Repository::CompareCommitsAsync) {
  class CompareCommitsAsyncWorker : public Nan::AsyncWorker {
    CompareCommitsWorker worker;

   public:
    void Execute() {
      worker.Execute();
    }

    void HandleOKCallback() {
      auto result = worker.Finish();
      Local<Value> argv[] = {result.first, result.second};
      callback->Call(2, argv);
    }

    CompareCommitsAsyncWorker(Nan::Callback *callback, git_repository *repository,
                         Local<Value> js_left_id, Local<Value> js_right_id)
      : Nan::AsyncWorker(callback), worker(repository, js_left_id, js_right_id) {}
  };

  if (info.Length() < 2) {
    info.GetReturnValue().Set(Nan::Null());
    return;
  }

  auto callback = new Nan::Callback(Local<Function>::Cast(info[0]));
  Nan::AsyncQueueWorker(new CompareCommitsAsyncWorker(callback, GetAsyncRepository(info), info[1], info[2]));
}

int Repository::DiffHunkCallback(const git_diff_delta* delta,
                                 const git_diff_hunk* range,
                                 void* payload) {
  std::vector<git_diff_hunk>* ranges =
      static_cast<std::vector<git_diff_hunk>*>(payload);
  ranges->push_back(*range);
  return GIT_OK;
}

NAN_METHOD(Repository::GetLineDiffs) {
  Nan::HandleScope scope;
  if (info.Length() < 2)
    return info.GetReturnValue().Set(Nan::Null());

  std::string text(*Nan::Utf8String(info[1]));

  git_repository* repo = GetRepository(info);

  git_blob* blob = NULL;

  int getBlobResult = GetBlob(info, repo, blob);

  if (getBlobResult != 0)
    return info.GetReturnValue().Set(Nan::Null());

  std::vector<git_diff_hunk> ranges;
  git_diff_options options = CreateDefaultGitDiffOptions();

  if (info.Length() >= 3) {
    Local<Object> optionsArg(Local<Object>::Cast(info[2]));
    // Set GIT_DIFF_IGNORE_WHITESPACE when ignoreWhitespace: true
    if (Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreAllSpace").ToLocalChecked()).ToLocalChecked()).FromJust())
      options.flags = GIT_DIFF_IGNORE_WHITESPACE;
    // Set GIT_DIFF_IGNORE_WHITESPACE_CHANGE when ignoreWhitespaceChange: true
    else if (Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreSpaceChange").ToLocalChecked()).ToLocalChecked()).FromJust())
      options.flags = GIT_DIFF_IGNORE_WHITESPACE_CHANGE;
    // Set GIT_DIFF_IGNORE_WHITESPACE_EOL when ignoreEolWhitespace: true
    else if (Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreSpaceAtEOL").ToLocalChecked()).ToLocalChecked()).FromJust()
      || Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreEolWhitespace").ToLocalChecked()).ToLocalChecked()).FromJust())
      options.flags = GIT_DIFF_IGNORE_WHITESPACE_EOL;
    // Set GIT_DIFF_NORMAL when none of the above are defined
    else
      options.flags = GIT_DIFF_NORMAL;
  }

  options.context_lines = 0;
  if (git_diff_blob_to_buffer(blob, NULL, text.data(), text.length(), NULL,
                              &options, NULL, NULL, DiffHunkCallback, NULL,
                              &ranges) == GIT_OK) {
    Local<Object> v8Ranges = Nan::New<Array>(ranges.size());
    for (size_t i = 0; i < ranges.size(); i++) {
      Local<Object> v8Range = Nan::New<Object>();
      Nan::Set(v8Range,
                Nan::New<String>("oldStart").ToLocalChecked(),
                Nan::New<Number>(ranges[i].old_start));
      Nan::Set(v8Range,
                Nan::New<String>("oldLines").ToLocalChecked(),
                Nan::New<Number>(ranges[i].old_lines));
      Nan::Set(v8Range,
                Nan::New<String>("newStart").ToLocalChecked(),
                Nan::New<Number>(ranges[i].new_start));
      Nan::Set(v8Range,
                Nan::New<String>("newLines").ToLocalChecked(),
                Nan::New<Number>(ranges[i].new_lines));
      Nan::Set(v8Ranges, i, v8Range);
    }
    git_blob_free(blob);
    return info.GetReturnValue().Set(v8Ranges);
  } else {
    git_blob_free(blob);
    return info.GetReturnValue().Set(Nan::Null());
  }
}

struct LineDiff {
  git_diff_hunk hunk;
  git_diff_line line;
};

int Repository::DiffLineCallback(const git_diff_delta* delta,
                                 const git_diff_hunk* range,
                                 const git_diff_line* line,
                                 void* payload) {
  LineDiff lineDiff;
  lineDiff.hunk = *range;
  lineDiff.line = *line;
  std::vector<LineDiff> * lineDiffs =
      static_cast<std::vector<LineDiff>*>(payload);
  lineDiffs->push_back(lineDiff);
  return GIT_OK;
}

NAN_METHOD(Repository::GetLineDiffDetails) {
  Nan::HandleScope scope;
  if (info.Length() < 2)
    return info.GetReturnValue().Set(Nan::Null());

  std::string text(*Nan::Utf8String(info[1]));

  git_repository* repo = GetRepository(info);

  git_blob* blob = NULL;

  int getBlobResult = GetBlob(info, repo, blob);

  if (getBlobResult != 0)
    return info.GetReturnValue().Set(Nan::Null());

  std::vector<LineDiff> lineDiffs;
  git_diff_options options = CreateDefaultGitDiffOptions();

  if (info.Length() >= 3) {
    Local<Object> optionsArg(Local<Object>::Cast(info[2]));
    // Set GIT_DIFF_IGNORE_WHITESPACE when ignoreWhitespace: true
    if (Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreAllSpace").ToLocalChecked()).ToLocalChecked()).FromJust())
      options.flags = GIT_DIFF_IGNORE_WHITESPACE;
    // Set GIT_DIFF_IGNORE_WHITESPACE_CHANGE when ignoreWhitespaceChange: true
    else if (Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreSpaceChange").ToLocalChecked()).ToLocalChecked()).FromJust())
      options.flags = GIT_DIFF_IGNORE_WHITESPACE_CHANGE;
    // Set GIT_DIFF_IGNORE_WHITESPACE_EOL when ignoreEolWhitespace: true
    else if (Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreSpaceAtEOL").ToLocalChecked()).ToLocalChecked()).FromJust()
      || Nan::To<bool>(Nan::Get(optionsArg, Nan::New<String>("ignoreEolWhitespace").ToLocalChecked()).ToLocalChecked()).FromJust())
      options.flags = GIT_DIFF_IGNORE_WHITESPACE_EOL;
    // Set GIT_DIFF_NORMAL when none of the above are defined
    else
      options.flags = GIT_DIFF_NORMAL;
  }

  options.context_lines = 0;
  if (git_diff_blob_to_buffer(blob, NULL, text.data(), text.length(), NULL,
                              &options, NULL, NULL, NULL, DiffLineCallback,
                              &lineDiffs) == GIT_OK) {
    Local<Object> v8Ranges = Nan::New<Array>(lineDiffs.size());
    for (size_t i = 0; i < lineDiffs.size(); i++) {
      Local<Object> v8Range = Nan::New<Object>();

      Nan::Set(v8Range,
                Nan::New<String>("oldLineNumber").ToLocalChecked(),
                Nan::New<Number>(lineDiffs[i].line.old_lineno));
      Nan::Set(v8Range,
                Nan::New<String>("newLineNumber").ToLocalChecked(),
                Nan::New<Number>(lineDiffs[i].line.new_lineno));
      Nan::Set(v8Range,
                Nan::New<String>("oldStart").ToLocalChecked(),
                Nan::New<Number>(lineDiffs[i].hunk.old_start));
      Nan::Set(v8Range,
                Nan::New<String>("newStart").ToLocalChecked(),
                Nan::New<Number>(lineDiffs[i].hunk.new_start));
      Nan::Set(v8Range,
                Nan::New<String>("oldLines").ToLocalChecked(),
                Nan::New<Number>(lineDiffs[i].hunk.old_lines));
      Nan::Set(v8Range,
                Nan::New<String>("newLines").ToLocalChecked(),
                Nan::New<Number>(lineDiffs[i].hunk.new_lines));
      Nan::Set(v8Range,
                Nan::New<String>("line").ToLocalChecked(),
                Nan::New<String>(lineDiffs[i].line.content,
                                lineDiffs[i].line.content_len)
                                    .ToLocalChecked());

      Nan::Set(v8Ranges, i, v8Range);
    }
    git_blob_free(blob);
    return info.GetReturnValue().Set(v8Ranges);
  } else {
    git_blob_free(blob);
    return info.GetReturnValue().Set(Nan::Null());
  }
}

Local<Value> Repository::ConvertStringVectorToV8Array(
    const std::vector<std::string>& vector) {
  size_t i = 0, size = vector.size();
  Local<Object> array = Nan::New<Array>(size);
  for (i = 0; i < size; i++)
    Nan::Set(array, i, Nan::New<String>(vector[i].c_str()).ToLocalChecked());

  return array;
}

NAN_METHOD(Repository::GetReferences) {
  Nan::HandleScope scope;

  Local<Object> references = Nan::New<Object>();
  std::vector<std::string> heads, remotes, tags;

  git_strarray strarray;
  git_reference_list(&strarray, GetRepository(info));

  for (unsigned int i = 0; i < strarray.count; i++)
    if (strncmp(strarray.strings[i], "refs/heads/", 11) == 0)
      heads.push_back(strarray.strings[i]);
    else if (strncmp(strarray.strings[i], "refs/remotes/", 13) == 0)
      remotes.push_back(strarray.strings[i]);
    else if (strncmp(strarray.strings[i], "refs/tags/", 10) == 0)
      tags.push_back(strarray.strings[i]);

  git_strarray_free(&strarray);

  Nan::Set(references,
            Nan::New<String>("heads").ToLocalChecked(),
            ConvertStringVectorToV8Array(heads));
  Nan::Set(references,
            Nan::New<String>("remotes").ToLocalChecked(),
            ConvertStringVectorToV8Array(remotes));
  Nan::Set(references,
            Nan::New<String>("tags").ToLocalChecked(),
            ConvertStringVectorToV8Array(tags));

  info.GetReturnValue().Set(references);
}

int branch_checkout(git_repository* repo, const char* refName) {
  git_reference* ref = NULL;
  git_object* git_obj = NULL;
  git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
  opts.checkout_strategy = GIT_CHECKOUT_SAFE;
  int success = -1;

  if (!(success = git_reference_lookup(&ref, repo, refName)) &&
      !(success = git_reference_peel(&git_obj, ref, GIT_OBJ_TREE)) &&
      !(success = git_checkout_tree(repo, git_obj, &opts)))
    success = git_repository_set_head(repo, refName);

  git_object_free(git_obj);
  git_obj = NULL;
  git_reference_free(ref);
  ref = NULL;

  return success;
}

NAN_METHOD(Repository::CheckoutReference) {
  Nan::HandleScope scope;

  if (info.Length() < 1)
    return info.GetReturnValue().Set(Nan::New<Boolean>(false));

  bool shouldCreateNewRef;
  if (info.Length() > 1 && Nan::To<bool>(info[1]).FromJust())
    shouldCreateNewRef = true;
  else
    shouldCreateNewRef = false;

  std::string strRefName(*Nan::Utf8String(info[0]));
  const char* refName = strRefName.c_str();

  git_repository* repo = GetRepository(info);

  if (branch_checkout(repo, refName) == GIT_OK) {
    return info.GetReturnValue().Set(Nan::New<Boolean>(true));
  } else if (shouldCreateNewRef) {
    git_reference* head;
    if (git_repository_head(&head, repo) != GIT_OK)
      return info.GetReturnValue().Set(Nan::New<Boolean>(false));

    const git_oid* sha = git_reference_target(head);
    git_commit* commit;
    int commitStatus = git_commit_lookup(&commit, repo, sha);
    git_reference_free(head);

    if (commitStatus != GIT_OK)
      return info.GetReturnValue().Set(Nan::New<Boolean>(false));

    git_reference* branch;
    // N.B.: git_branch_create needs a name like 'xxx', not 'refs/heads/xxx'
    const int kShortNameLength = strRefName.length() - 11;
    std::string shortRefName(strRefName.c_str() + 11, kShortNameLength);

    int branchCreateStatus = git_branch_create(
        &branch, repo, shortRefName.c_str(), commit, 0);
    git_commit_free(commit);

    if (branchCreateStatus != GIT_OK)
      return info.GetReturnValue().Set(Nan::New<Boolean>(false));

    git_reference_free(branch);

    if (branch_checkout(repo, refName) == GIT_OK)
      return info.GetReturnValue().Set(Nan::New<Boolean>(true));
  }

  return info.GetReturnValue().Set(Nan::New<Boolean>(false));
}

NAN_METHOD(Repository::Add) {
  Nan::HandleScope scope;

  git_repository* repository = GetRepository(info);
  std::string path(*Nan::Utf8String(info[0]));

  git_index* index;
  if (git_repository_index(&index, repository) != GIT_OK) {
    const git_error* e = giterr_last();
    if (e != NULL)
      return Nan::ThrowError(e->message);
    else
      return Nan::ThrowError("Unknown error opening index");
  }
  // Modify the in-memory index.
  if (git_index_add_bypath(index, path.c_str()) != GIT_OK) {
    git_index_free(index);
    const git_error* e = giterr_last();
    if (e != NULL)
      return Nan::ThrowError(e->message);
    else
      return Nan::ThrowError("Unknown error adding path to index");
  }
  // Write this change in the index back to disk, so it is persistent
  if (git_index_write(index) != GIT_OK) {
    git_index_free(index);
    const git_error* e = giterr_last();
    if (e != NULL)
      return Nan::ThrowError(e->message);
    else
      return Nan::ThrowError("Unknown error adding path to index");
  }
  git_index_free(index);
  info.GetReturnValue().Set(Nan::New<Boolean>(true));
}

Repository::Repository(Local<String> path, Local<Boolean> search) {
  Nan::HandleScope scope;

  int flags = 0;
  if (!Nan::To<bool>(search).FromJust()) {
    flags |= GIT_REPOSITORY_OPEN_NO_SEARCH;
  }

  Nan::Utf8String repository_path(path);
  int result = git_repository_open_ext(&repository, *repository_path, flags, NULL);
  if (result != GIT_OK) {
    repository = NULL;
    async_repository = NULL;
    return;
  }

  result = git_repository_open_ext(
    &async_repository,
    git_repository_path(repository),
    GIT_REPOSITORY_OPEN_NO_SEARCH,
    NULL
  );
  if (result != GIT_OK) {
    repository = NULL;
    async_repository = NULL;
    return;
  }
}

Repository::~Repository() {
  if (repository != NULL) {
    git_repository_free(repository);
    repository = NULL;
  }
  if (async_repository != NULL) {
    git_repository_free(async_repository);
    async_repository = NULL;
  }
}
