diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index 86a7a58a2..ddb9e63a6 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -1,4 +1,3 @@ - #include "git.hpp" #include #include @@ -47,7 +46,7 @@ Git::Git( const std::string& projectDir, const std::string& gitPath ) : mGitPath setProjectPath( projectDir ); } -bool Git::git( const std::string& args, const std::string& projectDir, std::string& buf ) const { +int Git::git( const std::string& args, const std::string& projectDir, std::string& buf ) const { buf.clear(); Process p; p.create( mGitPath, args, Process::CombinedStdoutStderr | Process::Options::NoWindow, @@ -55,7 +54,7 @@ bool Git::git( const std::string& args, const std::string& projectDir, std::stri p.readAllStdOut( buf ); int retCode; p.join( &retCode ); - return EXIT_SUCCESS == retCode; + return retCode; } void Git::gitSubmodules( const std::string& args, const std::string& projectDir, @@ -63,9 +62,16 @@ void Git::gitSubmodules( const std::string& args, const std::string& projectDir, git( String::format( "submodule foreach \"git %s\"", args ), projectDir, buf ); } +bool Git::isGitRepo( const std::string& projectDir ) { + std::string buf; + git( "rev-parse --is-inside-work-tree", projectDir, buf ); + String::trimInPlace( buf ); + return "true" == buf; +} + std::string Git::branch( const std::string& projectDir ) { std::string buf; - if ( git( "rev-parse --abbrev-ref HEAD", projectDir, buf ) ) + if ( EXIT_SUCCESS == git( "rev-parse --abbrev-ref HEAD", projectDir, buf ) ) return String::rTrim( buf, '\n' ); return "HEAD"; } @@ -112,6 +118,93 @@ std::string Git::setSafeDirectory( const std::string& projectDir ) const { return buf; } +Git::CheckoutResult Git::checkout( const std::string& branch, + const std::string& projectDir ) const { + std::string buf; + int retCode = git( String::format( "checkout %s", branch ), projectDir, buf ); + Git::CheckoutResult res; + if ( EXIT_SUCCESS != retCode ) { + res.returnCode = retCode; + res.error = buf; + } else { + res.branch = buf; + } + return res; +} + +Git::CheckoutResult Git::checkoutNewBranch( const std::string& newBranch, + const std::string& fromBranch, + const std::string& projectDir ) { + std::string buf; + std::string args( String::format( "checkout -q -b %s", newBranch ) ); + if ( !fromBranch.empty() ) + args += " " + fromBranch; + int retCode = git( args, projectDir, buf ); + Git::CheckoutResult res; + if ( EXIT_SUCCESS != retCode ) { + res.returnCode = retCode; + res.error = buf; + } else { + res.branch = buf; + } + return res; +} + +std::vector Git::getAllBranches( const std::string& projectDir ) { + return getAllBranchesAndTags( static_cast( RefType::Head | RefType::Remote ), + projectDir ); +} + +static Git::Branch parseLocalBranch( const std::string& raw ) { + static constexpr size_t len = std::string_view{ "refs/heads/"sv }.size(); + return Git::Branch{ raw.substr( len ), std::string{}, Git::RefType::Head, std::string{} }; +} + +static Git::Branch parseRemoteBranch( const std::string& raw ) { + static constexpr size_t len = std::string_view( "refs/remotes/"sv ).size(); + size_t indexOfRemote = raw.find_first_of( '/', len ); + if ( indexOfRemote != std::string::npos ) + return Git::Branch{ raw.substr( len ), raw.substr( len, indexOfRemote - len ), + Git::RefType::Remote, std::string{} }; + return {}; +} + +std::vector Git::getAllBranchesAndTags( RefType ref, const std::string& projectDir ) { + std::string args( "for-each-ref --format '%(refname)' --sort=-committerdate" ); + if ( ref & RefType::Head ) { + args.append( " refs/heads" ); + } + if ( ref & RefType::Remote ) { + args.append( " refs/remotes" ); + } + if ( ref & RefType::Tag ) { + args.append( " refs/tags" ); + args.append( " --sort=-taggerdate" ); + } + + std::vector branches; + std::string buf; + + if ( EXIT_SUCCESS != git( args, projectDir, buf ) ) + return branches; + + auto out = String::split( buf ); + branches.reserve( out.size() ); + for ( const auto& branch : out ) { + if ( ( ref & Head ) && String::startsWith( branch, "refs/heads/" ) ) { + branches.emplace_back( parseLocalBranch( branch ) ); + } else if ( ( ref & Remote ) && String::startsWith( branch, "refs/remotes/" ) ) { + branches.emplace_back( parseRemoteBranch( branch ) ); + } else if ( ( ref & Tag ) && String::startsWith( branch, "refs/tags/" ) ) { + static constexpr size_t len = std::string_view{ "refs/tags/"sv }.size(); + branches.push_back( + { branch.substr( len ), std::string{}, RefType::Tag, std::string{} } ); + } + } + + return branches; +} + bool Git::hasSubmodules( const std::string& projectDir ) { return ( !projectDir.empty() && FileSystem::fileExists( projectDir + ".gitmodules" ) ) || ( !mProjectPath.empty() && FileSystem::fileExists( mProjectPath + ".gitmodules" ) ); @@ -122,7 +215,7 @@ Git::Status Git::status( bool recurseSubmodules, const std::string& projectDir ) static constexpr auto STATUS_CMD = "-c color.status=never status -b -u -s"; Status s; std::string buf; - if ( !git( DIFF_CMD, projectDir, buf ) ) + if ( EXIT_SUCCESS != git( DIFF_CMD, projectDir, buf ) ) return s; auto parseNumStat = [&s, &buf]() { auto lastNL = 0; @@ -218,7 +311,7 @@ Git::Status Git::status( bool recurseSubmodules, const std::string& projectDir ) parseStatus(); } - for ( auto [_, val] : s.files ) { + for ( auto& [_, val] : s.files ) { if ( val.status == FileStatus::Added && val.inserts == 0 ) { std::string fileText; FileSystem::fileGet( ( projectDir.empty() ? mProjectPath : projectDir ) + val.file, @@ -245,8 +338,9 @@ Git::Blame Git::blame( const std::string& filepath, std::size_t line ) const { }; std::string workingDir( FileSystem::fileRemoveFileName( filepath ) ); - if ( !git( String::format( "blame %s -p -L%zu,%zu", filepath.data(), line, line ), workingDir, - buf ) ) + if ( EXIT_SUCCESS != + git( String::format( "blame %s -p -L%zu,%zu", filepath.data(), line, line ), workingDir, + buf ) ) return { buf }; if ( String::startsWith( buf, "fatal: " ) ) diff --git a/src/tools/ecode/plugins/git/git.hpp b/src/tools/ecode/plugins/git/git.hpp index a3cb3edad..0f819c955 100644 --- a/src/tools/ecode/plugins/git/git.hpp +++ b/src/tools/ecode/plugins/git/git.hpp @@ -55,12 +55,41 @@ class Git { } }; + struct Result { + std::string error; + int returnCode = 0; + }; + + struct CheckoutResult : public Result { + std::string branch; + }; + + enum RefType { + Head = 0x1, + Remote = 0x2, + Tag = 0x4, + All = 0x7, + }; + + struct Branch { + /** Branch name */ + std::string name; + /** remote name, will be empty for local branches */ + std::string remote; + /** Ref type @see RefType */ + RefType type = All; + /** last commit on this branch, may be empty **/ + std::string lastCommit; + }; + Git( const std::string& projectDir = "", const std::string& gitPath = "" ); - bool git( const std::string& args, const std::string& projectDir, std::string& buf ) const; + int git( const std::string& args, const std::string& projectDir, std::string& buf ) const; void gitSubmodules( const std::string& args, const std::string& projectDir, std::string& buf ); + bool isGitRepo( const std::string& projectDir ); + Blame blame( const std::string& filepath, std::size_t line ) const; std::string branch( const std::string& projectDir = "" ); @@ -77,6 +106,23 @@ class Git { std::string setSafeDirectory( const std::string& projectDir ) const; + CheckoutResult checkout( const std::string& branch, const std::string& projectDir = "" ) const; + + CheckoutResult checkoutNewBranch( const std::string& newBranch, + const std::string& fromBranch = "", + const std::string& projectDir = "" ); + + /** + * @brief get all local and remote branches + */ + std::vector getAllBranches( const std::string& projectDir = "" ); + + /** + * @brief get all local and remote branches + tags + */ + std::vector getAllBranchesAndTags( RefType ref = RefType::All, + const std::string& projectDir = "" ); + protected: std::string mGitPath; std::string mProjectPath;