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"
32 # define getpid() rand()
34 # include <sys/types.h>
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."},
64 : AbstractCommand("record")
66 CommandLineParser::addOptionDefinitions(options
);
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 AbstractCommand::ReturnCodes
Record::run()
81 if (! checkInRepository())
83 CommandLineParser
*args
= CommandLineParser::instance();
84 const bool all
= m_config
.isEmptyRepo() || m_config
.contains("all")
85 && !args
->contains("interactive") || args
->contains("all");
88 changeSet
.fillFromCurrentChanges(rebasedArguments());
90 bool shouldDoRecord
= changeSet
.count() > 0;
91 if (!shouldDoRecord
) {
92 Logger::info() << "No changes!" << endl
;
96 QString email
= args
->optionArgument("author", m_config
.optionArgument("author", getenv("EMAIL")));
97 QStringList environment
;
98 if (! email
.isEmpty()) {
99 QRegExp
re("(.*) <([@\\S]+)>");
100 if (re
.exactMatch(email
)) { // meaning its an email AND name
101 environment
<< "GIT_AUTHOR_NAME="+ re
.cap(1);
102 environment
<< "GIT_AUTHOR_EMAIL="+ re
.cap(2);
104 else if (!args
->contains("author")) // if its an account or shell wide option; just use the git defs.
105 environment
<< "GIT_AUTHOR_EMAIL="+ email
;
107 Logger::error() << "Author format invalid. Please provide author formatted like; `name <email@host>\n";
108 return InvalidOptions
;
112 if (shouldDoRecord
&& !all
) { // then do interview
113 HunksCursor
cursor(changeSet
);
114 cursor
.setConfiguration(m_config
);
115 Interview
interview(cursor
, name());
116 interview
.setUsePager(shouldUsePager());
117 if (! interview
.start()) {
118 Logger::info() << "Record cancelled." << endl
;
123 if (shouldDoRecord
&& !all
) { // check if there is anything selected
124 shouldDoRecord
= changeSet
.hasAcceptedChanges();
125 if (! shouldDoRecord
) {
126 Logger::info() << "Ok, if you don't want to record anything, that's fine!" <<endl
;
131 QString patchName
= args
->optionArgument("patch-name", m_config
.optionArgument("patch-name"));
132 if (patchName
.isEmpty())
133 patchName
= Interview::ask("What is the patch name? ");
137 AbstractCommand::ReturnCodes rc
= addFilesPerAcceptance(changeSet
, all
);
141 QStringList arguments
;
142 arguments
<< "write-tree";
144 GitRunner
runner(git
, arguments
);
145 rc
= runner
.start(GitRunner::WaitForStandardOutput
);
147 Logger::error() << "Git write-tree failed!, aborting commit\n";
151 Vng::readLine(&git
, buf
, sizeof(buf
));
153 git
.waitForFinished(); // patiently wait for it to finish..
154 Logger::debug() << "The tree got git ref; " << tree
;
155 Logger::debug().flush(); // flush since we do an ask next
158 git
.setEnvironment(environment
);
161 // parent = git-cat-file commit HEAD | grep parent
162 // if .git/MERGE_HEAD exists its a merge
163 // then we have multiple heads, one additional per line in .git/MERGE_HEAD
164 // also use .git/MERGE_MSG
166 arguments
<< "commit-tree" << tree
.left(40);
167 if (!m_config
.isEmptyRepo())
168 arguments
<< "-p" << "HEAD" ;
170 runner
.setArguments(arguments
);
171 rc
= runner
.start(GitRunner::WaitUntilReadyForWrite
);
173 Logger::error() << "Git commit-tree failed!, aborting commit\n";
176 git
.write(patchName
.toUtf8());
178 git
.closeWriteChannel();
179 Vng::readLine(&git
, buf
, sizeof(buf
));
181 Logger::debug() << "commit is ref; " << commit
;
182 git
.waitForFinished(); // patiently wait for it to finish..
183 if (commit
.isEmpty()) {
184 Logger::error() << "Git update-ref failed to produce a reference!, aborting commit\n";
189 arguments
<< "update-ref" << "HEAD" << commit
.left(40);
190 runner
.setArguments(arguments
);
191 rc
= runner
.start(GitRunner::WaitUntilFinished
);
193 Logger::error() << "Git update-ref failed!, aborting commit\n";
196 Logger::info() << "Finished recording patch `" << patchName
<< "'" << endl
;
200 AbstractCommand::ReturnCodes
Record::addFilesPerAcceptance(const ChangeSet
&changeSet
, bool allChanges
)
202 typedef QPair
<QString
, QString
> NamePair
;
207 foreach(NamePair pair
, m_fileNames
) {
208 QFile
orig(pair
.first
);
209 QFile
copy(pair
.second
);
211 orig
.rename(copy
.fileName());
214 void append(const QString
&orig
, const QString
©
) {
215 QPair
<QString
, QString
> pair(orig
, copy
);
216 m_fileNames
.append(pair
);
220 QList
< QPair
<QString
, QString
> > m_fileNames
;
222 RevertCopier reverter
;// this will revert all file changes we make when we exit the scope of this method.
223 ChangeSet patchChanges
;
225 QStringList filesForAdd
;
226 foreach (File file
, changeSet
.files()) {
227 bool someEnabled
= false;
228 bool allEnabled
= true;
229 const bool renamed
= file
.fileName() != file
.oldFileName();
230 const bool protectionChanged
= !renamed
&& file
.protection() != file
.oldProtection();
231 if (file
.renameAcceptance() == Vng::Accepted
&& renamed
232 || file
.protectionAcceptance() == Vng::Accepted
&& protectionChanged
) // record it.
235 foreach (Hunk hunk
, file
.hunks()) {
236 Vng::Acceptance a
= hunk
.acceptance();
237 if (a
== Vng::Accepted
|| a
== Vng::MixedAcceptance
)
241 if (someEnabled
&& !allEnabled
)
244 if (!allChanges
&& !someEnabled
)
246 if (file
.fileName().isEmpty())
247 filesForAdd
<< file
.oldFileName();
249 filesForAdd
<< file
.fileName();
250 if (allChanges
|| allEnabled
)
251 continue; // thats easy; whole file to add.
253 patchChanges
.addFile(file
);
254 // for the case where only some patches are selected we make a safety copy of the file.
255 QFile
sourceFile(file
.fileName());
256 Q_ASSERT(sourceFile
.exists());
257 QString fileName
= file
.fileName();
258 for(int i
=0; i
< 10; i
++) {
259 fileName
= fileName
+ ".orig";
260 if (sourceFile
.rename(fileName
))
261 break; // successful!
263 sourceFile
.open(QIODevice::WriteOnly
);
264 Logger::debug() << "cp " << fileName
<< " =>" << file
.fileName() << endl
;
265 QFile
orig(fileName
);
266 orig
.open(QIODevice::ReadOnly
);
267 reverter
.append(fileName
, file
.fileName());
270 qint64 len
= orig
.read(buf
, sizeof(buf
));
271 if (len
<= 0) { // done! // XXX the 0 should be -1, this works around a bug in Qt
277 qint64 written
= sourceFile
.write(buf
, len
);
278 if (written
== -1) // write error!
283 sourceFile
.setPermissions(file
.permissions());
286 QFile
patch(".vng.record." + QString::number(getpid()) + ".diff");
287 patchChanges
.writeDiff(patch
, ChangeSet::InvertedUserSelection
);
290 if (patch
.size() != 0) {
292 QStringList arguments
;
293 arguments
<< "apply" << "--apply" << "--reverse" << patch
.fileName();
294 GitRunner
runner(git
, arguments
);
295 AbstractCommand::ReturnCodes rc
= runner
.start(GitRunner::WaitUntilFinished
);
297 Logger::error() << "internal error; failed to patch, sorry, aborting commit\n";
298 return rc
; // note that copied files will be moved back to avoid partially patched files lying around.
303 // git add of all files.
305 QStringList arguments
;
306 arguments
<< "update-index" << "--remove" << filesForAdd
;
307 GitRunner
runner(git
, arguments
);
308 AbstractCommand::ReturnCodes rc
= runner
.start(GitRunner::WaitForStandardOutput
);
310 Logger::error() << "Adding files to the git index failed, aborting commit\n";
314 return Ok
; // exiting scope will revert all files in the local filesystem. We use the index from here on.
319 note that we might want to do a check to figure out if files have already been added to the git index.
320 I think we should just warn and not do anything special; if the user mixes git/vng he is responsible