]> git.lyx.org Git - lyx.git/blob - src/VCBackend.cpp
01d546d3d4637064cb7a4e232331ba5cd64a7ad5
[lyx.git] / src / VCBackend.cpp
1 /**
2  * \file VCBackend.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Lars Gullik Bjønnes
7  * \author Pavel Sanda
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "VCBackend.h"
15 #include "Buffer.h"
16 #include "LyX.h"
17 #include "FuncRequest.h"
18
19 #include "frontends/alert.h"
20 #include "frontends/Application.h"
21
22 #include "support/convert.h"
23 #include "support/debug.h"
24 #include "support/filetools.h"
25 #include "support/gettext.h"
26 #include "support/lstrings.h"
27 #include "support/PathChanger.h"
28 #include "support/Systemcall.h"
29 #include "support/TempFile.h"
30
31 #include <fstream>
32 #include <iomanip>
33 #include <regex>
34 #include <sstream>
35
36 using namespace std;
37 using namespace lyx::support;
38
39
40 namespace lyx {
41
42
43 int VCS::doVCCommandCall(string const & cmd, FileName const & path)
44 {
45         LYXERR(Debug::LYXVC, "doVCCommandCall: " << cmd);
46         Systemcall one;
47         support::PathChanger p(path);
48         return one.startscript(Systemcall::Wait, cmd, string(), string(), false);
49 }
50
51
52 int VCS::doVCCommand(string const & cmd, FileName const & path, bool reportError)
53 {
54         if (owner_)
55                 owner_->setBusy(true);
56
57         int const ret = doVCCommandCall(cmd, path);
58
59         if (owner_)
60                 owner_->setBusy(false);
61         if (ret && reportError) {
62                 docstring rcsmsg;
63                 if (prefixIs(cmd, "ci "))
64                         rcsmsg = "\n" + _("Perhaps the RCS package is not installed on your system?");
65                 frontend::Alert::error(_("Revision control error."),
66                         bformat(_("Some problem occurred while running the command:\n"
67                                   "'%1$s'.") + rcsmsg,
68                         from_utf8(cmd)));
69         }
70         return ret;
71 }
72
73
74 bool VCS::makeRCSRevision(string const &version, string &revis) const
75 {
76         string rev = revis;
77
78         if (isStrInt(rev)) {
79                 int back = convert<int>(rev);
80                 // if positive use as the last number in the whole revision string
81                 if (back > 0) {
82                         string base;
83                         rsplit(version, base , '.');
84                         rev = base + '.' + rev;
85                 }
86                 if (back == 0)
87                         rev = version;
88                 // we care about the last number from revision string
89                 // in case of backward indexing
90                 if (back < 0) {
91                         string cur, base;
92                         cur = rsplit(version, base , '.');
93                         if (!isStrInt(cur))
94                                 return false;
95                         int want = convert<int>(cur) + back;
96                         if (want <= 0)
97                                 return false;
98
99                         rev = base + '.' + convert<string>(want);
100                 }
101         }
102
103         revis = rev;
104         return true;
105 }
106
107
108 FileName VCS::checkParentDirs(FileName const & start, std::string const & file)
109 {
110         static FileName empty;
111         FileName dirname = start.onlyPath();
112         do {
113                 FileName tocheck = FileName(addPathName(dirname.absFileName(), file));
114                 LYXERR(Debug::LYXVC, "check file: " << tocheck.absFileName());
115                 if (tocheck.exists())
116                         return tocheck;
117                 // this construct because of #8295
118                 dirname = FileName(dirname.absFileName()).parentPath();
119         } while (!dirname.empty());
120         return empty;
121 }
122
123
124 /////////////////////////////////////////////////////////////////////
125 //
126 // RCS
127 //
128 /////////////////////////////////////////////////////////////////////
129
130 RCS::RCS(FileName const & m, Buffer * b) : VCS(b)
131 {
132         // Here we know that the buffer file is either already in RCS or
133         // about to be registered
134         master_ = m;
135         scanMaster();
136 }
137
138
139 FileName const RCS::findFile(FileName const & file)
140 {
141         // Check if *,v exists.
142         FileName tmp(file.absFileName() + ",v");
143         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under rcs: " << tmp);
144         if (tmp.isReadableFile()) {
145                 LYXERR(Debug::LYXVC, "Yes, " << file << " is under rcs.");
146                 return tmp;
147         }
148
149         // Check if RCS/*,v exists.
150         tmp = FileName(addName(addPath(onlyPath(file.absFileName()), "RCS"), file.absFileName()) + ",v");
151         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under rcs: " << tmp);
152         if (tmp.isReadableFile()) {
153                 LYXERR(Debug::LYXVC, "Yes, " << file << " is under rcs.");
154                 return tmp;
155         }
156
157         return FileName();
158 }
159
160
161 bool RCS::retrieve(FileName const & file)
162 {
163         LYXERR(Debug::LYXVC, "LyXVC::RCS: retrieve.\n\t" << file);
164         // The caller ensures that file does not exist, so no need to check that.
165         return doVCCommandCall("co -q -r " + quoteName(file.toFilesystemEncoding()),
166                                FileName()) == 0;
167 }
168
169
170 void RCS::scanMaster()
171 {
172         if (master_.empty())
173                 return;
174
175         LYXERR(Debug::LYXVC, "LyXVC::RCS: scanMaster: " << master_);
176
177         ifstream ifs(master_.toFilesystemEncoding().c_str());
178         // limit the size of strings we read to avoid memory problems
179         ifs >> setw(65636);
180
181         string token;
182         bool read_enough = false;
183
184         while (!read_enough && ifs >> token) {
185                 LYXERR(Debug::LYXVC, "LyXVC::scanMaster: current lex text: `"
186                         << token << '\'');
187
188                 if (token.empty())
189                         continue;
190                 else if (token == "head") {
191                         // get version here
192                         string tmv;
193                         ifs >> tmv;
194                         tmv = rtrim(tmv, ";");
195                         version_ = tmv;
196                         LYXERR(Debug::LYXVC, "LyXVC: version found to be " << tmv);
197                 } else if (contains(token, "access")
198                            || contains(token, "symbols")
199                            || contains(token, "strict")) {
200                         // nothing
201                 } else if (contains(token, "locks")) {
202                         // get locker here
203                         if (contains(token, ';')) {
204                                 locker_ = "Unlocked";
205                                 vcstatus_ = UNLOCKED;
206                                 continue;
207                         }
208                         string tmpt;
209                         string s1;
210                         string s2;
211                         do {
212                                 ifs >> tmpt;
213                                 s1 = rtrim(tmpt, ";");
214                                 // tmp is now in the format <user>:<version>
215                                 s1 = split(s1, s2, ':');
216                                 // s2 is user, and s1 is version
217                                 if (s1 == version_) {
218                                         locker_ = s2;
219                                         vcstatus_ = LOCKED;
220                                         break;
221                                 }
222                         } while (!contains(tmpt, ';'));
223
224                 } else if (token == "comment") {
225                         // we don't need to read any further than this.
226                         read_enough = true;
227                 } else {
228                         // unexpected
229                         LYXERR(Debug::LYXVC, "LyXVC::scanMaster(): unexpected token");
230                 }
231         }
232 }
233
234
235 void RCS::registrer(string const & msg)
236 {
237         string cmd = "ci -q -u -i -t-\"";
238         cmd += msg;
239         cmd += "\" ";
240         cmd += quoteName(onlyFileName(owner_->absFileName()));
241         doVCCommand(cmd, FileName(owner_->filePath()));
242 }
243
244
245 bool RCS::renameEnabled()
246 {
247         return false;
248 }
249
250
251 string RCS::rename(support::FileName const & /*newFile*/, string const & /*msg*/)
252 {
253         // not implemented, since a left-over file.lyx,v would be confusing.
254         return string();
255 }
256
257
258 bool RCS::copyEnabled()
259 {
260         return true;
261 }
262
263
264 string RCS::copy(support::FileName const & newFile, string const & msg)
265 {
266         // RCS has no real copy command, so we create a poor mans version
267         support::FileName const oldFile(owner_->absFileName());
268         if (!oldFile.copyTo(newFile))
269                 return string();
270         FileName path(oldFile.onlyPath());
271         string relFile(to_utf8(newFile.relPath(path.absFileName())));
272         string cmd = "ci -q -u -i -t-\"";
273         cmd += msg;
274         cmd += "\" ";
275         cmd += quoteName(relFile);
276         return doVCCommand(cmd, path) ? string() : "RCS: Proceeded";
277 }
278
279
280 LyXVC::CommandResult RCS::checkIn(string const & msg, string & log)
281 {
282         int ret = doVCCommand("ci -q -u -m\"" + msg + "\" "
283                     + quoteName(onlyFileName(owner_->absFileName())),
284                     FileName(owner_->filePath()));
285         if (ret)
286                 return LyXVC::ErrorCommand;
287         log = "RCS: Proceeded";
288         return LyXVC::VCSuccess;
289 }
290
291
292 bool RCS::checkInEnabled()
293 {
294         return owner_ && !owner_->hasReadonlyFlag();
295 }
296
297
298 bool RCS::isCheckInWithConfirmation()
299 {
300         // FIXME one day common getDiff for all backends
301         // docstring diff;
302         // if (getDiff(file, diff) && diff.empty())
303         //      return false;
304
305         TempFile tempfile("lyxvcout");
306         FileName tmpf = tempfile.name();
307         if (tmpf.empty()) {
308                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
309                 return true;
310         }
311
312         doVCCommandCall("rcsdiff " + quoteName(owner_->absFileName())
313                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
314                 FileName(owner_->filePath()));
315
316         docstring diff = tmpf.fileContents("UTF-8");
317
318         if (diff.empty())
319                 return false;
320
321         return true;
322 }
323
324
325 string RCS::checkOut()
326 {
327         owner_->markClean();
328         int ret = doVCCommand("co -q -l " + quoteName(onlyFileName(owner_->absFileName())),
329                     FileName(owner_->filePath()));
330         return ret ? string() : "RCS: Proceeded";
331 }
332
333
334 bool RCS::checkOutEnabled()
335 {
336         return owner_ && owner_->hasReadonlyFlag();
337 }
338
339
340 string RCS::repoUpdate()
341 {
342         lyxerr << "Sorry, not implemented." << endl;
343         return string();
344 }
345
346
347 bool RCS::repoUpdateEnabled()
348 {
349         return false;
350 }
351
352
353 string RCS::lockingToggle()
354 {
355         //FIXME this might be actually possible, study rcs -U, rcs -L.
356         //State should be easy to get inside scanMaster.
357         //It would fix #4370 and make rcs/svn usage even more closer.
358         lyxerr << "Sorry, not implemented." << endl;
359         return string();
360 }
361
362
363 bool RCS::lockingToggleEnabled()
364 {
365         return false;
366 }
367
368
369 bool RCS::revert()
370 {
371         if (doVCCommand("co -f -u" + version_ + ' '
372                     + quoteName(onlyFileName(owner_->absFileName())),
373                     FileName(owner_->filePath())))
374                 return false;
375         // We ignore changes and just reload!
376         owner_->markClean();
377         return true;
378 }
379
380
381 bool RCS::isRevertWithConfirmation()
382 {
383         //FIXME owner && diff ?
384         return true;
385 }
386
387
388 void RCS::undoLast()
389 {
390         LYXERR(Debug::LYXVC, "LyXVC: undoLast");
391         doVCCommand("rcs -o" + version_ + ' '
392                     + quoteName(onlyFileName(owner_->absFileName())),
393                     FileName(owner_->filePath()));
394 }
395
396
397 bool RCS::undoLastEnabled()
398 {
399         return owner_->hasReadonlyFlag();
400 }
401
402
403 void RCS::getLog(FileName const & tmpf)
404 {
405         doVCCommand("rlog " + quoteName(onlyFileName(owner_->absFileName()))
406                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
407                     FileName(owner_->filePath()));
408 }
409
410
411 bool RCS::toggleReadOnlyEnabled()
412 {
413         // This got broken somewhere along lfuns dispatch reorganization.
414         // reloadBuffer would be needed after this, but thats problematic
415         // since we are inside Buffer::dispatch.
416         // return true;
417         return false;
418 }
419
420
421 string RCS::revisionInfo(LyXVC::RevisionInfo const info)
422 {
423         if (info == LyXVC::File)
424                 return version_;
425         // fill the rest of the attributes for a single file
426         if (rev_date_cache_.empty())
427                 if (!getRevisionInfo())
428                         return string();
429
430         switch (info) {
431                 case LyXVC::Author:
432                         return rev_author_cache_;
433                 case LyXVC::Date:
434                         return rev_date_cache_;
435                 case LyXVC::Time:
436                         return rev_time_cache_;
437                 default:
438                         break;
439         }
440
441         return string();
442 }
443
444
445 bool RCS::getRevisionInfo()
446 {
447         TempFile tempfile("lyxvcout");
448         FileName tmpf = tempfile.name();
449         if (tmpf.empty()) {
450                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
451                 return false;
452         }
453         doVCCommand("rlog -r " + quoteName(onlyFileName(owner_->absFileName()))
454                 + " > " + quoteName(tmpf.toFilesystemEncoding()),
455                 FileName(owner_->filePath()));
456
457         if (tmpf.empty())
458                 return false;
459
460         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
461         string line;
462
463         // we reached to the entry, i.e. after initial log message
464         bool entry=false;
465         // line with critical info, e.g:
466         //"date: 2011/07/02 11:02:54;  author: sanda;  state: Exp;  lines: +17 -2"
467         string result;
468
469         while (ifs) {
470                 getline(ifs, line);
471                 LYXERR(Debug::LYXVC, line);
472                 if (entry && prefixIs(line, "date:")) {
473                         result = line;
474                         break;
475                 }
476                 if (prefixIs(line, "revision"))
477                         entry = true;
478         }
479         if (result.empty())
480                 return false;
481
482         rev_date_cache_ = token(result, ' ', 1);
483         rev_time_cache_ = rtrim(token(result, ' ', 2), ";");
484         rev_author_cache_ = trim(token(token(result, ';', 1), ':', 1));
485
486         return !rev_author_cache_.empty();
487 }
488
489 bool RCS::prepareFileRevision(string const &revis, string & f)
490 {
491         string rev = revis;
492         if (!VCS::makeRCSRevision(version_, rev))
493                 return false;
494
495         TempFile tempfile("lyxvcrev_" + rev + '_');
496         tempfile.setAutoRemove(false);
497         FileName tmpf = tempfile.name();
498         if (tmpf.empty()) {
499                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
500                 return false;
501         }
502
503         doVCCommand("co -p" + rev + ' '
504                       + quoteName(onlyFileName(owner_->absFileName()))
505                       + " > " + quoteName(tmpf.toFilesystemEncoding()),
506                 FileName(owner_->filePath()));
507         tmpf.refresh();
508         if (tmpf.isFileEmpty())
509                 return false;
510
511         f = tmpf.absFileName();
512         return true;
513 }
514
515
516 bool RCS::prepareFileRevisionEnabled()
517 {
518         return true;
519 }
520
521
522 /////////////////////////////////////////////////////////////////////
523 //
524 // CVS
525 //
526 /////////////////////////////////////////////////////////////////////
527
528 CVS::CVS(FileName const & m, Buffer * b) : VCS(b)
529 {
530         // Here we know that the buffer file is either already in CVS or
531         // about to be registered
532         master_ = m;
533         have_rev_info_ = false;
534         scanMaster();
535 }
536
537
538 FileName const CVS::findFile(FileName const & file)
539 {
540         LYXERR(Debug::LYXVC, "LyXVC: Checking if "
541                    << onlyFileName(file.absFileName()) << "is under cvs");
542         // First we look for the CVS/Entries in the same dir where we have file.
543         // Note that it is not necessary to search parent directories, since
544         // there will be a CVS/Entries file in every subdirectory.
545         FileName const entries(onlyPath(file.absFileName()) + "/CVS/Entries");
546         if (entries.isReadableFile()) {
547                 // We are in a CVS-managed directory
548                 // See if the file is known to CVS
549                 string const cmd = "cvs log " + quoteName(file.toFilesystemEncoding());
550                 int const ret = doVCCommandCall(cmd, file.onlyPath());
551                 if (ret == 0)
552                         return entries;
553         }
554         return FileName();
555 }
556
557
558 void CVS::scanMaster()
559 {
560         LYXERR(Debug::LYXVC, "LyXVC::CVS: scanMaster. \n     Checking: " << master_);
561         // Ok now we do the real scan...
562         ifstream ifs(master_.toFilesystemEncoding().c_str());
563         string const name = onlyFileName(owner_->absFileName());
564         string const tmpf = '/' + name + '/';
565         LYXERR(Debug::LYXVC, "\tlooking for `" << tmpf << '\'');
566         string line;
567         static regex const reg("/(.*)/(.*)/(.*)/(.*)/(.*)");
568         while (getline(ifs, line)) {
569                 LYXERR(Debug::LYXVC, "\t  line: " << line);
570                 if (contains(line, tmpf)) {
571                         // Ok extract the fields.
572                         smatch sm;
573                         if (!regex_match(line, sm, reg)) {
574                                 LYXERR(Debug::LYXVC, "\t  Cannot parse line. Skipping.");
575                                 continue;
576                         }
577
578                         //sm[0]; // whole matched string
579                         //sm[1]; // filename
580                         version_ = sm.str(2);
581                         string const file_date = sm.str(3);
582
583                         //sm[4]; // options
584                         //sm[5]; // tag or tagdate
585                         FileName file(owner_->absFileName());
586                         if (file.isReadableFile()) {
587                                 time_t const mod = file.lastModified();
588                                 string const mod_date = rtrim(asctime(gmtime(&mod)), "\n");
589                                 LYXERR(Debug::LYXVC, "Date in Entries: `" << file_date
590                                         << "'\nModification date of file: `" << mod_date << '\'');
591                                 if (file.isReadOnly()) {
592                                         // readonly checkout is unlocked
593                                         vcstatus_ = UNLOCKED;
594                                 } else {
595                                         FileName bdir(addPath(master_.onlyPath().absFileName(),"Base"));
596                                         FileName base(addName(bdir.absFileName(),name));
597                                         // if base version is existent "cvs edit" was used to lock
598                                         vcstatus_ = base.isReadableFile() ? LOCKED : NOLOCKING;
599                                 }
600                         } else {
601                                 vcstatus_ = NOLOCKING;
602                         }
603                         break;
604                 }
605         }
606 }
607
608
609 bool CVS::retrieve(FileName const & file)
610 {
611         LYXERR(Debug::LYXVC, "LyXVC::CVS: retrieve.\n\t" << file);
612         // The caller ensures that file does not exist, so no need to check that.
613         return doVCCommandCall("cvs -q update " + quoteName(file.toFilesystemEncoding()),
614                                file.onlyPath()) == 0;
615 }
616
617
618 string const CVS::getTarget(OperationMode opmode) const
619 {
620         switch(opmode) {
621         case Directory:
622                 // in client server mode CVS does not like full path operand for directory operation
623                 // since LyX switches to the repo dir "." is good enough as target
624                 return ".";
625         case File:
626                 return quoteName(onlyFileName(owner_->absFileName()));
627         }
628         return string();
629 }
630
631
632 docstring CVS::toString(CvsStatus status) const
633 {
634         switch (status) {
635         case UpToDate:
636                 return _("Up-to-date");
637         case LocallyModified:
638                 return _("Locally Modified");
639         case LocallyAdded:
640                 return _("Locally Added");
641         case NeedsMerge:
642                 return _("Needs Merge");
643         case NeedsCheckout:
644                 return _("Needs Checkout");
645         case NoCvsFile:
646                 return _("No CVS file");
647         case StatusError:
648                 return _("Cannot retrieve CVS status");
649         }
650         return docstring();
651 }
652
653
654 int CVS::doVCCommandWithOutput(string const & cmd, FileName const & path,
655         FileName const & output, bool reportError)
656 {
657         string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding());
658         return doVCCommand(cmd + redirection, path, reportError);
659 }
660
661
662 int CVS::doVCCommandCallWithOutput(std::string const & cmd,
663         support::FileName const & path,
664         support::FileName const & output)
665 {
666         string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding());
667         return doVCCommandCall(cmd + redirection, path);
668 }
669
670
671 CVS::CvsStatus CVS::getStatus()
672 {
673         TempFile tempfile("lyxvout");
674         FileName tmpf = tempfile.name();
675         if (tmpf.empty()) {
676                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
677                 return StatusError;
678         }
679
680         if (doVCCommandCallWithOutput("cvs status " + getTarget(File),
681                 FileName(owner_->filePath()), tmpf)) {
682                 return StatusError;
683         }
684
685         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
686         CvsStatus status = NoCvsFile;
687
688         while (ifs) {
689                 string line;
690                 getline(ifs, line);
691                 LYXERR(Debug::LYXVC, line << '\n');
692                 if (prefixIs(line, "File:")) {
693                         if (contains(line, "Up-to-date"))
694                                 status = UpToDate;
695                         else if (contains(line, "Locally Modified"))
696                                 status = LocallyModified;
697                         else if (contains(line, "Locally Added"))
698                                 status = LocallyAdded;
699                         else if (contains(line, "Needs Merge"))
700                                 status = NeedsMerge;
701                         else if (contains(line, "Needs Checkout"))
702                                 status = NeedsCheckout;
703                 }
704         }
705         return status;
706 }
707
708 void CVS::getRevisionInfo()
709 {
710         if (have_rev_info_)
711                 return;
712         have_rev_info_ = true;
713         TempFile tempfile("lyxvout");
714         FileName tmpf = tempfile.name();
715         if (tmpf.empty()) {
716                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
717                 return;
718         }
719
720         int rc = doVCCommandCallWithOutput("cvs log -r" + version_
721                 + ' ' + getTarget(File),
722                 FileName(owner_->filePath()), tmpf);
723         if (rc) {
724                 LYXERR(Debug::LYXVC, "cvs log failed with exit code " << rc);
725                 return;
726         }
727
728         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
729         static regex const reg("date: (.*) (.*) (.*);  author: (.*);  state: (.*);(.*)");
730
731         while (ifs) {
732                 string line;
733                 getline(ifs, line);
734                 LYXERR(Debug::LYXVC, line << '\n');
735                 if (prefixIs(line, "date:")) {
736                         smatch sm;
737                         if (regex_match(line, sm, reg)) {
738                           //sm[0]; // whole matched string
739                           rev_date_cache_ = sm[1];
740                           rev_time_cache_ = sm[2];
741                           //sm[3]; // GMT offset
742                           rev_author_cache_ = sm[4];
743                         } else
744                           LYXERR(Debug::LYXVC, "\tCannot parse line. Skipping."); 
745                         break;
746                 }
747         }
748         if (rev_author_cache_.empty())
749                 LYXERR(Debug::LYXVC,
750                    "Could not retrieve revision info for " << version_ <<
751                    " of " << getTarget(File));
752 }
753
754
755 void CVS::registrer(string const & msg)
756 {
757         doVCCommand("cvs -q add -m \"" + msg + "\" "
758                 + getTarget(File),
759                 FileName(owner_->filePath()));
760 }
761
762
763 bool CVS::renameEnabled()
764 {
765         return true;
766 }
767
768
769 string CVS::rename(support::FileName const & newFile, string const & msg)
770 {
771         // CVS has no real rename command, so we create a poor mans version
772         support::FileName const oldFile(owner_->absFileName());
773         string ret = copy(newFile, msg);
774         if (ret.empty())
775                 return ret;
776         string cmd = "cvs -q remove -m \"" + msg + "\" " +
777                 quoteName(oldFile.onlyFileName());
778         FileName path(oldFile.onlyPath());
779         return doVCCommand(cmd, path) ? string() : ret;
780 }
781
782
783 bool CVS::copyEnabled()
784 {
785         return true;
786 }
787
788
789 string CVS::copy(support::FileName const & newFile, string const & msg)
790 {
791         // CVS has no real copy command, so we create a poor mans version
792         support::FileName const oldFile(owner_->absFileName());
793         if (!oldFile.copyTo(newFile))
794                 return string();
795         FileName path(oldFile.onlyPath());
796         string relFile(to_utf8(newFile.relPath(path.absFileName())));
797         string cmd("cvs -q add -m \"" + msg + "\" " + quoteName(relFile));
798         return doVCCommand(cmd, path) ? string() : "CVS: Proceeded";
799 }
800
801
802 void CVS::getDiff(OperationMode opmode, FileName const & tmpf)
803 {
804         doVCCommandWithOutput("cvs diff " + getTarget(opmode),
805                 FileName(owner_->filePath()), tmpf, false);
806 }
807
808
809 int CVS::edit()
810 {
811         vcstatus_ = LOCKED;
812         return doVCCommand("cvs -q edit " + getTarget(File),
813                 FileName(owner_->filePath()));
814 }
815
816
817 int CVS::unedit()
818 {
819         vcstatus_ = UNLOCKED;
820         return doVCCommand("cvs -q unedit " + getTarget(File),
821                 FileName(owner_->filePath()));
822 }
823
824
825 int CVS::update(OperationMode opmode, FileName const & tmpf)
826 {
827         return doVCCommandWithOutput("cvs -q update "
828                 + getTarget(opmode),
829                 FileName(owner_->filePath()), tmpf, false);
830 }
831
832
833 string CVS::scanLogFile(FileName const & f, string & status)
834 {
835         ifstream ifs(f.toFilesystemEncoding().c_str());
836
837         while (ifs) {
838                 string line;
839                 getline(ifs, line);
840                 LYXERR(Debug::LYXVC, line << '\n');
841                 if (!line.empty())
842                         status += line + "; ";
843                 if (prefixIs(line, "C ")) {
844                         ifs.close();
845                         return line;
846                 }
847         }
848         ifs.close();
849         return string();
850 }
851
852
853 LyXVC::CommandResult CVS::checkIn(string const & msg, string & log)
854 {
855         CvsStatus status = getStatus();
856         switch (status) {
857         case UpToDate:
858                 if (vcstatus_ != NOLOCKING)
859                         if (unedit())
860                                 return LyXVC::ErrorCommand;
861                 log = "CVS: Proceeded";
862                 return LyXVC::VCSuccess;
863         case LocallyModified:
864         case LocallyAdded: {
865                 int rc = doVCCommand("cvs -q commit -m \"" + msg + "\" "
866                         + getTarget(File),
867                     FileName(owner_->filePath()));
868                 if (rc)
869                         return LyXVC::ErrorCommand;
870                 log = "CVS: Proceeded";
871                 return LyXVC::VCSuccess;
872         }
873         case NeedsMerge:
874         case NeedsCheckout:
875                 frontend::Alert::error(_("Revision control error."),
876                         _("The repository version is newer then the current check out.\n"
877                           "You have to update from repository first or revert your changes.")) ;
878                 break;
879         default:
880                 frontend::Alert::error(_("Revision control error."),
881                         bformat(_("Bad status when checking in changes.\n"
882                                           "\n'%1$s'\n\n"),
883                                 toString(status)));
884                 break;
885         }
886         return LyXVC::ErrorBefore;
887 }
888
889
890 bool CVS::isLocked() const
891 {
892         FileName fn(owner_->absFileName());
893         fn.refresh();
894         return !fn.isReadOnly();
895 }
896
897
898 bool CVS::checkInEnabled()
899 {
900         if (vcstatus_ != NOLOCKING)
901                 return isLocked();
902         else
903                 return true;
904 }
905
906
907 bool CVS::isCheckInWithConfirmation()
908 {
909         CvsStatus status = getStatus();
910         return status == LocallyModified || status == LocallyAdded;
911 }
912
913
914 string CVS::checkOut()
915 {
916         if (vcstatus_ != NOLOCKING && edit())
917                 return string();
918         TempFile tempfile("lyxvout");
919         FileName tmpf = tempfile.name();
920         if (tmpf.empty()) {
921                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
922                 return string();
923         }
924
925         int rc = update(File, tmpf);
926         string log;
927         string const res = scanLogFile(tmpf, log);
928         if (!res.empty()) {
929                 frontend::Alert::error(_("Revision control error."),
930                         bformat(_("Error when updating from repository.\n"
931                                 "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n"
932                                 "After pressing OK, LyX will try to reopen the resolved document."),
933                                 from_local8bit(res)));
934                 rc = 0;
935         }
936
937         return rc ? string() : log.empty() ? "CVS: Proceeded" : "CVS: " + log;
938 }
939
940
941 bool CVS::checkOutEnabled()
942 {
943         if (vcstatus_ != NOLOCKING)
944                 return !isLocked();
945         else
946                 return true;
947 }
948
949
950 string CVS::repoUpdate()
951 {
952         TempFile tempfile("lyxvout");
953         FileName tmpf = tempfile.name();
954         if (tmpf.empty()) {
955                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
956                 return string();
957         }
958
959         getDiff(Directory, tmpf);
960         docstring res = tmpf.fileContents("UTF-8");
961         if (!res.empty()) {
962                 LYXERR(Debug::LYXVC, "Diff detected:\n" << res);
963                 docstring const file = from_utf8(owner_->filePath());
964                 docstring text = bformat(_("There were detected changes "
965                                 "in the working directory:\n%1$s\n\n"
966                                 "Possible file conflicts must be then resolved manually "
967                                 "or you will need to revert back to the repository version."), file);
968                 int ret = frontend::Alert::prompt(_("Changes detected"),
969                                 text, 0, 1, _("&Continue"), _("&Abort"), _("View &Log ..."));
970                 if (ret == 2) {
971                         dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFileName()));
972                         ret = frontend::Alert::prompt(_("Changes detected"),
973                                 text, 0, 1, _("&Continue"), _("&Abort"));
974                         hideDialogs("file", nullptr);
975                 }
976                 if (ret == 1)
977                         return string();
978         }
979
980         int rc = update(Directory, tmpf);
981         res += "Update log:\n" + tmpf.fileContents("UTF-8");
982         LYXERR(Debug::LYXVC, res);
983
984         string log;
985         string sres = scanLogFile(tmpf, log);
986         if (!sres.empty()) {
987                 docstring const file = owner_->fileName().displayName(20);
988                 frontend::Alert::error(_("Revision control error."),
989                         bformat(_("Error when updating document %1$s from repository.\n"
990                                           "You have to manually resolve the conflicts NOW!\n'%2$s'.\n\n"
991                                           "After pressing OK, LyX will try to reopen the resolved document."),
992                                 file, from_local8bit(sres)));
993                 rc = 0;
994         }
995
996         return rc ? string() : log.empty() ? "CVS: Proceeded" : "CVS: " + log;
997 }
998
999
1000 bool CVS::repoUpdateEnabled()
1001 {
1002         return true;
1003 }
1004
1005
1006 string CVS::lockingToggle()
1007 {
1008         lyxerr << "Sorry, not implemented." << endl;
1009         return string();
1010 }
1011
1012
1013 bool CVS::lockingToggleEnabled()
1014 {
1015         return false;
1016 }
1017
1018
1019 bool CVS::isRevertWithConfirmation()
1020 {
1021         CvsStatus status = getStatus();
1022         return !owner_->isClean() || status == LocallyModified || status == NeedsMerge;
1023 }
1024
1025
1026 bool CVS::revert()
1027 {
1028         // Reverts to the version in CVS repository and
1029         // gets the updated version from the repository.
1030         CvsStatus status = getStatus();
1031         switch (status) {
1032         case UpToDate:
1033                 if (vcstatus_ != NOLOCKING)
1034                         return 0 == unedit();
1035                 break;
1036         case NeedsMerge:
1037         case NeedsCheckout:
1038         case LocallyModified: {
1039                 FileName f(owner_->absFileName());
1040                 f.removeFile();
1041                 update(File, FileName());
1042                 owner_->markClean();
1043                 break;
1044         }
1045         case LocallyAdded: {
1046                 docstring const file = owner_->fileName().displayName(20);
1047                 frontend::Alert::error(_("Revision control error."),
1048                         bformat(_("The document %1$s is not in repository.\n"
1049                                   "You have to check in the first revision before you can revert."),
1050                                 file)) ;
1051                 return false;
1052         }
1053         default: {
1054                 docstring const file = owner_->fileName().displayName(20);
1055                 frontend::Alert::error(_("Revision control error."),
1056                         bformat(_("Cannot revert document %1$s to repository version.\n"
1057                                   "The status '%2$s' is unexpected."),
1058                                 file, toString(status)));
1059                 return false;
1060                 }
1061         }
1062         return true;
1063 }
1064
1065
1066 void CVS::undoLast()
1067 {
1068         // merge the current with the previous version
1069         // in a reverse patch kind of way, so that the
1070         // result is to revert the last changes.
1071         lyxerr << "Sorry, not implemented." << endl;
1072 }
1073
1074
1075 bool CVS::undoLastEnabled()
1076 {
1077         return false;
1078 }
1079
1080
1081 void CVS::getLog(FileName const & tmpf)
1082 {
1083         doVCCommandWithOutput("cvs log " + getTarget(File),
1084                 FileName(owner_->filePath()),
1085                 tmpf);
1086 }
1087
1088
1089 bool CVS::toggleReadOnlyEnabled()
1090 {
1091         return false;
1092 }
1093
1094
1095 string CVS::revisionInfo(LyXVC::RevisionInfo const info)
1096 {
1097         if (!version_.empty()) {
1098                 getRevisionInfo();
1099                 switch (info) {
1100                 case LyXVC::File:
1101                         return version_;
1102                 case LyXVC::Author:
1103                         return rev_author_cache_;
1104                 case LyXVC::Date:
1105                         return rev_date_cache_;
1106                 case LyXVC::Time:
1107                         return rev_time_cache_;
1108                 default:
1109                         break;
1110                 }
1111         }
1112         return string();
1113 }
1114
1115
1116 bool CVS::prepareFileRevision(string const & revis, string & f)
1117 {
1118         string rev = revis;
1119         if (!VCS::makeRCSRevision(version_, rev))
1120                 return false;
1121
1122         TempFile tempfile("lyxvcrev_" + rev + '_');
1123         tempfile.setAutoRemove(false);
1124         FileName tmpf = tempfile.name();
1125         if (tmpf.empty()) {
1126                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1127                 return false;
1128         }
1129
1130         doVCCommandWithOutput("cvs update -p -r" + rev + ' '
1131                 + getTarget(File),
1132                 FileName(owner_->filePath()), tmpf);
1133         tmpf.refresh();
1134         if (tmpf.isFileEmpty())
1135                 return false;
1136
1137         f = tmpf.absFileName();
1138         return true;
1139 }
1140
1141
1142 bool CVS::prepareFileRevisionEnabled()
1143 {
1144         return true;
1145 }
1146
1147
1148 /////////////////////////////////////////////////////////////////////
1149 //
1150 // SVN
1151 //
1152 /////////////////////////////////////////////////////////////////////
1153
1154 SVN::SVN(Buffer * b) : VCS(b)
1155 {
1156         // Here we know that the buffer file is either already in SVN or
1157         // about to be registered
1158         locked_mode_ = false;
1159         scanMaster();
1160 }
1161
1162
1163 bool SVN::findFile(FileName const & file)
1164 {
1165         // First we check the existence of repository meta data.
1166         if (VCS::checkParentDirs(file, ".svn").empty()) {
1167                 LYXERR(Debug::LYXVC, "Cannot find SVN meta data for " << file);
1168                 return false;
1169         }
1170
1171         // Now we check the status of the file.
1172         string const fname = onlyFileName(file.absFileName());
1173         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under svn control for `" << fname << '\'');
1174         bool found = 0 == doVCCommandCall("svn info " + quoteName(fname),
1175                                                 file.onlyPath());
1176         LYXERR(Debug::LYXVC, "SVN control: " << (found ? "enabled" : "disabled"));
1177         return found;
1178 }
1179
1180
1181 void SVN::scanMaster()
1182 {
1183         // vcstatus code is somewhat superflous,
1184         // until we want to implement read-only toggle for svn.
1185         vcstatus_ = NOLOCKING;
1186         if (checkLockMode()) {
1187                 if (isLocked())
1188                         vcstatus_ = LOCKED;
1189                 else
1190                         vcstatus_ = UNLOCKED;
1191         }
1192 }
1193
1194
1195 bool SVN::checkLockMode()
1196 {
1197         TempFile tempfile("lyxvcout");
1198         FileName tmpf = tempfile.name();
1199         if (tmpf.empty()){
1200                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1201                 return false;
1202         }
1203
1204         LYXERR(Debug::LYXVC, "Detecting locking mode...");
1205         if (doVCCommandCall("svn proplist " + quoteName(onlyFileName(owner_->absFileName()))
1206                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1207                     FileName(owner_->filePath())))
1208                 return false;
1209
1210         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1211         string line;
1212         bool ret = false;
1213
1214         while (ifs && !ret) {
1215                 getline(ifs, line);
1216                 LYXERR(Debug::LYXVC, line);
1217                 if (contains(line, "svn:needs-lock"))
1218                         ret = true;
1219         }
1220         LYXERR(Debug::LYXVC, "Locking enabled: " << ret);
1221         ifs.close();
1222         locked_mode_ = ret;
1223         return ret;
1224
1225 }
1226
1227
1228 bool SVN::isLocked() const
1229 {
1230         FileName file(owner_->absFileName());
1231         file.refresh();
1232         return !file.isReadOnly();
1233 }
1234
1235
1236 bool SVN::retrieve(FileName const & file)
1237 {
1238         LYXERR(Debug::LYXVC, "LyXVC::SVN: retrieve.\n\t" << file);
1239         // The caller ensures that file does not exist, so no need to check that.
1240         return doVCCommandCall("svn update -q --non-interactive " + quoteName(file.onlyFileName()),
1241                                file.onlyPath()) == 0;
1242 }
1243
1244
1245 void SVN::registrer(string const & /*msg*/)
1246 {
1247         doVCCommand("svn add -q --parents " + quoteName(onlyFileName(owner_->absFileName())),
1248                     FileName(owner_->filePath()));
1249 }
1250
1251
1252 bool SVN::renameEnabled()
1253 {
1254         return true;
1255 }
1256
1257
1258 string SVN::rename(support::FileName const & newFile, string const & msg)
1259 {
1260         // svn move does not require a log message, since it does not commit.
1261         // In LyX we commit immediately afterwards, otherwise it could be
1262         // confusing to the user to have two uncommitted files.
1263         FileName path(owner_->filePath());
1264         string relFile(to_utf8(newFile.relPath(path.absFileName())));
1265         string cmd("svn move -q " + quoteName(onlyFileName(owner_->absFileName())) +
1266                    ' ' + quoteName(relFile));
1267         if (doVCCommand(cmd, path)) {
1268                 cmd = "svn revert -q " +
1269                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1270                         quoteName(relFile);
1271                 doVCCommand(cmd, path);
1272                 if (newFile.exists())
1273                         newFile.removeFile();
1274                 return string();
1275         }
1276         vector<support::FileName> f;
1277         f.push_back(owner_->fileName());
1278         f.push_back(newFile);
1279         string log;
1280         if (checkIn(f, msg, log) != LyXVC::VCSuccess) {
1281                 cmd = "svn revert -q " +
1282                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1283                         quoteName(relFile);
1284                 doVCCommand(cmd, path);
1285                 if (newFile.exists())
1286                         newFile.removeFile();
1287                 return string();
1288         }
1289         return log;
1290 }
1291
1292
1293 bool SVN::copyEnabled()
1294 {
1295         return true;
1296 }
1297
1298
1299 string SVN::copy(support::FileName const & newFile, string const & msg)
1300 {
1301         // svn copy does not require a log message, since it does not commit.
1302         // In LyX we commit immediately afterwards, otherwise it could be
1303         // confusing to the user to have an uncommitted file.
1304         FileName path(owner_->filePath());
1305         string relFile(to_utf8(newFile.relPath(path.absFileName())));
1306         string cmd("svn copy -q " + quoteName(onlyFileName(owner_->absFileName())) +
1307                    ' ' + quoteName(relFile));
1308         if (doVCCommand(cmd, path))
1309                 return string();
1310         vector<support::FileName> f(1, newFile);
1311         string log;
1312         if (checkIn(f, msg, log) == LyXVC::VCSuccess)
1313                 return log;
1314         return string();
1315 }
1316
1317
1318 LyXVC::CommandResult SVN::checkIn(string const & msg, string & log)
1319 {
1320         vector<support::FileName> f(1, owner_->fileName());
1321         return checkIn(f, msg, log);
1322 }
1323
1324
1325 LyXVC::CommandResult
1326 SVN::checkIn(vector<support::FileName> const & f, string const & msg, string & log)
1327 {
1328         TempFile tempfile("lyxvcout");
1329         FileName tmpf = tempfile.name();
1330         if (tmpf.empty()){
1331                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1332                 log = N_("Error: Could not generate logfile.");
1333                 return LyXVC::ErrorBefore;
1334         }
1335
1336         ostringstream os;
1337         os << "svn commit -m \"" << msg << '"';
1338         for (size_t i = 0; i < f.size(); ++i)
1339                 os << ' ' << quoteName(f[i].onlyFileName());
1340         os << " > " << quoteName(tmpf.toFilesystemEncoding());
1341         LyXVC::CommandResult ret =
1342                 doVCCommand(os.str(), FileName(owner_->filePath())) ?
1343                         LyXVC::ErrorCommand : LyXVC::VCSuccess;
1344
1345         string res = scanLogFile(tmpf, log);
1346         if (!res.empty()) {
1347                 frontend::Alert::error(_("Revision control error."),
1348                                 _("Error when committing to repository.\n"
1349                                 "You have to manually resolve the problem.\n"
1350                                 "LyX will reopen the document after you press OK."));
1351                 ret = LyXVC::ErrorCommand;
1352         }
1353         else
1354                 if (!fileLock(false, tmpf, log))
1355                         ret = LyXVC::ErrorCommand;
1356
1357         if (!log.empty())
1358                 log.insert(0, "SVN: ");
1359         if (ret == LyXVC::VCSuccess && log.empty())
1360                 log = "SVN: Proceeded";
1361         return ret;
1362 }
1363
1364
1365 bool SVN::checkInEnabled()
1366 {
1367         if (locked_mode_)
1368                 return isLocked();
1369         else
1370                 return true;
1371 }
1372
1373
1374 bool SVN::isCheckInWithConfirmation()
1375 {
1376         // FIXME one day common getDiff and perhaps OpMode for all backends
1377
1378         TempFile tempfile("lyxvcout");
1379         FileName tmpf = tempfile.name();
1380         if (tmpf.empty()) {
1381                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1382                 return true;
1383         }
1384
1385         doVCCommandCall("svn diff " + quoteName(owner_->absFileName())
1386                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1387                 FileName(owner_->filePath()));
1388
1389         docstring diff = tmpf.fileContents("UTF-8");
1390
1391         if (diff.empty())
1392                 return false;
1393
1394         return true;
1395 }
1396
1397
1398 // FIXME Correctly return code should be checked instead of this.
1399 // This would need another solution than just plain startscript.
1400 // Hint from Andre': QProcess::readAllStandardError()...
1401 string SVN::scanLogFile(FileName const & f, string & status)
1402 {
1403         ifstream ifs(f.toFilesystemEncoding().c_str());
1404         string line;
1405
1406         while (ifs) {
1407                 getline(ifs, line);
1408                 LYXERR(Debug::LYXVC, line << '\n');
1409                 if (!line.empty())
1410                         status += line + "; ";
1411                 if (prefixIs(line, "C ") || prefixIs(line, "CU ")
1412                                          || contains(line, "Commit failed")) {
1413                         ifs.close();
1414                         return line;
1415                 }
1416                 if (contains(line, "svn:needs-lock")) {
1417                         ifs.close();
1418                         return line;
1419                 }
1420         }
1421         ifs.close();
1422         return string();
1423 }
1424
1425
1426 bool SVN::fileLock(bool lock, FileName const & tmpf, string &status)
1427 {
1428         if (!locked_mode_ || (isLocked() == lock))
1429                 return true;
1430
1431         string const arg = lock ? "lock " : "unlock ";
1432         doVCCommand("svn "+ arg + quoteName(onlyFileName(owner_->absFileName()))
1433                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1434                     FileName(owner_->filePath()));
1435
1436         // Lock error messages go unfortunately on stderr and are unreachable this way.
1437         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1438         string line;
1439         while (ifs) {
1440                 getline(ifs, line);
1441                 if (!line.empty()) status += line + "; ";
1442         }
1443         ifs.close();
1444
1445         if (isLocked() == lock)
1446                 return true;
1447
1448         if (lock)
1449                 frontend::Alert::error(_("Revision control error."),
1450                         _("Error while acquiring write lock.\n"
1451                         "Another user is most probably editing\n"
1452                         "the current document now!\n"
1453                         "Also check the access to the repository."));
1454         else
1455                 frontend::Alert::error(_("Revision control error."),
1456                         _("Error while releasing write lock.\n"
1457                         "Check the access to the repository."));
1458         return false;
1459 }
1460
1461
1462 string SVN::checkOut()
1463 {
1464         TempFile tempfile("lyxvcout");
1465         FileName tmpf = tempfile.name();
1466         if (tmpf.empty()) {
1467                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1468                 return N_("Error: Could not generate logfile.");
1469         }
1470
1471         doVCCommand("svn update --non-interactive " + quoteName(onlyFileName(owner_->absFileName()))
1472                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1473                     FileName(owner_->filePath()));
1474
1475         string log;
1476         string const res = scanLogFile(tmpf, log);
1477         if (!res.empty())
1478                 frontend::Alert::error(_("Revision control error."),
1479                         bformat(_("Error when updating from repository.\n"
1480                                 "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n"
1481                                 "After pressing OK, LyX will try to reopen the resolved document."),
1482                         from_local8bit(res)));
1483
1484         fileLock(true, tmpf, log);
1485
1486         return log.empty() ? string() : "SVN: " + log;
1487 }
1488
1489
1490 bool SVN::checkOutEnabled()
1491 {
1492         if (locked_mode_)
1493                 return !isLocked();
1494         else
1495                 return true;
1496 }
1497
1498
1499 string SVN::repoUpdate()
1500 {
1501         TempFile tempfile("lyxvcout");
1502         FileName tmpf = tempfile.name();
1503         if (tmpf.empty()) {
1504                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1505                 return N_("Error: Could not generate logfile.");
1506         }
1507
1508         doVCCommand("svn diff " + quoteName(owner_->filePath())
1509                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1510                 FileName(owner_->filePath()));
1511         docstring res = tmpf.fileContents("UTF-8");
1512         if (!res.empty()) {
1513                 LYXERR(Debug::LYXVC, "Diff detected:\n" << res);
1514                 docstring const file = from_utf8(owner_->filePath());
1515                 docstring text = bformat(_("There were detected changes "
1516                                 "in the working directory:\n%1$s\n\n"
1517                                 "In case of file conflict version of the local directory files "
1518                                 "will be preferred."
1519                                 "\n\nContinue?"), file);
1520                 int ret = frontend::Alert::prompt(_("Changes detected"),
1521                                 text, 0, 1, _("&Yes"), _("&No"), _("View &Log ..."));
1522                 if (ret == 2) {
1523                         dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFileName()));
1524                         ret = frontend::Alert::prompt(_("Changes detected"),
1525                                 text, 0, 1, _("&Yes"), _("&No"));
1526                         hideDialogs("file", nullptr);
1527                 }
1528                 if (ret == 1)
1529                         return string();
1530         }
1531
1532         // Reverting looks too harsh, see bug #6255.
1533         // doVCCommand("svn revert -R " + quoteName(owner_->filePath())
1534         // + " > " + quoteName(tmpf.toFilesystemEncoding()),
1535         // FileName(owner_->filePath()));
1536         // res = "Revert log:\n" + tmpf.fileContents("UTF-8");
1537         doVCCommand("svn update --accept mine-full " + quoteName(owner_->filePath())
1538                 + " > " + quoteName(tmpf.toFilesystemEncoding()),
1539                 FileName(owner_->filePath()));
1540         res += "Update log:\n" + tmpf.fileContents("UTF-8");
1541
1542         LYXERR(Debug::LYXVC, res);
1543         return to_utf8(res);
1544 }
1545
1546
1547 bool SVN::repoUpdateEnabled()
1548 {
1549         return true;
1550 }
1551
1552
1553 string SVN::lockingToggle()
1554 {
1555         TempFile tempfile("lyxvcout");
1556         FileName tmpf = tempfile.name();
1557         if (tmpf.empty()) {
1558                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1559                 return N_("Error: Could not generate logfile.");
1560         }
1561
1562         int ret = doVCCommand("svn proplist " + quoteName(onlyFileName(owner_->absFileName()))
1563                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1564                     FileName(owner_->filePath()));
1565         if (ret)
1566                 return string();
1567
1568         string log;
1569         string res = scanLogFile(tmpf, log);
1570         bool locking = contains(res, "svn:needs-lock");
1571         if (!locking)
1572                 ret = doVCCommand("svn propset svn:needs-lock ON "
1573                     + quoteName(onlyFileName(owner_->absFileName()))
1574                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1575                     FileName(owner_->filePath()));
1576         else
1577                 ret = doVCCommand("svn propdel svn:needs-lock "
1578                     + quoteName(onlyFileName(owner_->absFileName()))
1579                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1580                     FileName(owner_->filePath()));
1581         if (ret)
1582                 return string();
1583
1584         frontend::Alert::warning(_("SVN File Locking"),
1585                 (locking ? _("Locking property unset.") : _("Locking property set.")) + '\n'
1586                 + _("Do not forget to commit the locking property into the repository."),
1587                 true);
1588
1589         return string("SVN: ") + (locking ?
1590                 N_("Locking property unset.") : N_("Locking property set."));
1591 }
1592
1593
1594 bool SVN::lockingToggleEnabled()
1595 {
1596         return true;
1597 }
1598
1599
1600 bool SVN::revert()
1601 {
1602         // Reverts to the version in SVN repository and
1603         // gets the updated version from the repository.
1604         string const fil = quoteName(onlyFileName(owner_->absFileName()));
1605
1606         if (doVCCommand("svn revert -q " + fil,
1607                     FileName(owner_->filePath())))
1608                 return false;
1609         owner_->markClean();
1610         return true;
1611 }
1612
1613
1614 bool SVN::isRevertWithConfirmation()
1615 {
1616         //FIXME owner && diff
1617         return true;
1618 }
1619
1620
1621 void SVN::undoLast()
1622 {
1623         // merge the current with the previous version
1624         // in a reverse patch kind of way, so that the
1625         // result is to revert the last changes.
1626         lyxerr << "Sorry, not implemented." << endl;
1627 }
1628
1629
1630 bool SVN::undoLastEnabled()
1631 {
1632         return false;
1633 }
1634
1635
1636 string SVN::revisionInfo(LyXVC::RevisionInfo const info)
1637 {
1638         if (info == LyXVC::Tree) {
1639                 if (rev_tree_cache_.empty())
1640                         if (!getTreeRevisionInfo())
1641                                 rev_tree_cache_ = "?";
1642                 if (rev_tree_cache_ == "?")
1643                         return string();
1644
1645                 return rev_tree_cache_;
1646         }
1647
1648         // fill the rest of the attributes for a single file
1649         if (rev_file_cache_.empty())
1650                 if (!getFileRevisionInfo())
1651                         rev_file_cache_ = "?";
1652
1653         switch (info) {
1654                 case LyXVC::File:
1655                         if (rev_file_cache_ == "?")
1656                                 return string();
1657                         return rev_file_cache_;
1658                 case LyXVC::Author:
1659                         return rev_author_cache_;
1660                 case LyXVC::Date:
1661                         return rev_date_cache_;
1662                 case LyXVC::Time:
1663                         return rev_time_cache_;
1664                 default:
1665                         break;
1666         }
1667
1668         return string();
1669 }
1670
1671
1672 bool SVN::getFileRevisionInfo()
1673 {
1674         TempFile tempfile("lyxvcout");
1675         FileName tmpf = tempfile.name();
1676         if (tmpf.empty()) {
1677                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1678                 return false;
1679         }
1680
1681         doVCCommand("svn info --xml " + quoteName(onlyFileName(owner_->absFileName()))
1682                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1683                     FileName(owner_->filePath()));
1684
1685         if (tmpf.empty())
1686                 return false;
1687
1688         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1689         string line;
1690         // commit log part
1691         bool c = false;
1692         string rev;
1693
1694         while (ifs) {
1695                 getline(ifs, line);
1696                 LYXERR(Debug::LYXVC, line);
1697                 if (prefixIs(line, "<commit"))
1698                         c = true;
1699                 if (c && prefixIs(line, "   revision=\"") && suffixIs(line, "\">")) {
1700                         string l1 = subst(line, "revision=\"", "");
1701                         string l2 = trim(subst(l1, "\">", ""));
1702                         if (isStrInt(l2))
1703                                 rev_file_cache_ = rev = l2;
1704                 }
1705                 if (c && prefixIs(line, "<author>") && suffixIs(line, "</author>")) {
1706                         string l1 = subst(line, "<author>", "");
1707                         string l2 = subst(l1, "</author>", "");
1708                         rev_author_cache_ = l2;
1709                 }
1710                 if (c && prefixIs(line, "<date>") && suffixIs(line, "</date>")) {
1711                         string l1 = subst(line, "<date>", "");
1712                         string l2 = subst(l1, "</date>", "");
1713                         l2 = split(l2, l1, 'T');
1714                         rev_date_cache_ = l1;
1715                         l2 = split(l2, l1, '.');
1716                         rev_time_cache_ = l1;
1717                 }
1718         }
1719
1720         ifs.close();
1721         return !rev.empty();
1722 }
1723
1724
1725 bool SVN::getTreeRevisionInfo()
1726 {
1727         TempFile tempfile("lyxvcout");
1728         FileName tmpf = tempfile.name();
1729         if (tmpf.empty()) {
1730                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1731                 return false;
1732         }
1733
1734         doVCCommand("svnversion -n . > " + quoteName(tmpf.toFilesystemEncoding()),
1735                     FileName(owner_->filePath()));
1736
1737         if (tmpf.empty())
1738                 return false;
1739
1740         // only first line in case something bad happens.
1741         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1742         string line;
1743         getline(ifs, line);
1744         ifs.close();
1745
1746         rev_tree_cache_ = line;
1747         return !line.empty();
1748 }
1749
1750
1751 void SVN::getLog(FileName const & tmpf)
1752 {
1753         doVCCommand("svn log " + quoteName(onlyFileName(owner_->absFileName()))
1754                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1755                     FileName(owner_->filePath()));
1756 }
1757
1758
1759 bool SVN::prepareFileRevision(string const & revis, string & f)
1760 {
1761         if (!isStrInt(revis))
1762                 return false;
1763
1764         int rev = convert<int>(revis);
1765         if (rev <= 0)
1766                 if (!getFileRevisionInfo())
1767                         return false;
1768         if (rev == 0)
1769                 rev = convert<int>(rev_file_cache_);
1770         // go back for minus rev
1771         else if (rev < 0) {
1772                 rev = rev + convert<int>(rev_file_cache_);
1773                 if (rev < 1)
1774                         return false;
1775         }
1776
1777         string revname = convert<string>(rev);
1778         TempFile tempfile("lyxvcrev_" + revname + '_');
1779         tempfile.setAutoRemove(false);
1780         FileName tmpf = tempfile.name();
1781         if (tmpf.empty()) {
1782                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1783                 return false;
1784         }
1785
1786         doVCCommand("svn cat -r " + revname + ' '
1787                       + quoteName(onlyFileName(owner_->absFileName()))
1788                       + " > " + quoteName(tmpf.toFilesystemEncoding()),
1789                 FileName(owner_->filePath()));
1790         tmpf.refresh();
1791         if (tmpf.isFileEmpty())
1792                 return false;
1793
1794         f = tmpf.absFileName();
1795         return true;
1796 }
1797
1798
1799 bool SVN::prepareFileRevisionEnabled()
1800 {
1801         return true;
1802 }
1803
1804
1805
1806 bool SVN::toggleReadOnlyEnabled()
1807 {
1808         return false;
1809 }
1810
1811
1812 /////////////////////////////////////////////////////////////////////
1813 //
1814 // GIT
1815 //
1816 /////////////////////////////////////////////////////////////////////
1817
1818 GIT::GIT(Buffer * b) : VCS(b)
1819 {
1820         // Here we know that the buffer file is either already in GIT or
1821         // about to be registered
1822         scanMaster();
1823 }
1824
1825
1826 bool GIT::findFile(FileName const & file)
1827 {
1828         // First we check the existence of repository meta data.
1829         if (VCS::checkParentDirs(file, ".git").empty()) {
1830                 LYXERR(Debug::LYXVC, "Cannot find GIT meta data for " << file);
1831                 return false;
1832         }
1833
1834         // Now we check if the file is known to git.
1835         string const fname = onlyFileName(file.absFileName());
1836         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under git control for `"
1837                         << fname << '\'');
1838         int const ret = doVCCommandCall("git log " + quoteName(fname),
1839                         file.onlyPath());
1840         bool const found = (ret == 0);
1841         LYXERR(Debug::LYXVC, "GIT control: " << (found ? "enabled" : "disabled"));
1842         return found;
1843 }
1844
1845
1846 void GIT::scanMaster()
1847 {
1848         // vcstatus code is somewhat superflous,
1849         // until we want to implement read-only toggle for git.
1850         vcstatus_ = NOLOCKING;
1851 }
1852
1853
1854 bool GIT::retrieve(FileName const & file)
1855 {
1856         LYXERR(Debug::LYXVC, "LyXVC::GIT: retrieve.\n\t" << file);
1857         // The caller ensures that file does not exist, so no need to check that.
1858         return doVCCommandCall("git checkout -q " + quoteName(file.onlyFileName()),
1859                                file.onlyPath()) == 0;
1860 }
1861
1862
1863 void GIT::registrer(string const & /*msg*/)
1864 {
1865         doVCCommand("git add " + quoteName(onlyFileName(owner_->absFileName())),
1866                     FileName(owner_->filePath()));
1867 }
1868
1869
1870 bool GIT::renameEnabled()
1871 {
1872         return true;
1873 }
1874
1875
1876 string GIT::rename(support::FileName const & newFile, string const & msg)
1877 {
1878         // git mv does not require a log message, since it does not commit.
1879         // In LyX we commit immediately afterwards, otherwise it could be
1880         // confusing to the user to have two uncommitted files.
1881         FileName path(owner_->filePath());
1882         string relFile(to_utf8(newFile.relPath(path.absFileName())));
1883         string cmd("git mv " + quoteName(onlyFileName(owner_->absFileName())) +
1884                    ' ' + quoteName(relFile));
1885         if (doVCCommand(cmd, path)) {
1886                 cmd = "git checkout -q " +
1887                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1888                         quoteName(relFile);
1889                 doVCCommand(cmd, path);
1890                 if (newFile.exists())
1891                         newFile.removeFile();
1892                 return string();
1893         }
1894         vector<support::FileName> f;
1895         f.push_back(owner_->fileName());
1896         f.push_back(newFile);
1897         string log;
1898         if (checkIn(f, msg, log) != LyXVC::VCSuccess) {
1899                 cmd = "git checkout -q " +
1900                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1901                         quoteName(relFile);
1902                 doVCCommand(cmd, path);
1903                 if (newFile.exists())
1904                         newFile.removeFile();
1905                 return string();
1906         }
1907         return log;
1908 }
1909
1910
1911 bool GIT::copyEnabled()
1912 {
1913         return false;
1914 }
1915
1916
1917 string GIT::copy(support::FileName const & /*newFile*/, string const & /*msg*/)
1918 {
1919         // git does not support copy with history preservation
1920         return string();
1921 }
1922
1923
1924 LyXVC::CommandResult GIT::checkIn(string const & msg, string & log)
1925 {
1926         vector<support::FileName> f(1, owner_->fileName());
1927         return checkIn(f, msg, log);
1928 }
1929
1930
1931 LyXVC::CommandResult
1932 GIT::checkIn(vector<support::FileName> const & f, string const & msg, string & log)
1933 {
1934         TempFile tempfile("lyxvcout");
1935         FileName tmpf = tempfile.name();
1936         if (tmpf.empty()){
1937                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1938                 log = N_("Error: Could not generate logfile.");
1939                 return LyXVC::ErrorBefore;
1940         }
1941
1942         ostringstream os;
1943         os << "git commit -m \"" << msg << '"';
1944         for (size_t i = 0; i < f.size(); ++i)
1945                 os << ' ' << quoteName(f[i].onlyFileName());
1946         os << " > " << quoteName(tmpf.toFilesystemEncoding());
1947         LyXVC::CommandResult ret =
1948                 doVCCommand(os.str(), FileName(owner_->filePath())) ?
1949                         LyXVC::ErrorCommand : LyXVC::VCSuccess;
1950
1951         string res = scanLogFile(tmpf, log);
1952         if (!res.empty()) {
1953                 frontend::Alert::error(_("Revision control error."),
1954                                 _("Error when committing to repository.\n"
1955                                 "You have to manually resolve the problem.\n"
1956                                 "LyX will reopen the document after you press OK."));
1957                 ret = LyXVC::ErrorCommand;
1958         }
1959
1960         if (!log.empty())
1961                 log.insert(0, "GIT: ");
1962         if (ret == LyXVC::VCSuccess && log.empty())
1963                 log = "GIT: Proceeded";
1964         return ret;
1965 }
1966
1967
1968 bool GIT::checkInEnabled()
1969 {
1970         return true;
1971 }
1972
1973
1974 bool GIT::isCheckInWithConfirmation()
1975 {
1976         // FIXME one day common getDiff and perhaps OpMode for all backends
1977
1978         TempFile tempfile("lyxvcout");
1979         FileName tmpf = tempfile.name();
1980         if (tmpf.empty()) {
1981                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1982                 return true;
1983         }
1984
1985         doVCCommandCall("git diff " + quoteName(owner_->absFileName())
1986                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1987                 FileName(owner_->filePath()));
1988
1989         docstring diff = tmpf.fileContents("UTF-8");
1990
1991         if (diff.empty())
1992                 return false;
1993
1994         return true;
1995 }
1996
1997
1998 // FIXME Correctly return code should be checked instead of this.
1999 // This would need another solution than just plain startscript.
2000 // Hint from Andre': QProcess::readAllStandardError()...
2001 string GIT::scanLogFile(FileName const & f, string & status)
2002 {
2003         ifstream ifs(f.toFilesystemEncoding().c_str());
2004         string line;
2005
2006         while (ifs) {
2007                 getline(ifs, line);
2008                 LYXERR(Debug::LYXVC, line << "\n");
2009                 if (!line.empty())
2010                         status += line + "; ";
2011                 if (prefixIs(line, "C ") || prefixIs(line, "CU ")
2012                                          || contains(line, "Commit failed")) {
2013                         ifs.close();
2014                         return line;
2015                 }
2016         }
2017         ifs.close();
2018         return string();
2019 }
2020
2021
2022 string GIT::checkOut()
2023 {
2024         return string();
2025 }
2026
2027
2028 bool GIT::checkOutEnabled()
2029 {
2030         return false;
2031 }
2032
2033
2034 string GIT::repoUpdate()
2035 {
2036         return string();
2037 }
2038
2039
2040 bool GIT::repoUpdateEnabled()
2041 {
2042         return false;
2043 }
2044
2045
2046 string GIT::lockingToggle()
2047 {
2048         return string();
2049 }
2050
2051
2052 bool GIT::lockingToggleEnabled()
2053 {
2054         return false;
2055 }
2056
2057
2058 bool GIT::revert()
2059 {
2060         // Reverts to the version in GIT repository and
2061         // gets the updated version from the repository.
2062         string const fil = quoteName(onlyFileName(owner_->absFileName()));
2063
2064         if (doVCCommand("git checkout -q " + fil,
2065                     FileName(owner_->filePath())))
2066                 return false;
2067         owner_->markClean();
2068         return true;
2069 }
2070
2071
2072 bool GIT::isRevertWithConfirmation()
2073 {
2074         //FIXME owner && diff
2075         return true;
2076 }
2077
2078
2079 void GIT::undoLast()
2080 {
2081         // merge the current with the previous version
2082         // in a reverse patch kind of way, so that the
2083         // result is to revert the last changes.
2084         lyxerr << "Sorry, not implemented." << endl;
2085 }
2086
2087
2088 bool GIT::undoLastEnabled()
2089 {
2090         return false;
2091 }
2092
2093
2094 string GIT::revisionInfo(LyXVC::RevisionInfo const info)
2095 {
2096         if (info == LyXVC::Tree) {
2097                 if (rev_tree_cache_.empty())
2098                         if (!getTreeRevisionInfo())
2099                                 rev_tree_cache_ = "?";
2100                 if (rev_tree_cache_ == "?")
2101                         return string();
2102
2103                 return rev_tree_cache_;
2104         }
2105
2106         // fill the rest of the attributes for a single file
2107         if (rev_file_cache_.empty())
2108                 if (!getFileRevisionInfo()) {
2109                         rev_file_cache_ = "?";
2110                         rev_file_abbrev_cache_ = "?";
2111     }
2112
2113         switch (info) {
2114                 case LyXVC::File:
2115                         if (rev_file_cache_ == "?")
2116                                 return string();
2117                         return rev_file_cache_;
2118                 case LyXVC::FileAbbrev:
2119                         if (rev_file_abbrev_cache_ == "?")
2120                                 return string();
2121                         return rev_file_abbrev_cache_;
2122                 case LyXVC::Author:
2123                         return rev_author_cache_;
2124                 case LyXVC::Date:
2125                         return rev_date_cache_;
2126                 case LyXVC::Time:
2127                         return rev_time_cache_;
2128                 default:
2129                         break;
2130         }
2131
2132         return string();
2133 }
2134
2135
2136 bool GIT::getFileRevisionInfo()
2137 {
2138         TempFile tempfile("lyxvcout");
2139         FileName tmpf = tempfile.name();
2140         if (tmpf.empty()) {
2141                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
2142                 return false;
2143         }
2144
2145         doVCCommand("git log -n 1 --pretty=format:%H%n%h%n%an%n%ai " + quoteName(onlyFileName(owner_->absFileName()))
2146                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
2147                     FileName(owner_->filePath()));
2148
2149         if (tmpf.empty())
2150                 return false;
2151
2152         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
2153
2154         if (ifs)
2155                 getline(ifs, rev_file_cache_);
2156         if (ifs)
2157                 getline(ifs, rev_file_abbrev_cache_);
2158         if (ifs)
2159                 getline(ifs, rev_author_cache_);
2160         if (ifs) {
2161                 string line;
2162                 getline(ifs, line);
2163                 rev_time_cache_ = split(line, rev_date_cache_, ' ');
2164         }
2165
2166         ifs.close();
2167         return !rev_file_cache_.empty();
2168 }
2169
2170
2171 bool GIT::getTreeRevisionInfo()
2172 {
2173         TempFile tempfile("lyxvcout");
2174         FileName tmpf = tempfile.name();
2175         if (tmpf.empty()) {
2176                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
2177                 return false;
2178         }
2179
2180         doVCCommand("git describe --abbrev --dirty --long > " + quoteName(tmpf.toFilesystemEncoding()),
2181                     FileName(owner_->filePath()),
2182                     false); //git describe returns $?=128 when no tag found (but git repo still exists)
2183
2184         if (tmpf.empty())
2185                 return false;
2186
2187         // only first line in case something bad happens.
2188         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
2189         getline(ifs, rev_tree_cache_);
2190         ifs.close();
2191
2192         return !rev_tree_cache_.empty();
2193 }
2194
2195
2196 void GIT::getLog(FileName const & tmpf)
2197 {
2198         doVCCommand("git log " + quoteName(onlyFileName(owner_->absFileName()))
2199                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
2200                     FileName(owner_->filePath()));
2201 }
2202
2203
2204 //at this moment we don't accept revision SHA, but just number of revision steps back
2205 //GUI and infrastucture needs to be changed first
2206 bool GIT::prepareFileRevision(string const & revis, string & f)
2207 {
2208         // anything positive means we got hash, not "0" or minus revision
2209         int rev = 1;
2210
2211         // hash is rarely number and should be long
2212         if (isStrInt(revis) && revis.length()<20)
2213                 rev = convert<int>(revis);
2214
2215         // revision and filename
2216         string pointer;
2217
2218         // go back for "minus" revisions
2219         if (rev <= 0)
2220                 pointer = "HEAD~" + convert<string>(-rev);
2221         // normal hash
2222         else
2223                 pointer = revis;
2224
2225         pointer += ':';
2226
2227         TempFile tempfile("lyxvcrev_" + revis + '_');
2228         tempfile.setAutoRemove(false);
2229         FileName tmpf = tempfile.name();
2230         if (tmpf.empty()) {
2231                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
2232                 return false;
2233         }
2234
2235         doVCCommand("git show " + pointer + "./"
2236                       + quoteName(onlyFileName(owner_->absFileName()))
2237                       + " > " + quoteName(tmpf.toFilesystemEncoding()),
2238                 FileName(owner_->filePath()));
2239         tmpf.refresh();
2240         if (tmpf.isFileEmpty())
2241                 return false;
2242
2243         f = tmpf.absFileName();
2244         return true;
2245 }
2246
2247
2248 bool GIT::prepareFileRevisionEnabled()
2249 {
2250         return true;
2251 }
2252
2253
2254 bool GIT::toggleReadOnlyEnabled()
2255 {
2256         return true;
2257 }
2258
2259
2260 } // namespace lyx