diff --git a/generate/templates/manual/include/lfs.h b/generate/templates/manual/include/lfs.h index 7aaf72279..c01f7d1b6 100644 --- a/generate/templates/manual/include/lfs.h +++ b/generate/templates/manual/include/lfs.h @@ -3,6 +3,12 @@ #include #include "context.h" +#include "lock_master.h" +#include "lfs_cmd.h" + +extern "C" { +#include +} class GitLFS : public Nan::ObjectWrap { public: @@ -12,6 +18,38 @@ class GitLFS : public Nan::ObjectWrap { GitLFS& operator=(GitLFS &&other) = delete; static void InitializeComponent(v8::Local target, nodegit::Context *nodegitContext); + + private: + struct InitializeBaton { + int error_code; + const git_error *error; + git_repository *repo; + std::unique_ptr cmd; + }; + class InitializeWorker : public nodegit::AsyncWorker { + public: + InitializeWorker( + InitializeBaton *_baton, + Nan::Callback *callback + ) : nodegit::AsyncWorker(callback, "nodegit:AsyncWorker:LFS:Initialize") + , baton(_baton) {}; + InitializeWorker(const InitializeWorker &) = delete; + InitializeWorker(InitializeWorker &&) = delete; + InitializeWorker &operator=(const InitializeWorker &) = delete; + InitializeWorker &operator=(InitializeWorker &&) = delete; + ~InitializeWorker() {}; + void Execute(); + void HandleErrorCallback(); + void HandleOKCallback(); + nodegit::LockMaster AcquireLocks(); + + private: + InitializeBaton *baton {nullptr}; + }; + + static NAN_METHOD(Initialize); + + }; #endif // GITLFS_H \ No newline at end of file diff --git a/generate/templates/manual/include/lfs_cmd.h b/generate/templates/manual/include/lfs_cmd.h index 59db5a6a1..aae0c84f2 100644 --- a/generate/templates/manual/include/lfs_cmd.h +++ b/generate/templates/manual/include/lfs_cmd.h @@ -21,6 +21,22 @@ struct LfsCmdOpts { virtual std::string BuildArgs() const = 0; }; +/** + * \struct LfsCmdOptsInitialize + */ +struct LfsCmdOptsInitialize final : public LfsCmdOpts { + LfsCmdOptsInitialize() = default; + ~LfsCmdOptsInitialize() = default; + LfsCmdOptsInitialize(const LfsCmdOptsInitialize &other) = delete; + LfsCmdOptsInitialize(LfsCmdOptsInitialize &&other) = delete; + LfsCmdOptsInitialize& operator=(const LfsCmdOptsInitialize &other) = delete; + LfsCmdOptsInitialize& operator=(LfsCmdOptsInitialize &&other) = delete; + + std::string BuildArgs() const; + + bool local {false}; +}; + /** * \class LfsCmd * An LFS command to execute. @@ -30,7 +46,7 @@ class LfsCmd : public Cmd { static const char* kStrLfsCmd; // Enumeration describing the type of LFS command - enum class Type {kNone}; + enum class Type {kNone, kInitialize}; LfsCmd(Type lfsCmdType, std::unique_ptr &&opts); LfsCmd() = delete; diff --git a/generate/templates/manual/src/lfs.cc b/generate/templates/manual/src/lfs.cc index ea427e7f5..7112e3078 100644 --- a/generate/templates/manual/src/lfs.cc +++ b/generate/templates/manual/src/lfs.cc @@ -1,7 +1,16 @@ #include + +extern "C" { + #include +} + #include "../include/nodegit.h" #include "../include/context.h" +#include "../include/lock_master.h" +#include "../include/functions/copy.h" #include "../include/lfs.h" +#include "../include/run_command.h" +#include "../include/repository.h" using namespace v8; @@ -10,6 +19,160 @@ void GitLFS::InitializeComponent(v8::Local target, nodegit::Context v8::Local lfs = Nan::New(); + Local nodegitExternal = Nan::New(nodegitContext); + Nan::SetMethod(lfs, "initialize", Initialize, nodegitExternal); + Nan::Set(target, Nan::New("LFS").ToLocalChecked(), lfs); nodegitContext->SaveToPersistent("LFS", lfs); +} + +NAN_METHOD(GitLFS::Initialize) { + if (info.Length() == 0 || !info[0]->IsObject()) { + return Nan::ThrowError("Repository repo is required."); + } + + if (info.Length() >= 3 && !info[1]->IsNull() && !info[1]->IsUndefined() && !info[1]->IsObject()) { + return Nan::ThrowError("Options must be an object, null, or undefined."); + } + + if (!info[info.Length() - 1]->IsFunction()) { + return Nan::ThrowError("Callback is required and must be a Function."); + } + + InitializeBaton* baton = new InitializeBaton(); + + baton->error_code = GIT_OK; + baton->error = NULL; + baton->repo = Nan::ObjectWrap::Unwrap(Nan::To(info[0]).ToLocalChecked())->GetValue(); + + std::unique_ptr lfsCmdOpts = std::make_unique(); + if (info.Length() == 3 && info[1]->IsObject()) { + v8::Local options = Nan::To(info[1]).ToLocalChecked(); + v8::Local maybeBool = nodegit::safeGetField(options, "local"); + if (!maybeBool.IsEmpty() && !maybeBool->IsUndefined() && !maybeBool->IsNull()) { + if (!maybeBool->IsBoolean()) { + return Nan::ThrowError("Must pass Boolean to local"); + } + lfsCmdOpts->local = static_cast(Nan::To(maybeBool).FromJust()); + } + } + baton->cmd = std::make_unique(nodegit::LfsCmd::Type::kInitialize, std::move(lfsCmdOpts)); + + Nan::Callback *callback = new Nan::Callback(v8::Local::Cast(info[info.Length() - 1])); + InitializeWorker *worker = new InitializeWorker(baton, callback); + + worker->Reference("repo", info[0]); + + nodegit::Context *nodegitContext = reinterpret_cast(info.Data().As()->Value()); + nodegitContext->QueueWorker(worker); + return; +} + +nodegit::LockMaster GitLFS::InitializeWorker::AcquireLocks() { + nodegit::LockMaster lockMaster( + /*asyncAction: */true + ,baton->repo + ); + return lockMaster; +} + +void GitLFS::InitializeWorker::Execute() { + git_error_clear(); + + int result = GIT_OK; + const char *repoPath = git_repository_workdir(baton->repo); + // TODO: execute command if repository is bare? + if (repoPath) { + baton->cmd->SetEnv(nodegit::Cmd::Env::kCWD, repoPath); + } + + // TODO: execute command if '.git/lfs' folder already exists? + if (!nodegit::runcommand::exec(baton->cmd.get())) { + result = GIT_EUSER; + } + + baton->error_code = result; + + if (result != GIT_OK && git_error_last() != NULL) { + baton->error = git_error_dup(git_error_last()); + } +} + +void GitLFS::InitializeWorker::HandleErrorCallback() { + if (!GetIsCancelled()) { + v8::Local err = Nan::To(Nan::Error(ErrorMessage())).ToLocalChecked(); + Nan::Set(err, Nan::New("errorFunction").ToLocalChecked(), Nan::New("LFS.initialize").ToLocalChecked()); + v8::Local argv[1] = { + err + }; + callback->Call(1, argv, async_resource); + } + + if (baton->error) { + if (baton->error->message) { + free((void *)baton->error->message); + } + + free((void *)baton->error); + } + + delete baton; +} + +void GitLFS::InitializeWorker::HandleOKCallback() { + if (baton->error_code == GIT_OK) { + v8::Local result = Nan::Undefined(); + + v8::Local argv[2] = { + Nan::Null(), + result + }; + callback->Call(2, argv, async_resource); + } else { + if (baton->error) { + v8::Local err; + if (baton->error->message) { + err = Nan::To(Nan::Error(baton->error->message)).ToLocalChecked(); + } else { + err = Nan::To(Nan::Error("Method initialize has thrown an error.")).ToLocalChecked(); + } + Nan::Set(err, Nan::New("errno").ToLocalChecked(), Nan::New(baton->error_code)); + Nan::Set(err, Nan::New("errorFunction").ToLocalChecked(), Nan::New("LFS.initialize").ToLocalChecked()); + v8::Local argv[1] = { + err + }; + callback->Call(1, argv, async_resource); + if (baton->error->message) { + free((void *)baton->error->message); + } + free((void *)baton->error); + } else if (baton->error_code < 0) { + bool callbackFired = false; + if (!callbackErrorHandle.IsEmpty()) { + v8::Local maybeError = Nan::New(callbackErrorHandle); + if (!maybeError->IsNull() && !maybeError->IsUndefined()) { + v8::Local argv[1] = { + maybeError + }; + callback->Call(1, argv, async_resource); + callbackFired = true; + } + } + + if (!callbackFired) { + std::string errorMessage = std::string("Method initialize has thrown an error: ").append(baton->cmd->errorMsg); + v8::Local err = Nan::To(Nan::Error(errorMessage.c_str())).ToLocalChecked(); + Nan::Set(err, Nan::New("errno").ToLocalChecked(), Nan::New(baton->error_code)); + Nan::Set(err, Nan::New("errorFunction").ToLocalChecked(), Nan::New("LFS.initialize").ToLocalChecked()); + v8::Local argv[1] = { + err + }; + callback->Call(1, argv, async_resource); + } + } else { + callback->Call(0, NULL, async_resource); + } + } + + delete baton; } \ No newline at end of file diff --git a/generate/templates/manual/src/lfs_cmd.cc b/generate/templates/manual/src/lfs_cmd.cc index 1bea3b82f..5484c3121 100644 --- a/generate/templates/manual/src/lfs_cmd.cc +++ b/generate/templates/manual/src/lfs_cmd.cc @@ -3,7 +3,21 @@ const char* nodegit::LfsCmd::kStrLfsCmd = "git lfs"; /** - * LfsCmd::LfsCmd + * nodegit::LfsCmdOptsInitialize::BuildArgs + * + * \return string with arguments for this LFS command. + */ +std::string nodegit::LfsCmdOptsInitialize::BuildArgs() const { + std::string strArgs {}; + + if (local) { + strArgs.append("--local"); + } + return strArgs; +} + +/** + * nodegit::LfsCmd::LfsCmd * * Constructor from type and options. */ @@ -20,6 +34,9 @@ std::string nodegit::LfsCmd::Command() const { std::string strCmd {nodegit::LfsCmd::kStrLfsCmd}; switch (m_lfsCmdType) { + case Type::kInitialize: + strCmd.append(" install"); + break; default: break; } diff --git a/generate/templates/templates/nodegit.js b/generate/templates/templates/nodegit.js index 3700727c6..2065f1f00 100644 --- a/generate/templates/templates/nodegit.js +++ b/generate/templates/templates/nodegit.js @@ -88,6 +88,10 @@ _FilterRegistry.register = promisify(_FilterRegistry_register); var _FilterRegistry_unregister = _FilterRegistry.unregister; _FilterRegistry.unregister = promisify(_FilterRegistry_unregister); +var _LFS = rawApi.LFS; +var _LFS_initialize = _LFS.initialize; +_LFS.initialize = promisify(_LFS_initialize); + /* jshint ignore:end */ // Set the exports prototype to the raw API. diff --git a/test/tests/lfs.js b/test/tests/lfs.js new file mode 100644 index 000000000..d94e1862e --- /dev/null +++ b/test/tests/lfs.js @@ -0,0 +1,46 @@ +var assert = require("assert"); +var RepoUtils = require("../utils/repository_setup"); +var path = require("path"); +var local = path.join.bind(path, __dirname); +const fse = require("fs-extra"); + +describe("LFS", function() { + var NodeGit = require("../../"); + + var LFS = NodeGit.LFS; + + var test; + var fileName = "foobar.js"; + var repoPath = local("../repos/lfsRepo"); + + beforeEach(function() { + test = this; + + return RepoUtils.createRepository(repoPath) + .then(function(repository) { + test.repository = repository; + + return RepoUtils.commitFileToRepo( + repository, + fileName, + "line1\nline2\nline3" + ); + }); + }); + + // TODO: need to test against bare repositories? + it("can initialize LFS repository", function(done) { + LFS.initialize(test.repository, { local: true }) + .then(function() { + let gitLfsDirPath = path.join(test.repository.path(), "/lfs"); + fse.stat(gitLfsDirPath, function(err, stat) { + if (!err && stat && stat.isDirectory()) { + done(); + } + else { + assert.fail("LFS repository initialization failed!"); + } + }); + }); + }); +});