mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-06-01 11:06:30 +03:00
853 lines
26 KiB
C++
853 lines
26 KiB
C++
#include "git.hpp"
|
|
#include <eepp/system/clock.hpp>
|
|
#include <eepp/system/filesystem.hpp>
|
|
#include <eepp/system/lock.hpp>
|
|
#include <eepp/system/log.hpp>
|
|
#include <eepp/system/luapattern.hpp>
|
|
#include <eepp/system/process.hpp>
|
|
#include <eepp/system/sys.hpp>
|
|
|
|
using namespace EE;
|
|
using namespace EE::System;
|
|
|
|
using namespace std::literals;
|
|
|
|
namespace ecode {
|
|
|
|
static size_t countLines( const std::string& text ) {
|
|
const char* startPtr = text.c_str();
|
|
const char* endPtr = text.c_str() + text.size();
|
|
size_t count = 0;
|
|
if ( startPtr != endPtr ) {
|
|
count = 1 + *startPtr == '\n' ? 1 : 0;
|
|
while ( ++startPtr && startPtr != endPtr )
|
|
count += ( '\n' == *startPtr ) ? 1 : 0;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void readAllLines( const std::string_view& buf,
|
|
std::function<void( const std::string_view& )> onLineRead ) {
|
|
auto lastNL = 0;
|
|
auto nextNL = buf.find_first_of( '\n' );
|
|
while ( nextNL != std::string_view::npos ) {
|
|
onLineRead( buf.substr( lastNL, nextNL - lastNL ) );
|
|
lastNL = nextNL + 1;
|
|
nextNL = buf.find_first_of( '\n', nextNL + 1 );
|
|
}
|
|
}
|
|
|
|
static constexpr auto sNotCommitedYetHash = "0000000000000000000000000000000000000000";
|
|
|
|
Git::Blame::Blame( const std::string& error ) : error( error ), line( 0 ) {}
|
|
|
|
Git::Blame::Blame( std::string&& author, std::string&& authorEmail, std::string&& date,
|
|
std::string&& commitHash, std::string&& commitShortHash,
|
|
std::string&& commitMessage, std::size_t line ) :
|
|
author( std::move( author ) ),
|
|
authorEmail( std::move( authorEmail ) ),
|
|
date( std::move( date ) ),
|
|
commitHash( std::move( commitHash ) ),
|
|
commitShortHash( std::move( commitShortHash ) ),
|
|
commitMessage( std::move( commitMessage ) ),
|
|
line( line ) {}
|
|
|
|
Git::Git( const std::string& projectDir, const std::string& gitPath ) : mGitPath( gitPath ) {
|
|
if ( gitPath.empty() )
|
|
mGitPath = Sys::which( "git" );
|
|
if ( !projectDir.empty() )
|
|
setProjectPath( projectDir );
|
|
}
|
|
|
|
int Git::git( const std::string& args, const std::string& projectDir, std::string& buf ) const {
|
|
Clock clock;
|
|
buf.clear();
|
|
Process p;
|
|
p.create( mGitPath, args,
|
|
Process::CombinedStdoutStderr | Process::Options::NoWindow |
|
|
Process::Options::InheritEnvironment,
|
|
{ { "LC_ALL", "en_US.UTF-8" } }, projectDir.empty() ? mProjectPath : projectDir );
|
|
p.readAllStdOut( buf );
|
|
int retCode = 0;
|
|
p.join( &retCode );
|
|
Log::info( "GitPlugin cmd in %s (%d): %s %s", clock.getElapsedTime().toString(), retCode,
|
|
mGitPath, args );
|
|
return retCode;
|
|
}
|
|
|
|
void Git::gitSubmodules( const std::string& args, const std::string& projectDir,
|
|
std::string& buf ) {
|
|
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 ( EXIT_SUCCESS == git( "rev-parse --abbrev-ref HEAD", projectDir, buf ) )
|
|
return String::rTrim( buf, '\n' );
|
|
|
|
return "HEAD";
|
|
}
|
|
|
|
std::unordered_map<std::string, std::string>
|
|
Git::branches( const std::vector<std::string>& repos ) {
|
|
std::unordered_map<std::string, std::string> ret;
|
|
for ( const auto& repo : repos )
|
|
ret[repo] = branch( repo );
|
|
return ret;
|
|
}
|
|
|
|
bool Git::setProjectPath( const std::string& projectPath ) {
|
|
auto lastProjectPath = mProjectPath;
|
|
mProjectPath = "";
|
|
mGitFolder = "";
|
|
mSubModules = {};
|
|
mSubModulesUpdated = true;
|
|
FileInfo f( projectPath );
|
|
if ( !f.isDirectory() )
|
|
return false;
|
|
std::string oriPath( f.getDirectoryPath() );
|
|
std::string path( oriPath );
|
|
std::string lPath;
|
|
FileSystem::dirAddSlashAtEnd( path );
|
|
while ( path != lPath ) {
|
|
std::string gitFolder( path + ".git" );
|
|
if ( FileSystem::fileExists( gitFolder ) ) {
|
|
mProjectPath = path;
|
|
mGitFolder = std::move( gitFolder );
|
|
if ( lastProjectPath != mProjectPath )
|
|
mSubModulesUpdated = false;
|
|
return true;
|
|
}
|
|
lPath = path;
|
|
path = FileSystem::removeLastFolderFromPath( path );
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const std::string& Git::getGitPath() const {
|
|
return mGitPath;
|
|
}
|
|
|
|
const std::string& Git::getProjectPath() const {
|
|
return mProjectPath;
|
|
}
|
|
|
|
const std::string& Git::getGitFolder() const {
|
|
return mGitFolder;
|
|
}
|
|
|
|
std::string Git::setSafeDirectory( const std::string& projectDir ) const {
|
|
std::string dir( projectDir.empty() ? mProjectPath : projectDir );
|
|
std::string buf;
|
|
git( String::format( "config --global --add safe.directory %s", dir ), dir, buf );
|
|
return buf;
|
|
}
|
|
|
|
Git::Result Git::pull( const std::string& projectDir ) {
|
|
return gitSimple( "pull", projectDir );
|
|
}
|
|
|
|
Git::Result Git::push( const std::string& projectDir ) {
|
|
return gitSimple( "push", projectDir );
|
|
}
|
|
|
|
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;
|
|
res.returnCode = retCode;
|
|
res.result = buf;
|
|
res.branch = branch;
|
|
return res;
|
|
}
|
|
|
|
Git::CheckoutResult Git::checkoutAndCreateLocalBranch( const std::string& remoteBranch,
|
|
const std::string& newBranch,
|
|
const std::string& projectDir ) const {
|
|
std::string newBranchName =
|
|
newBranch.empty() ? ( remoteBranch.find_last_of( '/' ) != std::string::npos
|
|
? remoteBranch.substr( remoteBranch.find_last_of( '/' ) + 1 )
|
|
: remoteBranch )
|
|
: newBranch;
|
|
Git::CheckoutResult res;
|
|
std::string buf;
|
|
int retCode =
|
|
git( String::format( "branch --no-track %s refs/remotes/%s", newBranchName, remoteBranch ),
|
|
projectDir, buf );
|
|
if ( retCode != EXIT_SUCCESS ) {
|
|
res.returnCode = retCode;
|
|
res.result = buf;
|
|
return res;
|
|
}
|
|
|
|
retCode = git( String::format( "branch --set-upstream-to=refs/remotes/%s %s", remoteBranch,
|
|
newBranchName ),
|
|
projectDir, buf );
|
|
if ( retCode != EXIT_SUCCESS ) {
|
|
res.returnCode = retCode;
|
|
res.result = buf;
|
|
return res;
|
|
}
|
|
|
|
return checkout( newBranchName, projectDir );
|
|
}
|
|
|
|
static std::string asList( std::vector<std::string>& files ) {
|
|
for ( auto& file : files )
|
|
file = "\"" + file + "\"";
|
|
return String::join( files );
|
|
}
|
|
|
|
Git::Result Git::add( std::vector<std::string> files, const std::string& projectDir ) {
|
|
return gitSimple( String::format( "add --force -- %s", asList( files ) ), projectDir );
|
|
}
|
|
|
|
Git::Result Git::restore( const std::string& file, const std::string& projectDir ) {
|
|
return gitSimple( String::format( "restore \"%s\"", file ), projectDir );
|
|
}
|
|
|
|
Git::Result Git::reset( std::vector<std::string> files, const std::string& projectDir ) {
|
|
return gitSimple( String::format( "reset -q HEAD -- %s", asList( files ) ), projectDir );
|
|
}
|
|
|
|
Git::Result Git::createBranch( const std::string& branchName, bool _checkout,
|
|
const std::string& projectDir ) {
|
|
auto res = gitSimple( String::format( "branch --no-track %s", branchName ), projectDir );
|
|
if ( _checkout )
|
|
checkout( branchName );
|
|
return res;
|
|
}
|
|
|
|
Git::Result Git::renameBranch( const std::string& branch, const std::string& newName,
|
|
const std::string& projectDir ) {
|
|
return gitSimple( String::format( "branch -M %s %s", branch, newName ), projectDir );
|
|
}
|
|
|
|
Git::Result Git::deleteBranch( const std::string& branch, const std::string& projectDir ) {
|
|
return gitSimple( String::format( "branch -D %s", branch ), projectDir );
|
|
}
|
|
|
|
Git::Result Git::commit( const std::string& commitMsg, bool ammend, bool byPassCommitHook,
|
|
const std::string& projectDir ) {
|
|
auto tmpPath = Sys::getTempPath() + ".ecode-git-commit-" + String::randString( 16 );
|
|
if ( !FileSystem::fileWrite( tmpPath, commitMsg ) ) {
|
|
Git::Result res;
|
|
res.returnCode = -1;
|
|
res.result = "Could not write commit message into a file";
|
|
return res;
|
|
}
|
|
std::string buf;
|
|
std::string opts;
|
|
if ( ammend )
|
|
opts += " --ammend";
|
|
|
|
if ( byPassCommitHook )
|
|
opts += " --no-verify";
|
|
|
|
int retCode = git(
|
|
String::format( "commit %s --cleanup=whitespace --allow-empty --file=%s", opts, tmpPath ),
|
|
projectDir, buf );
|
|
FileSystem::fileRemove( tmpPath );
|
|
Git::Result res;
|
|
res.returnCode = retCode;
|
|
res.result = buf;
|
|
return res;
|
|
}
|
|
|
|
Git::Result Git::fetch( const std::string& projectDir ) {
|
|
return gitSimple( "fetch --all --prune", projectDir );
|
|
}
|
|
|
|
Git::Result Git::fastForwardMerge( const std::string& projectDir ) {
|
|
return gitSimple( "merge --no-commit --ff --ff-only", projectDir );
|
|
}
|
|
|
|
Git::Result Git::updateRef( const std::string& headBranch, const std::string& toCommit,
|
|
const std::string& projectDir ) {
|
|
return gitSimple( String::format( "update-ref refs/heads/%s %s", headBranch, toCommit ),
|
|
projectDir );
|
|
}
|
|
|
|
Git::CountResult Git::branchHistoryPosition( const std::string& localBranch,
|
|
const std::string& remoteBranch,
|
|
const std::string& projectDir ) {
|
|
std::string buf;
|
|
int retCode =
|
|
git( String::format( "rev-list --left-right --count %s...%s", localBranch, remoteBranch ),
|
|
projectDir, buf );
|
|
Git::CountResult res;
|
|
res.returnCode = retCode;
|
|
if ( res.success() ) {
|
|
String::trimInPlace( buf );
|
|
auto results = String::split( buf, '\t' );
|
|
if ( results.size() == 2 ) {
|
|
int64_t behind = 0;
|
|
int64_t ahead = 0;
|
|
if ( String::fromString( ahead, results[0] ) &&
|
|
String::fromString( behind, results[1] ) ) {
|
|
res.ahead = ahead;
|
|
res.behind = behind;
|
|
}
|
|
}
|
|
} else {
|
|
res.result = buf;
|
|
return res;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
Git::CountResult Git::branchHistoryPosition( const Branch& branch, const std::string& projectDir ) {
|
|
return branchHistoryPosition( branch.name, branch.remote, projectDir );
|
|
}
|
|
|
|
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;
|
|
res.returnCode = retCode;
|
|
res.result = buf;
|
|
res.branch = buf;
|
|
return res;
|
|
}
|
|
|
|
std::vector<Git::Branch> Git::getAllBranches( const std::string& projectDir ) {
|
|
return getAllBranchesAndTags( static_cast<RefType>( RefType::Head | RefType::Remote ),
|
|
projectDir );
|
|
}
|
|
|
|
static void parseAheadBehind( std::string_view aheadBehind, Git::Branch& branch ) {
|
|
static constexpr auto BEHIND = "behind "sv;
|
|
static constexpr auto AHEAD = "ahead "sv;
|
|
if ( aheadBehind.empty() )
|
|
return;
|
|
auto split = String::split( aheadBehind, ',' );
|
|
for ( auto s : split ) {
|
|
s = String::trim( s );
|
|
if ( String::startsWith( s, BEHIND ) ) {
|
|
std::string numStr = std::string{ s.substr( BEHIND.size() ) };
|
|
int64_t val = 0;
|
|
if ( String::fromString( val, numStr ) )
|
|
branch.behind = val;
|
|
} else if ( String::startsWith( s, AHEAD ) ) {
|
|
std::string numStr = std::string{ s.substr( AHEAD.size() ) };
|
|
int64_t val = 0;
|
|
if ( String::fromString( val, numStr ) )
|
|
branch.ahead = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
Git::Branch parseLocalBranch( const std::string_view& raw ) {
|
|
auto split = String::split( raw, '\t', true );
|
|
if ( split.size() < 4 )
|
|
return {};
|
|
std::string name( std::string{ split[1] } );
|
|
std::string remote( std::string{ split[2] } );
|
|
std::string commitHash( std::string{ split[3] } );
|
|
auto ret = Git::Branch{ std::move( name ), std::move( remote ), Git::RefType::Head,
|
|
std::move( commitHash ) };
|
|
if ( split.size() > 4 )
|
|
parseAheadBehind( split[4], ret );
|
|
return ret;
|
|
}
|
|
|
|
static Git::Branch parseRemoteBranch( std::string_view raw ) {
|
|
auto split = String::split( raw, '\t', true );
|
|
if ( split.size() < 4 )
|
|
return {};
|
|
std::string name( std::string{ split[1] } );
|
|
std::string remote( std::string{ split[1] } );
|
|
std::string commitHash( std::string{ split[3] } );
|
|
auto ret = Git::Branch{ std::move( name ), std::move( remote ), Git::RefType::Remote,
|
|
std::move( commitHash ) };
|
|
if ( split.size() > 4 )
|
|
parseAheadBehind( split[4], ret );
|
|
return ret;
|
|
}
|
|
|
|
static Git::Branch parseTag( std::string_view raw ) {
|
|
auto split = String::split( raw, '\t', true );
|
|
if ( split.size() < 4 )
|
|
return {};
|
|
Git::Branch newBranch;
|
|
newBranch.name = std::string{ split[1] };
|
|
newBranch.lastCommit = std::string{ split[3] };
|
|
newBranch.type = Git::RefType::Tag;
|
|
if ( split.size() > 4 )
|
|
parseAheadBehind( split[4], newBranch );
|
|
return newBranch;
|
|
}
|
|
|
|
std::vector<Git::Branch> Git::getAllBranchesAndTags( RefType ref, std::string_view filterBranch,
|
|
const std::string& projectDir ) {
|
|
// clang-format off
|
|
std::string args( "for-each-ref --format '%(refname) %(refname:short) %(upstream:short) %(objectname) %(upstream:track,nobracket)' --sort=v:refname" );
|
|
// clang-format on
|
|
|
|
if ( filterBranch.empty() ) {
|
|
if ( ref & RefType::Head )
|
|
args.append( " refs/heads" );
|
|
if ( ref & RefType::Remote )
|
|
args.append( " refs/remotes" );
|
|
if ( ref & RefType::Tag )
|
|
args.append( " refs/tags" );
|
|
} else {
|
|
args.append( " " + filterBranch );
|
|
}
|
|
|
|
std::vector<Branch> branches;
|
|
std::string buf;
|
|
|
|
if ( EXIT_SUCCESS != git( args, projectDir, buf ) )
|
|
return branches;
|
|
|
|
getSubModules( projectDir );
|
|
|
|
branches.reserve( countLines( buf ) );
|
|
|
|
readAllLines( buf, [&]( const std::string_view& line ) {
|
|
auto branch = String::trim( String::trim( line, '\'' ), '\t' );
|
|
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/" ) ) {
|
|
branches.emplace_back( parseTag( branch ) );
|
|
}
|
|
} );
|
|
|
|
return branches;
|
|
}
|
|
|
|
std::vector<std::string> Git::fetchSubModules( const std::string& projectDir ) {
|
|
std::vector<std::string> submodules;
|
|
std::string buf;
|
|
FileSystem::fileGet( ( !projectDir.empty() ? projectDir : mProjectPath ) + ".gitmodules", buf );
|
|
LuaPattern pattern( "^%s*path%s*=%s*(.+)" );
|
|
readAllLines( buf, [&pattern, &submodules]( const std::string_view& line ) {
|
|
LuaPattern::Range matches[2];
|
|
if ( pattern.matches( line.data(), 0, matches, line.size() ) ) {
|
|
submodules.emplace_back( String::trim(
|
|
line.substr( matches[1].start, matches[1].end - matches[1].start ), '\n' ) );
|
|
}
|
|
} );
|
|
return submodules;
|
|
}
|
|
|
|
std::vector<std::string> Git::getSubModules( const std::string& projectDir ) {
|
|
Lock l( mSubModulesMutex );
|
|
if ( !mSubModulesUpdated ) {
|
|
mSubModules = fetchSubModules( projectDir );
|
|
mSubModulesUpdated = true;
|
|
}
|
|
return mSubModules;
|
|
}
|
|
|
|
bool Git::hasSubmodules( const std::string& projectDir ) {
|
|
return ( !projectDir.empty() && FileSystem::fileExists( projectDir + ".gitmodules" ) ) ||
|
|
( !mProjectPath.empty() && FileSystem::fileExists( mProjectPath + ".gitmodules" ) );
|
|
}
|
|
|
|
std::string Git::repoName( const std::string& file, bool allowExactMatch,
|
|
const std::string& projectDir ) {
|
|
for ( const auto& subRepo : mSubModules ) {
|
|
if ( String::startsWith( file, subRepo ) &&
|
|
( allowExactMatch || file.size() != subRepo.size() ) )
|
|
return subRepo;
|
|
}
|
|
return FileSystem::fileNameFromPath( !projectDir.empty() ? projectDir : mProjectPath );
|
|
}
|
|
|
|
std::string Git::repoPath( const std::string& file ) {
|
|
for ( const auto& subRepo : mSubModules ) {
|
|
if ( String::startsWith( file, subRepo ) && file.size() != subRepo.size() )
|
|
return mProjectPath + subRepo;
|
|
}
|
|
return mProjectPath;
|
|
}
|
|
|
|
Git::Result Git::gitSimple( const std::string& cmd, const std::string& projectDir ) {
|
|
std::string buf;
|
|
int retCode = git( cmd, projectDir, buf );
|
|
Git::Result res;
|
|
res.returnCode = retCode;
|
|
res.result = buf;
|
|
return res;
|
|
}
|
|
|
|
Git::Status Git::status( bool recurseSubmodules, const std::string& projectDir ) {
|
|
static constexpr auto DIFF_CMD = "diff --numstat";
|
|
static constexpr auto DIFF_STAGED_CMD = "diff --numstat --staged";
|
|
static constexpr auto STATUS_CMD = "-c color.status=never status -b -u -s";
|
|
Status s;
|
|
std::string buf;
|
|
|
|
getSubModules( projectDir );
|
|
bool submodules = hasSubmodules( projectDir );
|
|
|
|
LuaPattern subModulePattern( "^Entering '(.*)'" );
|
|
|
|
bool modifiedSubmodule = false;
|
|
auto parseStatus = [&s, &buf, &modifiedSubmodule, &projectDir, this, &subModulePattern]() {
|
|
std::string subModulePath = "";
|
|
LuaPattern pattern( "^([mMARTUD?%s][mMARTUD?%s])%s(.*)" );
|
|
size_t changesCount = countLines( buf );
|
|
|
|
if ( changesCount > 1000 )
|
|
return;
|
|
|
|
readAllLines( buf, [&]( const std::string_view& line ) {
|
|
LuaPattern::Range matches[3];
|
|
if ( subModulePattern.matches( line.data(), 0, matches, line.size() ) ) {
|
|
subModulePath = String::trim(
|
|
line.substr( matches[1].start, matches[1].end - matches[1].start ) );
|
|
FileSystem::dirAddSlashAtEnd( subModulePath );
|
|
} else if ( pattern.matches( line.data(), 0, matches, line.size() ) ) {
|
|
auto statusStr = line.substr( matches[1].start, matches[1].end - matches[1].start );
|
|
auto file = line.substr( matches[2].start, matches[2].end - matches[2].start );
|
|
if ( statusStr.size() < 2 )
|
|
return;
|
|
|
|
auto status = statusFromShortStatusStr( statusStr );
|
|
|
|
if ( status.status == GitStatus::NotSet )
|
|
return;
|
|
|
|
if ( status.symbol == GitStatusChar::ModifiedSubmodule ) {
|
|
modifiedSubmodule = true;
|
|
return;
|
|
}
|
|
|
|
bool isStagedAndModified =
|
|
status.type == GitStatusType::Staged && statusStr[1] != ' ';
|
|
|
|
auto filePath = subModulePath + file;
|
|
auto repo = repoName( filePath, false, projectDir );
|
|
auto repoIt = s.files.find( repo );
|
|
bool found = false;
|
|
if ( repoIt != s.files.end() ) {
|
|
for ( auto& fileIt : repoIt->second ) {
|
|
if ( fileIt.file == filePath ) {
|
|
fileIt.report = status;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ( !found ) {
|
|
s.files[repo].push_back( { std::move( filePath ), 0, 0, status } );
|
|
|
|
if ( isStagedAndModified ) {
|
|
status.type = GitStatusType::Changed;
|
|
|
|
s.files[repo].push_back( { std::move( filePath ), 0, 0, status } );
|
|
}
|
|
}
|
|
}
|
|
} );
|
|
};
|
|
|
|
if ( EXIT_SUCCESS != git( STATUS_CMD, projectDir, buf ) )
|
|
return s;
|
|
|
|
parseStatus();
|
|
|
|
if ( modifiedSubmodule && recurseSubmodules && submodules ) {
|
|
gitSubmodules( STATUS_CMD, projectDir, buf );
|
|
parseStatus();
|
|
}
|
|
|
|
auto parseNumStat = [&s, &buf, &projectDir, this, &subModulePattern]( bool isStaged ) {
|
|
LuaPattern pattern( "(%d+)%s+(%d+)%s+(.+)" );
|
|
std::string subModulePath = "";
|
|
readAllLines( buf, [&]( const std::string_view& line ) {
|
|
LuaPattern::Range matches[4];
|
|
if ( subModulePattern.matches( line.data(), 0, matches, line.size() ) ) {
|
|
subModulePath = String::trim(
|
|
line.substr( matches[1].start, matches[1].end - matches[1].start ) );
|
|
FileSystem::dirAddSlashAtEnd( subModulePath );
|
|
} else if ( pattern.matches( line.data(), 0, matches, line.size() ) ) {
|
|
auto inserted = line.substr( matches[1].start, matches[1].end - matches[1].start );
|
|
auto deleted = line.substr( matches[2].start, matches[2].end - matches[2].start );
|
|
auto file = line.substr( matches[3].start, matches[3].end - matches[3].start );
|
|
int inserts;
|
|
int deletes;
|
|
if ( String::fromString( inserts, inserted ) &&
|
|
String::fromString( deletes, deleted ) && ( inserts || deletes ) ) {
|
|
auto filePath = subModulePath + file;
|
|
auto repo = repoName( filePath, false, projectDir );
|
|
auto repoIt = s.files.find( repo );
|
|
GitStatusReport status = { GitStatus::NotSet, GitStatusType::Untracked,
|
|
GitStatusChar::Untracked };
|
|
bool found = false;
|
|
if ( repoIt != s.files.end() ) {
|
|
for ( auto& fileIt : repoIt->second ) {
|
|
if ( fileIt.file == filePath ) {
|
|
if ( isStaged && fileIt.report.type != Git::GitStatusType::Staged )
|
|
continue;
|
|
if ( !isStaged && fileIt.report.type == Git::GitStatusType::Staged )
|
|
continue;
|
|
fileIt.inserts = inserts;
|
|
fileIt.deletes = deletes;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ( !found ) {
|
|
s.files[repo].push_back(
|
|
{ std::move( filePath ), inserts, deletes, status } );
|
|
}
|
|
s.totalInserts += inserts;
|
|
s.totalDeletions += deletes;
|
|
}
|
|
}
|
|
} );
|
|
};
|
|
|
|
if ( EXIT_SUCCESS != git( DIFF_CMD, projectDir, buf ) )
|
|
return s;
|
|
|
|
parseNumStat( false );
|
|
|
|
git( DIFF_STAGED_CMD, projectDir, buf );
|
|
parseNumStat( true );
|
|
|
|
if ( recurseSubmodules && submodules ) {
|
|
gitSubmodules( DIFF_CMD, projectDir, buf );
|
|
parseNumStat( false );
|
|
|
|
gitSubmodules( DIFF_STAGED_CMD, projectDir, buf );
|
|
parseNumStat( true );
|
|
}
|
|
|
|
for ( auto& [_, repo] : s.files ) {
|
|
for ( auto& val : repo ) {
|
|
if ( val.report.symbol == GitStatusChar::Added && val.inserts == 0 ) {
|
|
std::string fileText;
|
|
FileSystem::fileGet( ( projectDir.empty() ? mProjectPath : projectDir ) + val.file,
|
|
fileText );
|
|
val.inserts = countLines( fileText );
|
|
s.totalInserts += val.inserts;
|
|
}
|
|
}
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
Git::Blame Git::blame( const std::string& filepath, std::size_t line ) const {
|
|
std::string buf;
|
|
const auto getText = [&buf]( const std::string_view& txt ) -> std::string {
|
|
std::string search = "\n" + txt + " ";
|
|
auto pos = buf.find( search );
|
|
if ( pos != std::string::npos ) {
|
|
pos = pos + search.length();
|
|
auto endPos = buf.find_first_of( '\n', pos );
|
|
if ( endPos != std::string::npos )
|
|
return buf.substr( pos, endPos - pos );
|
|
}
|
|
return "";
|
|
};
|
|
|
|
std::string workingDir( FileSystem::fileRemoveFileName( filepath ) );
|
|
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: " ) )
|
|
return { buf.substr( 7 ) };
|
|
|
|
auto hashEnd = buf.find_first_of( ' ' );
|
|
|
|
if ( hashEnd == std::string::npos )
|
|
return { "No commit hash found" };
|
|
|
|
auto commitHash = buf.substr( 0, hashEnd );
|
|
|
|
if ( commitHash == sNotCommitedYetHash )
|
|
return { "Not Committed Yet" };
|
|
|
|
auto author = getText( "author"sv );
|
|
auto authorEmail = getText( "author-mail"sv );
|
|
if ( authorEmail.size() > 3 )
|
|
authorEmail = authorEmail.substr( 1, authorEmail.size() - 2 );
|
|
auto datetime = getText( "author-time"sv );
|
|
auto tz = getText( "author-tz"sv );
|
|
Uint64 epoch;
|
|
if ( !datetime.empty() && String::fromString( epoch, datetime ) )
|
|
datetime = Sys::epochToString( epoch ) + ( tz.empty() ? "" : " " + tz );
|
|
|
|
auto commitMessage = getText( "summary"sv );
|
|
|
|
git( String::format( "rev-parse --short %s", commitHash ), workingDir, buf );
|
|
|
|
auto commitShortHash = String::rTrim( buf, '\n' );
|
|
|
|
return { std::move( author ),
|
|
std::move( authorEmail ),
|
|
std::move( datetime ),
|
|
std::move( commitHash ),
|
|
std::move( commitShortHash ),
|
|
std::move( commitMessage ),
|
|
line };
|
|
}
|
|
|
|
Git::GitStatusReport Git::statusFromShortStatusStr( const std::string_view& statusStr ) {
|
|
Uint16 status = git_xy( statusStr[0], statusStr[1] );
|
|
GitStatus gitStatus = GitStatus::NotSet;
|
|
GitStatusChar gitStatusChar = GitStatusChar::Unknown;
|
|
GitStatusType gitStatusType = GitStatusType::Untracked;
|
|
|
|
switch ( status ) {
|
|
case StatusXY::DD: {
|
|
gitStatus = GitStatus::Unmerge_BothDeleted;
|
|
gitStatusChar = GitStatusChar::Deleted;
|
|
gitStatusType = GitStatusType::Unmerged;
|
|
break;
|
|
}
|
|
case StatusXY::AU: {
|
|
gitStatus = GitStatus::Unmerge_AddedByUs;
|
|
gitStatusChar = GitStatusChar::Added;
|
|
gitStatusType = GitStatusType::Unmerged;
|
|
break;
|
|
}
|
|
case StatusXY::UD: {
|
|
gitStatus = GitStatus::Unmerge_DeletedByThem;
|
|
gitStatusChar = GitStatusChar::Deleted;
|
|
gitStatusType = GitStatusType::Unmerged;
|
|
break;
|
|
}
|
|
case StatusXY::UA: {
|
|
gitStatus = GitStatus::Unmerge_AddedByThem;
|
|
gitStatusChar = GitStatusChar::Added;
|
|
gitStatusType = GitStatusType::Unmerged;
|
|
break;
|
|
}
|
|
case StatusXY::DU: {
|
|
gitStatus = GitStatus::Unmerge_DeletedByUs;
|
|
gitStatusChar = GitStatusChar::Deleted;
|
|
gitStatusType = GitStatusType::Unmerged;
|
|
break;
|
|
}
|
|
case StatusXY::AA: {
|
|
gitStatus = GitStatus::Unmerge_BothAdded;
|
|
gitStatusChar = GitStatusChar::Added;
|
|
gitStatusType = GitStatusType::Unmerged;
|
|
break;
|
|
}
|
|
case StatusXY::UU: {
|
|
gitStatus = GitStatus::Unmerge_BothModified;
|
|
gitStatusChar = GitStatusChar::Modified;
|
|
gitStatusType = GitStatusType::Unmerged;
|
|
break;
|
|
}
|
|
case StatusXY::QQ: {
|
|
gitStatus = GitStatus::Untracked;
|
|
gitStatusChar = GitStatusChar::Untracked;
|
|
gitStatusType = GitStatusType::Untracked;
|
|
break;
|
|
}
|
|
case StatusXY::II: {
|
|
gitStatus = GitStatus::Ignored;
|
|
gitStatusChar = GitStatusChar::Ignored;
|
|
gitStatusType = GitStatusType::Ignored;
|
|
break;
|
|
}
|
|
case StatusXY::m: {
|
|
gitStatus = GitStatus::WorkingTree_ModifiedSubmodule;
|
|
gitStatusChar = GitStatusChar::ModifiedSubmodule;
|
|
gitStatusType = GitStatusType::Changed;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if ( gitStatus == GitStatus::NotSet ) {
|
|
char x = statusStr[0];
|
|
|
|
switch ( x ) {
|
|
case 'M': {
|
|
gitStatus = GitStatus::Index_Modified;
|
|
gitStatusChar = GitStatusChar::Modified;
|
|
gitStatusType = GitStatusType::Staged;
|
|
break;
|
|
}
|
|
case 'A': {
|
|
gitStatus = GitStatus::Index_Added;
|
|
gitStatusChar = GitStatusChar::Added;
|
|
gitStatusType = GitStatusType::Staged;
|
|
break;
|
|
}
|
|
case 'D': {
|
|
gitStatus = GitStatus::Index_Deleted;
|
|
gitStatusChar = GitStatusChar::Deleted;
|
|
gitStatusType = GitStatusType::Staged;
|
|
break;
|
|
}
|
|
case 'R': {
|
|
gitStatus = GitStatus::Index_Renamed;
|
|
gitStatusChar = GitStatusChar::Renamed;
|
|
gitStatusType = GitStatusType::Staged;
|
|
break;
|
|
}
|
|
case 'C': {
|
|
gitStatus = GitStatus::Index_Copied;
|
|
gitStatusChar = GitStatusChar::Copied;
|
|
gitStatusType = GitStatusType::Staged;
|
|
break;
|
|
}
|
|
case 'm': {
|
|
gitStatus = GitStatus::Index_ModifiedSubmodule;
|
|
gitStatusChar = GitStatusChar::ModifiedSubmodule;
|
|
gitStatusType = GitStatusType::Staged;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( gitStatus == GitStatus::NotSet ) {
|
|
char y = statusStr[1];
|
|
switch ( y ) {
|
|
case 'M': {
|
|
gitStatus = GitStatus::WorkingTree_Modified;
|
|
gitStatusChar = GitStatusChar::Modified;
|
|
gitStatusType = GitStatusType::Changed;
|
|
break;
|
|
}
|
|
case 'D': {
|
|
gitStatus = GitStatus::WorkingTree_Deleted;
|
|
gitStatusChar = GitStatusChar::Deleted;
|
|
gitStatusType = GitStatusType::Changed;
|
|
break;
|
|
}
|
|
case 'A': {
|
|
gitStatus = GitStatus::WorkingTree_IntentToAdd;
|
|
gitStatusChar = GitStatusChar::Added;
|
|
gitStatusType = GitStatusType::Changed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return { gitStatus, gitStatusType, gitStatusChar };
|
|
}
|
|
|
|
} // namespace ecode
|