Use 'record' in debug messages instead of 'commit'. Make record also work when the...
[vng.git] / Record.cpp
blob3fedb1b3be1fa97c0e86f6a8a89a159289365f5e
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>
30 #ifdef Q_OS_WIN
31 # include <cstdlib>
32 # define getpid() rand()
33 #else
34 # include <sys/types.h>
35 # include <unistd.h>
36 #endif
39 static const CommandLineOption options[] = {
40 {"-a, --all", "answer yes to all patches"},
41 {"-i, --interactive", "prompt user interactively"},
42 {"-m, --patch-name PATCHNAME", "name of patch"},
43 {"-A, --author EMAIL", "specify author id"},
44 //{"--logfile FILE", "give patch name and comment in file"},
45 //{"--delete-logfile", "delete the logfile when done"},
46 //{"--no-test", "don't run the test script"},
47 //{"--test", "run the test script"},
48 //{"--leave-test-directory", "don't remove the test directory"},
49 //{"--remove-test-directory", "remove the test directory"},
50 //{"--edit-long-comment", "Edit the long comment by default"},
51 //{"--skip-long-comment", "Don't give a long comment"},
52 //{"--prompt-long-comment", "Prompt for whether to edit the long comment"},
53 //{"-l, --look-for-adds", "Also look for files that are potentially pending addition"},
54 //{"--dont-look-for-adds", "Don't look for any files that could be added"},
55 //{"--umask UMASK", "specify umask to use when writing."},
56 //{"--posthook COMMAND", "specify command to run after this vng command."},
57 //{"--no-posthook", "Do not run posthook command."},
58 //{"--prompt-posthook", "Prompt before running posthook. [DEFAULT]"},
59 //{"--run-posthook", "Run posthook command without prompting."},
60 CommandLineLastOption
63 static AbstractCommand::ReturnCodes copyFile(QIODevice &from, QIODevice &to) {
64 to.open(QIODevice::WriteOnly);
65 char buf[4096];
66 while (true) {
67 from.waitForReadyRead(-1);
68 qint64 len = from.read(buf, sizeof(buf));
69 if (len <= 0) { // done!
70 to.close();
71 break;
73 while(len > 0) {
74 qint64 written = to.write(buf, len);
75 if (written == -1) // write error!
76 return AbstractCommand::WriteError;
77 len -= written;
80 return AbstractCommand::Ok;
84 Record::Record()
85 : AbstractCommand("record")
87 CommandLineParser::addOptionDefinitions(options);
88 CommandLineParser::setArgumentDefinition("record [FILE or DIRECTORY]" );
91 QString Record::argumentDescription() const
93 return "[FILE or DIRECTORY]";
96 QString Record::commandDescription() const
98 return "Record is used to name a set of changes and record the patch to the repository.\n";
101 AbstractCommand::ReturnCodes Record::run()
103 if (! checkInRepository())
104 return NotInRepo;
105 moveToRoot();
107 CommandLineParser *args = CommandLineParser::instance();
108 const bool all = m_config.isEmptyRepo() || m_config.contains("all")
109 && !args->contains("interactive") || args->contains("all");
111 ChangeSet changeSet;
112 changeSet.fillFromCurrentChanges(rebasedArguments());
114 bool shouldDoRecord = changeSet.count() > 0;
115 if (!shouldDoRecord) {
116 Logger::info() << "No changes!" << endl;
117 return Ok;
120 QString email = args->optionArgument("author", m_config.optionArgument("author", getenv("EMAIL")));
121 QStringList environment;
122 if (! email.isEmpty()) {
123 QRegExp re("(.*) <([@\\S]+)>");
124 if (re.exactMatch(email)) { // meaning its an email AND name
125 environment << "GIT_AUTHOR_NAME="+ re.cap(1);
126 environment << "GIT_AUTHOR_EMAIL="+ re.cap(2);
128 else if (!args->contains("author")) // if its an account or shell wide option; just use the git defs.
129 environment << "GIT_AUTHOR_EMAIL="+ email;
130 else {
131 Logger::error() << "Author format invalid. Please provide author formatted like; `name <email@host>\n";
132 return InvalidOptions;
136 if (shouldDoRecord && !all) { // then do interview
137 HunksCursor cursor(changeSet);
138 cursor.setConfiguration(m_config);
139 Interview interview(cursor, name());
140 interview.setUsePager(shouldUsePager());
141 if (! interview.start()) {
142 Logger::info() << "Record cancelled." << endl;
143 return Ok;
147 if (shouldDoRecord && !all) { // check if there is anything selected
148 shouldDoRecord = changeSet.hasAcceptedChanges();
149 if (! shouldDoRecord) {
150 Logger::info() << "Ok, if you don't want to record anything, that's fine!" <<endl;
151 return Ok;
154 QByteArray patchName = args->optionArgument("patch-name", m_config.optionArgument("patch-name")).toUtf8();
155 if (dryRun())
156 return Ok;
158 if (patchName.isEmpty() && getenv("EDITOR")) {
159 QObject parent;
160 QFile *commitMessage;
161 int i = 0;
162 do {
163 commitMessage = new QFile(QString("vng-record-%1").arg(i++), &parent);
164 } while (commitMessage->exists());
165 if (! commitMessage->open(QIODevice::WriteOnly)) {
166 Logger::error() << "vng-failed. Could not create a temporary file for the record message '"
167 << commitMessage->fileName() << "`\n";
168 return WriteError;
170 const char * defaultCommitMessage1 = "\n***END OF DESCRIPTION***"; // we will look for this string later
171 const char * defaultCommitMessage2 = "\n\nPlace the long patch description above the ***END OF DESCRIPTION*** marker."; // \n\nThis patch contains the following changes:\n";
172 commitMessage->write("\n", 1);
173 commitMessage->write(defaultCommitMessage1, strlen(defaultCommitMessage1));
174 commitMessage->write(defaultCommitMessage2, strlen(defaultCommitMessage2));
175 // TODO
176 // M foo -2
178 commitMessage->close();
179 QDateTime modification = QFileInfo(*commitMessage).lastModified();
180 QString command = QString("%1 %2").arg(getenv("EDITOR")).arg(commitMessage->fileName());
181 int rc = system(command.toAscii().data());
182 if (rc != 0) {
183 // this will keep patchName empty and we fall throuhg to the interview.
184 Logger::warn() << "Vng-Warning: Could not start editor '" << getenv("EDITOR") << "`\n";
185 Logger::warn().flush();
187 else if (modification == QFileInfo(*commitMessage).lastModified()) {
188 Logger::info() << "unchanged, won't record\n";
189 return Ok;
191 else {
192 // get data until the separator line.
193 commitMessage->open(QIODevice::ReadOnly);
194 patchName = commitMessage->readAll();
195 commitMessage->close();
196 int cuttoff = patchName.indexOf(defaultCommitMessage1);
197 if (cuttoff > 0)
198 patchName.truncate(cuttoff);
201 if (patchName.isEmpty())
202 patchName = Interview::ask("What is the patch name? ").toUtf8();
204 // first clean out the index so that previous stuff other tools did doesn't influence us.
205 QProcess git;
206 QStringList arguments;
207 arguments << "read-tree" << "--aggressive" << "HEAD";
208 GitRunner runner(git, arguments);
209 ReturnCodes rc = runner.start(GitRunner::WaitUntilFinished);
210 if (rc != Ok) {
211 Logger::error() << "internal error; failed to initialize record, sorry, aborting record\n";
212 return rc;
215 rc = addFilesPerAcceptance(changeSet, all);
216 if (rc != Ok)
217 return rc;
219 arguments.clear();
220 arguments << "write-tree";
221 runner.setArguments(arguments);
222 rc = runner.start(GitRunner::WaitForStandardOutput);
223 if (rc != Ok) {
224 Logger::error() << "Git write-tree failed!, aborting record\n";
225 return rc;
227 char buf[1024];
228 Vng::readLine(&git, buf, sizeof(buf));
229 QString tree(buf);
230 git.waitForFinished(); // patiently wait for it to finish..
231 Logger::debug() << "The tree got git ref; " << tree;
232 Logger::debug().flush(); // flush since we do an ask next
234 arguments.clear();
235 git.setEnvironment(environment);
236 // TODO
237 // if amend then;
238 // parent = git-cat-file commit HEAD | grep parent
239 // if .git/MERGE_HEAD exists its a merge
240 // then we have multiple heads, one additional per line in .git/MERGE_HEAD
241 // also use .git/MERGE_MSG
243 arguments << "commit-tree" << tree.left(40);
244 if (!m_config.isEmptyRepo())
245 arguments << "-p" << "HEAD" ;
247 runner.setArguments(arguments);
248 rc = runner.start(GitRunner::WaitUntilReadyForWrite);
249 if (rc != Ok) {
250 Logger::error() << "Git commit-tree failed!, aborting record\n";
251 return rc;
253 git.write(patchName);
254 git.write("\n");
255 git.closeWriteChannel();
256 Vng::readLine(&git, buf, sizeof(buf));
257 QString commit(buf);
258 Logger::debug() << "commit is ref; " << commit;
259 git.waitForFinished(); // patiently wait for it to finish..
260 if (commit.isEmpty()) {
261 Logger::error() << "Git update-ref failed to produce a reference!, aborting record\n";
262 return GitFailed;
265 arguments.clear();
266 arguments << "update-ref" << "HEAD" << commit.left(40);
267 runner.setArguments(arguments);
268 rc = runner.start(GitRunner::WaitUntilFinished);
269 if (rc != Ok) {
270 Logger::error() << "Git update-ref failed!, aborting record\n";
271 return rc;
274 // We removed files from the index in case they were freshly added, but the user didn't want it in this commit.
275 // we have to re-add those files.
276 arguments.clear();
277 arguments << "update-index" << "--add";
278 foreach (File file, changeSet.files()) {
279 if (! file.oldFileName().isEmpty())
280 continue; // not a new added file.
281 if (file.renameAcceptance() == Vng::Rejected)
282 arguments.append(file.fileName());
284 if (arguments.count() > 2) {
285 runner.setArguments(arguments);
286 runner.start(GitRunner::WaitUntilFinished);
289 int endOfLine = patchName.indexOf('\n');
290 if (endOfLine > 0)
291 patchName.truncate(endOfLine);
292 Logger::info() << "Finished recording patch `" << patchName << "'" << endl;
293 return Ok;
296 AbstractCommand::ReturnCodes Record::addFilesPerAcceptance(const ChangeSet &changeSet, bool allChanges)
298 typedef QPair<QString, QString> NamePair;
299 class RevertCopier {
300 public:
301 RevertCopier() {}
302 ~RevertCopier() {
303 foreach(NamePair pair, m_fileNames) {
304 QFile copy(pair.second);
305 copy.remove();
306 if (! pair.first.isEmpty()) {
307 QFile orig(pair.first);
308 orig.rename(copy.fileName());
312 void append(const QString &orig, const QString &copy) {
313 QPair<QString, QString> pair(orig, copy);
314 m_fileNames.append(pair);
316 private:
317 // thats orig, copy
318 QList< QPair<QString, QString> > m_fileNames;
320 RevertCopier reverter;// this will revert all file changes we make when we exit the scope of this method.
321 ChangeSet patchChanges;
323 QStringList filesForAdd;
324 foreach (File file, changeSet.files()) {
325 bool someEnabled = false;
326 bool allEnabled = true;
327 const bool renamed = file.fileName() != file.oldFileName();
328 const bool protectionChanged = !renamed && file.protection() != file.oldProtection();
329 if (file.renameAcceptance() == Vng::Accepted && renamed
330 || file.protectionAcceptance() == Vng::Accepted && protectionChanged) // record it.
331 someEnabled = true;
333 foreach (Hunk hunk, file.hunks()) {
334 Vng::Acceptance a = hunk.acceptance();
335 if (a == Vng::Accepted || a == Vng::MixedAcceptance)
336 someEnabled = true;
337 else
338 allEnabled = false;
339 if (someEnabled && !allEnabled)
340 break;
342 const bool removeFromIndex = !allChanges && !someEnabled; // user said 'no', lets make sure none of the changes are left in the index.
343 if (removeFromIndex && ! file.fileName().isEmpty() && ! file.oldFileName().isEmpty()) // just ignore file.
344 continue;
345 const bool addUnaltered = allChanges || allEnabled;
346 Logger::debug() << file.fileName() << "] addUnaltered: " << addUnaltered << ", removeFromIndex: "
347 << removeFromIndex << ", someChangesAccepted: " << someEnabled << ", allAccepted: " << allEnabled << endl;
348 if (file.fileName().isEmpty())
349 filesForAdd << file.oldFileName();
350 else
351 filesForAdd << file.fileName();
353 if (addUnaltered)
354 continue; // thats easy; whole file to add.
355 if (removeFromIndex && file.fileName().isEmpty()) { // user said no about recording a deleted file.
356 // this is a funny situation; *just* in case the user already somehow added the deletion to the index
357 // we need to reset that to make the index have the full file again. Notice that we need to remove the file afterwards.
358 Q_ASSERT(!file.oldFileName().isEmpty());
359 QProcess git;
360 QStringList arguments;
361 arguments << "cat-file" << "blob" << file.oldSha1();
362 GitRunner runner(git, arguments);
363 ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
364 if (rc == Ok) {
365 reverter.append(QString(), file.oldFileName());
366 QFile deletedFile(file.oldFileName());
367 Q_ASSERT(! deletedFile.exists());
368 deletedFile.open(QIODevice::WriteOnly);
369 rc = copyFile(git, deletedFile);
370 git.waitForFinished();
371 deletedFile.close();
372 if (rc)
373 return rc;
375 continue;
378 // for the case where only some patches are selected we make a safety copy of the file.
379 QFile sourceFile(file.fileName());
380 Q_ASSERT(sourceFile.exists());
381 QString fileName = file.fileName();
382 for(int i=0; i < 10; i++) {
383 fileName = fileName + ".orig";
384 if (sourceFile.rename(fileName))
385 break; // successful!
387 reverter.append(fileName, file.fileName());
388 if (removeFromIndex) // need to rename it only, we won't patch it.
389 continue;
391 patchChanges.addFile(file);
392 Logger::debug() << "cp " << fileName << " =>" << file.fileName() << endl;
393 QFile orig(fileName);
394 orig.open(QIODevice::ReadOnly);
395 copyFile(orig, sourceFile);
396 sourceFile.setPermissions(file.permissions());
399 QFile patch(".vng.record." + QString::number(getpid()) + ".diff");
400 patchChanges.writeDiff(patch, ChangeSet::InvertedUserSelection);
401 patch.close();
403 if (patch.size() != 0) {
404 QProcess git;
405 QStringList arguments;
406 arguments << "apply" << "--apply" << "--reverse" << patch.fileName();
407 GitRunner runner(git, arguments);
408 ReturnCodes rc = runner.start(GitRunner::WaitUntilFinished);
409 if (rc != Ok) {
410 Logger::error() << "internal error; failed to patch, sorry, aborting record\n";
411 return rc; // note that copied files will be moved back to avoid partially patched files lying around.
414 patch.remove();
416 // git add of all files.
417 QProcess git;
418 QStringList arguments;
419 arguments << "update-index" << "--remove" << filesForAdd;
420 GitRunner runner(git, arguments);
421 ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
422 if (rc != Ok) {
423 Logger::error() << "Adding files to the git index failed, aborting record\n";
424 return rc;
427 return Ok; // exiting scope will revert all files in the local filesystem. We use the index from here on.