Implement very basic git support

Only the local index is considered, no remote repo. Also getting the revision
(aka commit hash) is missing. How push and pull could be integrated with the
LyX VCS interface needs to be discussed, but the implemented functionality was
quite straight forward.
This commit is contained in:
Georg Baum 2013-02-06 22:07:31 +01:00
parent a3e6c7830c
commit 2f9ef2b98a
3 changed files with 476 additions and 10 deletions

View File

@ -53,6 +53,8 @@ bool LyXVC::fileInVC(FileName const & fn)
return true;
if (!SVN::findFile(fn).empty())
return true;
if (!GIT::findFile(fn).empty())
return true;
return false;
}
@ -75,6 +77,11 @@ bool LyXVC::file_found_hook(FileName const & fn)
vcs.reset(new SVN(found_file, owner_));
return true;
}
// Check if file is under GIT
if (!(found_file = GIT::findFile(fn)).empty()) {
vcs.reset(new GIT(found_file, owner_));
return true;
}
// file is not under any VCS.
vcs.reset(0);
@ -90,7 +97,8 @@ bool LyXVC::file_not_found_hook(FileName const & fn)
bool foundRCS = !RCS::findFile(fn).empty();
bool foundCVS = foundRCS ? false : !CVS::findFile(fn).empty();
bool foundSVN = (foundRCS || foundCVS) ? false : !SVN::findFile(fn).empty();
if (foundRCS || foundCVS || foundSVN) {
bool foundGIT = (foundRCS || foundCVS || foundSVN) ? false : !GIT::findFile(fn).empty();
if (foundRCS || foundCVS || foundSVN || foundGIT) {
docstring const file = makeDisplayPath(fn.absFileName(), 20);
docstring const text =
bformat(_("Do you want to retrieve the document"
@ -108,8 +116,10 @@ bool LyXVC::file_not_found_hook(FileName const & fn)
return RCS::retrieve(fn);
else if (foundCVS)
return CVS::retrieve(fn);
else
else if (foundSVN)
return SVN::retrieve(fn);
else
return GIT::retrieve(fn);
}
}
return false;
@ -139,8 +149,14 @@ bool LyXVC::registrer()
//check in the root directory of the document
FileName const cvs_entries(onlyPath(filename.absFileName()) + "/CVS/Entries");
FileName const svn_entries(onlyPath(filename.absFileName()) + "/.svn/entries");
FileName const git_index(onlyPath(filename.absFileName()) + "/.git/index");
if (svn_entries.isReadableFile()) {
if (git_index.isReadableFile()) {
LYXERR(Debug::LYXVC, "LyXVC: registering "
<< to_utf8(filename.displayName()) << " with GIT");
vcs.reset(new GIT(git_index, owner_));
} else if (svn_entries.isReadableFile()) {
LYXERR(Debug::LYXVC, "LyXVC: registering "
<< to_utf8(filename.displayName()) << " with SVN");
vcs.reset(new SVN(svn_entries, owner_));
@ -377,7 +393,9 @@ bool LyXVC::renameEnabled() const
bool LyXVC::copyEnabled() const
{
return inUse();
if (!inUse())
return false;
return vcs->copyEnabled();
}

View File

@ -246,6 +246,12 @@ string RCS::rename(support::FileName const & /*newFile*/, string const & /*msg*/
}
bool RCS::copyEnabled()
{
return true;
}
string RCS::copy(support::FileName const & newFile, string const & msg)
{
// RCS has no real copy command, so we create a poor mans version
@ -761,6 +767,12 @@ string CVS::rename(support::FileName const & newFile, string const & msg)
}
bool CVS::copyEnabled()
{
return true;
}
string CVS::copy(support::FileName const & newFile, string const & msg)
{
// CVS has no real copy command, so we create a poor mans version
@ -1153,6 +1165,7 @@ FileName const SVN::findFile(FileName const & file)
bool found = 0 == doVCCommandCall("svn info " + quoteName(fname)
+ " > " + quoteName(tmpf.toFilesystemEncoding()),
file.onlyPath());
tmpf.removeFile();
LYXERR(Debug::LYXVC, "SVN control: " << (found ? "enabled" : "disabled"));
return found ? file : FileName();
}
@ -1274,6 +1287,12 @@ string SVN::rename(support::FileName const & newFile, string const & msg)
}
bool SVN::copyEnabled()
{
return true;
}
string SVN::copy(support::FileName const & newFile, string const & msg)
{
// svn copy does not require a log message, since it does not commit.
@ -1616,13 +1635,13 @@ bool SVN::undoLastEnabled()
string SVN::revisionInfo(LyXVC::RevisionInfo const info)
{
if (info == LyXVC::Tree) {
if (rev_tree_cache_.empty())
if (!getTreeRevisionInfo())
rev_tree_cache_ = "?";
if (rev_tree_cache_ == "?")
return string();
if (rev_tree_cache_.empty())
if (!getTreeRevisionInfo())
rev_tree_cache_ = "?";
if (rev_tree_cache_ == "?")
return string();
return rev_tree_cache_;
return rev_tree_cache_;
}
// fill the rest of the attributes for a single file
@ -1786,4 +1805,343 @@ bool SVN::toggleReadOnlyEnabled()
}
/////////////////////////////////////////////////////////////////////
//
// GIT
//
/////////////////////////////////////////////////////////////////////
GIT::GIT(FileName const & m, Buffer * b) : VCS(b)
{
master_ = m;
scanMaster();
}
FileName const GIT::findFile(FileName const & file)
{
// First we check the existence of repository meta data.
if (!VCS::checkparentdirs(file, ".git")) {
LYXERR(Debug::LYXVC, "Cannot find GIT meta data for " << file);
return FileName();
}
// Now we check the status of the file.
FileName tmpf = FileName::tempName("lyxvcout");
if (tmpf.empty()) {
LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
return FileName();
}
// --porcelain selects a format that is supposed to be stable across
// git versions
string const fname = onlyFileName(file.absFileName());
LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under git control for `"
<< fname << '\'');
bool found = 0 == doVCCommandCall("git status --porcelain " +
quoteName(fname) + " > " +
quoteName(tmpf.toFilesystemEncoding()),
file.onlyPath());
if (found)
{
// The output contains a line starting with "??" for unknown
// files, no line for known unmodified files and a line
// starting with "M" or something else for modified/deleted
// etc. files.
ifstream ifs(tmpf.toFilesystemEncoding().c_str());
string test;
if ((ifs >> test))
found = (test != "??");
// else is no error
}
tmpf.removeFile();
LYXERR(Debug::LYXVC, "GIT control: " << (found ? "enabled" : "disabled"));
return found ? file : FileName();
}
void GIT::scanMaster()
{
// vcstatus code other than UNVERSIONED is somewhat superflous,
// until we want to implement read-only toggle for git.
FileName f = findFile(owner_->fileName());
if (f.empty())
vcstatus = UNVERSIONED;
else
vcstatus = NOLOCKING;
}
bool GIT::retrieve(FileName const & file)
{
LYXERR(Debug::LYXVC, "LyXVC::GIT: retrieve.\n\t" << file);
// The caller ensures that file does not exist, so no need to check that.
return doVCCommandCall("git checkout -q " + quoteName(file.onlyFileName()),
file.onlyPath()) == 0;
}
void GIT::registrer(string const & /*msg*/)
{
doVCCommand("git add " + quoteName(onlyFileName(owner_->absFileName())),
FileName(owner_->filePath()));
}
bool GIT::renameEnabled()
{
return true;
}
string GIT::rename(support::FileName const & newFile, string const & msg)
{
// git mv does not require a log message, since it does not commit.
// In LyX we commit immediately afterwards, otherwise it could be
// confusing to the user to have two uncommitted files.
FileName path(owner_->filePath());
string relFile(to_utf8(newFile.relPath(path.absFileName())));
string cmd("git mv " + quoteName(onlyFileName(owner_->absFileName())) +
' ' + quoteName(relFile));
if (doVCCommand(cmd, path)) {
cmd = "git checkout -q " +
quoteName(onlyFileName(owner_->absFileName())) + ' ' +
quoteName(relFile);
doVCCommand(cmd, path);
if (newFile.exists())
newFile.removeFile();
return string();
}
vector<support::FileName> f;
f.push_back(owner_->fileName());
f.push_back(newFile);
string log;
if (checkIn(f, msg, log) != LyXVC::Success) {
cmd = "git checkout -q " +
quoteName(onlyFileName(owner_->absFileName())) + ' ' +
quoteName(relFile);
doVCCommand(cmd, path);
if (newFile.exists())
newFile.removeFile();
return string();
}
return log;
}
bool GIT::copyEnabled()
{
return false;
}
string GIT::copy(support::FileName const & /*newFile*/, string const & /*msg*/)
{
// git does not support copy with history preservation
return string();
}
LyXVC::CommandResult GIT::checkIn(string const & msg, string & log)
{
vector<support::FileName> f(1, owner_->fileName());
return checkIn(f, msg, log);
}
LyXVC::CommandResult
GIT::checkIn(vector<support::FileName> const & f, string const & msg, string & log)
{
FileName tmpf = FileName::tempName("lyxvcout");
if (tmpf.empty()){
LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
log = N_("Error: Could not generate logfile.");
return LyXVC::ErrorBefore;
}
ostringstream os;
os << "git commit -m \"" << msg << '"';
for (size_t i = 0; i < f.size(); ++i)
os << ' ' << quoteName(f[i].onlyFileName());
os << " > " << quoteName(tmpf.toFilesystemEncoding());
LyXVC::CommandResult ret =
doVCCommand(os.str(), FileName(owner_->filePath())) ?
LyXVC::ErrorCommand : LyXVC::Success;
string res = scanLogFile(tmpf, log);
if (!res.empty()) {
frontend::Alert::error(_("Revision control error."),
_("Error when committing to repository.\n"
"You have to manually resolve the problem.\n"
"LyX will reopen the document after you press OK."));
ret = LyXVC::ErrorCommand;
}
tmpf.removeFile();
if (!log.empty())
log.insert(0, "GIT: ");
if (ret == LyXVC::Success && log.empty())
log = "GIT: Proceeded";
return ret;
}
bool GIT::checkInEnabled()
{
return true;
}
bool GIT::isCheckInWithConfirmation()
{
// FIXME one day common getDiff and perhaps OpMode for all backends
FileName tmpf = FileName::tempName("lyxvcout");
if (tmpf.empty()) {
LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
return true;
}
doVCCommandCall("git diff " + quoteName(owner_->absFileName())
+ " > " + quoteName(tmpf.toFilesystemEncoding()),
FileName(owner_->filePath()));
docstring diff = tmpf.fileContents("UTF-8");
tmpf.removeFile();
if (diff.empty())
return false;
return true;
}
// FIXME Correctly return code should be checked instead of this.
// This would need another solution than just plain startscript.
// Hint from Andre': QProcess::readAllStandardError()...
string GIT::scanLogFile(FileName const & f, string & status)
{
ifstream ifs(f.toFilesystemEncoding().c_str());
string line;
while (ifs) {
getline(ifs, line);
LYXERR(Debug::LYXVC, line << "\n");
if (!line.empty())
status += line + "; ";
if (prefixIs(line, "C ") || prefixIs(line, "CU ")
|| contains(line, "Commit failed")) {
ifs.close();
return line;
}
}
ifs.close();
return string();
}
string GIT::checkOut()
{
return string();
}
bool GIT::checkOutEnabled()
{
return false;
}
string GIT::repoUpdate()
{
return string();
}
bool GIT::repoUpdateEnabled()
{
return false;
}
string GIT::lockingToggle()
{
return string();
}
bool GIT::lockingToggleEnabled()
{
return false;
}
bool GIT::revert()
{
// Reverts to the version in GIT repository and
// gets the updated version from the repository.
string const fil = quoteName(onlyFileName(owner_->absFileName()));
if (doVCCommand("git checkout -q " + fil,
FileName(owner_->filePath())))
return false;
owner_->markClean();
return true;
}
bool GIT::isRevertWithConfirmation()
{
//FIXME owner && diff
return true;
}
void GIT::undoLast()
{
// merge the current with the previous version
// in a reverse patch kind of way, so that the
// result is to revert the last changes.
lyxerr << "Sorry, not implemented." << endl;
}
bool GIT::undoLastEnabled()
{
return false;
}
string GIT::revisionInfo(LyXVC::RevisionInfo const /*info*/)
{
return string();
}
void GIT::getLog(FileName const & tmpf)
{
doVCCommand("git log " + quoteName(onlyFileName(owner_->absFileName()))
+ " > " + quoteName(tmpf.toFilesystemEncoding()),
FileName(owner_->filePath()));
}
bool GIT::prepareFileRevision(string const & /*revis*/, string & /*f*/)
{
return false;
}
bool GIT::prepareFileRevisionEnabled()
{
return false;
}
bool GIT::toggleReadOnlyEnabled()
{
return false;
}
} // namespace lyx

View File

@ -47,6 +47,8 @@ public:
virtual bool renameEnabled() = 0;
/// rename a file. Return non-empty log on success, empty log on failure.
virtual std::string rename(support::FileName const &, std::string const &) = 0;
/// can this operation be processed in the current VCS?
virtual bool copyEnabled() = 0;
/// copy a file. Return non-empty log on success, empty log on failure.
virtual std::string copy(support::FileName const &, std::string const &) = 0;
/// check in the current revision.
@ -148,6 +150,7 @@ public:
/// return the revision file for the given file, if found
static support::FileName const findFile(support::FileName const & file);
/// get file from repo, the caller must ensure that it does not exist locally
static bool retrieve(support::FileName const & file);
virtual void registrer(std::string const & msg);
@ -156,6 +159,8 @@ public:
virtual std::string rename(support::FileName const &, std::string const &);
virtual bool copyEnabled();
virtual std::string copy(support::FileName const &, std::string const &);
virtual LyXVC::CommandResult
@ -229,6 +234,7 @@ public:
/// return the revision file for the given file, if found
static support::FileName const findFile(support::FileName const & file);
/// get file from repo, the caller must ensure that it does not exist locally
static bool retrieve(support::FileName const & file);
virtual void registrer(std::string const & msg);
@ -237,6 +243,8 @@ public:
virtual std::string rename(support::FileName const &, std::string const &);
virtual bool copyEnabled();
virtual std::string copy(support::FileName const &, std::string const &);
virtual LyXVC::CommandResult
@ -365,6 +373,7 @@ public:
/// return the revision file for the given file, if found
static support::FileName const findFile(support::FileName const & file);
/// get file from repo, the caller must ensure that it does not exist locally
static bool retrieve(support::FileName const & file);
virtual void registrer(std::string const & msg);
@ -373,6 +382,8 @@ public:
virtual std::string rename(support::FileName const &, std::string const &);
virtual bool copyEnabled();
virtual std::string copy(support::FileName const &, std::string const &);
virtual LyXVC::CommandResult
@ -454,6 +465,85 @@ private:
std::string rev_tree_cache_;
};
/**
* Very basic git support:
* Remote repos are completely ignored, only the local tree is considered.
* How push and pull could be integrated with the LyX VCS interface needs
* to be discussed.
*/
class GIT : public VCS {
public:
///
explicit
GIT(support::FileName const & m, Buffer * b);
/// return the revision file for the given file, if found
static support::FileName const findFile(support::FileName const & file);
/// get file from repo, the caller must ensure that it does not exist locally
static bool retrieve(support::FileName const & file);
virtual void registrer(std::string const & msg);
virtual bool renameEnabled();
virtual std::string rename(support::FileName const &, std::string const &);
virtual bool copyEnabled();
virtual std::string copy(support::FileName const &, std::string const &);
virtual LyXVC::CommandResult
checkIn(std::string const & msg, std::string & log);
virtual bool checkInEnabled();
virtual bool isCheckInWithConfirmation();
virtual std::string checkOut();
virtual bool checkOutEnabled();
virtual std::string repoUpdate();
virtual bool repoUpdateEnabled();
virtual std::string lockingToggle();
virtual bool lockingToggleEnabled();
virtual bool revert();
virtual bool isRevertWithConfirmation();
virtual void undoLast();
virtual bool undoLastEnabled();
virtual void getLog(support::FileName const &);
virtual std::string const versionString() const {
return "GIT: ?";
}
virtual bool toggleReadOnlyEnabled();
virtual std::string revisionInfo(LyXVC::RevisionInfo const info);
virtual bool prepareFileRevision(std::string const & rev, std::string & f);
virtual bool prepareFileRevisionEnabled();
protected:
virtual void scanMaster();
/// Check for messages in svn output. Returns error.
std::string scanLogFile(support::FileName const & f, std::string & status);
/// Check in files \p f with log \p msg
LyXVC::CommandResult checkIn(std::vector<support::FileName> const & f,
std::string const & msg, std::string & log);
};
} // namespace lyx
#endif // VCBACKEND_H