1 /*
2 	Author: Marco Costalba (C) 2005-2007
3 
4 	Copyright: See COPYING file that comes with this distribution
5 
6 
7  Definitions of complex namespace constants
8 
9  Complex constant objects are not folded in like integral types, so they
10  are declared 'extern' in namespace to avoid duplicating them as file scope
11  data in each file where QGit namespace is included.
12 
13 */
14 #include <QDir>
15 #include <QHash>
16 #include <QPixmap>
17 #include <QProcess>
18 #include <QSettings>
19 #include <QSplitter>
20 #include <QTemporaryFile>
21 #include <QTextStream>
22 #include <QWidget>
23 #include <QTextCodec>
24 #include "common.h"
25 #include "git.h"
26 #include "annotate.h"
27 
28 #ifdef Q_OS_WIN32 // *********  platform dependent code ******
29 
30 const QString QGit::SCRIPT_EXT = ".bat";
31 
adjustPath(QStringList & args,bool * winShell)32 static void adjustPath(QStringList& args, bool* winShell) {
33 /*
34    To run an application/script under Windows you need
35    to wrap the command line in the shell interpreter.
36    You need this also to start native commands as 'dir'.
37    An exception is if application is 'git' in that case we
38    call with absolute path to be sure to find it.
39 */
40 	if (args.first() == "git" || args.first().startsWith("git-")) {
41 
42 		if (!QGit::GIT_DIR.isEmpty()) // application built from sources
43 			args.first().prepend(QGit::GIT_DIR + '/');
44 
45 		if (winShell)
46 			*winShell = false;
47 
48 	} else if (winShell) {
49 		args.prepend("/c");
50 		args.prepend("cmd.exe");
51 		*winShell = true;
52 	}
53 }
54 
55 #elif defined(Q_OS_MACX) // MacOS X specific code
56 
57 #include <sys/types.h> // used by chmod()
58 #include <sys/stat.h>  // used by chmod()
59 
60 const QString QGit::SCRIPT_EXT = ".sh";
61 
adjustPath(QStringList & args,bool *)62 static void adjustPath(QStringList& args, bool*) {
63 /*
64     Under MacOS X, git typically doesn't live in the PATH
65     So use GIT_DIR from the settings if available
66 
67     Note: I (OC) think that this should be the default behaviour,
68           but I don't want to break other platforms, so I introduced
69           the MacOS X special case. Feel free to make this the default if
70           you do feel the same.
71 */
72 	if (args.first() == "git" || args.first().startsWith("git-")) {
73 
74 		if (!QGit::GIT_DIR.isEmpty()) // application built from sources
75 			args.first().prepend(QGit::GIT_DIR + '/');
76 
77 	}
78 }
79 
80 #else
81 
82 #include <sys/types.h> // used by chmod()
83 #include <sys/stat.h>  // used by chmod()
84 
85 const QString QGit::SCRIPT_EXT = ".sh";
86 
adjustPath(QStringList &,bool *)87 static void adjustPath(QStringList&, bool*) {}
88 
89 #endif // *********  end of platform dependent code ******
90 
91 // definition of an optimized sha hash function
hexVal(const uchar * ch)92 static inline uint hexVal(const uchar* ch) {
93 
94 	return (*ch < 64 ? *ch - 48 : *ch - 87);
95 }
96 
qHash(const ShaString & s)97 uint qHash(const ShaString& s) { // fast path, called 6-7 times per revision
98 
99 	const uchar* ch = reinterpret_cast<const uchar*>(s.latin1());
100 	return (hexVal(ch     ) << 24)
101 	     + (hexVal(ch +  2) << 20)
102 	     + (hexVal(ch +  4) << 16)
103 	     + (hexVal(ch +  6) << 12)
104 	     + (hexVal(ch +  8) <<  8)
105 	     + (hexVal(ch + 10) <<  4)
106 	     +  hexVal(ch + 12);
107 }
108 
109 /* Value returned by this function should be used only as function argument,
110  * and not stored in a variable because 'ba' value is overwritten at each
111  * call so the returned ShaString could became stale very quickly
112  */
toTempSha(const QString & sha)113 const ShaString QGit::toTempSha(const QString& sha) {
114 
115 	static QByteArray ba;
116 	ba = sha.toLatin1();
117 	return ShaString(sha.isEmpty() ? NULL : ba.constData());
118 }
119 
toPersistentSha(const QString & sha,QVector<QByteArray> & v)120 const ShaString QGit::toPersistentSha(const QString& sha, QVector<QByteArray>& v) {
121 
122 	v.append(sha.toLatin1());
123 	return ShaString(v.last().constData());
124 }
125 
126 // minimum git version required
127 const QString QGit::GIT_VERSION = "1.5.5";
128 
129 // colors
130 const QColor QGit::BROWN        = QColor(150, 75, 0);
131 const QColor QGit::ORANGE       = QColor(255, 160, 50);
132 const QColor QGit::DARK_ORANGE  = QColor(216, 144, 0);
133 const QColor QGit::LIGHT_ORANGE = QColor(255, 221, 170);
134 const QColor QGit::LIGHT_BLUE   = QColor(85, 255, 255);
135 const QColor QGit::PURPLE       = QColor(221, 221, 255);
136 const QColor QGit::DARK_GREEN   = QColor(0, 205, 0);
137 
138 // initialized at startup according to system wide settings
139 QColor QGit::ODD_LINE_COL;
140 QColor QGit::EVEN_LINE_COL;
141 QString QGit::GIT_DIR;
142 
143 /*
144    Default QFont c'tor calls static method QApplication::font() that could
145    be still NOT initialized at this time, so set a dummy font family instead,
146    it will be properly changed later, at startup
147 */
148 QFont QGit::STD_FONT("Helvetica");
149 QFont QGit::TYPE_WRITER_FONT("Helvetica");
150 
151 // patches drag and drop
152 const QString QGit::PATCHES_DIR  = "/.qgit_patches_copy";
153 const QString QGit::PATCHES_NAME = "qgit_import";
154 
155 // git index parameters
156 const QString QGit::ZERO_SHA        = "0000000000000000000000000000000000000000";
157 const QString QGit::CUSTOM_SHA      = "*** CUSTOM * CUSTOM * CUSTOM * CUSTOM **";
158 const QString QGit::ALL_MERGE_FILES = "ALL_MERGE_FILES";
159 
160 const QByteArray QGit::ZERO_SHA_BA(QGit::ZERO_SHA.toLatin1());
161 const ShaString  QGit::ZERO_SHA_RAW(QGit::ZERO_SHA_BA.constData());
162 
163 // settings keys
164 const QString QGit::ORG_KEY         = "qgit";
165 const QString QGit::APP_KEY         = "qgit4";
166 const QString QGit::GIT_DIR_KEY     = "msysgit_exec_dir";
167 const QString QGit::EXT_DIFF_KEY    = "external_diff_viewer";
168 const QString QGit::EXT_EDITOR_KEY  = "external_editor";
169 const QString QGit::REC_REP_KEY     = "recent_open_repos";
170 const QString QGit::STD_FNT_KEY     = "standard_font";
171 const QString QGit::TYPWRT_FNT_KEY  = "typewriter_font";
172 const QString QGit::FLAGS_KEY       = "flags";
173 const QString QGit::PATCH_DIR_KEY   = "Patch/last_dir";
174 const QString QGit::FMT_P_OPT_KEY   = "Patch/args";
175 const QString QGit::AM_P_OPT_KEY    = "Patch/args_2";
176 const QString QGit::EX_KEY          = "Working_dir/exclude_file_path";
177 const QString QGit::EX_PER_DIR_KEY  = "Working_dir/exclude_per_directory_file_name";
178 const QString QGit::CON_GEOM_KEY    = "Console/geometry";
179 const QString QGit::CMT_GEOM_KEY    = "Commit/geometry";
180 const QString QGit::MAIN_GEOM_KEY   = "Top_window/geometry";
181 const QString QGit::REV_GEOM_KEY    = "Rev_List_view/geometry";
182 const QString QGit::CMT_TEMPL_KEY   = "Commit/template_file_path";
183 const QString QGit::CMT_ARGS_KEY    = "Commit/args";
184 const QString QGit::RANGE_FROM_KEY  = "RangeSelect/from";
185 const QString QGit::RANGE_TO_KEY    = "RangeSelect/to";
186 const QString QGit::RANGE_OPT_KEY   = "RangeSelect/options";
187 const QString QGit::ACT_GEOM_KEY    = "Custom_actions/geometry";
188 const QString QGit::ACT_LIST_KEY    = "Custom_actions/list";
189 const QString QGit::ACT_GROUP_KEY   = "Custom_action_list/";
190 const QString QGit::ACT_TEXT_KEY    = "/commands";
191 const QString QGit::ACT_FLAGS_KEY   = "/flags";
192 
193 // settings default values
194 const QString QGit::CMT_TEMPL_DEF   = ".git/commit-template";
195 const QString QGit::EX_DEF          = ".git/info/exclude";
196 const QString QGit::EX_PER_DIR_DEF  = ".gitignore";
197 const QString QGit::EXT_DIFF_DEF    = "kompare";
198 const QString QGit::EXT_EDITOR_DEF  = "emacs";
199 
200 // cache file
201 const QString QGit::BAK_EXT          = ".bak";
202 const QString QGit::C_DAT_FILE       = "/qgit_cache.dat";
203 
204 // misc
205 const QString QGit::QUOTE_CHAR = "$";
206 
207 
208 using namespace QGit;
209 
210 // settings helpers
flags(SCRef flagsVariable)211 uint QGit::flags(SCRef flagsVariable) {
212 
213 	QSettings settings;
214 	return settings.value(flagsVariable, FLAGS_DEF).toUInt();
215 }
216 
testFlag(uint f,SCRef flagsVariable)217 bool QGit::testFlag(uint f, SCRef flagsVariable) {
218 
219 	return (flags(flagsVariable) & f);
220 }
221 
setFlag(uint f,bool b,SCRef flagsVariable)222 void QGit::setFlag(uint f, bool b, SCRef flagsVariable) {
223 
224 	QSettings settings;
225 	uint flags = settings.value(flagsVariable, FLAGS_DEF).toUInt();
226 	flags = b ? flags | f : flags & ~f;
227 	settings.setValue(flagsVariable, flags);
228 }
229 
230 // tree view icons helpers
231 static QHash<QString, const QPixmap*> mimePixMap;
232 
initMimePix()233 void QGit::initMimePix() {
234 
235 	if (!mimePixMap.empty()) // only once
236 		return;
237 
238 	QPixmap* pm = new QPixmap(QString::fromUtf8(":/icons/resources/folder.png"));
239 	mimePixMap.insert("#folder_closed", pm);
240 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/folder_open.png"));
241 	mimePixMap.insert("#folder_open", pm);
242 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/misc.png"));
243 	mimePixMap.insert("#default", pm);
244 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_c.png"));
245 	mimePixMap.insert("c", pm);
246 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_cpp.png"));
247 	mimePixMap.insert("cpp", pm);
248 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_h.png"));
249 	mimePixMap.insert("h", pm);
250 	pm = new QPixmap(*pm);
251 	mimePixMap.insert("hpp", pm);
252 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/txt.png"));
253 	mimePixMap.insert("txt", pm);
254 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/shellscript.png"));
255 	mimePixMap.insert("sh", pm);
256 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_pl.png"));
257 	mimePixMap.insert("perl", pm);
258 	pm = new QPixmap(*pm);
259 	mimePixMap.insert("pl", pm);
260 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_py.png"));
261 	mimePixMap.insert("py", pm);
262 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_java.png"));
263 	mimePixMap.insert("java", pm);
264 	pm = new QPixmap(*pm);
265 	mimePixMap.insert("jar", pm);
266 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/tar.png"));
267 	mimePixMap.insert("tar", pm);
268 	pm = new QPixmap(*pm);
269 	mimePixMap.insert("gz", pm);
270 	pm = new QPixmap(*pm);
271 	mimePixMap.insert("tgz", pm);
272 	pm = new QPixmap(*pm);
273 	mimePixMap.insert("zip", pm);
274 	pm = new QPixmap(*pm);
275 	mimePixMap.insert("bz", pm);
276 	pm = new QPixmap(*pm);
277 	mimePixMap.insert("bz2", pm);
278 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/html.png"));
279 	mimePixMap.insert("html", pm);
280 	pm = new QPixmap(*pm);
281 	mimePixMap.insert("xml", pm);
282 	pm = new QPixmap(QString::fromUtf8(":/icons/resources/image.png"));
283 	mimePixMap.insert("bmp", pm);
284 	pm = new QPixmap(*pm);
285 	mimePixMap.insert("gif", pm);
286 	pm = new QPixmap(*pm);
287 	mimePixMap.insert("jpg", pm);
288 	pm = new QPixmap(*pm);
289 	mimePixMap.insert("jpeg", pm);
290 	pm = new QPixmap(*pm);
291 	mimePixMap.insert("png", pm);
292 	pm = new QPixmap(*pm);
293 	mimePixMap.insert("pbm", pm);
294 	pm = new QPixmap(*pm);
295 	mimePixMap.insert("pgm", pm);
296 	pm = new QPixmap(*pm);
297 	mimePixMap.insert("ppm", pm);
298 	pm = new QPixmap(*pm);
299 	mimePixMap.insert("svg", pm);
300 	pm = new QPixmap(*pm);
301 	mimePixMap.insert("tiff", pm);
302 	pm = new QPixmap(*pm);
303 	mimePixMap.insert("xbm", pm);
304 	pm = new QPixmap(*pm);
305 	mimePixMap.insert("xpm", pm);
306 }
307 
freeMimePix()308 void QGit::freeMimePix() {
309 
310 	qDeleteAll(mimePixMap);
311 }
312 
mimePix(SCRef fileName)313 const QPixmap* QGit::mimePix(SCRef fileName) {
314 
315 	SCRef ext = fileName.section('.', -1, -1).toLower();
316 	if (mimePixMap.contains(ext))
317 		return mimePixMap.value(ext);
318 
319 	return mimePixMap.value("#default");
320 }
321 
322 // geometry settings helers
saveGeometrySetting(SCRef name,QWidget * w,splitVect * svPtr)323 void QGit::saveGeometrySetting(SCRef name, QWidget* w, splitVect* svPtr) {
324 
325 	QSettings settings;
326 	if (w && w->isVisible())
327 		settings.setValue(name + "_window", w->saveGeometry());
328 
329 	if (!svPtr)
330 		return;
331 
332 	int cnt = 0;
333 	FOREACH (splitVect, it, *svPtr) {
334 
335 		cnt++;
336 		if ((*it)->sizes().contains(0))
337 			continue;
338 
339 		QString nm(name + "_splitter_" + QString::number(cnt));
340 		settings.setValue(nm, (*it)->saveState());
341 	}
342 }
343 
restoreGeometrySetting(SCRef name,QWidget * w,splitVect * svPtr)344 void QGit::restoreGeometrySetting(SCRef name, QWidget* w, splitVect* svPtr) {
345 
346 	QSettings settings;
347 	QString nm;
348 	if (w) {
349 		nm = name + "_window";
350 		QVariant v = settings.value(nm);
351 		if (v.isValid())
352 			w->restoreGeometry(v.toByteArray());
353 	}
354 	if (!svPtr)
355 		return;
356 
357 	int cnt = 0;
358 	FOREACH (splitVect, it, *svPtr) {
359 
360 		cnt++;
361 		nm = name + "_splitter_" + QString::number(cnt);
362 		QVariant v = settings.value(nm);
363 		if (!v.isValid())
364 			continue;
365 
366 		(*it)->restoreState(v.toByteArray());
367 	}
368 }
369 
370 // misc helpers
stripPartialParaghraps(const QByteArray & ba,QString * dst,QString * prev)371 bool QGit::stripPartialParaghraps(const QByteArray& ba, QString* dst, QString* prev) {
372 
373     QTextCodec* tc = QTextCodec::codecForLocale();
374 
375 	if (ba.endsWith('\n')) { // optimize common case
376         *dst = tc->toUnicode(ba);
377 
378 		// handle rare case of a '\0' inside content
379         while (dst->size() < ba.size() && ba.at(dst->size()) == '\0') {
380             QString s = tc->toUnicode(ba.mid(dst->size() + 1)); // sizes should match
381             dst->append(" ").append(s);
382         }
383 
384 		dst->truncate(dst->size() - 1); // strip trailing '\n'
385 		if (!prev->isEmpty()) {
386 			dst->prepend(*prev);
387 			prev->clear();
388 		}
389 		return true;
390 	}
391     QString src = tc->toUnicode(ba);
392 	// handle rare case of a '\0' inside content
393     while (src.size() < ba.size() && ba.at(src.size()) == '\0') {
394         QString s = tc->toUnicode(ba.mid(src.size() + 1));
395         src.append(" ").append(s);
396     }
397 
398 	int idx = src.lastIndexOf('\n');
399 	if (idx == -1) {
400 		prev->append(src);
401 		dst->clear();
402 		return false;
403 	}
404 	*dst = src.left(idx).prepend(*prev); // strip trailing '\n'
405 	*prev = src.mid(idx + 1); // src[idx] is '\n', skip it
406 	return true;
407 }
408 
writeToFile(SCRef fileName,SCRef data,bool setExecutable)409 bool QGit::writeToFile(SCRef fileName, SCRef data, bool setExecutable) {
410 
411 	QFile file(fileName);
412 	if (!file.open(QIODevice::WriteOnly)) {
413 		dbp("ERROR: unable to write file %1", fileName);
414 		return false;
415 	}
416 	QString data2(data);
417 	QTextStream stream(&file);
418 
419 #ifdef Q_OS_WIN32
420 	data2.replace("\r\n", "\n"); // change windows CRLF to linux
421 	data2.replace("\n", "\r\n"); // then change all linux CRLF to windows
422 #endif
423 	stream << data2;
424 	file.close();
425 
426 #ifndef Q_OS_WIN32
427 	if (setExecutable)
428 		chmod(fileName.toLatin1().constData(), 0755);
429 #endif
430 	return true;
431 }
432 
writeToFile(SCRef fileName,const QByteArray & data,bool setExecutable)433 bool QGit::writeToFile(SCRef fileName, const QByteArray& data, bool setExecutable) {
434 
435 	QFile file(fileName);
436 	if (!file.open(QIODevice::WriteOnly)) {
437 		dbp("ERROR: unable to write file %1", fileName);
438 		return false;
439 	}
440 	QDataStream stream(&file);
441 	stream.writeRawData(data.constData(), data.size());
442 	file.close();
443 
444 #ifndef Q_OS_WIN32
445 	if (setExecutable)
446 		chmod(fileName.toLatin1().constData(), 0755);
447 #endif
448 	return true;
449 }
450 
readFromFile(SCRef fileName,QString & data)451 bool QGit::readFromFile(SCRef fileName, QString& data) {
452 
453 	data = "";
454 	QFile file(fileName);
455 	if (!file.open(QIODevice::ReadOnly)) {
456 		dbp("ERROR: unable to read file %1", fileName);
457 		return false;
458 	}
459 	QTextStream stream(&file);
460 	data = stream.readAll();
461 	file.close();
462 	return true;
463 }
464 
startProcess(QProcess * proc,SCList args,SCRef buf,bool * winShell)465 bool QGit::startProcess(QProcess* proc, SCList args, SCRef buf, bool* winShell) {
466 
467 	if (!proc || args.isEmpty())
468 		return false;
469 
470 	QStringList arguments(args);
471 	adjustPath(arguments, winShell);
472 
473 	QString prog(arguments.first());
474 	arguments.removeFirst();
475 	if (!buf.isEmpty()) {
476 	/*
477 	   On Windows buffer size of QProcess's standard input
478 	   pipe is quite limited and a crash can occur in case
479 	   a big chunk of data is written to process stdin.
480 	   As a workaround we use a temporary file to store data.
481 	   Process stdin will be redirected to this file
482 	*/
483 		QTemporaryFile* bufFile = new QTemporaryFile(proc);
484 		bufFile->open();
485 		QTextStream stream(bufFile);
486 		stream << buf;
487 		proc->setStandardInputFile(bufFile->fileName());
488 		bufFile->close();
489 	}
490 	QStringList env = QProcess::systemEnvironment();
491 	env << "GIT_TRACE=0"; // avoid choking on debug traces
492 	env << "GIT_FLUSH=0"; // skip the fflush() in 'git log'
493 	proc->setEnvironment(env);
494 
495 	proc->start(prog, arguments); // TODO test QIODevice::Unbuffered
496 	return proc->waitForStarted();
497 }
498