Merge pull request #7 from gaaf/patch-1
[qgit4/redivivus.git] / src / commitimpl.cpp
blobafd1834cbd4ceee5461cad08e8b66366aa44b9ff
1 /*
2 Description: changes commit dialog
4 Author: Marco Costalba (C) 2005-2007
6 Copyright: See COPYING file that comes with this distribution
7 */
8 #include <QTextCodec>
9 #include <QSettings>
10 #include <QMenu>
11 #include <QRegExp>
12 #include <QDir>
13 #include <QMessageBox>
14 #include <QInputDialog>
15 #include <QToolTip>
16 #include <QScrollBar>
17 #include <QKeyEvent>
18 #include "exceptionmanager.h"
19 #include "common.h"
20 #include "git.h"
21 #include "settingsimpl.h"
22 #include "commitimpl.h"
24 using namespace QGit;
26 QString CommitImpl::lastMsgBeforeError;
28 CommitImpl::CommitImpl(Git* g, bool amend) : git(g) {
30 // adjust GUI
31 setAttribute(Qt::WA_DeleteOnClose);
32 setupUi(this);
33 textEditMsg->setFont(TYPE_WRITER_FONT);
35 QVector<QSplitter*> v(1, splitter);
36 QGit::restoreGeometrySetting(CMT_GEOM_KEY, this, &v);
38 QSettings settings;
39 QString templ(settings.value(CMT_TEMPL_KEY, CMT_TEMPL_DEF).toString());
40 QString msg;
41 QDir d;
42 if (d.exists(templ))
43 readFromFile(templ, msg);
45 // set-up files list
46 const RevFile* f = git->getFiles(ZERO_SHA);
47 for (int i = 0; f && i < f->count(); ++i) { // in case of amend f could be null
49 bool inIndex = f->statusCmp(i, RevFile::IN_INDEX);
50 bool isNew = (f->statusCmp(i, RevFile::NEW) || f->statusCmp(i, RevFile::UNKNOWN));
51 QColor myColor = QPalette().color(QPalette::WindowText);
52 if (isNew)
53 myColor = Qt::darkGreen;
54 else if (f->statusCmp(i, RevFile::DELETED))
55 myColor = Qt::red;
57 QTreeWidgetItem* item = new QTreeWidgetItem(treeWidgetFiles);
58 item->setText(0, git->filePath(*f, i));
59 item->setText(1, inIndex ? "Updated in index" : "Not updated in index");
60 item->setCheckState(0, inIndex || !isNew ? Qt::Checked : Qt::Unchecked);
61 item->setForeground(0, myColor);
63 treeWidgetFiles->resizeColumnToContents(0);
65 // compute cursor offsets. Take advantage of fixed width font
66 textEditMsg->setPlainText("\nx\nx"); // cursor doesn't move on empty text
67 textEditMsg->moveCursor(QTextCursor::Start);
68 textEditMsg->verticalScrollBar()->setValue(0);
69 textEditMsg->horizontalScrollBar()->setValue(0);
70 int y0 = textEditMsg->cursorRect().y();
71 int x0 = textEditMsg->cursorRect().x();
72 textEditMsg->moveCursor(QTextCursor::Down);
73 textEditMsg->moveCursor(QTextCursor::Right);
74 textEditMsg->verticalScrollBar()->setValue(0);
75 int y1 = textEditMsg->cursorRect().y();
76 int x1 = textEditMsg->cursorRect().x();
77 ofsX = x1 - x0;
78 ofsY = y1 - y0;
79 textEditMsg->moveCursor(QTextCursor::Start);
80 textEditMsg_cursorPositionChanged();
82 if (lastMsgBeforeError.isEmpty()) {
83 // setup textEditMsg with old commit message to be amended
84 QString status("");
85 if (amend)
86 status = git->getLastCommitMsg();
88 // setup textEditMsg with default value if user opted to do so (default)
89 if (testFlag(USE_CMT_MSG_F, FLAGS_KEY))
90 status += git->getNewCommitMsg();
92 msg = status.trimmed();
93 } else
94 msg = lastMsgBeforeError;
96 textEditMsg->setPlainText(msg);
97 textEditMsg->setFocus();
99 // if message is not changed we avoid calling refresh
100 // to change patch name in stgCommit()
101 origMsg = msg;
103 // setup button functions
104 if (amend) {
105 if (git->isStGITStack()) {
106 pushButtonOk->setText("&Add to top");
107 pushButtonOk->setShortcut(QKeySequence("Alt+A"));
108 pushButtonOk->setToolTip("Refresh top stack patch");
109 } else {
110 pushButtonOk->setText("&Amend");
111 pushButtonOk->setShortcut(QKeySequence("Alt+A"));
112 pushButtonOk->setToolTip("Amend latest commit");
114 connect(pushButtonOk, SIGNAL(clicked()),
115 this, SLOT(pushButtonAmend_clicked()));
116 } else {
117 if (git->isStGITStack()) {
118 pushButtonOk->setText("&New patch");
119 pushButtonOk->setShortcut(QKeySequence("Alt+N"));
120 pushButtonOk->setToolTip("Create a new patch");
122 connect(pushButtonOk, SIGNAL(clicked()),
123 this, SLOT(pushButtonCommit_clicked()));
125 connect(treeWidgetFiles, SIGNAL(customContextMenuRequested(const QPoint&)),
126 this, SLOT(contextMenuPopup(const QPoint&)));
127 connect(textEditMsg, SIGNAL(cursorPositionChanged()),
128 this, SLOT(textEditMsg_cursorPositionChanged()));
130 textEditMsg->installEventFilter(this);
133 void CommitImpl::closeEvent(QCloseEvent*) {
135 QVector<QSplitter*> v(1, splitter);
136 QGit::saveGeometrySetting(CMT_GEOM_KEY, this, &v);
139 void CommitImpl::contextMenuPopup(const QPoint& pos) {
141 QMenu* contextMenu = new QMenu(this);
142 QAction* a = contextMenu->addAction("Select All");
143 connect(a, SIGNAL(triggered()), this, SLOT(checkAll()));
144 a = contextMenu->addAction("Unselect All");
145 connect(a, SIGNAL(triggered()), this, SLOT(unCheckAll()));
146 contextMenu->popup(mapToGlobal(pos));
149 void CommitImpl::checkAll() { checkUncheck(true); }
150 void CommitImpl::unCheckAll() { checkUncheck(false); }
152 void CommitImpl::checkUncheck(bool checkAll) {
154 QTreeWidgetItemIterator it(treeWidgetFiles);
155 while (*it) {
156 (*it)->setCheckState(0, checkAll ? Qt::Checked : Qt::Unchecked);
157 ++it;
161 bool CommitImpl::getFiles(SList selFiles) {
163 // check for files to commit
164 selFiles.clear();
165 QTreeWidgetItemIterator it(treeWidgetFiles);
166 while (*it) {
167 if ((*it)->checkState(0) == Qt::Checked)
168 selFiles.append((*it)->text(0));
169 ++it;
172 return !selFiles.isEmpty();
175 void CommitImpl::warnNoFiles() {
177 QMessageBox::warning(this, "Commit changes - QGit",
178 "Sorry, no files are selected for updating.",
179 QMessageBox::Ok, QMessageBox::NoButton);
182 bool CommitImpl::checkFiles(SList selFiles) {
184 if (getFiles(selFiles))
185 return true;
187 warnNoFiles();
188 return false;
191 bool CommitImpl::checkMsg(QString& msg) {
193 msg = textEditMsg->toPlainText();
194 msg.remove(QRegExp("(^|\\n)\\s*#[^\\n]*")); // strip comments
195 msg.replace(QRegExp("[ \\t\\r\\f\\v]+\\n"), "\n"); // strip line trailing cruft
196 msg = msg.trimmed();
197 if (msg.isEmpty()) {
198 QMessageBox::warning(this, "Commit changes - QGit",
199 "Sorry, I don't want an empty message.",
200 QMessageBox::Ok, QMessageBox::NoButton);
201 return false;
203 // split subject from message body
204 QString subj(msg.section('\n', 0, 0, QString::SectionIncludeTrailingSep));
205 QString body(msg.section('\n', 1).trimmed());
206 msg = subj + '\n' + body + '\n';
207 return true;
210 bool CommitImpl::checkPatchName(QString& patchName) {
212 bool ok;
213 patchName = patchName.simplified();
214 patchName.replace(' ', "_");
215 patchName = QInputDialog::getText(this, "Create new patch - QGit", "Enter patch name:",
216 QLineEdit::Normal, patchName, &ok);
217 if (!ok || patchName.isEmpty())
218 return false;
220 QString tmp(patchName.trimmed());
221 if (patchName != tmp.remove(' '))
222 QMessageBox::warning(this, "Create new patch - QGit", "Sorry, control "
223 "characters or spaces\n are not allowed in patch name.");
225 else if (git->isPatchName(patchName))
226 QMessageBox::warning(this, "Create new patch - QGit", "Sorry, patch name "
227 "already exists.\nPlease choose a different name.");
228 else
229 return true;
231 return false;
234 bool CommitImpl::checkConfirm(SCRef msg, SCRef patchName, SCList selFiles, bool amend) {
236 // QTextCodec* tc = QTextCodec::codecForCStrings();
237 // QTextCodec::setCodecForCStrings(0); // set temporary Latin-1
239 // NOTEME: i18n-ugly
240 QString whatToDo = amend ?
241 (git->isStGITStack() ? "refresh top patch with" :
242 "amend last commit with") :
243 (git->isStGITStack() ? "create a new patch with" : "commit");
245 QString text("Do you want to " + whatToDo);
247 bool const fullList = selFiles.size() < 20;
248 if (fullList)
249 text.append(" the following file(s)?\n\n" + selFiles.join("\n") +
250 "\n\nwith the message:\n\n");
251 else
252 text.append(" those " + QString::number(selFiles.size()) +
253 " files the with the message:\n\n");
255 text.append(msg);
256 if (git->isStGITStack())
257 text.append("\n\nAnd patch name: " + patchName);
259 // QTextCodec::setCodecForCStrings(tc);
261 QMessageBox msgBox(this);
262 msgBox.setWindowTitle("Commit changes - QGit");
263 msgBox.setText(text);
264 if (!fullList)
265 msgBox.setDetailedText(selFiles.join("\n"));
267 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
268 msgBox.setDefaultButton(QMessageBox::Yes);
270 return msgBox.exec() != QMessageBox::No;
273 void CommitImpl::pushButtonSettings_clicked() {
275 SettingsImpl setView(this, git, 3);
276 setView.exec();
279 void CommitImpl::pushButtonCancel_clicked() {
281 close();
284 void CommitImpl::pushButtonCommit_clicked() {
286 QStringList selFiles; // retrieve selected files
287 if (!checkFiles(selFiles))
288 return;
290 QString msg; // check for commit message and strip comments
291 if (!checkMsg(msg))
292 return;
294 QString patchName(msg.section('\n', 0, 0)); // the subject
295 if (git->isStGITStack() && !checkPatchName(patchName))
296 return;
298 // ask for confirmation
299 if (!checkConfirm(msg, patchName, selFiles, !Git::optAmend))
300 return;
302 // ok, let's go
303 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
304 EM_PROCESS_EVENTS; // to close message box
305 bool ok;
306 if (git->isStGITStack())
307 ok = git->stgCommit(selFiles, msg, patchName, !Git::optFold);
308 else
309 ok = git->commitFiles(selFiles, msg, !Git::optAmend);
311 lastMsgBeforeError = (ok ? "" : msg);
312 QApplication::restoreOverrideCursor();
313 hide();
314 emit changesCommitted(ok);
315 close();
318 void CommitImpl::pushButtonAmend_clicked() {
320 QStringList selFiles; // retrieve selected files
321 getFiles(selFiles);
322 // FIXME: If there are no files AND no changes to message, we should not
323 // commit. Disabling the commit button in such case might be preferable.
325 QString msg(textEditMsg->toPlainText());
326 if (msg == origMsg && selFiles.isEmpty()) {
327 warnNoFiles();
328 return;
331 if (msg == origMsg && git->isStGITStack())
332 msg = "";
333 else if (!checkMsg(msg))
334 // We are going to replace the message, so it better isn't empty
335 return;
337 // ask for confirmation
338 // FIXME: We don't need patch name for refresh, do we?
339 if (!checkConfirm(msg, "", selFiles, Git::optAmend))
340 return;
342 // ok, let's go
343 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
344 EM_PROCESS_EVENTS; // to close message box
345 bool ok;
346 if (git->isStGITStack())
347 ok = git->stgCommit(selFiles, msg, "", Git::optFold);
348 else
349 ok = git->commitFiles(selFiles, msg, Git::optAmend);
351 QApplication::restoreOverrideCursor();
352 hide();
353 emit changesCommitted(ok);
354 close();
357 void CommitImpl::pushButtonUpdateCache_clicked() {
359 QStringList selFiles;
360 if (!checkFiles(selFiles))
361 return;
363 bool ok = git->updateIndex(selFiles);
365 QApplication::restoreOverrideCursor();
366 emit changesCommitted(ok);
367 close();
370 void CommitImpl::textEditMsg_cursorPositionChanged() {
372 int col_pos, line_pos;
373 computePosition(col_pos, line_pos);
374 QString lineNumber = QString("Line: %1 Col: %2")
375 .arg(line_pos + 1).arg(col_pos + 1);
376 textLabelLineCol->setText(lineNumber);
379 void CommitImpl::computePosition(int &col_pos, int &line_pos) {
381 QRect r = textEditMsg->cursorRect();
382 int vs = textEditMsg->verticalScrollBar()->value();
383 int hs = textEditMsg->horizontalScrollBar()->value();
385 // when in start position r.x() = -r.width() / 2
386 col_pos = (r.x() + hs + r.width() / 2) / ofsX;
387 line_pos = (r.y() + vs) / ofsY;
390 bool CommitImpl::eventFilter(QObject* obj, QEvent* event) {
392 if (obj == textEditMsg) {
393 if (event->type() == QEvent::KeyPress) {
394 QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
395 if (( keyEvent->key() == Qt::Key_Return
396 || keyEvent->key() == Qt::Key_Enter
398 && keyEvent->modifiers() & Qt::ControlModifier) {
400 QMetaObject::invokeMethod(pushButtonOk, "clicked", Qt::QueuedConnection);
401 return true;
404 return false;
406 return QObject::eventFilter(obj, event);