X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2FVCBackend.cpp;h=963064c93a3ac77d71e562cf4b473e8a5f40a89a;hb=c7d29be153debac82e3d2e8865fcc849f0a5f40d;hp=623c4b7bbdf4f5f84f343e3e290ec84dd9503c61;hpb=1ec5b915afca6dd88d7242deb724d09b11b60d41;p=lyx.git diff --git a/src/VCBackend.cpp b/src/VCBackend.cpp index 623c4b7bbd..963064c93a 100644 --- a/src/VCBackend.cpp +++ b/src/VCBackend.cpp @@ -4,6 +4,7 @@ * Licence details can be found in the file COPYING. * * \author Lars Gullik Bjønnes + * \author Pavel Sanda * * Full author contact details are available in file CREDITS. */ @@ -12,29 +13,30 @@ #include "VCBackend.h" #include "Buffer.h" -#include "LyXFunc.h" +#include "DispatchResult.h" +#include "LyX.h" #include "FuncRequest.h" #include "frontends/alert.h" #include "frontends/Application.h" +#include "support/convert.h" #include "support/debug.h" #include "support/filetools.h" #include "support/gettext.h" #include "support/lstrings.h" -#include "support/Path.h" +#include "support/PathChanger.h" #include "support/Systemcall.h" - -#include +#include "support/regex.h" +#include "support/TempFile.h" #include +#include +#include using namespace std; using namespace lyx::support; -using boost::regex; -using boost::regex_match; -using boost::smatch; namespace lyx { @@ -44,11 +46,11 @@ int VCS::doVCCommandCall(string const & cmd, FileName const & path) LYXERR(Debug::LYXVC, "doVCCommandCall: " << cmd); Systemcall one; support::PathChanger p(path); - return one.startscript(Systemcall::Wait, cmd, false); + return one.startscript(Systemcall::Wait, cmd, string(), string(), false); } -int VCS::doVCCommand(string const & cmd, FileName const & path) +int VCS::doVCCommand(string const & cmd, FileName const & path, bool reportError) { if (owner_) owner_->setBusy(true); @@ -57,23 +59,78 @@ int VCS::doVCCommand(string const & cmd, FileName const & path) if (owner_) owner_->setBusy(false); - if (ret) + if (ret && reportError) { + docstring rcsmsg; + if (prefixIs(cmd, "ci ")) + rcsmsg = "\n" + _("Perhaps the RCS package is not installed on your system?"); frontend::Alert::error(_("Revision control error."), - bformat(_("Some problem occured while running the command:\n" - "'%1$s'."), + bformat(_("Some problem occurred while running the command:\n" + "'%1$s'.") + rcsmsg, from_utf8(cmd))); + } return ret; } +bool VCS::makeRCSRevision(string const &version, string &revis) const +{ + string rev = revis; + + if (isStrInt(rev)) { + int back = convert(rev); + // if positive use as the last number in the whole revision string + if (back > 0) { + string base; + rsplit(version, base , '.'); + rev = base + '.' + rev; + } + if (back == 0) + rev = version; + // we care about the last number from revision string + // in case of backward indexing + if (back < 0) { + string cur, base; + cur = rsplit(version, base , '.'); + if (!isStrInt(cur)) + return false; + int want = convert(cur) + back; + if (want <= 0) + return false; + + rev = base + '.' + convert(want); + } + } + + revis = rev; + return true; +} + + +bool VCS::checkparentdirs(FileName const & file, std::string const & vcsdir) +{ + FileName dirname = file.onlyPath(); + do { + FileName tocheck = FileName(addName(dirname.absFileName(), vcsdir)); + LYXERR(Debug::LYXVC, "check file: " << tocheck.absFileName()); + if (tocheck.exists()) + return true; + //this construct because of #8295 + dirname = FileName(dirname.absFileName()).parentPath(); + } while (!dirname.empty()); + return false; +} + + ///////////////////////////////////////////////////////////////////// // // RCS // ///////////////////////////////////////////////////////////////////// -RCS::RCS(FileName const & m) +RCS::RCS(FileName const & m, Buffer * b) : VCS(b) { + // Here we know that the buffer file is either already in RCS or + // about to be registered master_ = m; scanMaster(); } @@ -82,7 +139,7 @@ RCS::RCS(FileName const & m) FileName const RCS::findFile(FileName const & file) { // Check if *,v exists. - FileName tmp(file.absFilename() + ",v"); + FileName tmp(file.absFileName() + ",v"); LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under rcs: " << tmp); if (tmp.isReadableFile()) { LYXERR(Debug::LYXVC, "Yes, " << file << " is under rcs."); @@ -90,7 +147,7 @@ FileName const RCS::findFile(FileName const & file) } // Check if RCS/*,v exists. - tmp = FileName(addName(addPath(onlyPath(file.absFilename()), "RCS"), file.absFilename()) + ",v"); + tmp = FileName(addName(addPath(onlyPath(file.absFileName()), "RCS"), file.absFileName()) + ",v"); LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under rcs: " << tmp); if (tmp.isReadableFile()) { LYXERR(Debug::LYXVC, "Yes, " << file << " is under rcs."); @@ -101,11 +158,12 @@ FileName const RCS::findFile(FileName const & file) } -void RCS::retrieve(FileName const & file) +bool RCS::retrieve(FileName const & file) { LYXERR(Debug::LYXVC, "LyXVC::RCS: retrieve.\n\t" << file); - doVCCommandCall("co -q -r " + quoteName(file.toFilesystemEncoding()), - FileName()); + // The caller ensures that file does not exist, so no need to check that. + return doVCCommandCall("co -q -r " + quoteName(file.toFilesystemEncoding()), + FileName()) == 0; } @@ -117,6 +175,8 @@ void RCS::scanMaster() LYXERR(Debug::LYXVC, "LyXVC::RCS: scanMaster: " << master_); ifstream ifs(master_.toFilesystemEncoding().c_str()); + // limit the size of strings we read to avoid memory problems + ifs >> setw(65636); string token; bool read_enough = false; @@ -177,30 +237,95 @@ void RCS::registrer(string const & msg) string cmd = "ci -q -u -i -t-\""; cmd += msg; cmd += "\" "; - cmd += quoteName(onlyFilename(owner_->absFileName())); + cmd += quoteName(onlyFileName(owner_->absFileName())); doVCCommand(cmd, FileName(owner_->filePath())); } -string RCS::checkIn(string const & msg) +bool RCS::renameEnabled() +{ + return false; +} + + +string RCS::rename(support::FileName const & /*newFile*/, string const & /*msg*/) +{ + // not implemented, since a left-over file.lyx,v would be confusing. + return string(); +} + + +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 + support::FileName const oldFile(owner_->absFileName()); + if (!oldFile.copyTo(newFile)) + return string(); + FileName path(oldFile.onlyPath()); + string relFile(to_utf8(newFile.relPath(path.absFileName()))); + string cmd = "ci -q -u -i -t-\""; + cmd += msg; + cmd += "\" "; + cmd += quoteName(relFile); + return doVCCommand(cmd, path) ? string() : "RCS: Proceeded"; +} + + +LyXVC::CommandResult RCS::checkIn(string const & msg, string & log) { int ret = doVCCommand("ci -q -u -m\"" + msg + "\" " - + quoteName(onlyFilename(owner_->absFileName())), + + quoteName(onlyFileName(owner_->absFileName())), FileName(owner_->filePath())); - return ret ? string() : "RCS: Proceeded"; + if (ret) + return LyXVC::ErrorCommand; + log = "RCS: Proceeded"; + return LyXVC::VCSuccess; } bool RCS::checkInEnabled() { - return owner_ && !owner_->isReadonly(); + return owner_ && !owner_->hasReadonlyFlag(); +} + + +bool RCS::isCheckInWithConfirmation() +{ + // FIXME one day common getDiff for all backends + // docstring diff; + // if (getDiff(file, diff) && diff.empty()) + // return false; + + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return true; + } + + doVCCommandCall("rcsdiff " + quoteName(owner_->absFileName()) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + + docstring diff = tmpf.fileContents("UTF-8"); + + if (diff.empty()) + return false; + + return true; } string RCS::checkOut() { owner_->markClean(); - int ret = doVCCommand("co -q -l " + quoteName(onlyFilename(owner_->absFileName())), + int ret = doVCCommand("co -q -l " + quoteName(onlyFileName(owner_->absFileName())), FileName(owner_->filePath())); return ret ? string() : "RCS: Proceeded"; } @@ -208,7 +333,7 @@ string RCS::checkOut() bool RCS::checkOutEnabled() { - return owner_ && owner_->isReadonly(); + return owner_ && owner_->hasReadonlyFlag(); } @@ -227,6 +352,9 @@ bool RCS::repoUpdateEnabled() string RCS::lockingToggle() { + //FIXME this might be actually possible, study rcs -U, rcs -L. + //State should be easy to get inside scanMaster. + //It would fix #4370 and make rcs/svn usage even more closer. lyxerr << "Sorry, not implemented." << endl; return string(); } @@ -238,34 +366,43 @@ bool RCS::lockingToggleEnabled() } -void RCS::revert() +bool RCS::revert() { - doVCCommand("co -f -u" + version() + " " - + quoteName(onlyFilename(owner_->absFileName())), - FileName(owner_->filePath())); + if (doVCCommand("co -f -u" + version_ + ' ' + + quoteName(onlyFileName(owner_->absFileName())), + FileName(owner_->filePath()))) + return false; // We ignore changes and just reload! owner_->markClean(); + return true; +} + + +bool RCS::isRevertWithConfirmation() +{ + //FIXME owner && diff ? + return true; } void RCS::undoLast() { LYXERR(Debug::LYXVC, "LyXVC: undoLast"); - doVCCommand("rcs -o" + version() + " " - + quoteName(onlyFilename(owner_->absFileName())), + doVCCommand("rcs -o" + version_ + ' ' + + quoteName(onlyFileName(owner_->absFileName())), FileName(owner_->filePath())); } bool RCS::undoLastEnabled() { - return true; + return owner_->hasReadonlyFlag(); } void RCS::getLog(FileName const & tmpf) { - doVCCommand("rlog " + quoteName(onlyFilename(owner_->absFileName())) + doVCCommand("rlog " + quoteName(onlyFileName(owner_->absFileName())) + " > " + quoteName(tmpf.toFilesystemEncoding()), FileName(owner_->filePath())); } @@ -281,16 +418,119 @@ bool RCS::toggleReadOnlyEnabled() } +string RCS::revisionInfo(LyXVC::RevisionInfo const info) +{ + if (info == LyXVC::File) + return version_; + // fill the rest of the attributes for a single file + if (rev_date_cache_.empty()) + if (!getRevisionInfo()) + return string(); + + switch (info) { + case LyXVC::Author: + return rev_author_cache_; + case LyXVC::Date: + return rev_date_cache_; + case LyXVC::Time: + return rev_time_cache_; + default: + break; + } + + return string(); +} + + +bool RCS::getRevisionInfo() +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + doVCCommand("rlog -r " + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + + if (tmpf.empty()) + return false; + + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + string line; + + // we reached to the entry, i.e. after initial log message + bool entry=false; + // line with critical info, e.g: + //"date: 2011/07/02 11:02:54; author: sanda; state: Exp; lines: +17 -2" + string result; + + while (ifs) { + getline(ifs, line); + LYXERR(Debug::LYXVC, line); + if (entry && prefixIs(line, "date:")) { + result = line; + break; + } + if (prefixIs(line, "revision")) + entry = true; + } + if (result.empty()) + return false; + + rev_date_cache_ = token(result, ' ', 1); + rev_time_cache_ = rtrim(token(result, ' ', 2), ";"); + rev_author_cache_ = trim(token(token(result, ';', 1), ':', 1)); + + return !rev_author_cache_.empty(); +} + +bool RCS::prepareFileRevision(string const &revis, string & f) +{ + string rev = revis; + if (!VCS::makeRCSRevision(version_, rev)) + return false; + + TempFile tempfile("lyxvcrev_" + rev + '_'); + tempfile.setAutoRemove(false); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + + doVCCommand("co -p" + rev + ' ' + + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + tmpf.refresh(); + if (tmpf.isFileEmpty()) + return false; + + f = tmpf.absFileName(); + return true; +} + + +bool RCS::prepareFileRevisionEnabled() +{ + return true; +} + + ///////////////////////////////////////////////////////////////////// // // CVS // ///////////////////////////////////////////////////////////////////// -CVS::CVS(FileName const & m, FileName const & f) +CVS::CVS(FileName const & m, Buffer * b) : VCS(b) { + // Here we know that the buffer file is either already in CVS or + // about to be registered master_ = m; - file_ = f; + have_rev_info_ = false; scanMaster(); } @@ -299,8 +539,8 @@ FileName const CVS::findFile(FileName const & file) { // First we look for the CVS/Entries in the same dir // where we have file. - FileName const entries(onlyPath(file.absFilename()) + "/CVS/Entries"); - string const tmpf = '/' + onlyFilename(file.absFilename()) + '/'; + FileName const entries(onlyPath(file.absFileName()) + "/CVS/Entries"); + string const tmpf = '/' + onlyFileName(file.absFileName()) + '/'; LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under cvs in `" << entries << "' for `" << tmpf << '\''); if (entries.isReadableFile()) { @@ -324,7 +564,8 @@ void CVS::scanMaster() LYXERR(Debug::LYXVC, "LyXVC::CVS: scanMaster. \n Checking: " << master_); // Ok now we do the real scan... ifstream ifs(master_.toFilesystemEncoding().c_str()); - string tmpf = '/' + onlyFilename(file_.absFilename()) + '/'; + string name = onlyFileName(owner_->absFileName()); + string tmpf = '/' + name + '/'; LYXERR(Debug::LYXVC, "\tlooking for `" << tmpf << '\''); string line; static regex const reg("/(.*)/(.*)/(.*)/(.*)/(.*)"); @@ -333,8 +574,10 @@ void CVS::scanMaster() if (contains(line, tmpf)) { // Ok extract the fields. smatch sm; - - regex_match(line, sm, reg); + if (!regex_match(line, sm, reg)) { + LYXERR(Debug::LYXVC, "\t Cannot parse line. Skipping."); + continue; + } //sm[0]; // whole matched string //sm[1]; // filename @@ -343,21 +586,23 @@ void CVS::scanMaster() //sm[4]; // options //sm[5]; // tag or tagdate - // FIXME: must double check file is stattable/existing - time_t mod = file_.lastModified(); - string mod_date = rtrim(asctime(gmtime(&mod)), "\n"); - LYXERR(Debug::LYXVC, "Date in Entries: `" << file_date - << "'\nModification date of file: `" << mod_date << '\''); - //FIXME this whole locking bussiness is not working under cvs and the machinery - // conforms to the ci usage, not cvs. - if (file_date == mod_date) { - locker_ = "Unlocked"; - vcstatus = UNLOCKED; + FileName file(owner_->absFileName()); + if (file.isReadableFile()) { + time_t mod = file.lastModified(); + string mod_date = rtrim(asctime(gmtime(&mod)), "\n"); + LYXERR(Debug::LYXVC, "Date in Entries: `" << file_date + << "'\nModification date of file: `" << mod_date << '\''); + if (file.isReadOnly()) { + // readonly checkout is unlocked + vcstatus = UNLOCKED; + } else { + FileName bdir(addPath(master_.onlyPath().absFileName(),"Base")); + FileName base(addName(bdir.absFileName(),name)); + // if base version is existent "cvs edit" was used to lock + vcstatus = base.isReadableFile() ? LOCKED : NOLOCKING; + } } else { - // Here we should also to some more checking - // to see if there are conflicts or not. - locker_ = "Locked"; - vcstatus = LOCKED; + vcstatus = NOLOCKING; } break; } @@ -365,263 +610,1417 @@ void CVS::scanMaster() } -void CVS::registrer(string const & msg) +bool CVS::retrieve(FileName const & file) { - doVCCommand("cvs -q add -m \"" + msg + "\" " - + quoteName(onlyFilename(owner_->absFileName())), - FileName(owner_->filePath())); + LYXERR(Debug::LYXVC, "LyXVC::CVS: retrieve.\n\t" << file); + // The caller ensures that file does not exist, so no need to check that. + return doVCCommandCall("cvs -q update " + quoteName(file.toFilesystemEncoding()), + file.onlyPath()) == 0; } -string CVS::checkIn(string const & msg) +string const CVS::getTarget(OperationMode opmode) const { - int ret = doVCCommand("cvs -q commit -m \"" + msg + "\" " - + quoteName(onlyFilename(owner_->absFileName())), - FileName(owner_->filePath())); - return ret ? string() : "CVS: Proceeded"; + switch(opmode) { + case Directory: + // in client server mode CVS does not like full path operand for directory operation + // since LyX switches to the repo dir "." is good enough as target + return "."; + case File: + return quoteName(onlyFileName(owner_->absFileName())); + } + return string(); } -bool CVS::checkInEnabled() +docstring CVS::toString(CvsStatus status) const { - return true; + switch (status) { + case UpToDate: + return _("Up-to-date"); + case LocallyModified: + return _("Locally Modified"); + case LocallyAdded: + return _("Locally Added"); + case NeedsMerge: + return _("Needs Merge"); + case NeedsCheckout: + return _("Needs Checkout"); + case NoCvsFile: + return _("No CVS file"); + case StatusError: + return _("Cannot retrieve CVS status"); + } + return docstring(); } -string CVS::checkOut() +int CVS::doVCCommandWithOutput(string const & cmd, FileName const & path, + FileName const & output, bool reportError) { - // cvs update or perhaps for cvs this should be a noop - // we need to detect conflict (eg "C" in output) - // before we can do this. - lyxerr << "Sorry, not implemented." << endl; - return string(); + string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding()); + return doVCCommand(cmd + redirection, path, reportError); } -bool CVS::checkOutEnabled() +int CVS::doVCCommandCallWithOutput(std::string const & cmd, + support::FileName const & path, + support::FileName const & output) { - return false; + string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding()); + return doVCCommandCall(cmd + redirection, path); } -string CVS::repoUpdate() +CVS::CvsStatus CVS::getStatus() { - lyxerr << "Sorry, not implemented." << endl; - return string(); -} + TempFile tempfile("lyxvout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return StatusError; + } + if (doVCCommandCallWithOutput("cvs status " + getTarget(File), + FileName(owner_->filePath()), tmpf)) { + return StatusError; + } -bool CVS::repoUpdateEnabled() + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + CvsStatus status = NoCvsFile; + + while (ifs) { + string line; + getline(ifs, line); + LYXERR(Debug::LYXVC, line << '\n'); + if (prefixIs(line, "File:")) { + if (contains(line, "Up-to-date")) + status = UpToDate; + else if (contains(line, "Locally Modified")) + status = LocallyModified; + else if (contains(line, "Locally Added")) + status = LocallyAdded; + else if (contains(line, "Needs Merge")) + status = NeedsMerge; + else if (contains(line, "Needs Checkout")) + status = NeedsCheckout; + } + } + return status; +} + +void CVS::getRevisionInfo() { - return false; + if (have_rev_info_) + return; + have_rev_info_ = true; + TempFile tempfile("lyxvout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return; + } + + int rc = doVCCommandCallWithOutput("cvs log -r" + version_ + + ' ' + getTarget(File), + FileName(owner_->filePath()), tmpf); + if (rc) { + LYXERR(Debug::LYXVC, "cvs log failed with exit code " << rc); + return; + } + + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + static regex const reg("date: (.*) (.*) (.*); author: (.*); state: (.*);(.*)"); + + while (ifs) { + string line; + getline(ifs, line); + LYXERR(Debug::LYXVC, line << '\n'); + if (prefixIs(line, "date:")) { + smatch sm; + regex_match(line, sm, reg); + //sm[0]; // whole matched string + rev_date_cache_ = sm[1]; + rev_time_cache_ = sm[2]; + //sm[3]; // GMT offset + rev_author_cache_ = sm[4]; + break; + } + } + if (rev_author_cache_.empty()) + LYXERR(Debug::LYXVC, + "Could not retrieve revision info for " << version_ << + " of " << getTarget(File)); } -string CVS::lockingToggle() +void CVS::registrer(string const & msg) { - lyxerr << "Sorry, not implemented." << endl; - return string(); + doVCCommand("cvs -q add -m \"" + msg + "\" " + + getTarget(File), + FileName(owner_->filePath())); } -bool CVS::lockingToggleEnabled() +bool CVS::renameEnabled() { - return false; + return true; } -void CVS::revert() +string CVS::rename(support::FileName const & newFile, string const & msg) { - // Reverts to the version in CVS repository and - // gets the updated version from the repository. - string const fil = quoteName(onlyFilename(owner_->absFileName())); - // This is sensitive operation, so at lest some check about - // existence of cvs program and its file - if (doVCCommand("cvs log "+ fil, FileName(owner_->filePath()))) - return; - FileName f(owner_->absFileName()); - f.removeFile(); - doVCCommand("cvs update " + fil, - FileName(owner_->filePath())); - owner_->markClean(); + // CVS has no real rename command, so we create a poor mans version + support::FileName const oldFile(owner_->absFileName()); + string ret = copy(newFile, msg); + if (ret.empty()) + return ret; + string cmd = "cvs -q remove -m \"" + msg + "\" " + + quoteName(oldFile.onlyFileName()); + FileName path(oldFile.onlyPath()); + return doVCCommand(cmd, path) ? string() : ret; } -void CVS::undoLast() +bool CVS::copyEnabled() { - // 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; + return true; } -bool CVS::undoLastEnabled() +string CVS::copy(support::FileName const & newFile, string const & msg) { - return false; + // CVS has no real copy command, so we create a poor mans version + support::FileName const oldFile(owner_->absFileName()); + if (!oldFile.copyTo(newFile)) + return string(); + FileName path(oldFile.onlyPath()); + string relFile(to_utf8(newFile.relPath(path.absFileName()))); + string cmd("cvs -q add -m \"" + msg + "\" " + quoteName(relFile)); + return doVCCommand(cmd, path) ? string() : "CVS: Proceeded"; } -void CVS::getLog(FileName const & tmpf) +void CVS::getDiff(OperationMode opmode, FileName const & tmpf) { - doVCCommand("cvs log " + quoteName(onlyFilename(owner_->absFileName())) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); + doVCCommandWithOutput("cvs diff " + getTarget(opmode), + FileName(owner_->filePath()), tmpf, false); } -bool CVS::toggleReadOnlyEnabled() +int CVS::edit() { - return false; + vcstatus = LOCKED; + return doVCCommand("cvs -q edit " + getTarget(File), + FileName(owner_->filePath())); } -///////////////////////////////////////////////////////////////////// -// -// SVN -// -///////////////////////////////////////////////////////////////////// -SVN::SVN(FileName const & m, FileName const & f) +int CVS::unedit() { - owner_ = 0; - master_ = m; - file_ = f; - locked_mode_ = 0; - scanMaster(); + vcstatus = UNLOCKED; + return doVCCommand("cvs -q unedit " + getTarget(File), + FileName(owner_->filePath())); } -FileName const SVN::findFile(FileName const & file) +int CVS::update(OperationMode opmode, FileName const & tmpf) { - // First we look for the .svn/entries in the same dir - // where we have file. - FileName const entries(onlyPath(file.absFilename()) + "/.svn/entries"); - string const tmpf = onlyFilename(file.absFilename()); - LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under svn in `" << entries - << "' for `" << tmpf << '\''); - if (entries.isReadableFile()) { - // Ok we are at least in a SVN dir. Parse the .svn/entries - // and see if we can find this file. We do a fast and - // dirty parse here. - ifstream ifs(entries.toFilesystemEncoding().c_str()); - string line, oldline; - while (getline(ifs, line)) { - if (line == "dir" || line == "file") - LYXERR(Debug::LYXVC, "\tEntries: " << oldline); - if (oldline == tmpf && line == "file") - return entries; - oldline = line; - } - } - return FileName(); + return doVCCommandWithOutput("cvs -q update " + + getTarget(opmode), + FileName(owner_->filePath()), tmpf, false); } -void SVN::scanMaster() +string CVS::scanLogFile(FileName const & f, string & status) { - locker_.clear(); - // vcstatus code is somewhat superflous, until we want - // to implement read-only toggle for svn. - vcstatus = NOLOCKING; - if (checkLockMode()) { - if (isLocked()) { - locker_ = "Locked"; - vcstatus = LOCKED; - } else { - locker_ = "Unlocked"; - vcstatus = UNLOCKED; + ifstream ifs(f.toFilesystemEncoding().c_str()); + + while (ifs) { + string line; + getline(ifs, line); + LYXERR(Debug::LYXVC, line << '\n'); + if (!line.empty()) + status += line + "; "; + if (prefixIs(line, "C ")) { + ifs.close(); + return line; } } + ifs.close(); + return string(); } -bool SVN::checkLockMode() +LyXVC::CommandResult CVS::checkIn(string const & msg, string & log) { - FileName tmpf = FileName::tempName("lyxvcout"); - if (tmpf.empty()){ - LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); - return N_("Error: Could not generate logfile."); + CvsStatus status = getStatus(); + switch (status) { + case UpToDate: + if (vcstatus != NOLOCKING) + if (unedit()) + return LyXVC::ErrorCommand; + log = "CVS: Proceeded"; + return LyXVC::VCSuccess; + case LocallyModified: + case LocallyAdded: { + int rc = doVCCommand("cvs -q commit -m \"" + msg + "\" " + + getTarget(File), + FileName(owner_->filePath())); + if (rc) + return LyXVC::ErrorCommand; + log = "CVS: Proceeded"; + return LyXVC::VCSuccess; } + case NeedsMerge: + case NeedsCheckout: + frontend::Alert::error(_("Revision control error."), + _("The repository version is newer then the current check out.\n" + "You have to update from repository first or revert your changes.")) ; + break; + default: + frontend::Alert::error(_("Revision control error."), + bformat(_("Bad status when checking in changes.\n" + "\n'%1$s'\n\n"), + toString(status))); + break; + } + return LyXVC::ErrorBefore; +} - LYXERR(Debug::LYXVC, "Detecting locking mode..."); - if (doVCCommandCall("svn proplist " + quoteName(file_.onlyFileName()) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - file_.onlyPath())) - return false; - ifstream ifs(tmpf.toFilesystemEncoding().c_str()); +bool CVS::isLocked() const +{ + FileName fn(owner_->absFileName()); + fn.refresh(); + return !fn.isReadOnly(); +} + + +bool CVS::checkInEnabled() +{ + if (vcstatus != NOLOCKING) + return isLocked(); + else + return true; +} + + +bool CVS::isCheckInWithConfirmation() +{ + CvsStatus status = getStatus(); + return status == LocallyModified || status == LocallyAdded; +} + + +string CVS::checkOut() +{ + if (vcstatus != NOLOCKING && edit()) + return string(); + TempFile tempfile("lyxvout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return string(); + } + + int rc = update(File, tmpf); + string log; + string const res = scanLogFile(tmpf, log); + if (!res.empty()) { + frontend::Alert::error(_("Revision control error."), + bformat(_("Error when updating from repository.\n" + "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n" + "After pressing OK, LyX will try to reopen the resolved document."), + from_local8bit(res))); + rc = 0; + } + + return rc ? string() : log.empty() ? "CVS: Proceeded" : "CVS: " + log; +} + + +bool CVS::checkOutEnabled() +{ + if (vcstatus != NOLOCKING) + return !isLocked(); + else + return true; +} + + +string CVS::repoUpdate() +{ + TempFile tempfile("lyxvout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return string(); + } + + getDiff(Directory, tmpf); + docstring res = tmpf.fileContents("UTF-8"); + if (!res.empty()) { + LYXERR(Debug::LYXVC, "Diff detected:\n" << res); + docstring const file = from_utf8(owner_->filePath()); + docstring text = bformat(_("There were detected changes " + "in the working directory:\n%1$s\n\n" + "Possible file conflicts must be then resolved manually " + "or you will need to revert back to the repository version."), file); + int ret = frontend::Alert::prompt(_("Changes detected"), + text, 0, 1, _("&Continue"), _("&Abort"), _("View &Log ...")); + if (ret == 2) { + dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFileName())); + ret = frontend::Alert::prompt(_("Changes detected"), + text, 0, 1, _("&Continue"), _("&Abort")); + hideDialogs("file", 0); + } + if (ret == 1) + return string(); + } + + int rc = update(Directory, tmpf); + res += "Update log:\n" + tmpf.fileContents("UTF-8"); + LYXERR(Debug::LYXVC, res); + + string log; + string sres = scanLogFile(tmpf, log); + if (!sres.empty()) { + docstring const file = owner_->fileName().displayName(20); + frontend::Alert::error(_("Revision control error."), + bformat(_("Error when updating document %1$s from repository.\n" + "You have to manually resolve the conflicts NOW!\n'%2$s'.\n\n" + "After pressing OK, LyX will try to reopen the resolved document."), + file, from_local8bit(sres))); + rc = 0; + } + + return rc ? string() : log.empty() ? "CVS: Proceeded" : "CVS: " + log; +} + + +bool CVS::repoUpdateEnabled() +{ + return true; +} + + +string CVS::lockingToggle() +{ + lyxerr << "Sorry, not implemented." << endl; + return string(); +} + + +bool CVS::lockingToggleEnabled() +{ + return false; +} + + +bool CVS::isRevertWithConfirmation() +{ + CvsStatus status = getStatus(); + return !owner_->isClean() || status == LocallyModified || status == NeedsMerge; +} + + +bool CVS::revert() +{ + // Reverts to the version in CVS repository and + // gets the updated version from the repository. + CvsStatus status = getStatus(); + switch (status) { + case UpToDate: + if (vcstatus != NOLOCKING) + return 0 == unedit(); + break; + case NeedsMerge: + case NeedsCheckout: + case LocallyModified: { + FileName f(owner_->absFileName()); + f.removeFile(); + update(File, FileName()); + owner_->markClean(); + break; + } + case LocallyAdded: { + docstring const file = owner_->fileName().displayName(20); + frontend::Alert::error(_("Revision control error."), + bformat(_("The document %1$s is not in repository.\n" + "You have to check in the first revision before you can revert."), + file)) ; + return false; + } + default: { + docstring const file = owner_->fileName().displayName(20); + frontend::Alert::error(_("Revision control error."), + bformat(_("Cannot revert document %1$s to repository version.\n" + "The status '%2$s' is unexpected."), + file, toString(status))); + return false; + } + } + return true; +} + + +void CVS::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 CVS::undoLastEnabled() +{ + return false; +} + + +void CVS::getLog(FileName const & tmpf) +{ + doVCCommandWithOutput("cvs log " + getTarget(File), + FileName(owner_->filePath()), + tmpf); +} + + +bool CVS::toggleReadOnlyEnabled() +{ + return false; +} + + +string CVS::revisionInfo(LyXVC::RevisionInfo const info) +{ + if (!version_.empty()) { + getRevisionInfo(); + switch (info) { + case LyXVC::File: + return version_; + case LyXVC::Author: + return rev_author_cache_; + case LyXVC::Date: + return rev_date_cache_; + case LyXVC::Time: + return rev_time_cache_; + default: + break; + } + } + return string(); +} + + +bool CVS::prepareFileRevision(string const & revis, string & f) +{ + string rev = revis; + if (!VCS::makeRCSRevision(version_, rev)) + return false; + + TempFile tempfile("lyxvcrev_" + rev + '_'); + tempfile.setAutoRemove(false); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + + doVCCommandWithOutput("cvs update -p -r" + rev + ' ' + + getTarget(File), + FileName(owner_->filePath()), tmpf); + tmpf.refresh(); + if (tmpf.isFileEmpty()) + return false; + + f = tmpf.absFileName(); + return true; +} + + +bool CVS::prepareFileRevisionEnabled() +{ + return true; +} + + +///////////////////////////////////////////////////////////////////// +// +// SVN +// +///////////////////////////////////////////////////////////////////// + +SVN::SVN(FileName const & m, Buffer * b) : VCS(b) +{ + // Here we know that the buffer file is either already in SVN or + // about to be registered + master_ = m; + locked_mode_ = 0; + scanMaster(); +} + + +FileName const SVN::findFile(FileName const & file) +{ + // First we check the existence of repository meta data. + if (!VCS::checkparentdirs(file, ".svn")) { + LYXERR(Debug::LYXVC, "Cannot find SVN meta data for " << file); + return FileName(); + } + + // Now we check the status of the file. + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return FileName(); + } + + string const fname = onlyFileName(file.absFileName()); + LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under svn control for `" << fname << '\''); + bool found = 0 == doVCCommandCall("svn info " + quoteName(fname) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + file.onlyPath()); + LYXERR(Debug::LYXVC, "SVN control: " << (found ? "enabled" : "disabled")); + return found ? file : FileName(); +} + + +void SVN::scanMaster() +{ + // vcstatus code is somewhat superflous, + // until we want to implement read-only toggle for svn. + vcstatus = NOLOCKING; + if (checkLockMode()) { + if (isLocked()) + vcstatus = LOCKED; + else + vcstatus = UNLOCKED; + } +} + + +bool SVN::checkLockMode() +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()){ + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + + LYXERR(Debug::LYXVC, "Detecting locking mode..."); + if (doVCCommandCall("svn proplist " + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath()))) + return false; + + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); string line; bool ret = false; - while (ifs) { - getline(ifs, line); - LYXERR(Debug::LYXVC, line); - if (contains(line, "svn:needs-lock")) - ret = true; + while (ifs && !ret) { + getline(ifs, line); + LYXERR(Debug::LYXVC, line); + if (contains(line, "svn:needs-lock")) + ret = true; + } + LYXERR(Debug::LYXVC, "Locking enabled: " << ret); + ifs.close(); + locked_mode_ = ret; + return ret; + +} + + +bool SVN::isLocked() const +{ + FileName file(owner_->absFileName()); + file.refresh(); + return !file.isReadOnly(); +} + + +bool SVN::retrieve(FileName const & file) +{ + LYXERR(Debug::LYXVC, "LyXVC::SVN: retrieve.\n\t" << file); + // The caller ensures that file does not exist, so no need to check that. + return doVCCommandCall("svn update -q --non-interactive " + quoteName(file.onlyFileName()), + file.onlyPath()) == 0; +} + + +void SVN::registrer(string const & /*msg*/) +{ + doVCCommand("svn add -q " + quoteName(onlyFileName(owner_->absFileName())), + FileName(owner_->filePath())); +} + + +bool SVN::renameEnabled() +{ + return true; +} + + +string SVN::rename(support::FileName const & newFile, string const & msg) +{ + // svn move 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("svn move -q " + quoteName(onlyFileName(owner_->absFileName())) + + ' ' + quoteName(relFile)); + if (doVCCommand(cmd, path)) { + cmd = "svn revert -q " + + quoteName(onlyFileName(owner_->absFileName())) + ' ' + + quoteName(relFile); + doVCCommand(cmd, path); + if (newFile.exists()) + newFile.removeFile(); + return string(); + } + vector f; + f.push_back(owner_->fileName()); + f.push_back(newFile); + string log; + if (checkIn(f, msg, log) != LyXVC::VCSuccess) { + cmd = "svn revert -q " + + quoteName(onlyFileName(owner_->absFileName())) + ' ' + + quoteName(relFile); + doVCCommand(cmd, path); + if (newFile.exists()) + newFile.removeFile(); + return string(); + } + return log; +} + + +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. + // In LyX we commit immediately afterwards, otherwise it could be + // confusing to the user to have an uncommitted file. + FileName path(owner_->filePath()); + string relFile(to_utf8(newFile.relPath(path.absFileName()))); + string cmd("svn copy -q " + quoteName(onlyFileName(owner_->absFileName())) + + ' ' + quoteName(relFile)); + if (doVCCommand(cmd, path)) + return string(); + vector f(1, newFile); + string log; + if (checkIn(f, msg, log) == LyXVC::VCSuccess) + return log; + return string(); +} + + +LyXVC::CommandResult SVN::checkIn(string const & msg, string & log) +{ + vector f(1, owner_->fileName()); + return checkIn(f, msg, log); +} + + +LyXVC::CommandResult +SVN::checkIn(vector const & f, string const & msg, string & log) +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()){ + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + log = N_("Error: Could not generate logfile."); + return LyXVC::ErrorBefore; + } + + ostringstream os; + os << "svn 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::VCSuccess; + + 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; + } + else + if (!fileLock(false, tmpf, log)) + ret = LyXVC::ErrorCommand; + + if (!log.empty()) + log.insert(0, "SVN: "); + if (ret == LyXVC::VCSuccess && log.empty()) + log = "SVN: Proceeded"; + return ret; +} + + +bool SVN::checkInEnabled() +{ + if (locked_mode_) + return isLocked(); + else + return true; +} + + +bool SVN::isCheckInWithConfirmation() +{ + // FIXME one day common getDiff and perhaps OpMode for all backends + + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return true; + } + + doVCCommandCall("svn diff " + quoteName(owner_->absFileName()) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + + docstring diff = tmpf.fileContents("UTF-8"); + + 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 SVN::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; + } + if (contains(line, "svn:needs-lock")) { + ifs.close(); + return line; + } + } + ifs.close(); + return string(); +} + + +bool SVN::fileLock(bool lock, FileName const & tmpf, string &status) +{ + if (!locked_mode_ || (isLocked() == lock)) + return true; + + string const arg = lock ? "lock " : "unlock "; + doVCCommand("svn "+ arg + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + + // Lock error messages go unfortunately on stderr and are unreachable this way. + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + string line; + while (ifs) { + getline(ifs, line); + if (!line.empty()) status += line + "; "; + } + ifs.close(); + + if (isLocked() == lock) + return true; + + if (lock) + frontend::Alert::error(_("Revision control error."), + _("Error while acquiring write lock.\n" + "Another user is most probably editing\n" + "the current document now!\n" + "Also check the access to the repository.")); + else + frontend::Alert::error(_("Revision control error."), + _("Error while releasing write lock.\n" + "Check the access to the repository.")); + return false; +} + + +string SVN::checkOut() +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return N_("Error: Could not generate logfile."); + } + + doVCCommand("svn update --non-interactive " + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + + string log; + string const res = scanLogFile(tmpf, log); + if (!res.empty()) + frontend::Alert::error(_("Revision control error."), + bformat(_("Error when updating from repository.\n" + "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n" + "After pressing OK, LyX will try to reopen the resolved document."), + from_local8bit(res))); + + fileLock(true, tmpf, log); + + return log.empty() ? string() : "SVN: " + log; +} + + +bool SVN::checkOutEnabled() +{ + if (locked_mode_) + return !isLocked(); + else + return true; +} + + +string SVN::repoUpdate() +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return N_("Error: Could not generate logfile."); + } + + doVCCommand("svn diff " + quoteName(owner_->filePath()) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + docstring res = tmpf.fileContents("UTF-8"); + if (!res.empty()) { + LYXERR(Debug::LYXVC, "Diff detected:\n" << res); + docstring const file = from_utf8(owner_->filePath()); + docstring text = bformat(_("There were detected changes " + "in the working directory:\n%1$s\n\n" + "In case of file conflict version of the local directory files " + "will be preferred." + "\n\nContinue?"), file); + int ret = frontend::Alert::prompt(_("Changes detected"), + text, 0, 1, _("&Yes"), _("&No"), _("View &Log ...")); + if (ret == 2) { + dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFileName())); + ret = frontend::Alert::prompt(_("Changes detected"), + text, 0, 1, _("&Yes"), _("&No")); + hideDialogs("file", 0); + } + if (ret == 1) + return string(); + } + + // Reverting looks too harsh, see bug #6255. + // doVCCommand("svn revert -R " + quoteName(owner_->filePath()) + // + " > " + quoteName(tmpf.toFilesystemEncoding()), + // FileName(owner_->filePath())); + // res = "Revert log:\n" + tmpf.fileContents("UTF-8"); + doVCCommand("svn update --accept mine-full " + quoteName(owner_->filePath()) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + res += "Update log:\n" + tmpf.fileContents("UTF-8"); + + LYXERR(Debug::LYXVC, res); + return to_utf8(res); +} + + +bool SVN::repoUpdateEnabled() +{ + return true; +} + + +string SVN::lockingToggle() +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return N_("Error: Could not generate logfile."); + } + + int ret = doVCCommand("svn proplist " + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + if (ret) + return string(); + + string log; + string res = scanLogFile(tmpf, log); + bool locking = contains(res, "svn:needs-lock"); + if (!locking) + ret = doVCCommand("svn propset svn:needs-lock ON " + + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + else + ret = doVCCommand("svn propdel svn:needs-lock " + + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + if (ret) + return string(); + + frontend::Alert::warning(_("SVN File Locking"), + (locking ? _("Locking property unset.") : _("Locking property set.")) + '\n' + + _("Do not forget to commit the locking property into the repository."), + true); + + return string("SVN: ") + (locking ? + N_("Locking property unset.") : N_("Locking property set.")); +} + + +bool SVN::lockingToggleEnabled() +{ + return true; +} + + +bool SVN::revert() +{ + // Reverts to the version in SVN repository and + // gets the updated version from the repository. + string const fil = quoteName(onlyFileName(owner_->absFileName())); + + if (doVCCommand("svn revert -q " + fil, + FileName(owner_->filePath()))) + return false; + owner_->markClean(); + return true; +} + + +bool SVN::isRevertWithConfirmation() +{ + //FIXME owner && diff + return true; +} + + +void SVN::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 SVN::undoLastEnabled() +{ + return false; +} + + +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(); + + return rev_tree_cache_; + } + + // fill the rest of the attributes for a single file + if (rev_file_cache_.empty()) + if (!getFileRevisionInfo()) + rev_file_cache_ = "?"; + + switch (info) { + case LyXVC::File: + if (rev_file_cache_ == "?") + return string(); + return rev_file_cache_; + case LyXVC::Author: + return rev_author_cache_; + case LyXVC::Date: + return rev_date_cache_; + case LyXVC::Time: + return rev_time_cache_; + default: + break; + } + + return string(); +} + + +bool SVN::getFileRevisionInfo() +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + + doVCCommand("svn info --xml " + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + + if (tmpf.empty()) + return false; + + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + string line; + // commit log part + bool c = false; + string rev; + + while (ifs) { + getline(ifs, line); + LYXERR(Debug::LYXVC, line); + if (prefixIs(line, "")) { + string l1 = subst(line, "revision=\"", ""); + string l2 = trim(subst(l1, "\">", "")); + if (isStrInt(l2)) + rev_file_cache_ = rev = l2; + } + if (c && prefixIs(line, "") && suffixIs(line, "")) { + string l1 = subst(line, "", ""); + string l2 = subst(l1, "", ""); + rev_author_cache_ = l2; + } + if (c && prefixIs(line, "") && suffixIs(line, "")) { + string l1 = subst(line, "", ""); + string l2 = subst(l1, "", ""); + l2 = split(l2, l1, 'T'); + rev_date_cache_ = l1; + l2 = split(l2, l1, '.'); + rev_time_cache_ = l1; + } + } + + ifs.close(); + return !rev.empty(); +} + + +bool SVN::getTreeRevisionInfo() +{ + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + + doVCCommand("svnversion -n . > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + + if (tmpf.empty()) + return false; + + // only first line in case something bad happens. + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + string line; + getline(ifs, line); + ifs.close(); + + rev_tree_cache_ = line; + return !line.empty(); +} + + +void SVN::getLog(FileName const & tmpf) +{ + doVCCommand("svn log " + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); +} + + +bool SVN::prepareFileRevision(string const & revis, string & f) +{ + if (!isStrInt(revis)) + return false; + + int rev = convert(revis); + if (rev <= 0) + if (!getFileRevisionInfo()) + return false; + if (rev == 0) + rev = convert(rev_file_cache_); + // go back for minus rev + else if (rev < 0) { + rev = rev + convert(rev_file_cache_); + if (rev < 1) + return false; + } + + string revname = convert(rev); + TempFile tempfile("lyxvcrev_" + revname + '_'); + tempfile.setAutoRemove(false); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + + doVCCommand("svn cat -r " + revname + ' ' + + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + tmpf.refresh(); + if (tmpf.isFileEmpty()) + return false; + + f = tmpf.absFileName(); + return true; +} + + +bool SVN::prepareFileRevisionEnabled() +{ + return true; +} + + + +bool SVN::toggleReadOnlyEnabled() +{ + return false; +} + + +///////////////////////////////////////////////////////////////////// +// +// GIT +// +///////////////////////////////////////////////////////////////////// + +GIT::GIT(FileName const & m, Buffer * b) : VCS(b) +{ + // Here we know that the buffer file is either already in GIT or + // about to be registered + 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. + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return FileName(); + } + + string const fname = onlyFileName(file.absFileName()); + LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under git control for `" + << fname << '\''); + doVCCommandCall("git ls-files " + + quoteName(fname) + " > " + + quoteName(tmpf.toFilesystemEncoding()), + file.onlyPath()); + tmpf.refresh(); + bool found = !tmpf.isFileEmpty(); + LYXERR(Debug::LYXVC, "GIT control: " << (found ? "enabled" : "disabled")); + return found ? file : FileName(); +} + + +void GIT::scanMaster() +{ + // vcstatus code is somewhat superflous, + // until we want to implement read-only toggle for git. + 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(); } - LYXERR(Debug::LYXVC, "Locking enabled: " << ret); - ifs.close(); - locked_mode_ = ret; - return ret; + vector f; + f.push_back(owner_->fileName()); + f.push_back(newFile); + string log; + if (checkIn(f, msg, log) != LyXVC::VCSuccess) { + 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; } -bool SVN::isLocked() const +string GIT::copy(support::FileName const & /*newFile*/, string const & /*msg*/) { - file_.refresh(); - return !file_.isReadOnly(); + // git does not support copy with history preservation + return string(); } -void SVN::registrer(string const & /*msg*/) +LyXVC::CommandResult GIT::checkIn(string const & msg, string & log) { - doVCCommand("svn add -q " + quoteName(onlyFilename(owner_->absFileName())), - FileName(owner_->filePath())); + vector f(1, owner_->fileName()); + return checkIn(f, msg, log); } -string SVN::checkIn(string const & msg) +LyXVC::CommandResult +GIT::checkIn(vector const & f, string const & msg, string & log) { - FileName tmpf = FileName::tempName("lyxvcout"); + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); if (tmpf.empty()){ LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); - return N_("Error: Could not generate logfile."); + log = N_("Error: Could not generate logfile."); + return LyXVC::ErrorBefore; } - doVCCommand("svn commit -m \"" + msg + "\" " - + quoteName(onlyFilename(owner_->absFileName())) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); + 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::VCSuccess; - string log; string res = scanLogFile(tmpf, log); - if (!res.empty()) + 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.")); - else - fileLock(false, tmpf, log); + ret = LyXVC::ErrorCommand; + } - tmpf.erase(); - return log.empty() ? string() : "SVN: " + log; + if (!log.empty()) + log.insert(0, "GIT: "); + if (ret == LyXVC::VCSuccess && log.empty()) + log = "GIT: Proceeded"; + return ret; } -bool SVN::checkInEnabled() +bool GIT::checkInEnabled() { - if (locked_mode_) - return isLocked(); - else + return true; +} + + +bool GIT::isCheckInWithConfirmation() +{ + // FIXME one day common getDiff and perhaps OpMode for all backends + + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + 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"); + + 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 SVN::scanLogFile(FileName const & f, string & status) +string GIT::scanLogFile(FileName const & f, string & status) { ifstream ifs(f.toFilesystemEncoding().c_str()); string line; @@ -629,232 +2028,246 @@ string SVN::scanLogFile(FileName const & f, string & status) while (ifs) { getline(ifs, line); LYXERR(Debug::LYXVC, line << "\n"); - if (!line.empty()) + if (!line.empty()) status += line + "; "; if (prefixIs(line, "C ") || prefixIs(line, "CU ") || contains(line, "Commit failed")) { ifs.close(); return line; } - if (contains(line, "svn:needs-lock")) { - ifs.close(); - return line; - } } ifs.close(); return string(); } -void SVN::fileLock(bool lock, FileName const & tmpf, string &status) +string GIT::checkOut() { - if (!locked_mode_ || (isLocked() == lock)) - return; + return string(); +} - string const arg = lock ? "lock " : "unlock "; - doVCCommand("svn "+ arg + quoteName(onlyFilename(owner_->absFileName())) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); - // Lock error messages go unfortunately on stderr and are unreachible this way. - ifstream ifs(tmpf.toFilesystemEncoding().c_str()); - string line; - while (ifs) { - getline(ifs, line); - if (!line.empty()) status += line + "; "; - } - ifs.close(); +bool GIT::checkOutEnabled() +{ + return false; +} - if (!isLocked() && lock) - frontend::Alert::error(_("Revision control error."), - _("Error when acquiring write lock.\n" - "Most probably another user is editing\n" - "the current document now!\n" - "Also check the access to the repository.")); - if (isLocked() && !lock) - frontend::Alert::error(_("Revision control error."), - _("Error when releasing write lock.\n" - "Check the access to the repository.")); + +string GIT::repoUpdate() +{ + return string(); } -string SVN::checkOut() +bool GIT::repoUpdateEnabled() { - FileName tmpf = FileName::tempName("lyxvcout"); - if (tmpf.empty()) { - LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); - return N_("Error: Could not generate logfile."); - } + return false; +} - doVCCommand("svn update " + quoteName(onlyFilename(owner_->absFileName())) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); - string log; - string res = scanLogFile(tmpf, log); - if (!res.empty()) - frontend::Alert::error(_("Revision control error."), - bformat(_("Error when updating from repository.\n" - "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n" - "After pressing OK, LyX will try to reopen the resolved document."), - from_local8bit(res))); +string GIT::lockingToggle() +{ + return string(); +} - fileLock(true, tmpf, log); - tmpf.erase(); - return log.empty() ? string() : "SVN: " + log; +bool GIT::lockingToggleEnabled() +{ + return false; } -bool SVN::checkOutEnabled() +bool GIT::revert() { - if (locked_mode_) - return !isLocked(); - else - return true; + // 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; } -string SVN::repoUpdate() +bool GIT::isRevertWithConfirmation() { - FileName tmpf = FileName::tempName("lyxvcout"); - if (tmpf.empty()) { - LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); - return N_("Error: Could not generate logfile."); - } + //FIXME owner && diff + return true; +} - doVCCommand("svn diff " + quoteName(owner_->filePath()) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); - docstring res = tmpf.fileContents("UTF-8"); - if (!res.empty()) { - LYXERR(Debug::LYXVC, "Diff detected:\n" << res); - docstring const file = from_utf8(owner_->filePath()); - docstring text = bformat(_("There were detected changes " - "in the working directory:\n%1$s\n\n" - "In case of file conflict version of the local directory files " - "will be preferred." - "\n\nContinue?"), file); - int ret = frontend::Alert::prompt(_("Changes detected"), - text, 0, 1, _("&Yes"), _("&No"), _("View &Log ...")); - if (ret == 2 ) { - dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFilename())); - ret = frontend::Alert::prompt(_("Changes detected"), - text, 0, 1, _("&Yes"), _("&No")); - hideDialogs("file", 0); - } - if (ret == 1 ) { - tmpf.erase(); - return string(); - } - } - // Reverting looks too harsh, see bug #6255. - // doVCCommand("svn revert -R " + quoteName(owner_->filePath()) - // + " > " + quoteName(tmpf.toFilesystemEncoding()), - // FileName(owner_->filePath())); - // res = "Revert log:\n" + tmpf.fileContents("UTF-8"); - doVCCommand("svn update --accept mine-full " + quoteName(owner_->filePath()) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); - res += "Update log:\n" + tmpf.fileContents("UTF-8"); +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; +} - LYXERR(Debug::LYXVC, res); - tmpf.erase(); - return to_utf8(res); + +bool GIT::undoLastEnabled() +{ + return false; } -bool SVN::repoUpdateEnabled() +string GIT::revisionInfo(LyXVC::RevisionInfo const info) { - return true; + if (info == LyXVC::Tree) { + if (rev_tree_cache_.empty()) + if (!getTreeRevisionInfo()) + rev_tree_cache_ = "?"; + if (rev_tree_cache_ == "?") + return string(); + + return rev_tree_cache_; + } + + // fill the rest of the attributes for a single file + if (rev_file_cache_.empty()) + if (!getFileRevisionInfo()) + rev_file_cache_ = "?"; + + switch (info) { + case LyXVC::File: + if (rev_file_cache_ == "?") + return string(); + return rev_file_cache_; + case LyXVC::Author: + return rev_author_cache_; + case LyXVC::Date: + return rev_date_cache_; + case LyXVC::Time: + return rev_time_cache_; + default: + break; + } + + return string(); } -string SVN::lockingToggle() +bool GIT::getFileRevisionInfo() { - FileName tmpf = FileName::tempName("lyxvcout"); + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); if (tmpf.empty()) { LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); - return N_("Error: Could not generate logfile."); + return false; } - int ret = doVCCommand("svn proplist " + quoteName(onlyFilename(owner_->absFileName())) + doVCCommand("git log -n 1 --pretty=format:%H%n%an%n%ai " + quoteName(onlyFileName(owner_->absFileName())) + " > " + quoteName(tmpf.toFilesystemEncoding()), FileName(owner_->filePath())); - if (ret) - return string(); - string log; - string res = scanLogFile(tmpf, log); - bool locking = contains(res, "svn:needs-lock"); - if (!locking) - ret = doVCCommand("svn propset svn:needs-lock ON " - + quoteName(onlyFilename(owner_->absFileName())) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); - else - ret = doVCCommand("svn propdel svn:needs-lock " - + quoteName(onlyFilename(owner_->absFileName())) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); - if (ret) - return string(); + if (tmpf.empty()) + return false; - tmpf.erase(); - frontend::Alert::warning(_("VCN File Locking"), - (locking ? _("Locking property unset.") : _("Locking property set.")) + "\n" - + _("Do not forget to commit the locking property into the repository."), - true); + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + + if (ifs) + getline(ifs, rev_file_cache_); + if (ifs) + getline(ifs, rev_author_cache_); + if (ifs) { + string line; + getline(ifs, line); + rev_time_cache_ = split(line, rev_date_cache_, ' '); + } - return string("SVN: ") + N_("Locking property set."); + ifs.close(); + return !rev_file_cache_.empty(); } -bool SVN::lockingToggleEnabled() +bool GIT::getTreeRevisionInfo() { - return true; -} + TempFile tempfile("lyxvcout"); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + doVCCommand("git describe --abbrev --dirty --long > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath()), + false); //git describe returns $?=128 when no tag found (but git repo still exists) -void SVN::revert() -{ - // Reverts to the version in CVS repository and - // gets the updated version from the repository. - string const fil = quoteName(onlyFilename(owner_->absFileName())); + if (tmpf.empty()) + return false; - doVCCommand("svn revert -q " + fil, - FileName(owner_->filePath())); - owner_->markClean(); + // only first line in case something bad happens. + ifstream ifs(tmpf.toFilesystemEncoding().c_str()); + getline(ifs, rev_tree_cache_); + ifs.close(); + + return !rev_tree_cache_.empty(); } -void SVN::undoLast() +void GIT::getLog(FileName const & tmpf) { - // 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; + doVCCommand("git log " + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); } -bool SVN::undoLastEnabled() +//at this moment we don't accept revision SHA, but just number of revision steps back +//GUI and infrastucture needs to be changed first +bool GIT::prepareFileRevision(string const & revis, string & f) { - return false; + // anything positive means we got hash, not "0" or minus revision + int rev = 1; + + // hash is rarely number and should be long + if (isStrInt(revis) && revis.length()<20) + rev = convert(revis); + + // revision and filename + string pointer; + + // go back for "minus" revisions + if (rev <= 0) + pointer = "HEAD~" + convert(-rev); + // normal hash + else + pointer = revis; + + pointer += ':'; + + TempFile tempfile("lyxvcrev_" + revis + '_'); + tempfile.setAutoRemove(false); + FileName tmpf = tempfile.name(); + if (tmpf.empty()) { + LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); + return false; + } + + doVCCommand("git show " + pointer + "./" + + quoteName(onlyFileName(owner_->absFileName())) + + " > " + quoteName(tmpf.toFilesystemEncoding()), + FileName(owner_->filePath())); + tmpf.refresh(); + if (tmpf.isFileEmpty()) + return false; + + f = tmpf.absFileName(); + return true; } -void SVN::getLog(FileName const & tmpf) +bool GIT::prepareFileRevisionEnabled() { - doVCCommand("svn log " + quoteName(onlyFilename(owner_->absFileName())) - + " > " + quoteName(tmpf.toFilesystemEncoding()), - FileName(owner_->filePath())); + return true; } -bool SVN::toggleReadOnlyEnabled() +bool GIT::toggleReadOnlyEnabled() { - return false; + return true; }