Make using "Record::record()" be less chatty. The Record::run() has full chattyness...
[vng.git] / src / commands / Record.cpp
blob56e87bf73e592ca398a202cc53ed0c9cbadcd977
1 /*
2 * This file is part of the vng project
3 * Copyright (C) 2008 Thomas Zander <tzander@trolltech.com>
4 * Copyright (C) 2002-2004 David Roundy
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #include "Record.h"
20 #include "CommandLineParser.h"
21 #include "GitRunner.h"
22 #include "hunks/ChangeSet.h"
23 #include "hunks/HunksCursor.h"
24 #include "Logger.h"
25 #include "Interview.h"
26 #include "Vng.h"
28 #include <QDebug>
29 #include <QBuffer>
31 #ifdef Q_OS_WIN
32 # include <cstdlib>
33 # define getpid() rand()
34 #else
35 # include <sys/types.h>
36 # include <unistd.h>
37 #endif
40 static const CommandLineOption options[] = {
41 {"-a, --all", "answer yes to all patches"},
42 {"-i, --interactive", "prompt user interactively"},
43 {"-m, --patch-name PATCHNAME", "name of patch"},
44 {"-A, --author EMAIL", "specify author id"},
45 //{"--logfile FILE", "give patch name and comment in file"},
46 //{"--delete-logfile", "delete the logfile when done"},
47 //{"--no-test", "don't run the test script"},
48 //{"--test", "run the test script"},
49 //{"-l, --look-for-adds", "Also look for files that are potentially pending addition"},
50 //{"--dont-look-for-adds", "Don't look for any files that could be added"},
51 //{"--posthook COMMAND", "specify command to run after this vng command."},
52 //{"--no-posthook", "Do not run posthook command."},
53 //{"--prompt-posthook", "Prompt before running posthook. [DEFAULT]"},
54 //{"--run-posthook", "Run posthook command without prompting."},
55 CommandLineLastOption
58 Record::Record()
59 : AbstractCommand("record"),
60 m_mode(Unset),
61 m_editComment(false),
62 m_all(false),
63 m_logChatter(false)
65 CommandLineParser::addOptionDefinitions(options);
66 CommandLineParser::setArgumentDefinition("record [FILE or DIRECTORY]" );
69 QString Record::argumentDescription() const
71 return "[FILE or DIRECTORY]";
74 QString Record::commandDescription() const
76 return "Record is used to name a set of changes and record the patch to the repository.\n";
79 void Record::setPatchName(const QByteArray &message)
81 m_patchName = message;
84 QByteArray Record::patchName() const
86 return m_patchName;
89 void Record::setUsageMode(UsageMode mode)
91 m_mode = mode;
94 QString Record::sha1() const
96 return m_sha1;
99 AbstractCommand::ReturnCodes Record::run()
101 CommandLineParser *args = CommandLineParser::instance();
102 m_all = m_config.isEmptyRepo()
103 || (m_config.contains("all") && !args->contains("interactive"))
104 || args->contains("all");
105 m_patchName = args->optionArgument("patch-name", m_config.optionArgument("patch-name")).toUtf8();
106 m_author = args->optionArgument("author", m_config.optionArgument("author"));
107 m_logChatter = true;
109 return record();
112 AbstractCommand::ReturnCodes Record::record()
114 if (! checkInRepository())
115 return NotInRepo;
116 moveToRoot(CheckFileSystem);
118 if (m_mode != Unset)
119 m_all = m_mode == AllChanges;
120 const bool needHunks = !m_all || m_patchName.isEmpty();
122 ChangeSet changeSet;
123 changeSet.fillFromCurrentChanges(rebasedArguments(), needHunks);
125 changeSet.waitForFinishFirstFile();
126 bool shouldDoRecord = changeSet.count() > 0;
127 if (!shouldDoRecord) {
128 if (m_logChatter)
129 Logger::warn() << "No changes!" << endl;
130 return Ok;
133 QString email = m_author;
134 if (email.isEmpty())
135 email = getenv("EMAIL");
136 QStringList environment;
137 if (! email.isEmpty()) {
138 QRegExp re("(.*) <([@\\S]+)>");
139 if (re.exactMatch(email)) { // meaning its an email AND name
140 environment << "GIT_AUTHOR_NAME="+ re.cap(1);
141 environment << "GIT_COMMITTER_NAME="+ re.cap(1);
142 environment << "GIT_AUTHOR_EMAIL="+ re.cap(2);
143 environment << "GIT_COMMITTER_EMAIL="+ re.cap(2);
145 else if (m_author.isEmpty()) { // if its an account or shell wide option; just use the git defs.
146 environment << "GIT_AUTHOR_EMAIL="+ email;
147 environment << "GIT_COMMITTER_EMAIL="+ email;
149 else {
150 Logger::error() << "Author format invalid. Please provide author formatted like; `name <email@host>\n";
151 return InvalidOptions;
155 if (shouldDoRecord && !m_all && m_mode != Index) { // then do interview
156 HunksCursor cursor(changeSet);
157 cursor.setConfiguration(m_config);
158 Interview interview(cursor, name());
159 interview.setUsePager(shouldUsePager());
160 if (! interview.start()) {
161 Logger::warn() << "Cancelled." << endl;
162 return UserCancelled;
166 if (shouldDoRecord && !m_all && m_mode != Index) { // check if there is anything selected
167 shouldDoRecord = changeSet.hasAcceptedChanges();
168 if (! shouldDoRecord) {
169 Logger::warn() << "Ok, if you don't want to record anything, that's fine!" <<endl;
170 return UserCancelled;
173 if (dryRun())
174 return Ok;
176 if ((m_editComment || m_patchName.isEmpty()) && getenv("EDITOR")) {
177 class Deleter : public QObject {
178 public:
179 Deleter() : commitMessage(0) { }
180 ~Deleter() {
181 if (commitMessage)
182 commitMessage->remove();
184 QFile *commitMessage;
186 Deleter parent;
187 QFile *commitMessage;
188 int i = 0;
189 do {
190 commitMessage = new QFile(QString("vng-record-%1").arg(i++), &parent);
191 } while (commitMessage->exists());
192 parent.commitMessage = commitMessage; // make sure the file is removed from FS.
193 if (! commitMessage->open(QIODevice::WriteOnly)) {
194 Logger::error() << "Vng failed. Could not create a temporary file for the record message '"
195 << commitMessage->fileName() << "`\n";
196 return WriteError;
198 const char * defaultCommitMessage1 = "\n***END OF DESCRIPTION***"; // we will look for this string later
199 const char * defaultCommitMessage2 = "\nPlace the long patch description above the ***END OF DESCRIPTION*** marker.\n\nThis patch contains the following changes:\n\n";
200 if (! m_patchName.isEmpty())
201 commitMessage->write(m_patchName);
202 else
203 commitMessage->write("\n", 1);
204 commitMessage->write(defaultCommitMessage1, strlen(defaultCommitMessage1));
205 commitMessage->write(defaultCommitMessage2, strlen(defaultCommitMessage2));
206 QBuffer buffer;
207 changeSet.writeDiff(buffer, m_all ? ChangeSet::AllHunks : ChangeSet::UserSelection);
208 ChangeSet actualChanges;
209 actualChanges.fillFromDiffFile(buffer);
210 QTextStream out (commitMessage);
211 for (int i=0; i < actualChanges.count(); ++i) {
212 File file = actualChanges.file(i);
213 file.outputWhatsChanged(out, m_config, true, false);
215 for (int i=0; i < changeSet.count(); ++i) {
216 File file = changeSet.file(i);
217 if (file.isBinary() && (m_all || file.binaryChangeAcceptance() == Vng::Accepted))
218 out << "M " << QString::fromLocal8Bit(file.fileName());
219 else if (file.fileName().isEmpty() && (m_all || file.renameAcceptance() == Vng::Accepted))
220 out << "D " << QString::fromLocal8Bit(file.oldFileName());
222 out.flush();
224 commitMessage->close();
225 QDateTime modification = QFileInfo(*commitMessage).lastModified();
226 QString command = QString("%1 %2").arg(getenv("EDITOR")).arg(commitMessage->fileName());
227 int rc = system(command.toAscii().data());
228 if (rc != 0) {
229 // this will keep patchName empty and we fall through to the interview.
230 Logger::warn() << "Vng-Warning: Could not start editor '" << getenv("EDITOR") << "`\n";
231 Logger::warn().flush();
233 else if (modification == QFileInfo(*commitMessage).lastModified()) {
234 if (m_logChatter)
235 Logger::warn() << "unchanged, won't record\n";
236 return UserCancelled;
238 else {
239 // get data until the separator line.
240 commitMessage->open(QIODevice::ReadOnly);
241 m_patchName = commitMessage->readAll();
242 commitMessage->close();
243 int cuttoff = m_patchName.indexOf(defaultCommitMessage1);
244 if (cuttoff > 0)
245 m_patchName.truncate(cuttoff);
246 for (int i = m_patchName.length()-1; i >= 0; --i) {
247 if (m_patchName[i] == '\n' || m_patchName[i] == '\r' || m_patchName[i] == ' ')
248 m_patchName.resize(i); // shrink
249 else break;
253 if (m_patchName.isEmpty())
254 m_patchName = Interview::ask("What is the patch name? ").toUtf8();
256 ReturnCodes rc = addFilesPerAcceptance(changeSet, m_all);
257 if (rc != Ok)
258 return rc;
260 QProcess git;
261 QStringList arguments;
262 arguments << "write-tree";
263 GitRunner runner(git, arguments);
264 rc = runner.start(GitRunner::WaitForStandardOutput);
265 if (rc != Ok) {
266 Logger::error() << "Git write-tree failed!, aborting record\n";
267 return rc;
269 char buf[1024];
270 Vng::readLine(&git, buf, sizeof(buf));
271 QString tree(buf);
272 git.waitForFinished(); // patiently wait for it to finish..
273 Logger::debug() << "The tree got git ref; " << tree;
274 Logger::debug().flush(); // flush since we do an ask next
276 arguments.clear();
277 git.setEnvironment(environment);
279 arguments << "commit-tree" << tree.left(40);
280 if (!m_config.isEmptyRepo())
281 arguments << "-p" << "HEAD" ;
283 runner.setArguments(arguments);
284 rc = runner.start(GitRunner::WaitUntilReadyForWrite);
285 if (rc != Ok) {
286 Logger::error() << "Git commit-tree failed!, aborting record\n";
287 return rc;
289 git.write(m_patchName);
290 git.write("\n");
291 git.closeWriteChannel();
292 Vng::readLine(&git, buf, sizeof(buf));
293 QString commit(buf);
294 Logger::debug() << "commit is ref; " << commit;
295 git.waitForFinished(); // patiently wait for it to finish..
296 if (commit.isEmpty()) {
297 Logger::error() << "Git update-ref failed to produce a reference!, aborting record\n";
298 return GitFailed;
300 m_sha1 = commit.left(40);
302 arguments.clear();
303 arguments << "update-ref" << "HEAD" << m_sha1;
304 runner.setArguments(arguments);
305 rc = runner.start(GitRunner::WaitUntilFinished);
306 if (rc != Ok) {
307 Logger::error() << "Git update-ref failed!, aborting record\n";
308 return rc;
311 // We removed files from the index in case they were freshly added, but the user didn't want it in this commit.
312 // we have to re-add those files.
313 arguments.clear();
314 arguments << "update-index" << "--add";
315 for (int i=0; i < changeSet.count(); ++i) {
316 File file = changeSet.file(i);
317 if (! file.oldFileName().isEmpty())
318 continue; // not a new added file.
319 if (file.renameAcceptance() == Vng::Rejected)
320 arguments.append(file.fileName());
322 if (arguments.count() > 2) {
323 runner.setArguments(arguments);
324 runner.start(GitRunner::WaitUntilFinished);
327 int endOfLine = m_patchName.indexOf('\n');
328 if (endOfLine > 0)
329 m_patchName.truncate(endOfLine);
330 if (m_logChatter)
331 Logger::warn() << "Finished recording patch `" << m_patchName << "'" << endl;
332 return Ok;
335 AbstractCommand::ReturnCodes Record::addFilesPerAcceptance(const ChangeSet &changeSet, bool allChanges)
337 typedef QPair<QString, QString> NamePair;
338 class RevertCopier {
339 public:
340 RevertCopier() {}
341 ~RevertCopier() {
342 foreach(NamePair pair, m_fileNames) {
343 QFile copy(pair.second);
344 copy.remove();
345 if (! pair.first.isEmpty()) {
346 QFile orig(pair.first);
347 orig.rename(copy.fileName());
351 void append(const QString &orig, const QString &copy) {
352 QPair<QString, QString> pair(orig, copy);
353 m_fileNames.append(pair);
355 private:
356 // thats orig, copy
357 QList< QPair<QString, QString> > m_fileNames;
359 RevertCopier reverter;// this will revert all file changes we make when we exit the scope of this method.
360 ChangeSet patchChanges;
362 QStringList filesForAdd;
363 QStringList notUsedFiles;
364 for (int i=0; i < changeSet.count(); ++i) {
365 File file = changeSet.file(i);
366 bool someEnabled = false;
367 bool allEnabled = true;
368 const bool renamed = file.fileName() != file.oldFileName();
369 const bool protectionChanged = !renamed && file.protection() != file.oldProtection();
370 if ((file.renameAcceptance() == Vng::Accepted && renamed)
371 || (file.protectionAcceptance() == Vng::Accepted && protectionChanged)) // record it.
372 someEnabled = true;
374 foreach (Hunk hunk, file.hunks()) {
375 Vng::Acceptance a = hunk.acceptance();
376 if (a == Vng::Accepted) {
377 someEnabled = true;
378 } else if (a == Vng::MixedAcceptance) {
379 someEnabled = true;
380 allEnabled = false;
381 } else {
382 allEnabled = false;
384 if (someEnabled && !allEnabled)
385 break;
387 const bool addUnaltered = allChanges || allEnabled;
389 const bool removeFromIndex = !allChanges && !someEnabled; // user said 'no', lets make sure none of the changes are left in the index.
390 if (removeFromIndex && ! file.fileName().isEmpty() && ! file.oldFileName().isEmpty()) { // just ignore file.
391 notUsedFiles << file.fileName();
392 continue;
394 Logger::debug() << "'" << file.fileName() << "` addUnaltered: " << addUnaltered << ", removeFromIndex: "
395 << removeFromIndex << ", someChangesAccepted: " << someEnabled << ", allAccepted: " << allEnabled << endl;
396 if (file.fileName().isEmpty())
397 filesForAdd << file.oldFileName();
398 else {
399 filesForAdd << file.fileName();
400 if (renamed)
401 filesForAdd << file.oldFileName();
404 if (addUnaltered)
405 continue; // thats easy; whole file to add.
406 if (removeFromIndex && file.fileName().isEmpty()) { // user said no about recording a deleted file.
407 // this is a funny situation; *just* in case the user already somehow added the deletion to the index
408 // we need to reset that to make the index have the full file again. Notice that we need to remove the file afterwards.
409 Q_ASSERT(!file.oldFileName().isEmpty());
410 QProcess git;
411 QStringList arguments;
412 arguments << "cat-file" << "blob" << file.oldSha1();
413 GitRunner runner(git, arguments);
414 ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
415 Logger::debug() << "restoring '" << file.oldFileName() << "`\n";
416 if (rc == Ok) {
417 reverter.append(QString(), file.oldFileName());
418 QFile deletedFile(file.oldFileName());
419 Q_ASSERT(! deletedFile.exists());
420 bool success = Vng::copyFile(git, deletedFile);
421 git.waitForFinished();
422 deletedFile.close();
423 if (! success)
424 return WriteError;
426 continue;
429 // for the case where only some patches are selected we make a safety copy of the file.
430 QFile sourceFile(file.fileName());
431 Q_ASSERT(sourceFile.exists());
432 QString fileName = file.fileName();
433 for(int i=0; i < 10; i++) {
434 fileName = fileName + ".orig";
435 if (sourceFile.rename(fileName))
436 break; // successful!
438 reverter.append(fileName, file.fileName());
439 if (removeFromIndex) // need to rename it only, we won't patch it.
440 continue;
442 patchChanges.addFile(file);
443 Logger::debug() << "cp " << fileName << " =>" << file.fileName() << endl;
444 QFile orig(fileName);
445 orig.open(QIODevice::ReadOnly);
446 Vng::copyFile(orig, sourceFile); // TODO check return code
447 sourceFile.setPermissions(file.permissions());
450 QFile patch(".vng.record." + QString::number(getpid()) + ".diff");
451 patchChanges.writeDiff(patch, ChangeSet::InvertedUserSelection);
452 patch.close();
454 if (patch.size() != 0) {
455 QProcess git;
456 QStringList arguments;
457 arguments << "apply" << "--apply" << "--reverse" << patch.fileName();
458 GitRunner runner(git, arguments);
459 ReturnCodes rc = runner.start(GitRunner::WaitUntilFinished);
460 if (rc != Ok) {
461 Logger::error() << "Vng failed: failed to patch, sorry! Aborting record.\n";
462 return rc; // note that copied files will be moved back to avoid partially patched files lying around.
465 patch.remove();
466 // first clean out the index so that previous stuff other tools did doesn't influence us.
467 if (!m_config.isEmptyRepo() && ! notUsedFiles.isEmpty()) {
468 QProcess git;
469 QStringList arguments;
470 arguments << "reset" << "--mixed" << "-q" << "HEAD"; // -q stands for quiet.
471 arguments += notUsedFiles;
472 GitRunner runner(git, arguments);
473 runner.start(GitRunner::WaitUntilFinished);
476 // git add of all files to get a nice list of changes into the index.
477 while (!filesForAdd.isEmpty()) {
478 QProcess git;
479 QStringList arguments;
480 arguments << "update-index" << "--add" << "--remove";
481 int count = 25; // length of arguments
482 do {
483 QString first = filesForAdd[0];
484 if (count + first.length() > 32000)
485 break;
486 count += first.length();
487 arguments.append(first);
488 filesForAdd.removeFirst();
489 } while (!filesForAdd.isEmpty());
490 GitRunner runner(git, arguments);
491 ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
492 if (rc != Ok) {
493 Logger::error() << "Vng failed: Did not manage to add files to the git index, aborting record\n";
494 return rc;
498 return Ok; // exiting scope will revert all files in the local filesystem. We use the index from here on.