Be memory efficient and clear hunk data after displaying it
[vng.git] / commands / Record.cpp
blobe10cc8ba115aa06c5adda2d3f1dca4d85cff0059
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 //{"--leave-test-directory", "don't remove the test directory"},
50 //{"--remove-test-directory", "remove the test directory"},
51 //{"--edit-long-comment", "Edit the long comment by default"},
52 //{"--skip-long-comment", "Don't give a long comment"},
53 //{"--prompt-long-comment", "Prompt for whether to edit the long comment"},
54 //{"-l, --look-for-adds", "Also look for files that are potentially pending addition"},
55 //{"--dont-look-for-adds", "Don't look for any files that could be added"},
56 //{"--umask UMASK", "specify umask to use when writing."},
57 //{"--posthook COMMAND", "specify command to run after this vng command."},
58 //{"--no-posthook", "Do not run posthook command."},
59 //{"--prompt-posthook", "Prompt before running posthook. [DEFAULT]"},
60 //{"--run-posthook", "Run posthook command without prompting."},
61 CommandLineLastOption
64 static AbstractCommand::ReturnCodes copyFile(QIODevice &from, QIODevice &to) {
65 to.open(QIODevice::WriteOnly);
66 char buf[4096];
67 while (true) {
68 from.waitForReadyRead(-1);
69 qint64 len = from.read(buf, sizeof(buf));
70 if (len <= 0) { // done!
71 to.close();
72 break;
74 while(len > 0) {
75 qint64 written = to.write(buf, len);
76 if (written == -1) // write error!
77 return AbstractCommand::WriteError;
78 len -= written;
81 return AbstractCommand::Ok;
85 Record::Record()
86 : AbstractCommand("record")
88 CommandLineParser::addOptionDefinitions(options);
89 CommandLineParser::setArgumentDefinition("record [FILE or DIRECTORY]" );
92 QString Record::argumentDescription() const
94 return "[FILE or DIRECTORY]";
97 QString Record::commandDescription() const
99 return "Record is used to name a set of changes and record the patch to the repository.\n";
102 AbstractCommand::ReturnCodes Record::run()
104 if (! checkInRepository())
105 return NotInRepo;
106 moveToRoot();
108 CommandLineParser *args = CommandLineParser::instance();
109 const bool all = m_config.isEmptyRepo() || m_config.contains("all")
110 && !args->contains("interactive") || args->contains("all");
112 ChangeSet changeSet;
113 changeSet.fillFromCurrentChanges(rebasedArguments());
115 bool shouldDoRecord = changeSet.count() > 0;
116 if (!shouldDoRecord) {
117 Logger::warn() << "No changes!" << endl;
118 return Ok;
121 QString email = args->optionArgument("author", m_config.optionArgument("author", getenv("EMAIL")));
122 QStringList environment;
123 if (! email.isEmpty()) {
124 QRegExp re("(.*) <([@\\S]+)>");
125 if (re.exactMatch(email)) { // meaning its an email AND name
126 environment << "GIT_AUTHOR_NAME="+ re.cap(1);
127 environment << "GIT_AUTHOR_EMAIL="+ re.cap(2);
129 else if (!args->contains("author")) // if its an account or shell wide option; just use the git defs.
130 environment << "GIT_AUTHOR_EMAIL="+ email;
131 else {
132 Logger::error() << "Author format invalid. Please provide author formatted like; `name <email@host>\n";
133 return InvalidOptions;
137 if (shouldDoRecord && !all) { // then do interview
138 HunksCursor cursor(changeSet);
139 cursor.setConfiguration(m_config);
140 Interview interview(cursor, name());
141 interview.setUsePager(shouldUsePager());
142 if (! interview.start()) {
143 Logger::warn() << "Record cancelled." << endl;
144 return Ok;
148 if (shouldDoRecord && !all) { // check if there is anything selected
149 shouldDoRecord = changeSet.hasAcceptedChanges();
150 if (! shouldDoRecord) {
151 Logger::warn() << "Ok, if you don't want to record anything, that's fine!" <<endl;
152 return Ok;
155 QByteArray patchName = args->optionArgument("patch-name", m_config.optionArgument("patch-name")).toUtf8();
156 if (dryRun())
157 return Ok;
159 if (patchName.isEmpty() && getenv("EDITOR")) {
160 class Deleter : public QObject {
161 public:
162 Deleter() : commitMessage(0) { }
163 ~Deleter() {
164 if (commitMessage)
165 commitMessage->remove();
167 QFile *commitMessage;
169 Deleter parent;
170 QFile *commitMessage;
171 int i = 0;
172 do {
173 commitMessage = new QFile(QString("vng-record-%1").arg(i++), &parent);
174 } while (commitMessage->exists());
175 parent.commitMessage = commitMessage; // make sure the file is removed from FS.
176 if (! commitMessage->open(QIODevice::WriteOnly)) {
177 Logger::error() << "Vng failed. Could not create a temporary file for the record message '"
178 << commitMessage->fileName() << "`\n";
179 return WriteError;
181 const char * defaultCommitMessage1 = "\n***END OF DESCRIPTION***"; // we will look for this string later
182 const char * defaultCommitMessage2 = "\nPlace the long patch description above the ***END OF DESCRIPTION*** marker.\n\nThis patch contains the following changes:\n\n";
183 commitMessage->write("\n", 1);
184 commitMessage->write(defaultCommitMessage1, strlen(defaultCommitMessage1));
185 commitMessage->write(defaultCommitMessage2, strlen(defaultCommitMessage2));
186 QBuffer buffer;
187 changeSet.writeDiff(buffer, ChangeSet::UserSelection);
188 ChangeSet actualChanges;
189 actualChanges.fillFromDiffFile(buffer);
190 QTextStream out (commitMessage);
191 for (int i=0; i < actualChanges.count(); ++i) {
192 File file = actualChanges.file(i);
193 file.outputWhatsChanged(out, m_config, true, false);
195 out.flush();
197 commitMessage->close();
198 QDateTime modification = QFileInfo(*commitMessage).lastModified();
199 QString command = QString("%1 %2").arg(getenv("EDITOR")).arg(commitMessage->fileName());
200 int rc = system(command.toAscii().data());
201 if (rc != 0) {
202 // this will keep patchName empty and we fall through to the interview.
203 Logger::warn() << "Vng-Warning: Could not start editor '" << getenv("EDITOR") << "`\n";
204 Logger::warn().flush();
206 else if (modification == QFileInfo(*commitMessage).lastModified()) {
207 Logger::warn() << "unchanged, won't record\n";
208 return Ok;
210 else {
211 // get data until the separator line.
212 commitMessage->open(QIODevice::ReadOnly);
213 patchName = commitMessage->readAll();
214 commitMessage->close();
215 int cuttoff = patchName.indexOf(defaultCommitMessage1);
216 if (cuttoff > 0)
217 patchName.truncate(cuttoff);
220 if (patchName.isEmpty())
221 patchName = Interview::ask("What is the patch name? ").toUtf8();
223 ReturnCodes rc = addFilesPerAcceptance(changeSet, all);
224 if (rc != Ok)
225 return rc;
227 QProcess git;
228 QStringList arguments;
229 arguments << "write-tree";
230 GitRunner runner(git, arguments);
231 rc = runner.start(GitRunner::WaitForStandardOutput);
232 if (rc != Ok) {
233 Logger::error() << "Git write-tree failed!, aborting record\n";
234 return rc;
236 char buf[1024];
237 Vng::readLine(&git, buf, sizeof(buf));
238 QString tree(buf);
239 git.waitForFinished(); // patiently wait for it to finish..
240 Logger::debug() << "The tree got git ref; " << tree;
241 Logger::debug().flush(); // flush since we do an ask next
243 arguments.clear();
244 git.setEnvironment(environment);
245 // TODO
246 // if amend then;
247 // parent = git-cat-file commit HEAD | grep parent
248 // if .git/MERGE_HEAD exists its a merge
249 // then we have multiple heads, one additional per line in .git/MERGE_HEAD
250 // also use .git/MERGE_MSG
252 arguments << "commit-tree" << tree.left(40);
253 if (!m_config.isEmptyRepo())
254 arguments << "-p" << "HEAD" ;
256 runner.setArguments(arguments);
257 rc = runner.start(GitRunner::WaitUntilReadyForWrite);
258 if (rc != Ok) {
259 Logger::error() << "Git commit-tree failed!, aborting record\n";
260 return rc;
262 git.write(patchName);
263 git.write("\n");
264 git.closeWriteChannel();
265 Vng::readLine(&git, buf, sizeof(buf));
266 QString commit(buf);
267 Logger::debug() << "commit is ref; " << commit;
268 git.waitForFinished(); // patiently wait for it to finish..
269 if (commit.isEmpty()) {
270 Logger::error() << "Git update-ref failed to produce a reference!, aborting record\n";
271 return GitFailed;
274 arguments.clear();
275 arguments << "update-ref" << "HEAD" << commit.left(40);
276 runner.setArguments(arguments);
277 rc = runner.start(GitRunner::WaitUntilFinished);
278 if (rc != Ok) {
279 Logger::error() << "Git update-ref failed!, aborting record\n";
280 return rc;
283 // We removed files from the index in case they were freshly added, but the user didn't want it in this commit.
284 // we have to re-add those files.
285 arguments.clear();
286 arguments << "update-index" << "--add";
287 for (int i=0; i < changeSet.count(); ++i) {
288 File file = changeSet.file(i);
289 if (! file.oldFileName().isEmpty())
290 continue; // not a new added file.
291 if (file.renameAcceptance() == Vng::Rejected)
292 arguments.append(file.fileName());
294 if (arguments.count() > 2) {
295 runner.setArguments(arguments);
296 runner.start(GitRunner::WaitUntilFinished);
299 int endOfLine = patchName.indexOf('\n');
300 if (endOfLine > 0)
301 patchName.truncate(endOfLine);
302 Logger::warn() << "Finished recording patch `" << patchName << "'" << endl;
303 return Ok;
306 AbstractCommand::ReturnCodes Record::addFilesPerAcceptance(const ChangeSet &changeSet, bool allChanges)
308 typedef QPair<QString, QString> NamePair;
309 class RevertCopier {
310 public:
311 RevertCopier() {}
312 ~RevertCopier() {
313 foreach(NamePair pair, m_fileNames) {
314 QFile copy(pair.second);
315 copy.remove();
316 if (! pair.first.isEmpty()) {
317 QFile orig(pair.first);
318 orig.rename(copy.fileName());
322 void append(const QString &orig, const QString &copy) {
323 QPair<QString, QString> pair(orig, copy);
324 m_fileNames.append(pair);
326 private:
327 // thats orig, copy
328 QList< QPair<QString, QString> > m_fileNames;
330 RevertCopier reverter;// this will revert all file changes we make when we exit the scope of this method.
331 ChangeSet patchChanges;
333 QStringList filesForAdd;
334 QStringList notUsedFiles;
335 for (int i=0; i < changeSet.count(); ++i) {
336 File file = changeSet.file(i);
337 bool someEnabled = false;
338 bool allEnabled = true;
339 const bool renamed = file.fileName() != file.oldFileName();
340 const bool protectionChanged = !renamed && file.protection() != file.oldProtection();
341 if (file.renameAcceptance() == Vng::Accepted && renamed
342 || file.protectionAcceptance() == Vng::Accepted && protectionChanged) // record it.
343 someEnabled = true;
345 foreach (Hunk hunk, file.hunks()) {
346 Vng::Acceptance a = hunk.acceptance();
347 if (a == Vng::Accepted || a == Vng::MixedAcceptance)
348 someEnabled = true;
349 else
350 allEnabled = false;
351 if (someEnabled && !allEnabled)
352 break;
354 const bool removeFromIndex = !allChanges && !someEnabled; // user said 'no', lets make sure none of the changes are left in the index.
355 if (removeFromIndex && ! file.fileName().isEmpty() && ! file.oldFileName().isEmpty()) { // just ignore file.
356 notUsedFiles << file.fileName();
357 continue;
359 const bool addUnaltered = allChanges || allEnabled;
360 Logger::debug() << "'" << file.fileName() << "` addUnaltered: " << addUnaltered << ", removeFromIndex: "
361 << removeFromIndex << ", someChangesAccepted: " << someEnabled << ", allAccepted: " << allEnabled << endl;
362 if (file.fileName().isEmpty())
363 filesForAdd << file.oldFileName();
364 else {
365 filesForAdd << file.fileName();
366 if (renamed)
367 filesForAdd << file.oldFileName();
370 if (addUnaltered)
371 continue; // thats easy; whole file to add.
372 if (removeFromIndex && file.fileName().isEmpty()) { // user said no about recording a deleted file.
373 // this is a funny situation; *just* in case the user already somehow added the deletion to the index
374 // we need to reset that to make the index have the full file again. Notice that we need to remove the file afterwards.
375 Q_ASSERT(!file.oldFileName().isEmpty());
376 QProcess git;
377 QStringList arguments;
378 arguments << "cat-file" << "blob" << file.oldSha1();
379 GitRunner runner(git, arguments);
380 ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
381 Logger::debug() << "restoring '" << file.oldFileName() << "`\n";
382 if (rc == Ok) {
383 reverter.append(QString(), file.oldFileName());
384 QFile deletedFile(file.oldFileName());
385 Q_ASSERT(! deletedFile.exists());
386 rc = copyFile(git, deletedFile);
387 git.waitForFinished();
388 deletedFile.close();
389 if (rc)
390 return rc;
392 continue;
395 // for the case where only some patches are selected we make a safety copy of the file.
396 QFile sourceFile(file.fileName());
397 Q_ASSERT(sourceFile.exists());
398 QString fileName = file.fileName();
399 for(int i=0; i < 10; i++) {
400 fileName = fileName + ".orig";
401 if (sourceFile.rename(fileName))
402 break; // successful!
404 reverter.append(fileName, file.fileName());
405 if (removeFromIndex) // need to rename it only, we won't patch it.
406 continue;
408 patchChanges.addFile(file);
409 Logger::debug() << "cp " << fileName << " =>" << file.fileName() << endl;
410 QFile orig(fileName);
411 orig.open(QIODevice::ReadOnly);
412 copyFile(orig, sourceFile);
413 sourceFile.setPermissions(file.permissions());
416 QFile patch(".vng.record." + QString::number(getpid()) + ".diff");
417 patchChanges.writeDiff(patch, ChangeSet::InvertedUserSelection);
418 patch.close();
420 if (patch.size() != 0) {
421 QProcess git;
422 QStringList arguments;
423 arguments << "apply" << "--apply" << "--reverse" << patch.fileName();
424 GitRunner runner(git, arguments);
425 ReturnCodes rc = runner.start(GitRunner::WaitUntilFinished);
426 if (rc != Ok) {
427 Logger::error() << "Vng failed: failed to patch, sorry! Aborting record.\n";
428 return rc; // note that copied files will be moved back to avoid partially patched files lying around.
431 patch.remove();
432 // first clean out the index so that previous stuff other tools did doesn't influence us.
433 if (!m_config.isEmptyRepo() && ! notUsedFiles.isEmpty()) {
434 QProcess git;
435 QStringList arguments;
436 arguments << "reset" << "-q" << "HEAD"; // -q stands for quiet.
437 arguments += notUsedFiles;
438 GitRunner runner(git, arguments);
439 ReturnCodes rc = runner.start(GitRunner::WaitUntilFinished);
440 // if (rc != Ok) { // why o why does 'git reset' return 1 when it succeeded just fine.
441 // Logger::error() << "Cleaning the index failed, aborting record\n";
442 // return rc;
443 // }
446 // git add of all files to get a nice list of changes into the index.
447 QProcess git;
448 QStringList arguments;
449 arguments << "update-index" << "--add" << "--remove" << filesForAdd;
450 GitRunner runner(git, arguments);
451 ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
452 if (rc != Ok) {
453 Logger::error() << "Vng failed: Did not manage to add files to the git index, aborting record\n";
454 return rc;
457 return Ok; // exiting scope will revert all files in the local filesystem. We use the index from here on.