Make detection of empty repo also work after a git gc
[vng.git] / src / commands / Changes.cpp
blob57a9931ef089364720c267ff2fa83b3afc54bed1
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 "Changes.h"
20 #include "CommandLineParser.h"
21 #include "GitRunner.h"
22 #include "Logger.h"
23 #include "Vng.h"
24 #include "hunks/ChangeSet.h"
25 #include "patches/Commit.h"
26 #include "patches/CommitsMatcher.h"
28 #include <QDebug>
30 class CommitIterator {
31 public:
32 enum Direction {
33 Forward,
34 Reverse
37 CommitIterator(QProcess &input, Direction direction, int maxCommitCount);
39 bool hasNext();
40 Commit next();
41 Commit current();
43 private:
44 Commit getNext();
46 Direction m_direction;
47 QList<Commit> m_commitCache;
48 QProcess &m_process;
49 Commit m_current;
50 CommitsMatcher m_matcher;
51 int m_maxCommitCount;
54 CommitIterator::CommitIterator(QProcess &input, Direction direction, int maxCommitCount)
55 : m_direction(direction),
56 m_process(input),
57 m_maxCommitCount(maxCommitCount)
59 if (m_direction == Forward) {
60 m_current = getNext();
61 } else {
62 if (maxCommitCount == -1)
63 maxCommitCount = 1000; // caching more sounds bad
64 while (m_commitCache.count () < maxCommitCount) {
65 Commit commit = getNext();
66 if (commit.isValid())
67 m_commitCache << commit;
68 else
69 break;
71 m_process.terminate();
72 if (!m_commitCache.isEmpty())
73 m_current = m_commitCache.takeLast();
77 Commit CommitIterator::next()
79 if (m_direction == Forward) {
80 if (m_current.isValid())
81 m_current = getNext();
82 else
83 m_current = Commit();
84 } else {
85 if (!m_commitCache.isEmpty())
86 m_current = m_commitCache.takeLast();
87 else
88 m_current = Commit();
90 return m_current;
93 Commit CommitIterator::current()
95 return m_current;
98 Commit CommitIterator::getNext()
100 if (m_maxCommitCount == 0) {
101 m_process.terminate();
102 return Commit();
104 while (true) {
105 Commit commit = Commit::createFromStream(&m_process);
106 if (! commit.isValid())
107 return commit;
108 switch(m_matcher.match(commit)) {
109 case CommitsMatcher::SkipPatch:
110 continue;
111 case CommitsMatcher::ShowPatch:
112 --m_maxCommitCount;
113 return commit;
114 case CommitsMatcher::Exit:
115 m_process.terminate();
116 return Commit();
122 static const CommandLineOption options[] = {
123 // {"-a, --all", "answer yes to all patches"},
124 {"--to-match PATTERN", "select changes up to a patch matching PATTERN"},
125 {"--to-patch REGEXP", "select changes up to a patch matching REGEXP"},
126 // {"--to-tag REGEXP", "select changes up to a tag matching REGEXP"},
127 {"--from-match PATTERN", "select changes starting with a patch matching PATTERN"},
128 {"--from-patch REGEXP", "select changes starting with a patch matching REGEXP"},
129 // {"--from-tag REGEXP", "select changes starting with a tag matching REGEXP"},
130 {"-n, --last NUMBER", "select the last NUMBER patches"},
131 {"--match PATTERN", "select patches matching PATTERN"},
132 {"-p, --patches REGEXP", "select patches matching REGEXP"},
133 // {"-t, --tags=REGEXP", "select tags matching REGEXP"},
134 // {"--context", "give output suitable for get --context"},
135 // {"--xml-output", "generate XML formatted output"},
136 // {"--human-readable", "give human-readable output"},
137 {"-s, --summary", "summarize changes"},
138 {"--no-summary", "don't summarize changes"},
139 {"--reverse", "show changes in reverse order"},
140 {"--no-reverse", "don't show changes in reverse order"},
141 CommandLineLastOption
144 Changes::Changes()
145 : AbstractCommand("changes")
147 CommandLineParser::addOptionDefinitions(options);
148 CommandLineParser::setArgumentDefinition("changes [FILE or DIRECTORY]" );
151 QString Changes::argumentDescription() const
153 return QLatin1String("[FILE or DIRECTORY]");
156 QString Changes::commandDescription() const
158 return QLatin1String("Changes gives a changelog-style summary of the repository history.\n");
161 AbstractCommand::ReturnCodes Changes::run()
163 if (! checkInRepository())
164 return NotInRepo;
165 QString revisions; // ignored
166 return printChangeList(false, revisions);
169 AbstractCommand::ReturnCodes Changes::printChangeList(bool tight, QString &revisions)
171 CommandLineParser *args = CommandLineParser::instance();
173 QProcess git;
174 QStringList arguments;
175 arguments << QLatin1String("whatchanged") << QLatin1String("--no-abbrev") << QLatin1String("--pretty=raw");
177 int maxPatches = -1;
178 if (args->contains(QLatin1String("last"))) {
179 QString last = args->optionArgument(QLatin1String("last"));
180 bool ok;
181 maxPatches = last.toInt(&ok);
182 if (!ok) {
183 Logger::warn() << "\nFailed parsing your 'last' parameter. Reading your mind failed too, so I'll just use 1\n";
184 Logger::warn().flush();
185 maxPatches = 1;
189 QList<int> usedArguments;
190 usedArguments << 0;
191 if (args->arguments().count() > 1) {
192 foreach (Branch branch, m_config.allBranches()) {
193 QString branchName = branch.branchName();
194 if (branchName.endsWith(QLatin1String("/HEAD")))
195 continue;
196 bool first = true;
197 int index = -1;
198 foreach (QString arg, args->arguments()) {
199 index++;
200 if (first) {
201 first = false; // skip command, args for this command are next.
202 continue;
204 if (branchName == arg || (branchName.endsWith(arg) && branchName[branchName.length() - arg.length() - 1].unicode() == '/')) {
205 arguments << branchName;
206 usedArguments << index;
207 break;
213 // now we have to use the rest of the arguments the user passed.
214 int index = 0;
215 QStringList unknownArguments;
216 foreach (QString arg, args->arguments()) {
217 if (! usedArguments.contains(index++)) {
218 arguments << arg;
219 unknownArguments << arg;
223 GitRunner runner(git, arguments);
224 runner.setTimeout(-1);
225 ReturnCodes rc = runner.start(GitRunner::WaitForStandardOutput);
226 if (rc) {
227 Logger::error() << "Vng failed: Unknown branch or revision: ";
228 foreach(QString arg, unknownArguments)
229 Logger::error() << "`" << arg << "' ";;
230 Logger::error() << endl;
231 return rc;
233 if (!tight && shouldUsePager())
234 Logger::startPager();
236 const bool showSummery = (m_config.contains(QLatin1String("summary")) && !args->contains(QLatin1String("no-summary")))
237 || args->contains(QLatin1String("summary"));
238 QTextStream &out = Logger::standardOut();
239 CommitsMatcher matcher;
240 QString firstCommit, lastCommit;
242 const bool reverse = (m_config.contains(QLatin1String("reverse")) && !args->contains(QLatin1String("no-reverse")))
243 || args->contains(QLatin1String("reverse"));
244 CommitIterator iter(git, reverse ? CommitIterator::Reverse : CommitIterator::Forward, maxPatches);
245 while (true) {
246 Commit commit = iter.current();
247 if (! commit.isValid())
248 break;
249 if (firstCommit.isEmpty())
250 firstCommit = commit.commitTreeIsmSha1();
251 lastCommit = commit.commitTreeIsmSha1() + QLatin1Char('^');
253 out << commit.commitTime().toString() << QLatin1Char(' ');
254 m_config.colorize(out);
255 out << commit.author();
256 m_config.normalColor(out);
257 out << endl;
258 if (commit.author() != commit.committer()) {
259 out << " By ";
260 m_config.colorize(out);
261 out << commit.committer();
262 m_config.normalColor(out);
263 out << endl;
265 out << " ID " << commit.commitTreeIsm() << endl;
266 out << commit.logMessage();
267 if (!tight && (Logger::verbosity() >= Logger::Verbose || showSummery)) {
268 ChangeSet cs = commit.changeSet();
269 cs.generateHunks();
270 if (cs.count())
271 out << endl;
272 for (int i=0; i < cs.count(); ++i) {
273 File file = cs.file(i);
274 if (file.oldFileName().isEmpty())
275 out <<" A " << QString::fromUtf8(file.fileName());
276 else if (file.fileName().isEmpty())
277 out <<" D " << QString::fromUtf8(file.oldFileName());
278 else
279 out <<" M " << QString::fromUtf8(file.fileName());
280 if (Logger::verbosity() < Logger::Verbose) {
281 if (file.linesRemoved() > 0)
282 out << " -" << file.linesRemoved();
283 if (file.linesAdded() > 0)
284 out << " +" << file.linesAdded();
286 out << endl;
287 if (Logger::verbosity() >= Logger::Verbose)
288 file.outputWhatsChanged(out, m_config, false, false);
291 if (!tight)
292 out << endl;
293 Logger::flushPager();
294 if (! out.device()->isWritable()) { // output cancelled; lets kill git.
295 git.kill();
296 break;
298 iter.next();
300 if (!tight)
301 Logger::stopPager();
302 git.waitForFinished();
303 revisions = firstCommit +QLatin1Char(' ')+ lastCommit;
304 return Ok;