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