realign contributors addition (waiting for whitespace cleanup)
[qgit4/redivivus.git] / src / commitimpl.cpp
blob9f4a9a547259611ec309dd2aa137b49c8e1b216c
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 "exceptionmanager.h"
18 #include "common.h"
19 #include "git.h"
20 #include "settingsimpl.h"
21 #include "commitimpl.h"
23 using namespace QGit;
25 QString CommitImpl::lastMsgBeforeError;
27 CommitImpl::CommitImpl(Git* g, bool amend) : git(g) {
29 // adjust GUI
30 setAttribute(Qt::WA_DeleteOnClose);
31 setupUi(this);
32 textEditMsg->setFont(TYPE_WRITER_FONT);
34 QVector<QSplitter*> v(1, splitter);
35 QGit::restoreGeometrySetting(CMT_GEOM_KEY, this, &v);
37 QSettings settings;
38 QString templ(settings.value(CMT_TEMPL_KEY, CMT_TEMPL_DEF).toString());
39 QString msg;
40 QDir d;
41 if (d.exists(templ))
42 readFromFile(templ, msg);
44 // set-up files list
45 const RevFile* f = git->getFiles(ZERO_SHA);
46 for (int i = 0; f && i < f->count(); ++i) { // in case of amend f could be null
48 bool inIndex = f->statusCmp(i, RevFile::IN_INDEX);
49 bool isNew = (f->statusCmp(i, RevFile::NEW) || f->statusCmp(i, RevFile::UNKNOWN));
50 QColor myColor = Qt::black;
51 if (isNew)
52 myColor = Qt::darkGreen;
53 else if (f->statusCmp(i, RevFile::DELETED))
54 myColor = Qt::red;
56 QTreeWidgetItem* item = new QTreeWidgetItem(treeWidgetFiles);
57 item->setText(0, git->filePath(*f, i));
58 item->setText(1, inIndex ? "Updated in index" : "Not updated in index");
59 item->setCheckState(0, inIndex || !isNew ? Qt::Checked : Qt::Unchecked);
60 item->setForeground(0, myColor);
62 treeWidgetFiles->resizeColumnToContents(0);
64 // compute cursor offsets. Take advantage of fixed width font
65 textEditMsg->setPlainText("\nx\nx"); // cursor doesn't move on empty text
66 textEditMsg->moveCursor(QTextCursor::Start);
67 textEditMsg->verticalScrollBar()->setValue(0);
68 textEditMsg->horizontalScrollBar()->setValue(0);
69 int y0 = textEditMsg->cursorRect().y();
70 int x0 = textEditMsg->cursorRect().x();
71 textEditMsg->moveCursor(QTextCursor::Down);
72 textEditMsg->moveCursor(QTextCursor::Right);
73 textEditMsg->verticalScrollBar()->setValue(0);
74 int y1 = textEditMsg->cursorRect().y();
75 int x1 = textEditMsg->cursorRect().x();
76 ofsX = x1 - x0;
77 ofsY = y1 - y0;
78 textEditMsg->moveCursor(QTextCursor::Start);
79 textEditMsg_cursorPositionChanged();
81 if (lastMsgBeforeError.isEmpty()) {
82 // setup textEditMsg with old commit message to be amended
83 QString status("");
84 if (amend)
85 status = git->getLastCommitMsg();
87 // setup textEditMsg with default value if user opted to do so (default)
88 if (testFlag(USE_CMT_MSG_F, FLAGS_KEY))
89 status += git->getNewCommitMsg();
91 // prepend commit msg with template if available
92 if (!amend)
93 status.prepend('\n').replace(QRegExp("\\n([^#])"), "\n#\\1"); // comment all the lines
95 msg = status.trimmed();
96 } else
97 msg = lastMsgBeforeError;
99 textEditMsg->setPlainText(msg);
100 textEditMsg->setFocus();
102 // if message is not changed we avoid calling refresh
103 // to change patch name in stgCommit()
104 origMsg = msg;
106 // setup button functions
107 if (amend) {
108 if (git->isStGITStack()) {
109 pushButtonOk->setText("&Add to top");
110 pushButtonOk->setShortcut(QKeySequence("Alt+A"));
111 pushButtonOk->setToolTip("Refresh top stack patch");
112 } else {
113 pushButtonOk->setText("&Amend");
114 pushButtonOk->setShortcut(QKeySequence("Alt+A"));
115 pushButtonOk->setToolTip("Amend latest commit");
117 connect(pushButtonOk, SIGNAL(clicked()),
118 this, SLOT(pushButtonAmend_clicked()));
119 } else {
120 if (git->isStGITStack()) {
121 pushButtonOk->setText("&New patch");
122 pushButtonOk->setShortcut(QKeySequence("Alt+N"));
123 pushButtonOk->setToolTip("Create a new patch");
125 connect(pushButtonOk, SIGNAL(clicked()),
126 this, SLOT(pushButtonCommit_clicked()));
128 connect(treeWidgetFiles, SIGNAL(customContextMenuRequested(const QPoint&)),
129 this, SLOT(contextMenuPopup(const QPoint&)));
130 connect(textEditMsg, SIGNAL(cursorPositionChanged()),
131 this, SLOT(textEditMsg_cursorPositionChanged()));
134 void CommitImpl::closeEvent(QCloseEvent*) {
136 QVector<QSplitter*> v(1, splitter);
137 QGit::saveGeometrySetting(CMT_GEOM_KEY, this, &v);
140 void CommitImpl::contextMenuPopup(const QPoint& pos) {
142 QMenu* contextMenu = new QMenu(this);
143 QAction* a = contextMenu->addAction("Select All");
144 connect(a, SIGNAL(triggered()), this, SLOT(checkAll()));
145 a = contextMenu->addAction("Unselect All");
146 connect(a, SIGNAL(triggered()), this, SLOT(unCheckAll()));
147 contextMenu->popup(mapToGlobal(pos));
150 void CommitImpl::checkAll() { checkUncheck(true); }
151 void CommitImpl::unCheckAll() { checkUncheck(false); }
153 void CommitImpl::checkUncheck(bool checkAll) {
155 QTreeWidgetItemIterator it(treeWidgetFiles);
156 while (*it) {
157 (*it)->setCheckState(0, checkAll ? Qt::Checked : Qt::Unchecked);
158 ++it;
162 bool CommitImpl::getFiles(SList selFiles) {
164 // check for files to commit
165 selFiles.clear();
166 QTreeWidgetItemIterator it(treeWidgetFiles);
167 while (*it) {
168 if ((*it)->checkState(0) == Qt::Checked)
169 selFiles.append((*it)->text(0));
170 ++it;
173 return !selFiles.isEmpty();
176 void CommitImpl::warnNoFiles() {
178 QMessageBox::warning(this, "Commit changes - QGit",
179 "Sorry, no files are selected for updating.",
180 QMessageBox::Ok, QMessageBox::NoButton);
183 bool CommitImpl::checkFiles(SList selFiles) {
185 if (getFiles(selFiles))
186 return true;
188 warnNoFiles();
189 return false;
192 bool CommitImpl::checkMsg(QString& msg) {
194 msg = textEditMsg->toPlainText();
195 msg.remove(QRegExp("(^|\\n)\\s*#[^\\n]*")); // strip comments
196 msg.replace(QRegExp("[ \\t\\r\\f\\v]+\\n"), "\n"); // strip line trailing cruft
197 msg = msg.trimmed();
198 if (msg.isEmpty()) {
199 QMessageBox::warning(this, "Commit changes - QGit",
200 "Sorry, I don't want an empty message.",
201 QMessageBox::Ok, QMessageBox::NoButton);
202 return false;
204 // split subject from message body
205 QString subj(msg.section('\n', 0, 0, QString::SectionIncludeTrailingSep));
206 QString body(msg.section('\n', 1).trimmed());
207 msg = subj + '\n' + body + '\n';
208 return true;
211 bool CommitImpl::checkPatchName(QString& patchName) {
213 bool ok;
214 patchName = patchName.simplified();
215 patchName.replace(' ', "_");
216 patchName = QInputDialog::getText(this, "Create new patch - QGit", "Enter patch name:",
217 QLineEdit::Normal, patchName, &ok);
218 if (!ok || patchName.isEmpty())
219 return false;
221 QString tmp(patchName.trimmed());
222 if (patchName != tmp.remove(' '))
223 QMessageBox::warning(this, "Create new patch - QGit", "Sorry, control "
224 "characters or spaces\n are not allowed in patch name.");
226 else if (git->isPatchName(patchName))
227 QMessageBox::warning(this, "Create new patch - QGit", "Sorry, patch name "
228 "already exists.\nPlease choose a different name.");
229 else
230 return true;
232 return false;
235 bool CommitImpl::checkConfirm(SCRef msg, SCRef patchName, SCList selFiles, bool amend) {
237 QTextCodec* tc = QTextCodec::codecForCStrings();
238 QTextCodec::setCodecForCStrings(0); // set temporary Latin-1
240 // NOTEME: i18n-ugly
241 QString whatToDo = amend ?
242 (git->isStGITStack() ? "refresh top patch with" :
243 "amend last commit with") :
244 (git->isStGITStack() ? "create a new patch with" : "commit");
246 QString text("Do you want to " + whatToDo);
248 bool const fullList = selFiles.size() < 20;
249 if (fullList)
250 text.append(" the following file(s)?\n\n" + selFiles.join("\n") +
251 "\n\nwith the message:\n\n");
252 else
253 text.append(" those " + QString::number(selFiles.size()) +
254 " files the with the message:\n\n");
256 text.append(msg);
257 if (git->isStGITStack())
258 text.append("\n\nAnd patch name: " + patchName);
260 QTextCodec::setCodecForCStrings(tc);
262 QMessageBox msgBox(this);
263 msgBox.setWindowTitle("Commit changes - QGit");
264 msgBox.setText(text);
265 if (!fullList)
266 msgBox.setDetailedText(selFiles.join("\n"));
268 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
269 msgBox.setDefaultButton(QMessageBox::Yes);
271 return msgBox.exec() != QMessageBox::No;
274 void CommitImpl::pushButtonSettings_clicked() {
276 SettingsImpl setView(this, git, 3);
277 setView.exec();
280 void CommitImpl::pushButtonCancel_clicked() {
282 close();
285 void CommitImpl::pushButtonCommit_clicked() {
287 QStringList selFiles; // retrieve selected files
288 if (!checkFiles(selFiles))
289 return;
291 QString msg; // check for commit message and strip comments
292 if (!checkMsg(msg))
293 return;
295 QString patchName(msg.section('\n', 0, 0)); // the subject
296 if (git->isStGITStack() && !checkPatchName(patchName))
297 return;
299 // ask for confirmation
300 if (!checkConfirm(msg, patchName, selFiles, !Git::optAmend))
301 return;
303 // ok, let's go
304 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
305 EM_PROCESS_EVENTS; // to close message box
306 bool ok;
307 if (git->isStGITStack())
308 ok = git->stgCommit(selFiles, msg, patchName, !Git::optFold);
309 else
310 ok = git->commitFiles(selFiles, msg, !Git::optAmend);
312 lastMsgBeforeError = (ok ? "" : msg);
313 QApplication::restoreOverrideCursor();
314 hide();
315 emit changesCommitted(ok);
316 close();
319 void CommitImpl::pushButtonAmend_clicked() {
321 QStringList selFiles; // retrieve selected files
322 getFiles(selFiles);
323 // FIXME: If there are no files AND no changes to message, we should not
324 // commit. Disabling the commit button in such case might be preferable.
326 QString msg(textEditMsg->toPlainText());
327 if (msg == origMsg && selFiles.isEmpty()) {
328 warnNoFiles();
329 return;
332 if (msg == origMsg && git->isStGITStack())
333 msg = "";
334 else if (!checkMsg(msg))
335 // We are going to replace the message, so it better isn't empty
336 return;
338 // ask for confirmation
339 // FIXME: We don't need patch name for refresh, do we?
340 if (!checkConfirm(msg, "", selFiles, Git::optAmend))
341 return;
343 // ok, let's go
344 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
345 EM_PROCESS_EVENTS; // to close message box
346 bool ok;
347 if (git->isStGITStack())
348 ok = git->stgCommit(selFiles, msg, "", Git::optFold);
349 else
350 ok = git->commitFiles(selFiles, msg, Git::optAmend);
352 QApplication::restoreOverrideCursor();
353 hide();
354 emit changesCommitted(ok);
355 close();
358 void CommitImpl::pushButtonUpdateCache_clicked() {
360 QStringList selFiles;
361 if (!checkFiles(selFiles))
362 return;
364 bool ok = git->updateIndex(selFiles);
366 QApplication::restoreOverrideCursor();
367 emit changesCommitted(ok);
368 close();
371 void CommitImpl::textEditMsg_cursorPositionChanged() {
373 int col_pos, line_pos;
374 computePosition(col_pos, line_pos);
375 QString lineNumber = QString("Line: %1 Col: %2")
376 .arg(line_pos + 1).arg(col_pos + 1);
377 textLabelLineCol->setText(lineNumber);
380 void CommitImpl::computePosition(int &col_pos, int &line_pos) {
382 QRect r = textEditMsg->cursorRect();
383 int vs = textEditMsg->verticalScrollBar()->value();
384 int hs = textEditMsg->horizontalScrollBar()->value();
386 // when in start position r.x() = -r.width() / 2
387 col_pos = (r.x() + hs + r.width() / 2) / ofsX;
388 line_pos = (r.y() + vs) / ofsY;