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