Files
eepp/src/tools/ecode/plugins/git/git.cpp
Martín Lucas Golini e953aa3687 Add UIAbstractView SelectionKind to set if Multiple selection is enabled.
Git plugin minor changes.
Replace getTranslatorString in favor of i18n when possible.
UIAbstractTableView allow to start editing with keyboard shortcuts.
Some micro-optimizations.
2024-02-06 00:42:56 -03:00

955 lines
30 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,
char sep = '\n' ) {
auto lastNL = 0;
auto nextNL = buf.find_first_of( sep );
while ( nextNL != std::string_view::npos ) {
onLineRead( buf.substr( lastNL, nextNL - lastNL ) );
lastNL = nextNL + 1;
nextNL = buf.find_first_of( sep, 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 );
if ( mLogLevel == LogLevel::Info ||
( mLogLevel >= LogLevel::Warning && retCode != EXIT_SUCCESS ) ) {
Log::instance()->writef( mLogLevel, "GitPlugin cmd in %s (%d): %s %s",
clock.getElapsedTime().toString(), retCode, mGitPath, args );
}
Log::debug( "%s", buf );
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::Result Git::pushNewBranch( const std::string& branch, const std::string& projectDir ) {
return gitSimple(
String::format(
"push --porcelain --recurse-submodules=check origin refs/heads/%s:refs/heads/%s",
branch, branch ),
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::stash( std::vector<std::string> files, const std::string& projectDir ) {
return gitSimple( String::format( "stash push --include-untracked -- %s", asList( files ) ),
projectDir );
}
Git::Result Git::restore( std::vector<std::string> files, const std::string& projectDir ) {
return gitSimple( String::format( "restore -- %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::diff( const std::string& file, bool isStaged, const std::string& projectDir ) {
return gitSimple( String::format( "diff%s \"%s\"", isStaged ? " --staged" : "", file ),
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::mergeBranch( const std::string& branch, const std::string& projectDir ) {
return gitSimple( String::format( "merge --no-ff %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 ) ) {
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 ) );
}
} );
}
if ( ( ref & RefType::Stash ) && EXIT_SUCCESS == git( "stash list", projectDir, buf ) ) {
branches.reserve( branches.size() + countLines( buf ) );
LuaPattern pattern( "(stash@{%d+}):%s(.*)" );
readAllLines( buf, [&]( const std::string_view& line ) {
LuaPattern::Range matches[3];
if ( pattern.matches( line.data(), 0, matches, line.size() ) ) {
std::string id(
line.substr( matches[1].start, matches[1].end - matches[1].start ) );
std::string name(
line.substr( matches[2].start, matches[2].end - matches[2].start ) );
Git::Branch newBranch;
newBranch.type = RefType::Stash;
newBranch.name = std::move( name );
newBranch.remote = std::move( id );
branches.emplace_back( std::move( newBranch ) );
}
} );
}
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( std::string file, bool allowExactMatch, const std::string& projectDir ) {
if ( String::startsWith( file, !projectDir.empty() ? projectDir : mProjectPath ) )
FileSystem::filePathRemoveBasePath( !projectDir.empty() ? projectDir : mProjectPath, file );
Lock l( mSubModulesMutex );
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 ) {
Lock l( mSubModulesMutex );
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;
std::vector<std::string> curSubModules;
{
Lock l( mSubModulesMutex );
curSubModules = mSubModules;
}
const auto isSubmodule = [&curSubModules]( const std::string_view& file ) -> bool {
return std::any_of( curSubModules.begin(), curSubModules.end(),
[&file]( const auto& submodule ) { return submodule == file; } );
};
auto parseStatus = [&s, &buf, &modifiedSubmodule, &projectDir, this, &subModulePattern,
submodules, &isSubmodule]() {
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;
}
if ( submodules && !mSubModules.empty() && isSubmodule( file ) )
modifiedSubmodule = true;
bool isStagedAndModified =
status.type == GitStatusType::Staged && statusStr[1] != ' ';
if ( status.symbol == GitStatusChar::Renamed ) {
LuaPattern rpattern( ".*%s%-%>%s(.*)" );
LuaPattern::Range rranges[2];
if ( rpattern.matches( file.data(), 0, rranges, file.size() ) )
file = file.substr( rranges[1].start, rranges[1].end - rranges[1].start );
}
std::string 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( { 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 );
std::string file = std::string{
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 ) ) {
LuaPattern pattern( "(.*)%{.*%s->%s(.*)%}" );
if ( pattern.matches( file.data(), 0, matches, file.size() ) ) {
file = file.substr( matches[1].start, matches[1].end - matches[1].start ) +
file.substr( matches[2].start, matches[2].end - matches[2].start );
}
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 };
}
Git::Result Git::stashPush( std::vector<std::string> files, const std::string& name, bool keepIndex,
const std::string& projectDir ) {
std::string args;
if ( keepIndex )
args += " --keep-index";
if ( !name.empty() )
args += " --message " + name;
return gitSimple(
String::format( "stash push --include-untracked %s -- %s", args, asList( files ) ),
projectDir );
}
Git::Result Git::stashApply( const std::string& stashId, bool restoreIndex,
const std::string& projectDir ) {
return gitSimple( String::format( "stash apply%s %s", restoreIndex ? " --index" : "", stashId ),
projectDir );
}
Git::Result Git::stashDrop( const std::string& stashId, const std::string& projectDir ) {
return gitSimple( String::format( "stash drop %s", stashId ), projectDir );
}
} // namespace ecode