]> git.lyx.org Git - lyx.git/blob - src/VCBackend.cpp
7f97455a4ed7074ad664f14b408b65bf875af424
[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                 vcstatus_ = isLocked() ? LOCKED : UNLOCKED;
1188 }
1189
1190
1191 bool SVN::checkLockMode()
1192 {
1193         TempFile tempfile("lyxvcout");
1194         FileName tmpf = tempfile.name();
1195         if (tmpf.empty()){
1196                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1197                 return false;
1198         }
1199
1200         LYXERR(Debug::LYXVC, "Detecting locking mode...");
1201         if (doVCCommandCall("svn proplist " + quoteName(onlyFileName(owner_->absFileName()))
1202                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1203                     FileName(owner_->filePath())))
1204                 return false;
1205
1206         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1207         string line;
1208         bool ret = false;
1209
1210         while (ifs && !ret) {
1211                 getline(ifs, line);
1212                 LYXERR(Debug::LYXVC, line);
1213                 if (contains(line, "svn:needs-lock"))
1214                         ret = true;
1215         }
1216         LYXERR(Debug::LYXVC, "Locking enabled: " << ret);
1217         ifs.close();
1218         locked_mode_ = ret;
1219         return ret;
1220
1221 }
1222
1223
1224 bool SVN::isLocked() const
1225 {
1226         FileName file(owner_->absFileName());
1227         file.refresh();
1228         return !file.isReadOnly();
1229 }
1230
1231
1232 bool SVN::retrieve(FileName const & file)
1233 {
1234         LYXERR(Debug::LYXVC, "LyXVC::SVN: retrieve.\n\t" << file);
1235         // The caller ensures that file does not exist, so no need to check that.
1236         return doVCCommandCall("svn update -q --non-interactive " + quoteName(file.onlyFileName()),
1237                                file.onlyPath()) == 0;
1238 }
1239
1240
1241 void SVN::registrer(string const & /*msg*/)
1242 {
1243         doVCCommand("svn add -q --parents " + quoteName(onlyFileName(owner_->absFileName())),
1244                     FileName(owner_->filePath()));
1245 }
1246
1247
1248 bool SVN::renameEnabled()
1249 {
1250         return true;
1251 }
1252
1253
1254 string SVN::rename(support::FileName const & newFile, string const & msg)
1255 {
1256         // svn move does not require a log message, since it does not commit.
1257         // In LyX we commit immediately afterwards, otherwise it could be
1258         // confusing to the user to have two uncommitted files.
1259         FileName path(owner_->filePath());
1260         string relFile(to_utf8(newFile.relPath(path.absFileName())));
1261         string cmd("svn move -q " + quoteName(onlyFileName(owner_->absFileName())) +
1262                    ' ' + quoteName(relFile));
1263         if (doVCCommand(cmd, path)) {
1264                 cmd = "svn revert -q " +
1265                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1266                         quoteName(relFile);
1267                 doVCCommand(cmd, path);
1268                 if (newFile.exists())
1269                         newFile.removeFile();
1270                 return string();
1271         }
1272         vector<support::FileName> f;
1273         f.push_back(owner_->fileName());
1274         f.push_back(newFile);
1275         string log;
1276         if (checkIn(f, msg, log) != LyXVC::VCSuccess) {
1277                 cmd = "svn revert -q " +
1278                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1279                         quoteName(relFile);
1280                 doVCCommand(cmd, path);
1281                 if (newFile.exists())
1282                         newFile.removeFile();
1283                 return string();
1284         }
1285         return log;
1286 }
1287
1288
1289 bool SVN::copyEnabled()
1290 {
1291         return true;
1292 }
1293
1294
1295 string SVN::copy(support::FileName const & newFile, string const & msg)
1296 {
1297         // svn copy does not require a log message, since it does not commit.
1298         // In LyX we commit immediately afterwards, otherwise it could be
1299         // confusing to the user to have an uncommitted file.
1300         FileName path(owner_->filePath());
1301         string relFile(to_utf8(newFile.relPath(path.absFileName())));
1302         string cmd("svn copy -q " + quoteName(onlyFileName(owner_->absFileName())) +
1303                    ' ' + quoteName(relFile));
1304         if (doVCCommand(cmd, path))
1305                 return string();
1306         vector<support::FileName> f(1, newFile);
1307         string log;
1308         if (checkIn(f, msg, log) == LyXVC::VCSuccess)
1309                 return log;
1310         return string();
1311 }
1312
1313
1314 LyXVC::CommandResult SVN::checkIn(string const & msg, string & log)
1315 {
1316         vector<support::FileName> f(1, owner_->fileName());
1317         return checkIn(f, msg, log);
1318 }
1319
1320
1321 LyXVC::CommandResult
1322 SVN::checkIn(vector<support::FileName> const & f, string const & msg, string & log)
1323 {
1324         TempFile tempfile("lyxvcout");
1325         FileName tmpf = tempfile.name();
1326         if (tmpf.empty()){
1327                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1328                 log = N_("Error: Could not generate logfile.");
1329                 return LyXVC::ErrorBefore;
1330         }
1331
1332         ostringstream os;
1333         os << "svn commit -m \"" << msg << '"';
1334         for (size_t i = 0; i < f.size(); ++i)
1335                 os << ' ' << quoteName(f[i].onlyFileName());
1336         os << " > " << quoteName(tmpf.toFilesystemEncoding());
1337         LyXVC::CommandResult ret =
1338                 doVCCommand(os.str(), FileName(owner_->filePath())) ?
1339                         LyXVC::ErrorCommand : LyXVC::VCSuccess;
1340
1341         string res = scanLogFile(tmpf, log);
1342         if (!res.empty()) {
1343                 frontend::Alert::error(_("Revision control error."),
1344                                 _("Error when committing to repository.\n"
1345                                 "You have to manually resolve the problem.\n"
1346                                 "LyX will reopen the document after you press OK."));
1347                 ret = LyXVC::ErrorCommand;
1348         }
1349         else
1350                 if (!fileLock(false, tmpf, log))
1351                         ret = LyXVC::ErrorCommand;
1352
1353         if (!log.empty())
1354                 log.insert(0, "SVN: ");
1355         if (ret == LyXVC::VCSuccess && log.empty())
1356                 log = "SVN: Proceeded";
1357         return ret;
1358 }
1359
1360
1361 bool SVN::checkInEnabled()
1362 {
1363         if (locked_mode_)
1364                 return isLocked();
1365         else
1366                 return true;
1367 }
1368
1369
1370 bool SVN::isCheckInWithConfirmation()
1371 {
1372         // FIXME one day common getDiff and perhaps OpMode for all backends
1373
1374         TempFile tempfile("lyxvcout");
1375         FileName tmpf = tempfile.name();
1376         if (tmpf.empty()) {
1377                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1378                 return true;
1379         }
1380
1381         doVCCommandCall("svn diff " + quoteName(owner_->absFileName())
1382                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1383                 FileName(owner_->filePath()));
1384
1385         docstring diff = tmpf.fileContents("UTF-8");
1386
1387         if (diff.empty())
1388                 return false;
1389
1390         return true;
1391 }
1392
1393
1394 // FIXME Correctly return code should be checked instead of this.
1395 // This would need another solution than just plain startscript.
1396 // Hint from Andre': QProcess::readAllStandardError()...
1397 string SVN::scanLogFile(FileName const & f, string & status)
1398 {
1399         ifstream ifs(f.toFilesystemEncoding().c_str());
1400         string line;
1401
1402         while (ifs) {
1403                 getline(ifs, line);
1404                 LYXERR(Debug::LYXVC, line << '\n');
1405                 if (!line.empty())
1406                         status += line + "; ";
1407                 if (prefixIs(line, "C ") || prefixIs(line, "CU ")
1408                                          || contains(line, "Commit failed")) {
1409                         ifs.close();
1410                         return line;
1411                 }
1412                 if (contains(line, "svn:needs-lock")) {
1413                         ifs.close();
1414                         return line;
1415                 }
1416         }
1417         ifs.close();
1418         return string();
1419 }
1420
1421
1422 bool SVN::fileLock(bool lock, FileName const & tmpf, string &status)
1423 {
1424         if (!locked_mode_ || (isLocked() == lock))
1425                 return true;
1426
1427         string const arg = lock ? "lock " : "unlock ";
1428         doVCCommand("svn "+ arg + quoteName(onlyFileName(owner_->absFileName()))
1429                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1430                     FileName(owner_->filePath()));
1431
1432         // Lock error messages go unfortunately on stderr and are unreachable this way.
1433         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1434         string line;
1435         while (ifs) {
1436                 getline(ifs, line);
1437                 if (!line.empty()) status += line + "; ";
1438         }
1439         ifs.close();
1440
1441         if (isLocked() == lock)
1442                 return true;
1443
1444         if (lock)
1445                 frontend::Alert::error(_("Revision control error."),
1446                         _("Error while acquiring write lock.\n"
1447                         "Another user is most probably editing\n"
1448                         "the current document now!\n"
1449                         "Also check the access to the repository."));
1450         else
1451                 frontend::Alert::error(_("Revision control error."),
1452                         _("Error while releasing write lock.\n"
1453                         "Check the access to the repository."));
1454         return false;
1455 }
1456
1457
1458 string SVN::checkOut()
1459 {
1460         TempFile tempfile("lyxvcout");
1461         FileName tmpf = tempfile.name();
1462         if (tmpf.empty()) {
1463                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1464                 return N_("Error: Could not generate logfile.");
1465         }
1466
1467         doVCCommand("svn update --non-interactive " + quoteName(onlyFileName(owner_->absFileName()))
1468                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1469                     FileName(owner_->filePath()));
1470
1471         string log;
1472         string const res = scanLogFile(tmpf, log);
1473         if (!res.empty())
1474                 frontend::Alert::error(_("Revision control error."),
1475                         bformat(_("Error when updating from repository.\n"
1476                                 "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n"
1477                                 "After pressing OK, LyX will try to reopen the resolved document."),
1478                         from_local8bit(res)));
1479
1480         fileLock(true, tmpf, log);
1481
1482         return log.empty() ? string() : "SVN: " + log;
1483 }
1484
1485
1486 bool SVN::checkOutEnabled()
1487 {
1488         if (locked_mode_)
1489                 return !isLocked();
1490         else
1491                 return true;
1492 }
1493
1494
1495 string SVN::repoUpdate()
1496 {
1497         TempFile tempfile("lyxvcout");
1498         FileName tmpf = tempfile.name();
1499         if (tmpf.empty()) {
1500                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1501                 return N_("Error: Could not generate logfile.");
1502         }
1503
1504         doVCCommand("svn diff " + quoteName(owner_->filePath())
1505                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1506                 FileName(owner_->filePath()));
1507         docstring res = tmpf.fileContents("UTF-8");
1508         if (!res.empty()) {
1509                 LYXERR(Debug::LYXVC, "Diff detected:\n" << res);
1510                 docstring const file = from_utf8(owner_->filePath());
1511                 docstring text = bformat(_("There were detected changes "
1512                                 "in the working directory:\n%1$s\n\n"
1513                                 "In case of file conflict version of the local directory files "
1514                                 "will be preferred."
1515                                 "\n\nContinue?"), file);
1516                 int ret = frontend::Alert::prompt(_("Changes detected"),
1517                                 text, 0, 1, _("&Yes"), _("&No"), _("View &Log ..."));
1518                 if (ret == 2) {
1519                         dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFileName()));
1520                         ret = frontend::Alert::prompt(_("Changes detected"),
1521                                 text, 0, 1, _("&Yes"), _("&No"));
1522                         hideDialogs("file", nullptr);
1523                 }
1524                 if (ret == 1)
1525                         return string();
1526         }
1527
1528         // Reverting looks too harsh, see bug #6255.
1529         // doVCCommand("svn revert -R " + quoteName(owner_->filePath())
1530         // + " > " + quoteName(tmpf.toFilesystemEncoding()),
1531         // FileName(owner_->filePath()));
1532         // res = "Revert log:\n" + tmpf.fileContents("UTF-8");
1533         doVCCommand("svn update --accept mine-full " + quoteName(owner_->filePath())
1534                 + " > " + quoteName(tmpf.toFilesystemEncoding()),
1535                 FileName(owner_->filePath()));
1536         res += "Update log:\n" + tmpf.fileContents("UTF-8");
1537
1538         LYXERR(Debug::LYXVC, res);
1539         return to_utf8(res);
1540 }
1541
1542
1543 bool SVN::repoUpdateEnabled()
1544 {
1545         return true;
1546 }
1547
1548
1549 string SVN::lockingToggle()
1550 {
1551         TempFile tempfile("lyxvcout");
1552         FileName tmpf = tempfile.name();
1553         if (tmpf.empty()) {
1554                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1555                 return N_("Error: Could not generate logfile.");
1556         }
1557
1558         int ret = doVCCommand("svn proplist " + quoteName(onlyFileName(owner_->absFileName()))
1559                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1560                     FileName(owner_->filePath()));
1561         if (ret)
1562                 return string();
1563
1564         string log;
1565         string res = scanLogFile(tmpf, log);
1566         bool locking = contains(res, "svn:needs-lock");
1567         if (!locking)
1568                 ret = doVCCommand("svn propset svn:needs-lock ON "
1569                     + quoteName(onlyFileName(owner_->absFileName()))
1570                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1571                     FileName(owner_->filePath()));
1572         else
1573                 ret = doVCCommand("svn propdel svn:needs-lock "
1574                     + quoteName(onlyFileName(owner_->absFileName()))
1575                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1576                     FileName(owner_->filePath()));
1577         if (ret)
1578                 return string();
1579
1580         frontend::Alert::warning(_("SVN File Locking"),
1581                 (locking ? _("Locking property unset.") : _("Locking property set.")) + '\n'
1582                 + _("Do not forget to commit the locking property into the repository."),
1583                 true);
1584
1585         return string("SVN: ") + (locking ?
1586                 N_("Locking property unset.") : N_("Locking property set."));
1587 }
1588
1589
1590 bool SVN::lockingToggleEnabled()
1591 {
1592         return true;
1593 }
1594
1595
1596 bool SVN::revert()
1597 {
1598         // Reverts to the version in SVN repository and
1599         // gets the updated version from the repository.
1600         string const fil = quoteName(onlyFileName(owner_->absFileName()));
1601
1602         if (doVCCommand("svn revert -q " + fil,
1603                     FileName(owner_->filePath())))
1604                 return false;
1605         owner_->markClean();
1606         return true;
1607 }
1608
1609
1610 bool SVN::isRevertWithConfirmation()
1611 {
1612         //FIXME owner && diff
1613         return true;
1614 }
1615
1616
1617 void SVN::undoLast()
1618 {
1619         // merge the current with the previous version
1620         // in a reverse patch kind of way, so that the
1621         // result is to revert the last changes.
1622         lyxerr << "Sorry, not implemented." << endl;
1623 }
1624
1625
1626 bool SVN::undoLastEnabled()
1627 {
1628         return false;
1629 }
1630
1631
1632 string SVN::revisionInfo(LyXVC::RevisionInfo const info)
1633 {
1634         if (info == LyXVC::Tree) {
1635                 if (rev_tree_cache_.empty())
1636                         if (!getTreeRevisionInfo())
1637                                 rev_tree_cache_ = "?";
1638                 if (rev_tree_cache_ == "?")
1639                         return string();
1640
1641                 return rev_tree_cache_;
1642         }
1643
1644         // fill the rest of the attributes for a single file
1645         if (rev_file_cache_.empty())
1646                 if (!getFileRevisionInfo())
1647                         rev_file_cache_ = "?";
1648
1649         switch (info) {
1650                 case LyXVC::File:
1651                         if (rev_file_cache_ == "?")
1652                                 return string();
1653                         return rev_file_cache_;
1654                 case LyXVC::Author:
1655                         return rev_author_cache_;
1656                 case LyXVC::Date:
1657                         return rev_date_cache_;
1658                 case LyXVC::Time:
1659                         return rev_time_cache_;
1660                 default:
1661                         break;
1662         }
1663
1664         return string();
1665 }
1666
1667
1668 bool SVN::getFileRevisionInfo()
1669 {
1670         TempFile tempfile("lyxvcout");
1671         FileName tmpf = tempfile.name();
1672         if (tmpf.empty()) {
1673                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1674                 return false;
1675         }
1676
1677         doVCCommand("svn info --xml " + quoteName(onlyFileName(owner_->absFileName()))
1678                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1679                     FileName(owner_->filePath()));
1680
1681         if (tmpf.empty())
1682                 return false;
1683
1684         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1685         string line;
1686         // commit log part
1687         bool c = false;
1688         string rev;
1689
1690         while (ifs) {
1691                 getline(ifs, line);
1692                 LYXERR(Debug::LYXVC, line);
1693                 if (prefixIs(line, "<commit"))
1694                         c = true;
1695                 if (c && prefixIs(line, "   revision=\"") && suffixIs(line, "\">")) {
1696                         string l1 = subst(line, "revision=\"", "");
1697                         string l2 = trim(subst(l1, "\">", ""));
1698                         if (isStrInt(l2))
1699                                 rev_file_cache_ = rev = l2;
1700                 }
1701                 if (c && prefixIs(line, "<author>") && suffixIs(line, "</author>")) {
1702                         string l1 = subst(line, "<author>", "");
1703                         string l2 = subst(l1, "</author>", "");
1704                         rev_author_cache_ = l2;
1705                 }
1706                 if (c && prefixIs(line, "<date>") && suffixIs(line, "</date>")) {
1707                         string l1 = subst(line, "<date>", "");
1708                         string l2 = subst(l1, "</date>", "");
1709                         l2 = split(l2, l1, 'T');
1710                         rev_date_cache_ = l1;
1711                         l2 = split(l2, l1, '.');
1712                         rev_time_cache_ = l1;
1713                 }
1714         }
1715
1716         ifs.close();
1717         return !rev.empty();
1718 }
1719
1720
1721 bool SVN::getTreeRevisionInfo()
1722 {
1723         TempFile tempfile("lyxvcout");
1724         FileName tmpf = tempfile.name();
1725         if (tmpf.empty()) {
1726                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1727                 return false;
1728         }
1729
1730         doVCCommand("svnversion -n . > " + quoteName(tmpf.toFilesystemEncoding()),
1731                     FileName(owner_->filePath()));
1732
1733         if (tmpf.empty())
1734                 return false;
1735
1736         // only first line in case something bad happens.
1737         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1738         string line;
1739         getline(ifs, line);
1740         ifs.close();
1741
1742         rev_tree_cache_ = line;
1743         return !line.empty();
1744 }
1745
1746
1747 void SVN::getLog(FileName const & tmpf)
1748 {
1749         doVCCommand("svn log " + quoteName(onlyFileName(owner_->absFileName()))
1750                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1751                     FileName(owner_->filePath()));
1752 }
1753
1754
1755 bool SVN::prepareFileRevision(string const & revis, string & f)
1756 {
1757         if (!isStrInt(revis))
1758                 return false;
1759
1760         int rev = convert<int>(revis);
1761         if (rev <= 0)
1762                 if (!getFileRevisionInfo())
1763                         return false;
1764         if (rev == 0)
1765                 rev = convert<int>(rev_file_cache_);
1766         // go back for minus rev
1767         else if (rev < 0) {
1768                 rev = rev + convert<int>(rev_file_cache_);
1769                 if (rev < 1)
1770                         return false;
1771         }
1772
1773         string revname = convert<string>(rev);
1774         TempFile tempfile("lyxvcrev_" + revname + '_');
1775         tempfile.setAutoRemove(false);
1776         FileName tmpf = tempfile.name();
1777         if (tmpf.empty()) {
1778                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1779                 return false;
1780         }
1781
1782         doVCCommand("svn cat -r " + revname + ' '
1783                       + quoteName(onlyFileName(owner_->absFileName()))
1784                       + " > " + quoteName(tmpf.toFilesystemEncoding()),
1785                 FileName(owner_->filePath()));
1786         tmpf.refresh();
1787         if (tmpf.isFileEmpty())
1788                 return false;
1789
1790         f = tmpf.absFileName();
1791         return true;
1792 }
1793
1794
1795 bool SVN::prepareFileRevisionEnabled()
1796 {
1797         return true;
1798 }
1799
1800
1801
1802 bool SVN::toggleReadOnlyEnabled()
1803 {
1804         return false;
1805 }
1806
1807
1808 /////////////////////////////////////////////////////////////////////
1809 //
1810 // GIT
1811 //
1812 /////////////////////////////////////////////////////////////////////
1813
1814 GIT::GIT(Buffer * b) : VCS(b)
1815 {
1816         // Here we know that the buffer file is either already in GIT or
1817         // about to be registered
1818         scanMaster();
1819 }
1820
1821
1822 bool GIT::findFile(FileName const & file)
1823 {
1824         // First we check the existence of repository meta data.
1825         if (VCS::checkParentDirs(file, ".git").empty()) {
1826                 LYXERR(Debug::LYXVC, "Cannot find GIT meta data for " << file);
1827                 return false;
1828         }
1829
1830         // Now we check if the file is known to git.
1831         string const fname = onlyFileName(file.absFileName());
1832         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under git control for `"
1833                         << fname << '\'');
1834         int const ret = doVCCommandCall("git log " + quoteName(fname),
1835                         file.onlyPath());
1836         bool const found = (ret == 0);
1837         LYXERR(Debug::LYXVC, "GIT control: " << (found ? "enabled" : "disabled"));
1838         return found;
1839 }
1840
1841
1842 void GIT::scanMaster()
1843 {
1844         // vcstatus code is somewhat superflous,
1845         // until we want to implement read-only toggle for git.
1846         vcstatus_ = NOLOCKING;
1847 }
1848
1849
1850 bool GIT::retrieve(FileName const & file)
1851 {
1852         LYXERR(Debug::LYXVC, "LyXVC::GIT: retrieve.\n\t" << file);
1853         // The caller ensures that file does not exist, so no need to check that.
1854         return doVCCommandCall("git checkout -q " + quoteName(file.onlyFileName()),
1855                                file.onlyPath()) == 0;
1856 }
1857
1858
1859 void GIT::registrer(string const & /*msg*/)
1860 {
1861         doVCCommand("git add " + quoteName(onlyFileName(owner_->absFileName())),
1862                     FileName(owner_->filePath()));
1863 }
1864
1865
1866 bool GIT::renameEnabled()
1867 {
1868         return true;
1869 }
1870
1871
1872 string GIT::rename(support::FileName const & newFile, string const & msg)
1873 {
1874         // git mv does not require a log message, since it does not commit.
1875         // In LyX we commit immediately afterwards, otherwise it could be
1876         // confusing to the user to have two uncommitted files.
1877         FileName path(owner_->filePath());
1878         string relFile(to_utf8(newFile.relPath(path.absFileName())));
1879         string cmd("git mv " + quoteName(onlyFileName(owner_->absFileName())) +
1880                    ' ' + quoteName(relFile));
1881         if (doVCCommand(cmd, path)) {
1882                 cmd = "git checkout -q " +
1883                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1884                         quoteName(relFile);
1885                 doVCCommand(cmd, path);
1886                 if (newFile.exists())
1887                         newFile.removeFile();
1888                 return string();
1889         }
1890         vector<support::FileName> f;
1891         f.push_back(owner_->fileName());
1892         f.push_back(newFile);
1893         string log;
1894         if (checkIn(f, msg, log) != LyXVC::VCSuccess) {
1895                 cmd = "git checkout -q " +
1896                         quoteName(onlyFileName(owner_->absFileName())) + ' ' +
1897                         quoteName(relFile);
1898                 doVCCommand(cmd, path);
1899                 if (newFile.exists())
1900                         newFile.removeFile();
1901                 return string();
1902         }
1903         return log;
1904 }
1905
1906
1907 bool GIT::copyEnabled()
1908 {
1909         return false;
1910 }
1911
1912
1913 string GIT::copy(support::FileName const & /*newFile*/, string const & /*msg*/)
1914 {
1915         // git does not support copy with history preservation
1916         return string();
1917 }
1918
1919
1920 LyXVC::CommandResult GIT::checkIn(string const & msg, string & log)
1921 {
1922         vector<support::FileName> f(1, owner_->fileName());
1923         return checkIn(f, msg, log);
1924 }
1925
1926
1927 LyXVC::CommandResult
1928 GIT::checkIn(vector<support::FileName> const & f, string const & msg, string & log)
1929 {
1930         TempFile tempfile("lyxvcout");
1931         FileName tmpf = tempfile.name();
1932         if (tmpf.empty()){
1933                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1934                 log = N_("Error: Could not generate logfile.");
1935                 return LyXVC::ErrorBefore;
1936         }
1937
1938         ostringstream os;
1939         os << "git commit -m \"" << msg << '"';
1940         for (size_t i = 0; i < f.size(); ++i)
1941                 os << ' ' << quoteName(f[i].onlyFileName());
1942         os << " > " << quoteName(tmpf.toFilesystemEncoding());
1943         LyXVC::CommandResult ret =
1944                 doVCCommand(os.str(), FileName(owner_->filePath())) ?
1945                         LyXVC::ErrorCommand : LyXVC::VCSuccess;
1946
1947         string res = scanLogFile(tmpf, log);
1948         if (!res.empty()) {
1949                 frontend::Alert::error(_("Revision control error."),
1950                                 _("Error when committing to repository.\n"
1951                                 "You have to manually resolve the problem.\n"
1952                                 "LyX will reopen the document after you press OK."));
1953                 ret = LyXVC::ErrorCommand;
1954         }
1955
1956         if (!log.empty())
1957                 log.insert(0, "GIT: ");
1958         if (ret == LyXVC::VCSuccess && log.empty())
1959                 log = "GIT: Proceeded";
1960         return ret;
1961 }
1962
1963
1964 bool GIT::checkInEnabled()
1965 {
1966         return true;
1967 }
1968
1969
1970 bool GIT::isCheckInWithConfirmation()
1971 {
1972         // FIXME one day common getDiff and perhaps OpMode for all backends
1973
1974         TempFile tempfile("lyxvcout");
1975         FileName tmpf = tempfile.name();
1976         if (tmpf.empty()) {
1977                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1978                 return true;
1979         }
1980
1981         doVCCommandCall("git diff " + quoteName(owner_->absFileName())
1982                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1983                 FileName(owner_->filePath()));
1984
1985         docstring diff = tmpf.fileContents("UTF-8");
1986
1987         if (diff.empty())
1988                 return false;
1989
1990         return true;
1991 }
1992
1993
1994 // FIXME Correctly return code should be checked instead of this.
1995 // This would need another solution than just plain startscript.
1996 // Hint from Andre': QProcess::readAllStandardError()...
1997 string GIT::scanLogFile(FileName const & f, string & status)
1998 {
1999         ifstream ifs(f.toFilesystemEncoding().c_str());
2000         string line;
2001
2002         while (ifs) {
2003                 getline(ifs, line);
2004                 LYXERR(Debug::LYXVC, line << "\n");
2005                 if (!line.empty())
2006                         status += line + "; ";
2007                 if (prefixIs(line, "C ") || prefixIs(line, "CU ")
2008                                          || contains(line, "Commit failed")) {
2009                         ifs.close();
2010                         return line;
2011                 }
2012         }
2013         ifs.close();
2014         return string();
2015 }
2016
2017
2018 string GIT::checkOut()
2019 {
2020         return string();
2021 }
2022
2023
2024 bool GIT::checkOutEnabled()
2025 {
2026         return false;
2027 }
2028
2029
2030 string GIT::repoUpdate()
2031 {
2032         return string();
2033 }
2034
2035
2036 bool GIT::repoUpdateEnabled()
2037 {
2038         return false;
2039 }
2040
2041
2042 string GIT::lockingToggle()
2043 {
2044         return string();
2045 }
2046
2047
2048 bool GIT::lockingToggleEnabled()
2049 {
2050         return false;
2051 }
2052
2053
2054 bool GIT::revert()
2055 {
2056         // Reverts to the version in GIT repository and
2057         // gets the updated version from the repository.
2058         string const fil = quoteName(onlyFileName(owner_->absFileName()));
2059
2060         if (doVCCommand("git checkout -q " + fil,
2061                     FileName(owner_->filePath())))
2062                 return false;
2063         owner_->markClean();
2064         return true;
2065 }
2066
2067
2068 bool GIT::isRevertWithConfirmation()
2069 {
2070         //FIXME owner && diff
2071         return true;
2072 }
2073
2074
2075 void GIT::undoLast()
2076 {
2077         // merge the current with the previous version
2078         // in a reverse patch kind of way, so that the
2079         // result is to revert the last changes.
2080         lyxerr << "Sorry, not implemented." << endl;
2081 }
2082
2083
2084 bool GIT::undoLastEnabled()
2085 {
2086         return false;
2087 }
2088
2089
2090 string GIT::revisionInfo(LyXVC::RevisionInfo const info)
2091 {
2092         if (info == LyXVC::Tree) {
2093                 if (rev_tree_cache_.empty())
2094                         if (!getTreeRevisionInfo())
2095                                 rev_tree_cache_ = "?";
2096                 if (rev_tree_cache_ == "?")
2097                         return string();
2098
2099                 return rev_tree_cache_;
2100         }
2101
2102         // fill the rest of the attributes for a single file
2103         if (rev_file_cache_.empty())
2104                 if (!getFileRevisionInfo()) {
2105                         rev_file_cache_ = "?";
2106                         rev_file_abbrev_cache_ = "?";
2107     }
2108
2109         switch (info) {
2110                 case LyXVC::File:
2111                         if (rev_file_cache_ == "?")
2112                                 return string();
2113                         return rev_file_cache_;
2114                 case LyXVC::FileAbbrev:
2115                         if (rev_file_abbrev_cache_ == "?")
2116                                 return string();
2117                         return rev_file_abbrev_cache_;
2118                 case LyXVC::Author:
2119                         return rev_author_cache_;
2120                 case LyXVC::Date:
2121                         return rev_date_cache_;
2122                 case LyXVC::Time:
2123                         return rev_time_cache_;
2124                 default:
2125                         break;
2126         }
2127
2128         return string();
2129 }
2130
2131
2132 bool GIT::getFileRevisionInfo()
2133 {
2134         TempFile tempfile("lyxvcout");
2135         FileName tmpf = tempfile.name();
2136         if (tmpf.empty()) {
2137                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
2138                 return false;
2139         }
2140
2141         doVCCommand("git log -n 1 --pretty=format:%H%n%h%n%an%n%ai " + quoteName(onlyFileName(owner_->absFileName()))
2142                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
2143                     FileName(owner_->filePath()));
2144
2145         if (tmpf.empty())
2146                 return false;
2147
2148         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
2149
2150         if (ifs)
2151                 getline(ifs, rev_file_cache_);
2152         if (ifs)
2153                 getline(ifs, rev_file_abbrev_cache_);
2154         if (ifs)
2155                 getline(ifs, rev_author_cache_);
2156         if (ifs) {
2157                 string line;
2158                 getline(ifs, line);
2159                 rev_time_cache_ = split(line, rev_date_cache_, ' ');
2160         }
2161
2162         ifs.close();
2163         return !rev_file_cache_.empty();
2164 }
2165
2166
2167 bool GIT::getTreeRevisionInfo()
2168 {
2169         TempFile tempfile("lyxvcout");
2170         FileName tmpf = tempfile.name();
2171         if (tmpf.empty()) {
2172                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
2173                 return false;
2174         }
2175
2176         doVCCommand("git describe --abbrev --dirty --long > " + quoteName(tmpf.toFilesystemEncoding()),
2177                     FileName(owner_->filePath()),
2178                     false); //git describe returns $?=128 when no tag found (but git repo still exists)
2179
2180         if (tmpf.empty())
2181                 return false;
2182
2183         // only first line in case something bad happens.
2184         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
2185         getline(ifs, rev_tree_cache_);
2186         ifs.close();
2187
2188         return !rev_tree_cache_.empty();
2189 }
2190
2191
2192 void GIT::getLog(FileName const & tmpf)
2193 {
2194         doVCCommand("git log " + quoteName(onlyFileName(owner_->absFileName()))
2195                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
2196                     FileName(owner_->filePath()));
2197 }
2198
2199
2200 //at this moment we don't accept revision SHA, but just number of revision steps back
2201 //GUI and infrastucture needs to be changed first
2202 bool GIT::prepareFileRevision(string const & revis, string & f)
2203 {
2204         // anything positive means we got hash, not "0" or minus revision
2205         int rev = 1;
2206
2207         // hash is rarely number and should be long
2208         if (isStrInt(revis) && revis.length()<20)
2209                 rev = convert<int>(revis);
2210
2211         // revision and filename
2212         string pointer;
2213
2214         // go back for "minus" revisions
2215         if (rev <= 0)
2216                 pointer = "HEAD~" + convert<string>(-rev);
2217         // normal hash
2218         else
2219                 pointer = revis;
2220
2221         pointer += ':';
2222
2223         TempFile tempfile("lyxvcrev_" + revis + '_');
2224         tempfile.setAutoRemove(false);
2225         FileName tmpf = tempfile.name();
2226         if (tmpf.empty()) {
2227                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
2228                 return false;
2229         }
2230
2231         doVCCommand("git show " + pointer + "./"
2232                       + quoteName(onlyFileName(owner_->absFileName()))
2233                       + " > " + quoteName(tmpf.toFilesystemEncoding()),
2234                 FileName(owner_->filePath()));
2235         tmpf.refresh();
2236         if (tmpf.isFileEmpty())
2237                 return false;
2238
2239         f = tmpf.absFileName();
2240         return true;
2241 }
2242
2243
2244 bool GIT::prepareFileRevisionEnabled()
2245 {
2246         return true;
2247 }
2248
2249
2250 bool GIT::toggleReadOnlyEnabled()
2251 {
2252         return true;
2253 }
2254
2255
2256 } // namespace lyx