]> git.lyx.org Git - features.git/blob - src/VCBackend.cpp
9f065eb22157d04edfd9ec53a15b745e6fbe8b89
[features.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/Path.h"
28 #include "support/Systemcall.h"
29 #include "support/regex.h"
30
31 #include <fstream>
32
33 using namespace std;
34 using namespace lyx::support;
35
36
37
38 namespace lyx {
39
40
41 int VCS::doVCCommandCall(string const & cmd, FileName const & path)
42 {
43         LYXERR(Debug::LYXVC, "doVCCommandCall: " << cmd);
44         Systemcall one;
45         support::PathChanger p(path);
46         return one.startscript(Systemcall::Wait, cmd, false);
47 }
48
49
50 int VCS::doVCCommand(string const & cmd, FileName const & path, bool reportError)
51 {
52         if (owner_)
53                 owner_->setBusy(true);
54
55         int const ret = doVCCommandCall(cmd, path);
56
57         if (owner_)
58                 owner_->setBusy(false);
59         if (ret && reportError)
60                 frontend::Alert::error(_("Revision control error."),
61                         bformat(_("Some problem occured while running the command:\n"
62                                   "'%1$s'."),
63                         from_utf8(cmd)));
64         return ret;
65 }
66
67
68 /////////////////////////////////////////////////////////////////////
69 //
70 // RCS
71 //
72 /////////////////////////////////////////////////////////////////////
73
74 RCS::RCS(FileName const & m)
75 {
76         master_ = m;
77         scanMaster();
78 }
79
80
81 FileName const RCS::findFile(FileName const & file)
82 {
83         // Check if *,v exists.
84         FileName tmp(file.absFileName() + ",v");
85         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under rcs: " << tmp);
86         if (tmp.isReadableFile()) {
87                 LYXERR(Debug::LYXVC, "Yes, " << file << " is under rcs.");
88                 return tmp;
89         }
90
91         // Check if RCS/*,v exists.
92         tmp = FileName(addName(addPath(onlyPath(file.absFileName()), "RCS"), file.absFileName()) + ",v");
93         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under rcs: " << tmp);
94         if (tmp.isReadableFile()) {
95                 LYXERR(Debug::LYXVC, "Yes, " << file << " is under rcs.");
96                 return tmp;
97         }
98
99         return FileName();
100 }
101
102
103 void RCS::retrieve(FileName const & file)
104 {
105         LYXERR(Debug::LYXVC, "LyXVC::RCS: retrieve.\n\t" << file);
106         doVCCommandCall("co -q -r " + quoteName(file.toFilesystemEncoding()),
107                          FileName());
108 }
109
110
111 void RCS::scanMaster()
112 {
113         if (master_.empty())
114                 return;
115
116         LYXERR(Debug::LYXVC, "LyXVC::RCS: scanMaster: " << master_);
117
118         ifstream ifs(master_.toFilesystemEncoding().c_str());
119
120         string token;
121         bool read_enough = false;
122
123         while (!read_enough && ifs >> token) {
124                 LYXERR(Debug::LYXVC, "LyXVC::scanMaster: current lex text: `"
125                         << token << '\'');
126
127                 if (token.empty())
128                         continue;
129                 else if (token == "head") {
130                         // get version here
131                         string tmv;
132                         ifs >> tmv;
133                         tmv = rtrim(tmv, ";");
134                         version_ = tmv;
135                         LYXERR(Debug::LYXVC, "LyXVC: version found to be " << tmv);
136                 } else if (contains(token, "access")
137                            || contains(token, "symbols")
138                            || contains(token, "strict")) {
139                         // nothing
140                 } else if (contains(token, "locks")) {
141                         // get locker here
142                         if (contains(token, ';')) {
143                                 locker_ = "Unlocked";
144                                 vcstatus = UNLOCKED;
145                                 continue;
146                         }
147                         string tmpt;
148                         string s1;
149                         string s2;
150                         do {
151                                 ifs >> tmpt;
152                                 s1 = rtrim(tmpt, ";");
153                                 // tmp is now in the format <user>:<version>
154                                 s1 = split(s1, s2, ':');
155                                 // s2 is user, and s1 is version
156                                 if (s1 == version_) {
157                                         locker_ = s2;
158                                         vcstatus = LOCKED;
159                                         break;
160                                 }
161                         } while (!contains(tmpt, ';'));
162
163                 } else if (token == "comment") {
164                         // we don't need to read any further than this.
165                         read_enough = true;
166                 } else {
167                         // unexpected
168                         LYXERR(Debug::LYXVC, "LyXVC::scanMaster(): unexpected token");
169                 }
170         }
171 }
172
173
174 void RCS::registrer(string const & msg)
175 {
176         string cmd = "ci -q -u -i -t-\"";
177         cmd += msg;
178         cmd += "\" ";
179         cmd += quoteName(onlyFileName(owner_->absFileName()));
180         doVCCommand(cmd, FileName(owner_->filePath()));
181 }
182
183
184 string RCS::checkIn(string const & msg)
185 {
186         int ret = doVCCommand("ci -q -u -m\"" + msg + "\" "
187                     + quoteName(onlyFileName(owner_->absFileName())),
188                     FileName(owner_->filePath()));
189         return ret ? string() : "RCS: Proceeded";
190 }
191
192
193 bool RCS::checkInEnabled()
194 {
195         return owner_ && !owner_->isReadonly();
196 }
197
198
199 bool RCS::isCheckInWithConfirmation()
200 {
201         // FIXME one day common getDiff for all backends
202         // docstring diff;
203         // if (getDiff(file, diff) && diff.empty())
204         //      return false;
205
206         FileName tmpf = FileName::tempName("lyxvcout");
207         if (tmpf.empty()) {
208                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
209                 return true;
210         }
211
212         doVCCommandCall("rcsdiff " + quoteName(owner_->absFileName())
213                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
214                 FileName(owner_->filePath()));
215
216         docstring diff = tmpf.fileContents("UTF-8");
217         tmpf.erase();
218
219         if (diff.empty())
220                 return false;
221
222         return true;
223 }
224
225
226 string RCS::checkOut()
227 {
228         owner_->markClean();
229         int ret = doVCCommand("co -q -l " + quoteName(onlyFileName(owner_->absFileName())),
230                     FileName(owner_->filePath()));
231         return ret ? string() : "RCS: Proceeded";
232 }
233
234
235 bool RCS::checkOutEnabled()
236 {
237         return owner_ && owner_->isReadonly();
238 }
239
240
241 string RCS::repoUpdate()
242 {
243         lyxerr << "Sorry, not implemented." << endl;
244         return string();
245 }
246
247
248 bool RCS::repoUpdateEnabled()
249 {
250         return false;
251 }
252
253
254 string RCS::lockingToggle()
255 {
256         //FIXME this might be actually possible, study rcs -U, rcs -L.
257         //State should be easy to get inside scanMaster.
258         //It would fix #4370 and make rcs/svn usage even more closer.
259         lyxerr << "Sorry, not implemented." << endl;
260         return string();
261 }
262
263
264 bool RCS::lockingToggleEnabled()
265 {
266         return false;
267 }
268
269
270 void RCS::revert()
271 {
272         doVCCommand("co -f -u" + version_ + " "
273                     + quoteName(onlyFileName(owner_->absFileName())),
274                     FileName(owner_->filePath()));
275         // We ignore changes and just reload!
276         owner_->markClean();
277 }
278
279
280 bool RCS::isRevertWithConfirmation()
281 {
282         //FIXME owner && diff ?
283         return true;
284 }
285
286
287 void RCS::undoLast()
288 {
289         LYXERR(Debug::LYXVC, "LyXVC: undoLast");
290         doVCCommand("rcs -o" + version_ + " "
291                     + quoteName(onlyFileName(owner_->absFileName())),
292                     FileName(owner_->filePath()));
293 }
294
295
296 bool RCS::undoLastEnabled()
297 {
298         return true;
299 }
300
301
302 void RCS::getLog(FileName const & tmpf)
303 {
304         doVCCommand("rlog " + quoteName(onlyFileName(owner_->absFileName()))
305                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
306                     FileName(owner_->filePath()));
307 }
308
309
310 bool RCS::toggleReadOnlyEnabled()
311 {
312         // This got broken somewhere along lfuns dispatch reorganization.
313         // reloadBuffer would be needed after this, but thats problematic
314         // since we are inside Buffer::dispatch.
315         // return true;
316         return false;
317 }
318
319 string RCS::revisionInfo(LyXVC::RevisionInfo const info)
320 {
321         if (info == LyXVC::File)
322                 return version_;
323         return string();
324 }
325
326
327 bool RCS::prepareFileRevision(string const &revis, string & f)
328 {
329         string rev = revis;
330
331         if (isStrInt(rev)) {
332                 int back = convert<int>(rev);
333                 // if positive use as the last number in the whole revision string
334                 if (back > 0) {
335                         string base;
336                         rsplit(version_, base , '.' );
337                         rev = base + "." + rev;
338                 }
339                 if (back == 0)
340                         rev = version_;
341                 // we care about the last number from revision string
342                 // in case of backward indexing
343                 if (back < 0) {
344                         string cur, base;
345                         cur = rsplit(version_, base , '.' );
346                         if (!isStrInt(cur))
347                                 return false;
348                         int want = convert<int>(cur) + back;
349                         if (want <= 0)
350                                 return false;
351
352                         rev = base + "." + convert<string>(want);
353                 }
354         }
355
356         FileName tmpf = FileName::tempName("lyxvcrev_" + rev + "_");
357         if (tmpf.empty()) {
358                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
359                 return N_("Error: Could not generate logfile.");
360         }
361
362         doVCCommand("co -p" + rev + " "
363                       + quoteName(onlyFileName(owner_->absFileName()))
364                       + " > " + quoteName(tmpf.toFilesystemEncoding()),
365                 FileName(owner_->filePath()));
366         if (tmpf.isFileEmpty())
367                 return false;
368
369         f = tmpf.absFileName();
370         return true;
371 }
372
373
374 bool RCS::prepareFileRevisionEnabled()
375 {
376         return true;
377 }
378
379
380 /////////////////////////////////////////////////////////////////////
381 //
382 // CVS
383 //
384 /////////////////////////////////////////////////////////////////////
385
386 CVS::CVS(FileName const & m, FileName const & f)
387 {
388         master_ = m;
389         file_ = f;
390         have_rev_info_ = false;
391         scanMaster();
392 }
393
394
395 FileName const CVS::findFile(FileName const & file)
396 {
397         // First we look for the CVS/Entries in the same dir
398         // where we have file.
399         FileName const entries(onlyPath(file.absFileName()) + "/CVS/Entries");
400         string const tmpf = '/' + onlyFileName(file.absFileName()) + '/';
401         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under cvs in `" << entries
402                              << "' for `" << tmpf << '\'');
403         if (entries.isReadableFile()) {
404                 // Ok we are at least in a CVS dir. Parse the CVS/Entries
405                 // and see if we can find this file. We do a fast and
406                 // dirty parse here.
407                 ifstream ifs(entries.toFilesystemEncoding().c_str());
408                 string line;
409                 while (getline(ifs, line)) {
410                         LYXERR(Debug::LYXVC, "\tEntries: " << line);
411                         if (contains(line, tmpf))
412                                 return entries;
413                 }
414         }
415         return FileName();
416 }
417
418
419 void CVS::scanMaster()
420 {
421         LYXERR(Debug::LYXVC, "LyXVC::CVS: scanMaster. \n     Checking: " << master_);
422         // Ok now we do the real scan...
423         ifstream ifs(master_.toFilesystemEncoding().c_str());
424         string name = onlyFileName(file_.absFileName());
425         string tmpf = '/' + name + '/';
426         LYXERR(Debug::LYXVC, "\tlooking for `" << tmpf << '\'');
427         string line;
428         static regex const reg("/(.*)/(.*)/(.*)/(.*)/(.*)");
429         while (getline(ifs, line)) {
430                 LYXERR(Debug::LYXVC, "\t  line: " << line);
431                 if (contains(line, tmpf)) {
432                         // Ok extract the fields.
433                         smatch sm;
434
435                         regex_match(line, sm, reg);
436
437                         //sm[0]; // whole matched string
438                         //sm[1]; // filename
439                         version_ = sm.str(2);
440                         string const file_date = sm.str(3);
441
442                         //sm[4]; // options
443                         //sm[5]; // tag or tagdate
444                         if (file_.isReadableFile()) {
445                                 time_t mod = file_.lastModified();
446                                 string mod_date = rtrim(asctime(gmtime(&mod)), "\n");
447                                 LYXERR(Debug::LYXVC, "Date in Entries: `" << file_date
448                                         << "'\nModification date of file: `" << mod_date << '\'');
449                                 if (file_.isReadOnly()) {
450                                         // readonly checkout is unlocked
451                                         vcstatus = UNLOCKED;
452                                 } else {
453                                         FileName bdir(addPath(master_.onlyPath().absFileName(),"Base"));
454                                         FileName base(addName(bdir.absFileName(),name));
455                                         // if base version is existent "cvs edit" was used to lock
456                                         vcstatus = base.isReadableFile() ? LOCKED : NOLOCKING;
457                                 }
458                         } else {
459                                 vcstatus = NOLOCKING;
460                         }
461                         break;
462                 }
463         }
464 }
465
466
467 string const CVS::getTarget(OperationMode opmode) const
468 {
469         switch(opmode) {
470         case Directory:
471                 // in client server mode CVS does not like full path operand for directory operation
472                 // since LyX switches to the repo dir "." is good enough as target
473                 return ".";
474         case File:
475                 return quoteName(onlyFileName(owner_->absFileName()));
476         }
477         return string();
478 }
479
480
481 docstring CVS::toString(CvsStatus status) const
482 {
483         switch (status) {
484         case UpToDate:
485                 return _("Up-to-date");
486         case LocallyModified:
487                 return _("Locally Modified");
488         case LocallyAdded:
489                 return _("Locally Added");
490         case NeedsMerge:
491                 return _("Needs Merge");
492         case NeedsCheckout:
493                 return _("Needs Checkout");
494         case NoCvsFile:
495                 return _("No CVS file");
496         case StatusError:
497                 return _("Cannot retrieve CVS status");
498         }
499         return 0;
500 }
501
502
503 int CVS::doVCCommandWithOutput(string const & cmd, FileName const & path,
504         FileName const & output, bool reportError)
505 {
506         string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding());
507         return doVCCommand(cmd + redirection, path, reportError);
508 }
509
510
511 int CVS::doVCCommandCallWithOutput(std::string const & cmd,
512         support::FileName const & path,
513         support::FileName const & output)
514 {
515         string redirection = output.empty() ? "" : " > " + quoteName(output.toFilesystemEncoding());
516         return doVCCommandCall(cmd + redirection, path);
517 }
518
519
520 CVS::CvsStatus CVS::getStatus()
521 {
522         FileName tmpf = FileName::tempName("lyxvcout");
523         if (tmpf.empty()) {
524                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
525                 return StatusError;
526         }
527
528         if (doVCCommandCallWithOutput("cvs status " + getTarget(File),
529                 FileName(owner_->filePath()), tmpf)) {
530                 tmpf.removeFile();
531                 return StatusError;
532         }
533
534         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
535         CvsStatus status = NoCvsFile;
536
537         while (ifs) {
538                 string line;
539                 getline(ifs, line);
540                 LYXERR(Debug::LYXVC, line << "\n");
541                 if (prefixIs(line, "File:")) {
542                         if (contains(line, "Up-to-date"))
543                                 status = UpToDate;
544                         else if (contains(line, "Locally Modified"))
545                                 status = LocallyModified;
546                         else if (contains(line, "Locally Added"))
547                                 status = LocallyAdded;
548                         else if (contains(line, "Needs Merge"))
549                                 status = NeedsMerge;
550                         else if (contains(line, "Needs Checkout"))
551                                 status = NeedsCheckout;
552                 }
553         }
554         tmpf.removeFile();
555         return status;
556 }
557
558 void CVS::getRevisionInfo()
559 {
560         if (have_rev_info_)
561                 return;
562         have_rev_info_ = true;
563         FileName tmpf = FileName::tempName("lyxvcout");
564         if (tmpf.empty()) {
565                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
566                 return;
567         }
568         
569         int rc = doVCCommandCallWithOutput("cvs log -r" + version_ 
570                 + " " + getTarget(File),
571                 FileName(owner_->filePath()), tmpf);
572         if (rc) {
573                 tmpf.removeFile();
574                 LYXERR(Debug::LYXVC, "cvs log failed with exit code " << rc);
575                 return;
576         }
577         
578         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
579         static regex const reg("date: (.*) (.*) (.*);  author: (.*);  state: (.*);(.*)");
580
581         while (ifs) {
582                 string line;
583                 getline(ifs, line);
584                 LYXERR(Debug::LYXVC, line << "\n");
585                 if (prefixIs(line, "date:")) {
586                         smatch sm;
587                         regex_match(line, sm, reg);
588                         //sm[0]; // whole matched string
589                         rev_date_cache_ = sm[1];
590                         rev_time_cache_ = sm[2];
591                         //sm[3]; // GMT offset
592                         rev_author_cache_ = sm[4];
593                         break;
594                 }
595         }
596         tmpf.removeFile();
597         if (rev_author_cache_.empty())
598                 LYXERR(Debug::LYXVC,
599                    "Could not retrieve revision info for " << version_ <<
600                    " of " << getTarget(File));
601 }
602
603
604 void CVS::registrer(string const & msg)
605 {
606         doVCCommand("cvs -q add -m \"" + msg + "\" "
607                 + getTarget(File),
608                 FileName(owner_->filePath()));
609 }
610
611
612 void CVS::getDiff(OperationMode opmode, FileName const & tmpf)
613 {
614         doVCCommandWithOutput("cvs diff " + getTarget(opmode),
615                 FileName(owner_->filePath()), tmpf, false);
616 }
617
618
619 int CVS::edit()
620 {
621         vcstatus = LOCKED;
622         return doVCCommand("cvs -q edit " + getTarget(File),
623                 FileName(owner_->filePath()));
624 }
625
626
627 int CVS::unedit()
628 {
629         vcstatus = UNLOCKED;
630         return doVCCommand("cvs -q unedit " + getTarget(File),
631                 FileName(owner_->filePath()));
632 }
633
634
635 int CVS::update(OperationMode opmode, FileName const & tmpf)
636 {
637         return doVCCommandWithOutput("cvs -q update "
638                 + getTarget(opmode),
639                 FileName(owner_->filePath()), tmpf, false);
640 }
641
642
643 string CVS::scanLogFile(FileName const & f, string & status)
644 {
645         ifstream ifs(f.toFilesystemEncoding().c_str());
646
647         while (ifs) {
648                 string line;
649                 getline(ifs, line);
650                 LYXERR(Debug::LYXVC, line << "\n");
651                 if (!line.empty())
652                         status += line + "; ";
653                 if (prefixIs(line, "C ")) {
654                         ifs.close();
655                         return line;
656                 }
657         }
658         ifs.close();
659         return string();
660 }
661         
662         
663 string CVS::checkIn(string const & msg)
664 {
665         CvsStatus status = getStatus();
666         switch (status) {
667         case UpToDate:
668                 if (vcstatus != NOLOCKING)
669                         unedit();
670                 return "CVS: Proceeded";
671         case LocallyModified:
672         case LocallyAdded: {
673                 int rc = doVCCommand("cvs -q commit -m \"" + msg + "\" "
674                         + getTarget(File),
675                     FileName(owner_->filePath()));
676                 return rc ? string() : "CVS: Proceeded";
677         }
678         case NeedsMerge:
679         case NeedsCheckout:
680                 frontend::Alert::error(_("Revision control error."),
681                         _("The repository version is newer then the current check out.\n"
682                           "You have to update from repository first or revert your changes.")) ;
683                 break;
684         default:
685                 frontend::Alert::error(_("Revision control error."),
686                         bformat(_("Bad status when checking in changes.\n"
687                                           "\n'%1$s'\n\n"),
688                                 toString(status)));
689                 break;
690         }
691         return string();
692 }
693
694
695 bool CVS::isLocked() const
696 {
697         FileName fn(owner_->absFileName());
698         fn.refresh();
699         return !fn.isReadOnly();
700 }
701
702
703 bool CVS::checkInEnabled()
704 {
705         if (vcstatus != NOLOCKING)
706                 return isLocked();
707         else
708                 return true;
709 }
710
711
712 bool CVS::isCheckInWithConfirmation()
713 {
714         CvsStatus status = getStatus();
715         return status == LocallyModified || status == LocallyAdded;
716 }
717
718
719 string CVS::checkOut()
720 {
721         if (vcstatus != NOLOCKING && edit())
722                 return string();
723         FileName tmpf = FileName::tempName("lyxvcout");
724         if (tmpf.empty()) {
725                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
726                 return string();
727         }
728         
729         int rc = update(File, tmpf);
730         string log;
731         string const res = scanLogFile(tmpf, log);
732         if (!res.empty()) {
733                 frontend::Alert::error(_("Revision control error."),
734                         bformat(_("Error when updating from repository.\n"
735                                 "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n"
736                                 "After pressing OK, LyX will try to reopen the resolved document."),
737                                 from_local8bit(res)));
738                 rc = 0;
739         }
740         
741         tmpf.erase();
742         return rc ? string() : log.empty() ? "CVS: Proceeded" : "CVS: " + log;
743 }
744
745
746 bool CVS::checkOutEnabled()
747 {
748         if (vcstatus != NOLOCKING)
749                 return !isLocked();
750         else
751                 return true;
752 }
753
754
755 string CVS::repoUpdate()
756 {
757         FileName tmpf = FileName::tempName("lyxvcout");
758         if (tmpf.empty()) {
759                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
760                 return string();
761         }
762         
763         getDiff(Directory, tmpf);
764         docstring res = tmpf.fileContents("UTF-8");
765         if (!res.empty()) {
766                 LYXERR(Debug::LYXVC, "Diff detected:\n" << res);
767                 docstring const file = from_utf8(owner_->filePath());
768                 docstring text = bformat(_("There were detected changes "
769                                 "in the working directory:\n%1$s\n\n"
770                                 "In case of file conflict you have to resolve them "
771                                 "manually or revert to repository version later."), file);
772                 int ret = frontend::Alert::prompt(_("Changes detected"),
773                                 text, 0, 1, _("&Continue"), _("&Abort"), _("View &Log ..."));
774                 if (ret == 2 ) {
775                         dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFileName()));
776                         ret = frontend::Alert::prompt(_("Changes detected"),
777                                 text, 0, 1, _("&Continue"), _("&Abort"));
778                         hideDialogs("file", 0);
779                 }
780                 if (ret == 1 ) {
781                         tmpf.removeFile();
782                         return string();
783                 }
784         }
785
786         int rc = update(Directory, tmpf);
787         res += "Update log:\n" + tmpf.fileContents("UTF-8");
788         LYXERR(Debug::LYXVC, res);
789
790         string log;
791         string sres = scanLogFile(tmpf, log);
792         if (!sres.empty()) {
793                 docstring const file = owner_->fileName().displayName(20);
794                 frontend::Alert::error(_("Revision control error."),
795                         bformat(_("Error when updating document %1$s from repository.\n"
796                                           "You have to manually resolve the conflicts NOW!\n'%2$s'.\n\n"
797                                           "After pressing OK, LyX will try to reopen the resolved document."),
798                                 file, from_local8bit(sres)));
799                 rc = 0;
800         }
801         
802         tmpf.removeFile();
803
804         return rc ? string() : log.empty() ? "CVS: Proceeded" : "CVS: " + log;
805 }
806
807
808 bool CVS::repoUpdateEnabled()
809 {
810         return true;
811 }
812
813
814 string CVS::lockingToggle()
815 {
816         lyxerr << "Sorry, not implemented." << endl;
817         return string();
818 }
819
820
821 bool CVS::lockingToggleEnabled()
822 {
823         return false;
824 }
825
826
827 bool CVS::isRevertWithConfirmation()
828 {
829         CvsStatus status = getStatus();
830         return !owner_->isClean() || status == LocallyModified || status == NeedsMerge;
831 }
832
833
834 void CVS::revert()
835 {
836         // Reverts to the version in CVS repository and
837         // gets the updated version from the repository.
838         CvsStatus status = getStatus();
839         switch (status) {
840         case UpToDate:
841                 if (vcstatus != NOLOCKING)
842                         unedit();
843                 break;
844         case NeedsMerge:
845         case NeedsCheckout:
846         case LocallyModified: {
847                 FileName f(owner_->absFileName());
848                 f.removeFile();
849                 update(File, FileName());
850                 owner_->markClean();
851                 break;
852         }
853         case LocallyAdded: {
854                 docstring const file = owner_->fileName().displayName(20);
855                 frontend::Alert::error(_("Revision control error."),
856                         bformat(_("The document %1$s is not in repository.\n"
857                                   "You have to check in the first revision before you can revert."),
858                                 file)) ;
859                 break;
860         }
861         default: {
862                 docstring const file = owner_->fileName().displayName(20);
863                 frontend::Alert::error(_("Revision control error."),
864                         bformat(_("Cannot revert document %1$s to repository version.\n"
865                                   "The status '%2$s' is unexpected."),
866                                 file, toString(status)));
867                 break;
868                 }
869         }
870 }
871
872
873 void CVS::undoLast()
874 {
875         // merge the current with the previous version
876         // in a reverse patch kind of way, so that the
877         // result is to revert the last changes.
878         lyxerr << "Sorry, not implemented." << endl;
879 }
880
881
882 bool CVS::undoLastEnabled()
883 {
884         return false;
885 }
886
887
888 void CVS::getLog(FileName const & tmpf)
889 {
890         doVCCommandWithOutput("cvs log " + getTarget(File),
891                 FileName(owner_->filePath()),
892                 tmpf);
893 }
894
895
896 bool CVS::toggleReadOnlyEnabled()
897 {
898         return false;
899 }
900
901
902 string CVS::revisionInfo(LyXVC::RevisionInfo const info)
903 {
904         if (!version_.empty()) {
905                 getRevisionInfo();
906                 switch (info) {
907                 case LyXVC::File:
908                         return version_;
909                 case LyXVC::Author:
910                         return rev_author_cache_;
911                 case LyXVC::Date:
912                         return rev_date_cache_;
913                 case LyXVC::Time:
914                         return rev_time_cache_;
915                 default: ;
916                 }
917         }
918         return string();
919 }
920
921
922 bool CVS::prepareFileRevision(string const &, string &)
923 {
924         return false;
925 }
926
927
928 bool CVS::prepareFileRevisionEnabled()
929 {
930         return false;
931 }
932
933
934 /////////////////////////////////////////////////////////////////////
935 //
936 // SVN
937 //
938 /////////////////////////////////////////////////////////////////////
939
940 SVN::SVN(FileName const & m, FileName const & f)
941 {
942         owner_ = 0;
943         master_ = m;
944         file_ = f;
945         locked_mode_ = 0;
946         scanMaster();
947 }
948
949
950 FileName const SVN::findFile(FileName const & file)
951 {
952         // First we look for the .svn/entries in the same dir
953         // where we have file.
954         FileName const entries(onlyPath(file.absFileName()) + "/.svn/entries");
955         string const tmpf = onlyFileName(file.absFileName());
956         LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under svn in `" << entries
957                              << "' for `" << tmpf << '\'');
958         if (entries.isReadableFile()) {
959                 // Ok we are at least in a SVN dir. Parse the .svn/entries
960                 // and see if we can find this file. We do a fast and
961                 // dirty parse here.
962                 ifstream ifs(entries.toFilesystemEncoding().c_str());
963                 string line, oldline;
964                 while (getline(ifs, line)) {
965                         if (line == "dir" || line == "file")
966                                 LYXERR(Debug::LYXVC, "\tEntries: " << oldline);
967                         if (oldline == tmpf && line == "file")
968                                 return entries;
969                         oldline = line;
970                 }
971         }
972         return FileName();
973 }
974
975
976 void SVN::scanMaster()
977 {
978         // vcstatus code is somewhat superflous, until we want
979         // to implement read-only toggle for svn.
980         vcstatus = NOLOCKING;
981         if (checkLockMode()) {
982                 if (isLocked()) {
983                         vcstatus = LOCKED;
984                 } else {
985                         vcstatus = UNLOCKED;
986                 }
987         }
988 }
989
990
991 bool SVN::checkLockMode()
992 {
993         FileName tmpf = FileName::tempName("lyxvcout");
994         if (tmpf.empty()){
995                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
996                 return N_("Error: Could not generate logfile.");
997         }
998
999         LYXERR(Debug::LYXVC, "Detecting locking mode...");
1000         if (doVCCommandCall("svn proplist " + quoteName(file_.onlyFileName())
1001                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1002                     file_.onlyPath()))
1003                 return false;
1004
1005         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1006         string line;
1007         bool ret = false;
1008
1009         while (ifs) {
1010                 getline(ifs, line);
1011                 LYXERR(Debug::LYXVC, line);
1012                 if (contains(line, "svn:needs-lock"))
1013                         ret = true;
1014         }
1015         LYXERR(Debug::LYXVC, "Locking enabled: " << ret);
1016         ifs.close();
1017         locked_mode_ = ret;
1018         return ret;
1019
1020 }
1021
1022
1023 bool SVN::isLocked() const
1024 {
1025         file_.refresh();
1026         return !file_.isReadOnly();
1027 }
1028
1029
1030 void SVN::registrer(string const & /*msg*/)
1031 {
1032         doVCCommand("svn add -q " + quoteName(onlyFileName(owner_->absFileName())),
1033                     FileName(owner_->filePath()));
1034 }
1035
1036
1037 string SVN::checkIn(string const & msg)
1038 {
1039         FileName tmpf = FileName::tempName("lyxvcout");
1040         if (tmpf.empty()){
1041                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1042                 return N_("Error: Could not generate logfile.");
1043         }
1044
1045         doVCCommand("svn commit -m \"" + msg + "\" "
1046                     + quoteName(onlyFileName(owner_->absFileName()))
1047                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1048                     FileName(owner_->filePath()));
1049
1050         string log;
1051         string res = scanLogFile(tmpf, log);
1052         if (!res.empty())
1053                 frontend::Alert::error(_("Revision control error."),
1054                                 _("Error when committing to repository.\n"
1055                                 "You have to manually resolve the problem.\n"
1056                                 "LyX will reopen the document after you press OK."));
1057         else
1058                 fileLock(false, tmpf, log);
1059
1060         tmpf.erase();
1061         return log.empty() ? string() : "SVN: " + log;
1062 }
1063
1064
1065 bool SVN::checkInEnabled()
1066 {
1067         if (locked_mode_)
1068                 return isLocked();
1069         else
1070                 return true;
1071 }
1072
1073
1074 bool SVN::isCheckInWithConfirmation()
1075 {
1076         // FIXME one day common getDiff and perhaps OpMode for all backends
1077
1078         FileName tmpf = FileName::tempName("lyxvcout");
1079         if (tmpf.empty()) {
1080                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1081                 return true;
1082         }
1083
1084         doVCCommandCall("svn diff " + quoteName(owner_->absFileName())
1085                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1086                 FileName(owner_->filePath()));
1087
1088         docstring diff = tmpf.fileContents("UTF-8");
1089         tmpf.erase();
1090
1091         if (diff.empty())
1092                 return false;
1093
1094         return true;
1095 }
1096
1097
1098 // FIXME Correctly return code should be checked instead of this.
1099 // This would need another solution than just plain startscript.
1100 // Hint from Andre': QProcess::readAllStandardError()...
1101 string SVN::scanLogFile(FileName const & f, string & status)
1102 {
1103         ifstream ifs(f.toFilesystemEncoding().c_str());
1104         string line;
1105
1106         while (ifs) {
1107                 getline(ifs, line);
1108                 LYXERR(Debug::LYXVC, line << "\n");
1109                 if (!line.empty()) 
1110                         status += line + "; ";
1111                 if (prefixIs(line, "C ") || prefixIs(line, "CU ")
1112                                          || contains(line, "Commit failed")) {
1113                         ifs.close();
1114                         return line;
1115                 }
1116                 if (contains(line, "svn:needs-lock")) {
1117                         ifs.close();
1118                         return line;
1119                 }
1120         }
1121         ifs.close();
1122         return string();
1123 }
1124
1125
1126 void SVN::fileLock(bool lock, FileName const & tmpf, string &status)
1127 {
1128         if (!locked_mode_ || (isLocked() == lock))
1129                 return;
1130
1131         string const arg = lock ? "lock " : "unlock ";
1132         doVCCommand("svn "+ arg + quoteName(onlyFileName(owner_->absFileName()))
1133                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1134                     FileName(owner_->filePath()));
1135
1136         // Lock error messages go unfortunately on stderr and are unreachible this way.
1137         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1138         string line;
1139         while (ifs) {
1140                 getline(ifs, line);
1141                 if (!line.empty()) status += line + "; ";
1142         }
1143         ifs.close();
1144
1145         if (!isLocked() && lock)
1146                 frontend::Alert::error(_("Revision control error."),
1147                         _("Error while acquiring write lock.\n"
1148                         "Another user is most probably editing\n"
1149                         "the current document now!\n"
1150                         "Also check the access to the repository."));
1151         if (isLocked() && !lock)
1152                 frontend::Alert::error(_("Revision control error."),
1153                         _("Error while releasing write lock.\n"
1154                         "Check the access to the repository."));
1155 }
1156
1157
1158 string SVN::checkOut()
1159 {
1160         FileName tmpf = FileName::tempName("lyxvcout");
1161         if (tmpf.empty()) {
1162                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1163                 return N_("Error: Could not generate logfile.");
1164         }
1165
1166         doVCCommand("svn update --non-interactive " + quoteName(onlyFileName(owner_->absFileName()))
1167                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1168                     FileName(owner_->filePath()));
1169
1170         string log;
1171         string const res = scanLogFile(tmpf, log);
1172         if (!res.empty())
1173                 frontend::Alert::error(_("Revision control error."),
1174                         bformat(_("Error when updating from repository.\n"
1175                                 "You have to manually resolve the conflicts NOW!\n'%1$s'.\n\n"
1176                                 "After pressing OK, LyX will try to reopen the resolved document."),
1177                         from_local8bit(res)));
1178
1179         fileLock(true, tmpf, log);
1180
1181         tmpf.erase();
1182         return log.empty() ? string() : "SVN: " + log;
1183 }
1184
1185
1186 bool SVN::checkOutEnabled()
1187 {
1188         if (locked_mode_)
1189                 return !isLocked();
1190         else
1191                 return true;
1192 }
1193
1194
1195 string SVN::repoUpdate()
1196 {
1197         FileName tmpf = FileName::tempName("lyxvcout");
1198         if (tmpf.empty()) {
1199                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1200                 return N_("Error: Could not generate logfile.");
1201         }
1202
1203         doVCCommand("svn diff " + quoteName(owner_->filePath())
1204                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1205                 FileName(owner_->filePath()));
1206         docstring res = tmpf.fileContents("UTF-8");
1207         if (!res.empty()) {
1208                 LYXERR(Debug::LYXVC, "Diff detected:\n" << res);
1209                 docstring const file = from_utf8(owner_->filePath());
1210                 docstring text = bformat(_("There were detected changes "
1211                                 "in the working directory:\n%1$s\n\n"
1212                                 "In case of file conflict version of the local directory files "
1213                                 "will be preferred."
1214                                 "\n\nContinue?"), file);
1215                 int ret = frontend::Alert::prompt(_("Changes detected"),
1216                                 text, 0, 1, _("&Yes"), _("&No"), _("View &Log ..."));
1217                 if (ret == 2 ) {
1218                         dispatch(FuncRequest(LFUN_DIALOG_SHOW, "file " + tmpf.absFileName()));
1219                         ret = frontend::Alert::prompt(_("Changes detected"),
1220                                 text, 0, 1, _("&Yes"), _("&No"));
1221                         hideDialogs("file", 0);
1222                 }
1223                 if (ret == 1 ) {
1224                         tmpf.erase();
1225                         return string();
1226                 }
1227         }
1228
1229         // Reverting looks too harsh, see bug #6255.
1230         // doVCCommand("svn revert -R " + quoteName(owner_->filePath())
1231         // + " > " + quoteName(tmpf.toFilesystemEncoding()),
1232         // FileName(owner_->filePath()));
1233         // res = "Revert log:\n" + tmpf.fileContents("UTF-8");
1234         doVCCommand("svn update --accept mine-full " + quoteName(owner_->filePath())
1235                 + " > " + quoteName(tmpf.toFilesystemEncoding()),
1236                 FileName(owner_->filePath()));
1237         res += "Update log:\n" + tmpf.fileContents("UTF-8");
1238
1239         LYXERR(Debug::LYXVC, res);
1240         tmpf.erase();
1241         return to_utf8(res);
1242 }
1243
1244
1245 bool SVN::repoUpdateEnabled()
1246 {
1247         return true;
1248 }
1249
1250
1251 string SVN::lockingToggle()
1252 {
1253         FileName tmpf = FileName::tempName("lyxvcout");
1254         if (tmpf.empty()) {
1255                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1256                 return N_("Error: Could not generate logfile.");
1257         }
1258
1259         int ret = doVCCommand("svn proplist " + quoteName(onlyFileName(owner_->absFileName()))
1260                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1261                     FileName(owner_->filePath()));
1262         if (ret)
1263                 return string();
1264
1265         string log;
1266         string res = scanLogFile(tmpf, log);
1267         bool locking = contains(res, "svn:needs-lock");
1268         if (!locking)
1269                 ret = doVCCommand("svn propset svn:needs-lock ON "
1270                     + quoteName(onlyFileName(owner_->absFileName()))
1271                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1272                     FileName(owner_->filePath()));
1273         else
1274                 ret = doVCCommand("svn propdel svn:needs-lock "
1275                     + quoteName(onlyFileName(owner_->absFileName()))
1276                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1277                     FileName(owner_->filePath()));
1278         if (ret)
1279                 return string();
1280
1281         tmpf.erase();
1282         frontend::Alert::warning(_("VCN File Locking"),
1283                 (locking ? _("Locking property unset.") : _("Locking property set.")) + "\n"
1284                 + _("Do not forget to commit the locking property into the repository."),
1285                 true);
1286
1287         return string("SVN: ") +  N_("Locking property set.");
1288 }
1289
1290
1291 bool SVN::lockingToggleEnabled()
1292 {
1293         return true;
1294 }
1295
1296
1297 void SVN::revert()
1298 {
1299         // Reverts to the version in SVN repository and
1300         // gets the updated version from the repository.
1301         string const fil = quoteName(onlyFileName(owner_->absFileName()));
1302
1303         doVCCommand("svn revert -q " + fil,
1304                     FileName(owner_->filePath()));
1305         owner_->markClean();
1306 }
1307
1308
1309 bool SVN::isRevertWithConfirmation()
1310 {
1311         //FIXME owner && diff
1312         return true;
1313 }
1314
1315
1316 void SVN::undoLast()
1317 {
1318         // merge the current with the previous version
1319         // in a reverse patch kind of way, so that the
1320         // result is to revert the last changes.
1321         lyxerr << "Sorry, not implemented." << endl;
1322 }
1323
1324
1325 bool SVN::undoLastEnabled()
1326 {
1327         return false;
1328 }
1329
1330
1331 string SVN::revisionInfo(LyXVC::RevisionInfo const info)
1332 {
1333         if (info == LyXVC::Tree) {
1334                         if (rev_tree_cache_.empty())
1335                                 if (!getTreeRevisionInfo())
1336                                         rev_tree_cache_ = "?";
1337                         if (rev_tree_cache_ == "?")
1338                                 return string();
1339
1340                         return rev_tree_cache_;
1341         }
1342
1343         // fill the rest of the attributes for a single file
1344         if (rev_file_cache_.empty())
1345                 if (!getFileRevisionInfo())
1346                         rev_file_cache_ = "?";
1347
1348         switch (info) {
1349                 case LyXVC::File:
1350                         if (rev_file_cache_ == "?")
1351                                 return string();
1352                         return rev_file_cache_;
1353                 case LyXVC::Author:
1354                         return rev_author_cache_;
1355                 case LyXVC::Date:
1356                         return rev_date_cache_;
1357                 case LyXVC::Time:
1358                         return rev_time_cache_;
1359                 default: ;
1360
1361         }
1362
1363         return string();
1364 }
1365
1366
1367 bool SVN::getFileRevisionInfo()
1368 {
1369         FileName tmpf = FileName::tempName("lyxvcout");
1370         if (tmpf.empty()) {
1371                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1372                 return N_("Error: Could not generate logfile.");
1373         }
1374
1375         doVCCommand("svn info --xml " + quoteName(onlyFileName(owner_->absFileName()))
1376                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1377                     FileName(owner_->filePath()));
1378
1379         if (tmpf.empty())
1380                 return false;
1381
1382         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1383         string line;
1384         // commit log part
1385         bool c = false;
1386         string rev;
1387
1388         while (ifs) {
1389                 getline(ifs, line);
1390                 LYXERR(Debug::LYXVC, line);
1391                 if (prefixIs(line, "<commit"))
1392                         c = true;
1393                 if (c && prefixIs(line, "   revision=\"") && suffixIs(line, "\">")) {
1394                         string l1 = subst(line, "revision=\"", "");
1395                         string l2 = trim(subst(l1, "\">", ""));
1396                         if (isStrInt(l2))
1397                                 rev_file_cache_ = rev = l2;
1398                 }
1399                 if (c && prefixIs(line, "<author>") && suffixIs(line, "</author>")) {
1400                         string l1 = subst(line, "<author>", "");
1401                         string l2 = subst(l1, "</author>", "");
1402                         rev_author_cache_ = l2;
1403                 }
1404                 if (c && prefixIs(line, "<date>") && suffixIs(line, "</date>")) {
1405                         string l1 = subst(line, "<date>", "");
1406                         string l2 = subst(l1, "</date>", "");
1407                         l2 = split(l2, l1, 'T');
1408                         rev_date_cache_ = l1;
1409                         l2 = split(l2, l1, '.');
1410                         rev_time_cache_ = l1;
1411                 }
1412         }
1413
1414         ifs.close();
1415         tmpf.erase();
1416         return !rev.empty();
1417 }
1418
1419
1420 bool SVN::getTreeRevisionInfo()
1421 {
1422         FileName tmpf = FileName::tempName("lyxvcout");
1423         if (tmpf.empty()) {
1424                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1425                 return N_("Error: Could not generate logfile.");
1426         }
1427
1428         doVCCommand("svnversion -n . > " + quoteName(tmpf.toFilesystemEncoding()),
1429                     FileName(owner_->filePath()));
1430
1431         if (tmpf.empty())
1432                 return false;
1433
1434         // only first line in case something bad happens.
1435         ifstream ifs(tmpf.toFilesystemEncoding().c_str());
1436         string line;
1437         getline(ifs, line);
1438         ifs.close();
1439         tmpf.erase();
1440
1441         rev_tree_cache_ = line;
1442         return !line.empty();
1443 }
1444
1445
1446 void SVN::getLog(FileName const & tmpf)
1447 {
1448         doVCCommand("svn log " + quoteName(onlyFileName(owner_->absFileName()))
1449                     + " > " + quoteName(tmpf.toFilesystemEncoding()),
1450                     FileName(owner_->filePath()));
1451 }
1452
1453
1454 bool SVN::prepareFileRevision(string const & revis, string & f)
1455 {
1456         if (!isStrInt(revis))
1457                 return false;
1458
1459         int rev = convert<int>(revis);
1460         if (rev <= 0)
1461                 if (!getFileRevisionInfo())
1462                         return false;
1463         if (rev == 0)
1464                 rev = convert<int>(rev_file_cache_);
1465         // go back for minus rev
1466         else if (rev < 0) {
1467                 rev = rev + convert<int>(rev_file_cache_);
1468                 if (rev < 1)
1469                         return false;
1470         }
1471
1472         string revname = convert<string>(rev);
1473         FileName tmpf = FileName::tempName("lyxvcrev_" + revname + "_");
1474         if (tmpf.empty()) {
1475                 LYXERR(Debug::LYXVC, "Could not generate logfile " << tmpf);
1476                 return N_("Error: Could not generate logfile.");
1477         }
1478
1479         doVCCommand("svn cat -r " + revname + " "
1480                       + quoteName(onlyFileName(owner_->absFileName()))
1481                       + " > " + quoteName(tmpf.toFilesystemEncoding()),
1482                 FileName(owner_->filePath()));
1483         if (tmpf.isFileEmpty())
1484                 return false;
1485
1486         f = tmpf.absFileName();
1487         return true;
1488 }
1489
1490
1491 bool SVN::prepareFileRevisionEnabled()
1492 {
1493         return true;
1494 }
1495
1496
1497
1498 bool SVN::toggleReadOnlyEnabled()
1499 {
1500         return false;
1501 }
1502
1503
1504 } // namespace lyx