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