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/>.
20 #include "CommandLineParser.h"
21 #include "GitRunner.h"
22 #include "hunks/ChangeSet.h"
23 #include "hunks/HunksCursor.h"
25 #include "Interview.h"
33 # define getpid() rand()
35 # include <sys/types.h>
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."},
64 static AbstractCommand::ReturnCodes
copyFile(QIODevice
&from
, QIODevice
&to
) {
65 to
.open(QIODevice::WriteOnly
);
68 from
.waitForReadyRead(-1);
69 qint64 len
= from
.read(buf
, sizeof(buf
));
70 if (len
<= 0) { // done!
75 qint64 written
= to
.write(buf
, len
);
76 if (written
== -1) // write error!
77 return AbstractCommand::WriteError
;
81 return AbstractCommand::Ok
;
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())
108 CommandLineParser
*args
= CommandLineParser::instance();
109 const bool all
= m_config
.isEmptyRepo() || m_config
.contains("all")
110 && !args
->contains("interactive") || args
->contains("all");
113 changeSet
.fillFromCurrentChanges(rebasedArguments());
115 bool shouldDoRecord
= changeSet
.count() > 0;
116 if (!shouldDoRecord
) {
117 Logger::warn() << "No changes!" << endl
;
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
;
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
;
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
;
155 QByteArray patchName
= args
->optionArgument("patch-name", m_config
.optionArgument("patch-name")).toUtf8();
159 if (patchName
.isEmpty() && getenv("EDITOR")) {
160 class Deleter
: public QObject
{
162 Deleter() : commitMessage(0) { }
165 commitMessage
->remove();
167 QFile
*commitMessage
;
170 QFile
*commitMessage
;
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";
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";
183 commitMessage
->write("\n", 1);
184 commitMessage
->write(defaultCommitMessage1
, strlen(defaultCommitMessage1
));
185 commitMessage
->write(defaultCommitMessage2
, strlen(defaultCommitMessage2
));
187 changeSet
.writeDiff(buffer
, ChangeSet::UserSelection
);
188 ChangeSet actualChanges
;
189 actualChanges
.fillFromDiffFile(buffer
);
190 QTextStream
out (commitMessage
);
191 foreach (File file
, actualChanges
.files())
192 file
.outputWhatsChanged(out
, m_config
, true, false);
195 commitMessage
->close();
196 QDateTime modification
= QFileInfo(*commitMessage
).lastModified();
197 QString command
= QString("%1 %2").arg(getenv("EDITOR")).arg(commitMessage
->fileName());
198 int rc
= system(command
.toAscii().data());
200 // this will keep patchName empty and we fall through to the interview.
201 Logger::warn() << "Vng-Warning: Could not start editor '" << getenv("EDITOR") << "`\n";
202 Logger::warn().flush();
204 else if (modification
== QFileInfo(*commitMessage
).lastModified()) {
205 Logger::warn() << "unchanged, won't record\n";
209 // get data until the separator line.
210 commitMessage
->open(QIODevice::ReadOnly
);
211 patchName
= commitMessage
->readAll();
212 commitMessage
->close();
213 int cuttoff
= patchName
.indexOf(defaultCommitMessage1
);
215 patchName
.truncate(cuttoff
);
218 if (patchName
.isEmpty())
219 patchName
= Interview::ask("What is the patch name? ").toUtf8();
221 // first clean out the index so that previous stuff other tools did doesn't influence us.
223 QStringList arguments
;
224 arguments
<< "read-tree" << "--aggressive" << "HEAD";
225 GitRunner
runner(git
, arguments
);
226 ReturnCodes rc
= runner
.start(GitRunner::WaitUntilFinished
);
228 Logger::error() << "internal error; failed to initialize record, sorry, aborting record\n";
232 rc
= addFilesPerAcceptance(changeSet
, all
);
237 arguments
<< "write-tree";
238 runner
.setArguments(arguments
);
239 rc
= runner
.start(GitRunner::WaitForStandardOutput
);
241 Logger::error() << "Git write-tree failed!, aborting record\n";
245 Vng::readLine(&git
, buf
, sizeof(buf
));
247 git
.waitForFinished(); // patiently wait for it to finish..
248 Logger::debug() << "The tree got git ref; " << tree
;
249 Logger::debug().flush(); // flush since we do an ask next
252 git
.setEnvironment(environment
);
255 // parent = git-cat-file commit HEAD | grep parent
256 // if .git/MERGE_HEAD exists its a merge
257 // then we have multiple heads, one additional per line in .git/MERGE_HEAD
258 // also use .git/MERGE_MSG
260 arguments
<< "commit-tree" << tree
.left(40);
261 if (!m_config
.isEmptyRepo())
262 arguments
<< "-p" << "HEAD" ;
264 runner
.setArguments(arguments
);
265 rc
= runner
.start(GitRunner::WaitUntilReadyForWrite
);
267 Logger::error() << "Git commit-tree failed!, aborting record\n";
270 git
.write(patchName
);
272 git
.closeWriteChannel();
273 Vng::readLine(&git
, buf
, sizeof(buf
));
275 Logger::debug() << "commit is ref; " << commit
;
276 git
.waitForFinished(); // patiently wait for it to finish..
277 if (commit
.isEmpty()) {
278 Logger::error() << "Git update-ref failed to produce a reference!, aborting record\n";
283 arguments
<< "update-ref" << "HEAD" << commit
.left(40);
284 runner
.setArguments(arguments
);
285 rc
= runner
.start(GitRunner::WaitUntilFinished
);
287 Logger::error() << "Git update-ref failed!, aborting record\n";
291 // We removed files from the index in case they were freshly added, but the user didn't want it in this commit.
292 // we have to re-add those files.
294 arguments
<< "update-index" << "--add";
295 foreach (File file
, changeSet
.files()) {
296 if (! file
.oldFileName().isEmpty())
297 continue; // not a new added file.
298 if (file
.renameAcceptance() == Vng::Rejected
)
299 arguments
.append(file
.fileName());
301 if (arguments
.count() > 2) {
302 runner
.setArguments(arguments
);
303 runner
.start(GitRunner::WaitUntilFinished
);
306 int endOfLine
= patchName
.indexOf('\n');
308 patchName
.truncate(endOfLine
);
309 Logger::warn() << "Finished recording patch `" << patchName
<< "'" << endl
;
313 AbstractCommand::ReturnCodes
Record::addFilesPerAcceptance(const ChangeSet
&changeSet
, bool allChanges
)
315 typedef QPair
<QString
, QString
> NamePair
;
320 foreach(NamePair pair
, m_fileNames
) {
321 QFile
copy(pair
.second
);
323 if (! pair
.first
.isEmpty()) {
324 QFile
orig(pair
.first
);
325 orig
.rename(copy
.fileName());
329 void append(const QString
&orig
, const QString
©
) {
330 QPair
<QString
, QString
> pair(orig
, copy
);
331 m_fileNames
.append(pair
);
335 QList
< QPair
<QString
, QString
> > m_fileNames
;
337 RevertCopier reverter
;// this will revert all file changes we make when we exit the scope of this method.
338 ChangeSet patchChanges
;
340 QStringList filesForAdd
;
341 foreach (File file
, changeSet
.files()) {
342 bool someEnabled
= false;
343 bool allEnabled
= true;
344 const bool renamed
= file
.fileName() != file
.oldFileName();
345 const bool protectionChanged
= !renamed
&& file
.protection() != file
.oldProtection();
346 if (file
.renameAcceptance() == Vng::Accepted
&& renamed
347 || file
.protectionAcceptance() == Vng::Accepted
&& protectionChanged
) // record it.
350 foreach (Hunk hunk
, file
.hunks()) {
351 Vng::Acceptance a
= hunk
.acceptance();
352 if (a
== Vng::Accepted
|| a
== Vng::MixedAcceptance
)
356 if (someEnabled
&& !allEnabled
)
359 const bool removeFromIndex
= !allChanges
&& !someEnabled
; // user said 'no', lets make sure none of the changes are left in the index.
360 if (removeFromIndex
&& ! file
.fileName().isEmpty() && ! file
.oldFileName().isEmpty()) // just ignore file.
362 const bool addUnaltered
= allChanges
|| allEnabled
;
363 Logger::debug() << file
.fileName() << "] addUnaltered: " << addUnaltered
<< ", removeFromIndex: "
364 << removeFromIndex
<< ", someChangesAccepted: " << someEnabled
<< ", allAccepted: " << allEnabled
<< endl
;
365 if (file
.fileName().isEmpty())
366 filesForAdd
<< file
.oldFileName();
368 filesForAdd
<< file
.fileName();
370 filesForAdd
<< file
.oldFileName();
374 continue; // thats easy; whole file to add.
375 if (removeFromIndex
&& file
.fileName().isEmpty()) { // user said no about recording a deleted file.
376 // this is a funny situation; *just* in case the user already somehow added the deletion to the index
377 // we need to reset that to make the index have the full file again. Notice that we need to remove the file afterwards.
378 Q_ASSERT(!file
.oldFileName().isEmpty());
380 QStringList arguments
;
381 arguments
<< "cat-file" << "blob" << file
.oldSha1();
382 GitRunner
runner(git
, arguments
);
383 ReturnCodes rc
= runner
.start(GitRunner::WaitForStandardOutput
);
385 reverter
.append(QString(), file
.oldFileName());
386 QFile
deletedFile(file
.oldFileName());
387 Q_ASSERT(! deletedFile
.exists());
388 deletedFile
.open(QIODevice::WriteOnly
);
389 rc
= copyFile(git
, deletedFile
);
390 git
.waitForFinished();
398 // for the case where only some patches are selected we make a safety copy of the file.
399 QFile
sourceFile(file
.fileName());
400 Q_ASSERT(sourceFile
.exists());
401 QString fileName
= file
.fileName();
402 for(int i
=0; i
< 10; i
++) {
403 fileName
= fileName
+ ".orig";
404 if (sourceFile
.rename(fileName
))
405 break; // successful!
407 reverter
.append(fileName
, file
.fileName());
408 if (removeFromIndex
) // need to rename it only, we won't patch it.
411 patchChanges
.addFile(file
);
412 Logger::debug() << "cp " << fileName
<< " =>" << file
.fileName() << endl
;
413 QFile
orig(fileName
);
414 orig
.open(QIODevice::ReadOnly
);
415 copyFile(orig
, sourceFile
);
416 sourceFile
.setPermissions(file
.permissions());
419 QFile
patch(".vng.record." + QString::number(getpid()) + ".diff");
420 patchChanges
.writeDiff(patch
, ChangeSet::InvertedUserSelection
);
423 if (patch
.size() != 0) {
425 QStringList arguments
;
426 arguments
<< "apply" << "--apply" << "--reverse" << patch
.fileName();
427 GitRunner
runner(git
, arguments
);
428 ReturnCodes rc
= runner
.start(GitRunner::WaitUntilFinished
);
430 Logger::error() << "internal error; failed to patch, sorry, aborting record\n";
431 return rc
; // note that copied files will be moved back to avoid partially patched files lying around.
436 // git add of all files to get a nice list of changes into the index.
438 QStringList arguments
;
439 arguments
<< "update-index" << "--add" << "--remove" << filesForAdd
;
440 GitRunner
runner(git
, arguments
);
441 ReturnCodes rc
= runner
.start(GitRunner::WaitForStandardOutput
);
443 Logger::error() << "Adding files to the git index failed, aborting record\n";
447 return Ok
; // exiting scope will revert all files in the local filesystem. We use the index from here on.