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 //{"-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."},
59 : AbstractCommand("record"),
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
89 void Record::setUsageMode(UsageMode mode
)
94 QString
Record::sha1() const
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"));
112 AbstractCommand::ReturnCodes
Record::record()
114 if (! checkInRepository())
116 moveToRoot(CheckFileSystem
);
119 m_all
= m_mode
== AllChanges
;
120 const bool needHunks
= !m_all
|| m_patchName
.isEmpty();
123 changeSet
.fillFromCurrentChanges(rebasedArguments(), needHunks
);
125 changeSet
.waitForFinishFirstFile();
126 bool shouldDoRecord
= changeSet
.count() > 0;
127 if (!shouldDoRecord
) {
129 Logger::warn() << "No changes!" << endl
;
133 QString email
= m_author
;
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
;
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
, "Shall I record this change?");
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
;
176 if ((m_editComment
|| m_patchName
.isEmpty()) && getenv("EDITOR")) {
177 class Deleter
: public QObject
{
179 Deleter() : commitMessage(0) { }
182 commitMessage
->remove();
184 QFile
*commitMessage
;
187 QFile
*commitMessage
;
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";
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
);
203 commitMessage
->write("\n", 1);
204 commitMessage
->write(defaultCommitMessage1
, strlen(defaultCommitMessage1
));
205 commitMessage
->write(defaultCommitMessage2
, strlen(defaultCommitMessage2
));
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());
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());
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()) {
235 Logger::warn() << "unchanged, won't record\n";
236 return UserCancelled
;
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
);
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
253 if (m_patchName
.isEmpty())
254 m_patchName
= Interview::ask("What is the patch name? ").toUtf8();
256 ReturnCodes rc
= addFilesPerAcceptance(changeSet
, m_all
);
261 QStringList arguments
;
262 arguments
<< "write-tree";
263 GitRunner
runner(git
, arguments
);
264 rc
= runner
.start(GitRunner::WaitForStandardOutput
);
266 Logger::error() << "Git write-tree failed!, aborting record\n";
270 Vng::readLine(&git
, buf
, sizeof(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
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
);
286 Logger::error() << "Git commit-tree failed!, aborting record\n";
289 git
.write(m_patchName
);
291 git
.closeWriteChannel();
292 Vng::readLine(&git
, buf
, sizeof(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";
300 m_sha1
= commit
.left(40);
303 arguments
<< "update-ref" << "HEAD" << m_sha1
;
304 runner
.setArguments(arguments
);
305 rc
= runner
.start(GitRunner::WaitUntilFinished
);
307 Logger::error() << "Git update-ref failed!, aborting record\n";
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.
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');
329 m_patchName
.truncate(endOfLine
);
331 Logger::warn() << "Finished recording patch `" << m_patchName
<< "'" << endl
;
335 AbstractCommand::ReturnCodes
Record::addFilesPerAcceptance(const ChangeSet
&changeSet
, bool allChanges
)
337 typedef QPair
<QString
, QString
> NamePair
;
342 foreach(NamePair pair
, m_fileNames
) {
343 QFile
copy(pair
.second
);
345 if (! pair
.first
.isEmpty()) {
346 QFile
orig(pair
.first
);
347 orig
.rename(copy
.fileName());
351 void append(const QString
&orig
, const QString
©
) {
352 QPair
<QString
, QString
> pair(orig
, copy
);
353 m_fileNames
.append(pair
);
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.
374 foreach (Hunk hunk
, file
.hunks()) {
375 Vng::Acceptance a
= hunk
.acceptance();
376 if (a
== Vng::Accepted
) {
378 } else if (a
== Vng::MixedAcceptance
) {
384 if (someEnabled
&& !allEnabled
)
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();
394 Logger::debug() << "'" << file
.fileName() << "` addUnaltered: " << addUnaltered
<< ", removeFromIndex: "
395 << removeFromIndex
<< ", someChangesAccepted: " << someEnabled
<< ", allAccepted: " << allEnabled
<< endl
;
396 if (file
.fileName().isEmpty())
397 filesForAdd
<< QString::fromUtf8(file
.oldFileName());
399 filesForAdd
<< QString::fromUtf8(file
.fileName());
401 filesForAdd
<< QString::fromUtf8(file
.oldFileName());
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());
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";
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();
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.
442 patchChanges
.addFile(file
);
443 Logger::debug() << "cp " << fileName
<< " =>" << file
.fileName() << endl
;
444 #if (QT_VERSION <= 0x040500)
445 // in 451 the fix was made that sourceFile followed the rename, before that it didn't
446 QFile
orig(fileName
);
447 orig
.copy(file
.fileName());
449 sourceFile
.copy(file
.fileName());
453 QFile
patch(".vng.record." + QString::number(getpid()) + ".diff");
454 patchChanges
.writeDiff(patch
, ChangeSet::InvertedUserSelection
);
457 if (patch
.size() != 0) {
459 QStringList arguments
;
460 arguments
<< "apply" << "--apply" << "--reverse" << patch
.fileName();
461 GitRunner
runner(git
, arguments
);
462 ReturnCodes rc
= runner
.start(GitRunner::WaitUntilFinished
);
464 Logger::error() << "Vng failed: failed to patch, sorry! Aborting record.\n";
465 return rc
; // note that copied files will be moved back to avoid partially patched files lying around.
469 // first clean out the index so that previous stuff other tools did doesn't influence us.
470 if (!m_config
.isEmptyRepo() && ! notUsedFiles
.isEmpty()) {
472 QStringList arguments
;
473 arguments
<< "reset" << "--mixed" << "-q" << "HEAD"; // -q stands for quiet.
474 arguments
+= notUsedFiles
;
475 GitRunner
runner(git
, arguments
);
476 runner
.start(GitRunner::WaitUntilFinished
);
479 // git add of all files to get a nice list of changes into the index.
480 while (!filesForAdd
.isEmpty()) {
482 QStringList arguments
;
483 arguments
<< "update-index" << "--add" << "--remove";
484 int count
= 25; // length of arguments
486 QString first
= filesForAdd
[0];
487 if (count
+ first
.length() > 32000)
489 count
+= first
.length();
490 arguments
.append(first
);
491 filesForAdd
.removeFirst();
492 } while (!filesForAdd
.isEmpty());
493 GitRunner
runner(git
, arguments
);
494 ReturnCodes rc
= runner
.start(GitRunner::WaitForStandardOutput
);
496 Logger::error() << "Vng failed: Did not manage to add files to the git index, aborting record\n";
501 return Ok
; // exiting scope will revert all files in the local filesystem. We use the index from here on.