Don't allow to unrecord past the branch point
[vng.git] / Record.cpp
blob333a7a876d02d3929d2c1b7406ca36fc134322a8
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 Record::Record()
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())
82 return NotInRepo;
83 CommandLineParser *args = CommandLineParser::instance();
84 const bool all = m_config.isEmptyRepo() || m_config.contains("all")
85 && !args->contains("interactive") || args->contains("all");
87 ChangeSet changeSet;
88 changeSet.fillFromCurrentChanges(rebasedArguments());
90 bool shouldDoRecord = changeSet.count() > 0;
91 if (!shouldDoRecord) {
92 Logger::info() << "No changes!" << endl;
93 return Ok;
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;
106 else {
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;
119 return Ok;
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;
127 return Ok;
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? ");
134 if (dryRun())
135 return Ok;
137 AbstractCommand::ReturnCodes rc = addFilesPerAcceptance(changeSet, all);
138 if (rc != Ok)
139 return rc;
141 QStringList arguments;
142 arguments << "write-tree";
143 QProcess git;
144 GitRunner runner(git, arguments);
145 rc = runner.start(GitRunner::WaitForStandardOutput);
146 if (rc != Ok) {
147 Logger::error() << "Git write-tree failed!, aborting commit\n";
148 return rc;
150 char buf[1024];
151 Vng::readLine(&git, buf, sizeof(buf));
152 QString tree(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
157 arguments.clear();
158 git.setEnvironment(environment);
159 // TODO
160 // if amend then;
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);
172 if (rc != Ok) {
173 Logger::error() << "Git commit-tree failed!, aborting commit\n";
174 return rc;
176 git.write(patchName.toUtf8());
177 git.write("\n");
178 git.closeWriteChannel();
179 Vng::readLine(&git, buf, sizeof(buf));
180 QString commit(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";
185 return GitFailed;
188 arguments.clear();
189 arguments << "update-ref" << "HEAD" << commit.left(40);
190 runner.setArguments(arguments);
191 rc = runner.start(GitRunner::WaitUntilFinished);
192 if (rc != Ok) {
193 Logger::error() << "Git update-ref failed!, aborting commit\n";
194 return rc;
196 Logger::info() << "Finished recording patch `" << patchName << "'" << endl;
197 return Ok;
200 AbstractCommand::ReturnCodes Record::addFilesPerAcceptance(const ChangeSet &changeSet, bool allChanges)
202 typedef QPair<QString, QString> NamePair;
203 class RevertCopier {
204 public:
205 RevertCopier() {}
206 ~RevertCopier() {
207 foreach(NamePair pair, m_fileNames) {
208 QFile orig(pair.first);
209 QFile copy(pair.second);
210 copy.remove();
211 orig.rename(copy.fileName());
214 void append(const QString &orig, const QString &copy) {
215 QPair<QString, QString> pair(orig, copy);
216 m_fileNames.append(pair);
218 private:
219 // thats orig, copy
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.
233 someEnabled = true;
235 foreach (Hunk hunk, file.hunks()) {
236 Vng::Acceptance a = hunk.acceptance();
237 if (a == Vng::Accepted || a == Vng::MixedAcceptance)
238 someEnabled = true;
239 else
240 allEnabled = false;
241 if (someEnabled && !allEnabled)
242 break;
244 if (!allChanges && !someEnabled)
245 continue;
246 if (file.fileName().isEmpty())
247 filesForAdd << file.oldFileName();
248 else
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());
268 char buf[4096];
269 while(true) {
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
272 sourceFile.close();
273 orig.close();
274 break;
276 while(len > 0) {
277 qint64 written = sourceFile.write(buf, len);
278 if (written == -1) // write error!
279 return WriteError;
280 len -= written;
283 sourceFile.setPermissions(file.permissions());
286 QFile patch(".vng.record." + QString::number(getpid()) + ".diff");
287 patchChanges.writeDiff(patch, ChangeSet::InvertedUserSelection);
288 patch.close();
290 if (patch.size() != 0) {
291 QProcess git;
292 QStringList arguments;
293 arguments << "apply" << "--apply" << "--reverse" << patch.fileName();
294 GitRunner runner(git, arguments);
295 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitUntilFinished);
296 if (rc != Ok) {
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.
301 patch.remove();
303 // git add of all files.
304 QProcess git;
305 QStringList arguments;
306 arguments << "update-index" << "--remove" << filesForAdd;
307 GitRunner runner(git, arguments);
308 AbstractCommand::ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
309 if (rc != Ok) {
310 Logger::error() << "Adding files to the git index failed, aborting commit\n";
311 return rc;
314 return Ok; // exiting scope will revert all files in the local filesystem. We use the index from here on.
318 /* TODO
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
321 for that.