/** * \file VCBackend.cpp * This file is part of LyX, the document processor. * 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. */ #include #include "VCBackend.h" #include "Buffer.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/PathChanger.h" #include "support/Systemcall.h" #include "support/regex.h" #include "support/TempFile.h" #include #include #include using namespace std; using namespace lyx::support; namespace lyx { 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, string(), string(), false); } int VCS::doVCCommand(string const & cmd, FileName const & path, bool reportError) { if (owner_) owner_->setBusy(true); int const ret = doVCCommandCall(cmd, path); if (owner_) owner_->setBusy(false); if (ret && reportError) frontend::Alert::error(_("Revision control error."), bformat(_("Some problem occurred while running the command:\n" "'%1$s'."), 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, Buffer * b) : VCS(b) { // Here we know that the buffer file is either already in RCS or // about to be registered master_ = m; scanMaster(); } FileName const RCS::findFile(FileName const & file) { // Check if *,v exists. 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."); return tmp; } // Check if RCS/*,v exists. 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."); return tmp; } return FileName(); } bool RCS::retrieve(FileName const & file) { LYXERR(Debug::LYXVC, "LyXVC::RCS: retrieve.\n\t" << file); // The caller ensures that file does not exist, so no need to check that. return doVCCommandCall("co -q -r " + quoteName(file.toFilesystemEncoding()), FileName()) == 0; } void RCS::scanMaster() { if (master_.empty()) return; 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; while (!read_enough && ifs >> token) { LYXERR(Debug::LYXVC, "LyXVC::scanMaster: current lex text: `" << token << '\''); if (token.empty()) continue; else if (token == "head") { // get version here string tmv; ifs >> tmv; tmv = rtrim(tmv, ";"); version_ = tmv; LYXERR(Debug::LYXVC, "LyXVC: version found to be " << tmv); } else if (contains(token, "access") || contains(token, "symbols") || contains(token, "strict")) { // nothing } else if (contains(token, "locks")) { // get locker here if (contains(token, ';')) { locker_ = "Unlocked"; vcstatus = UNLOCKED; continue; } string tmpt; string s1; string s2; do { ifs >> tmpt; s1 = rtrim(tmpt, ";"); // tmp is now in the format : s1 = split(s1, s2, ':'); // s2 is user, and s1 is version if (s1 == version_) { locker_ = s2; vcstatus = LOCKED; break; } } while (!contains(tmpt, ';')); } else if (token == "comment") { // we don't need to read any further than this. read_enough = true; } else { // unexpected LYXERR(Debug::LYXVC, "LyXVC::scanMaster(): unexpected token"); } } } void RCS::registrer(string const & msg) { string cmd = "ci -q -u -i -t-\""; cmd += msg; cmd += "\" "; cmd += quoteName(onlyFileName(owner_->absFileName())); doVCCommand(cmd, FileName(owner_->filePath())); } 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())), FileName(owner_->filePath())); if (ret) return LyXVC::ErrorCommand; log = "RCS: Proceeded"; return LyXVC::VCSuccess; } bool RCS::checkInEnabled() { 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())), FileName(owner_->filePath())); return ret ? string() : "RCS: Proceeded"; } bool RCS::checkOutEnabled() { return owner_ && owner_->hasReadonlyFlag(); } string RCS::repoUpdate() { lyxerr << "Sorry, not implemented." << endl; return string(); } bool RCS::repoUpdateEnabled() { return false; } 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(); } bool RCS::lockingToggleEnabled() { return false; } bool RCS::revert() { 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())), FileName(owner_->filePath())); } bool RCS::undoLastEnabled() { return owner_->hasReadonlyFlag(); } void RCS::getLog(FileName const & tmpf) { doVCCommand("rlog " + quoteName(onlyFileName(owner_->absFileName())) + " > " + quoteName(tmpf.toFilesystemEncoding()), FileName(owner_->filePath())); } bool RCS::toggleReadOnlyEnabled() { // This got broken somewhere along lfuns dispatch reorganization. // reloadBuffer would be needed after this, but thats problematic // since we are inside Buffer::dispatch. // return true; return false; } 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, Buffer * b) : VCS(b) { // Here we know that the buffer file is either already in CVS or // about to be registered master_ = m; have_rev_info_ = false; scanMaster(); } 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()) + '/'; LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under cvs in `" << entries << "' for `" << tmpf << '\''); if (entries.isReadableFile()) { // Ok we are at least in a CVS dir. Parse the CVS/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; while (getline(ifs, line)) { LYXERR(Debug::LYXVC, "\tEntries: " << line); if (contains(line, tmpf)) return entries; } } return FileName(); } 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 name = onlyFileName(owner_->absFileName()); string tmpf = '/' + name + '/'; LYXERR(Debug::LYXVC, "\tlooking for `" << tmpf << '\''); string line; static regex const reg("/(.*)/(.*)/(.*)/(.*)/(.*)"); while (getline(ifs, line)) { LYXERR(Debug::LYXVC, "\t line: " << line); if (contains(line, tmpf)) { // Ok extract the fields. smatch sm; if (!regex_match(line, sm, reg)) { LYXERR(Debug::LYXVC, "\t Cannot parse line. Skipping."); continue; } //sm[0]; // whole matched string //sm[1]; // filename version_ = sm.str(2); string const file_date = sm.str(3); //sm[4]; // options //sm[5]; // tag or tagdate 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 { vcstatus = NOLOCKING; } break; } } } bool CVS::retrieve(FileName const & file) { 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 const CVS::getTarget(OperationMode opmode) const { 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(); } docstring CVS::toString(CvsStatus status) const { 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(); } int CVS::doVCCommandWithOutput(string const & cmd, FileName const & path, FileName const & output, bool reportError) { string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding()); return doVCCommand(cmd + redirection, path, reportError); } int CVS::doVCCommandCallWithOutput(std::string const & cmd, support::FileName const & path, support::FileName const & output) { string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding()); return doVCCommandCall(cmd + redirection, path); } CVS::CvsStatus CVS::getStatus() { 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; } 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() { 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; if (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]; } else LYXERR(Debug::LYXVC, "\tCannot parse line. Skipping."); break; } } if (rev_author_cache_.empty()) LYXERR(Debug::LYXVC, "Could not retrieve revision info for " << version_ << " of " << getTarget(File)); } void CVS::registrer(string const & msg) { doVCCommand("cvs -q add -m \"" + msg + "\" " + getTarget(File), FileName(owner_->filePath())); } bool CVS::renameEnabled() { return true; } string CVS::rename(support::FileName const & newFile, string const & msg) { // 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; } 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 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::getDiff(OperationMode opmode, FileName const & tmpf) { doVCCommandWithOutput("cvs diff " + getTarget(opmode), FileName(owner_->filePath()), tmpf, false); } int CVS::edit() { vcstatus = LOCKED; return doVCCommand("cvs -q edit " + getTarget(File), FileName(owner_->filePath())); } int CVS::unedit() { vcstatus = UNLOCKED; return doVCCommand("cvs -q unedit " + getTarget(File), FileName(owner_->filePath())); } int CVS::update(OperationMode opmode, FileName const & tmpf) { return doVCCommandWithOutput("cvs -q update " + getTarget(opmode), FileName(owner_->filePath()), tmpf, false); } string CVS::scanLogFile(FileName const & f, string & status) { 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(); } LyXVC::CommandResult CVS::checkIn(string const & msg, string & log) { 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; } 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 && !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(); } 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; } 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 f(1, owner_->fileName()); return checkIn(f, msg, log); } LyXVC::CommandResult GIT::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 << "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 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; } if (!log.empty()) log.insert(0, "GIT: "); if (ret == LyXVC::VCSuccess && 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 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 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) { 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 GIT::getFileRevisionInfo() { TempFile tempfile("lyxvcout"); FileName tmpf = tempfile.name(); if (tmpf.empty()) { LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf); return false; } doVCCommand("git log -n 1 --pretty=format:%H%n%an%n%ai " + quoteName(onlyFileName(owner_->absFileName())) + " > " + quoteName(tmpf.toFilesystemEncoding()), FileName(owner_->filePath())); if (tmpf.empty()) return false; 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_, ' '); } ifs.close(); return !rev_file_cache_.empty(); } bool GIT::getTreeRevisionInfo() { 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())); if (tmpf.empty()) return false; // 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 GIT::getLog(FileName const & tmpf) { doVCCommand("git log " + quoteName(onlyFileName(owner_->absFileName())) + " > " + quoteName(tmpf.toFilesystemEncoding()), FileName(owner_->filePath())); } //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) { // 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; } bool GIT::prepareFileRevisionEnabled() { return true; } bool GIT::toggleReadOnlyEnabled() { return true; } } // namespace lyx