Page MenuHomePhabricator (Chris)

No OneTemporary

Size
162 KB
Referenced Files
None
Subscribers
None
diff --git a/src/CommitExploreWindow.cpp b/src/CommitExploreWindow.cpp
index 1b1ef95..9b76a4e 100644
--- a/src/CommitExploreWindow.cpp
+++ b/src/CommitExploreWindow.cpp
@@ -1,279 +1,279 @@
#include "CommitExploreWindow.h"
#include "ui_CommitExploreWindow.h"
#include "GitObjectManager.h"
#include "ImageViewWidget.h"
#include "MainWindow.h"
#include "common/misc.h"
#include "main.h"
#include "platform.h"
#include <QFileIconProvider>
#include <QMenu>
#include <memory>
static QTreeWidgetItem *newQTreeWidgetItem()
{
auto *item = new QTreeWidgetItem;
item->setSizeHint(0, QSize(20, 20));
return item;
}
enum {
ItemTypeRole = Qt::UserRole,
ObjectIdRole,
FilePathRole,
};
struct CommitExploreWindow::Private {
BasicMainWindow *mainwindow;
GitObjectCache *objcache;
Git::CommitItem const *commit;
QString root_tree_id;
GitTreeItemList tree_item_list;
Git::Object content_object;
ObjectContent content;
TextEditorEnginePtr text_editor_engine;
};
CommitExploreWindow::CommitExploreWindow(QWidget *parent, BasicMainWindow *mainwin, GitObjectCache *objcache, Git::CommitItem const *commit)
: QDialog(parent)
, ui(new Ui::CommitExploreWindow)
, m(new Private)
{
ui->setupUi(this);
Qt::WindowFlags flags = windowFlags();
flags &= ~Qt::WindowContextHelpButtonHint;
flags |= Qt::WindowMaximizeButtonHint;
setWindowFlags(flags);
m->mainwindow = mainwin;
m->objcache = objcache;
m->commit = commit;
m->text_editor_engine = std::make_shared<TextEditorEngine>();
ui->widget_fileview->bind(mainwin, nullptr, ui->verticalScrollBar, ui->horizontalScrollBar, mainwin->themeForTextEditor());
ui->widget_fileview->setDiffMode(m->text_editor_engine, ui->verticalScrollBar, ui->horizontalScrollBar);
ui->splitter->setSizes({100, 100, 200});
// set text
ui->lineEdit_commit_id->setText(commit->commit_id);
ui->lineEdit_date->setText(misc::makeDateTimeString(commit->commit_date));
ui->lineEdit_author->setText(commit->author);
{
GitCommit c;
- c.parseCommit(objcache, m->commit->commit_id);
+ GitCommit::parseCommit(objcache, m->commit->commit_id, &c);
m->root_tree_id = c.tree_id;
}
{
GitCommitTree tree(objcache);
tree.parseTree(m->root_tree_id);
}
{
QTreeWidgetItem *rootitem = newQTreeWidgetItem();
rootitem->setText(0, tr("Commit"));
rootitem->setData(0, ItemTypeRole, (int)GitTreeItem::TREE);
rootitem->setData(0, ObjectIdRole, m->root_tree_id);
ui->treeWidget->addTopLevelItem(rootitem);
loadTree(m->root_tree_id);
rootitem->setExpanded(true);
}
}
CommitExploreWindow::~CommitExploreWindow()
{
delete m;
delete ui;
}
BasicMainWindow *CommitExploreWindow::mainwindow()
{
return m->mainwindow;
}
void CommitExploreWindow::clearContent()
{
m->content = ObjectContent();
}
void CommitExploreWindow::expandTreeItem_(QTreeWidgetItem *item)
{
if (item->childCount() == 1) {
if (item->child(0)->text(0).isEmpty()) {
delete item->takeChild(0);
}
}
if (item->childCount() == 0) {
QFileIconProvider icons;
QString path = item->data(0, FilePathRole).toString();
QString tree_id = item->data(0, ObjectIdRole).toString();
loadTree(tree_id);
for (GitTreeItem const &ti : m->tree_item_list) {
if (ti.type == GitTreeItem::TREE) {
QTreeWidgetItem *child = newQTreeWidgetItem();
child->setIcon(0, icons.icon(QFileIconProvider::Folder));
child->setText(0, ti.name);
child->setData(0, ItemTypeRole, (int)ti.type);
child->setData(0, ObjectIdRole, ti.id);
child->setData(0, FilePathRole, misc::joinWithSlash(path, ti.name));
QTreeWidgetItem *placeholder = newQTreeWidgetItem();
child->addChild(placeholder);
item->addChild(child);
}
}
}
}
void CommitExploreWindow::on_treeWidget_itemExpanded(QTreeWidgetItem *item)
{
expandTreeItem_(item);
}
void CommitExploreWindow::loadTree(QString const &tree_id)
{
GitCommitTree tree(m->objcache);
tree.parseTree(tree_id);
m->tree_item_list = *tree.treelist();
std::sort(m->tree_item_list.begin(), m->tree_item_list.end(), [](GitTreeItem const &left, GitTreeItem const &right){
int l = (left.type == GitTreeItem::TREE) ? 0 : 1;
int r = (right.type == GitTreeItem::TREE) ? 0 : 1;
if (l != r) return l < r;
return left.name.compare(right.name, Qt::CaseInsensitive) < 0;
});
}
void CommitExploreWindow::doTreeItemChanged_(QTreeWidgetItem *current)
{
ui->listWidget->clear();
QString path = current->data(0, FilePathRole).toString();
QString tree_id = current->data(0, ObjectIdRole).toString();
loadTree(tree_id);
QFileIconProvider icons;
for (GitTreeItem const &ti : m->tree_item_list) {
QIcon icon;
if (ti.type == GitTreeItem::TREE) {
icon = icons.icon(QFileIconProvider::Folder);
} else {
#ifdef Q_OS_WIN
{
int i = ti.name.lastIndexOf('.');
if (i > 0) {
QString ext = ti.name.mid(i + 1);
icon = winIconFromExtensionLarge(ext);
}
}
#endif
if (icon.isNull()) {
icon = icons.icon(QFileIconProvider::File);
}
}
auto *p = new QListWidgetItem;
p->setIcon(icon);
p->setText(ti.name);
p->setData(ItemTypeRole, (int)ti.type);
p->setData(ObjectIdRole, ti.id);
p->setData(FilePathRole, misc::joinWithSlash(path, ti.name));
ui->listWidget->addItem(p);
}
}
void CommitExploreWindow::on_treeWidget_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem * /*previous*/)
{
clearContent();
doTreeItemChanged_(current);
}
void CommitExploreWindow::on_listWidget_itemDoubleClicked(QListWidgetItem *item)
{
Q_ASSERT(item);
GitTreeItem::Type type = (GitTreeItem::Type)item->data(ItemTypeRole).toInt();
if (type == GitTreeItem::TREE) {
QString tree_id = item->data(ObjectIdRole).toString();
clearContent();
QTreeWidgetItem *parent = ui->treeWidget->currentItem();
expandTreeItem_(parent);
parent->setExpanded(true);
int n = parent->childCount();
for (int i = 0; i < n; i++) {
QTreeWidgetItem *child = parent->child(i);
if (child->data(0, ItemTypeRole).toInt() == GitTreeItem::TREE) {
QString tid = child->data(0, ObjectIdRole).toString();
if (tid == tree_id) {
child->setExpanded(true);
ui->treeWidget->setCurrentItem(child);
break;
}
}
}
}
}
void CommitExploreWindow::on_listWidget_currentItemChanged(QListWidgetItem *current, QListWidgetItem * /*previous*/)
{
if (!current) return;
GitTreeItem::Type type = (GitTreeItem::Type)current->data(ItemTypeRole).toInt();
if (type == GitTreeItem::BLOB) {
QString id = current->data(ObjectIdRole).toString();
m->content_object = m->objcache->catFile(id);
QString path = current->data(FilePathRole).toString();
clearContent();
QString mimetype = mainwindow()->determinFileType(m->content_object.content, true);
if (misc::isImage(mimetype)) {
ui->widget_fileview->setImage(mimetype, m->content_object.content, id, path);
} else {
ui->widget_fileview->setText(m->content_object.content, mainwindow(), id, path);
}
} else {
clearContent();
}
}
void CommitExploreWindow::on_verticalScrollBar_valueChanged(int)
{
ui->widget_fileview->refrectScrollBar();
}
void CommitExploreWindow::on_horizontalScrollBar_valueChanged(int)
{
ui->widget_fileview->refrectScrollBar();
}
void CommitExploreWindow::on_listWidget_customContextMenuRequested(const QPoint &pos)
{
(void)pos;
QListWidgetItem *current = ui->listWidget->currentItem();
if (!current) return;
GitTreeItem::Type type = (GitTreeItem::Type)current->data(ItemTypeRole).toInt();
if (type == GitTreeItem::BLOB) {
QMenu menu;
QAction *a_history = menu.addAction("History");
QAction *a = menu.exec(QCursor::pos() + QPoint(8, -8));
if (a) {
if (a == a_history) {
QString path = current->data(FilePathRole).toString();
mainwindow()->execFileHistory(path);
return;
}
}
}
}
diff --git a/src/Git.cpp b/src/Git.cpp
index e1695be..b76fcce 100644
--- a/src/Git.cpp
+++ b/src/Git.cpp
@@ -1,1569 +1,1612 @@
#include "Git.h"
#include "GitObjectManager.h"
#include "MyProcess.h"
#include "common/joinpath.h"
#include "common/misc.h"
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QProcess>
#include <QThread>
#include <QTimer>
#include <set>
#define DEBUGLOG 0
using callback_t = Git::callback_t;
struct Git::Private {
QString git_command;
QString ssh_command;// = "C:/Program Files/Git/usr/bin/ssh.exe";
QString ssh_key_override;// = "C:/a/id_rsa";
std::vector<char> result;
QString error_message;
int process_exit_code = 0;
QString working_repo_dir;
callback_t fn_log_writer_callback = nullptr;
void *callback_cookie = nullptr;
};
Git::Git()
: m(new Private)
{
}
Git::Git(const Context &cx, QString const &repodir, const QString &sshkey)
: m(new Private)
{
setGitCommand(cx.git_command, cx.ssh_command);
setWorkingRepositoryDir(repodir, sshkey);
}
Git::~Git()
{
delete m;
}
void Git::setLogCallback(callback_t func, void *cookie)
{
m->fn_log_writer_callback = func;
m->callback_cookie = cookie;
}
void Git::setWorkingRepositoryDir(QString const &repo, QString const &sshkey)
{
m->working_repo_dir = repo;
m->ssh_key_override = sshkey;
}
QString const &Git::workingRepositoryDir() const
{
return m->working_repo_dir;
}
QString const &Git::sshKey() const
{
return m->ssh_key_override;
}
void Git::setSshKey(QString const &sshkey) const
{
m->ssh_key_override = sshkey;
}
bool Git::isValidID(QString const &id)
{
int zero = 0;
int n = id.size();
if (n >= 4 && n <= GIT_ID_LENGTH) {
ushort const *p = id.utf16();
for (int i = 0; i < n; i++) {
uchar c = p[i];
if (c == '0') {
zero++;
} else if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
// ok
} else {
return false;
}
}
if (zero == GIT_ID_LENGTH) { // 全部 0 の時は false を返す
return false;
}
return true; // ok
}
return false;
}
QString Git::status()
{
git("status");
return resultText();
}
QByteArray Git::toQByteArray() const
{
if (m->result.empty()) return QByteArray();
return QByteArray(&m->result[0], m->result.size());
}
QString Git::resultText() const
{
return QString::fromUtf8(toQByteArray());
}
void Git::setGitCommand(QString const &gitcmd, QString const &sshcmd)
{
m->git_command = gitcmd;
m->ssh_command = sshcmd;
}
QString Git::gitCommand() const
{
Q_ASSERT(m);
return m->git_command;
}
void Git::clearResult()
{
m->result.clear();
m->process_exit_code = 0;
m->error_message = QString();
}
QString Git::errorMessage() const
{
return m->error_message;
}
int Git::getProcessExitCode() const
{
return m->process_exit_code;
}
bool Git::chdirexec(std::function<bool()> const &fn)
{
bool ok = false;
QString cwd = QDir::currentPath();
QString dir = workingRepositoryDir();
if (QDir::setCurrent(dir)) {
ok = fn();
QDir::setCurrent(cwd);
}
return ok;
}
bool Git::git(QString const &arg, bool chdir, bool errout, AbstractPtyProcess *pty)
{
QFileInfo info(gitCommand());
if (!info.isExecutable()) {
qDebug() << "Invalid git command: " << gitCommand();
return false;
}
#if DEBUGLOG
qDebug() << "exec: git " << arg;
QTime timer;
timer.start();
#endif
clearResult();
QString env;
if (m->ssh_command.isEmpty() || m->ssh_key_override.isEmpty()) {
// nop
} else {
if (m->ssh_command.indexOf('\"') >= 0) return false;
if (m->ssh_key_override.indexOf('\"') >= 0) return false;
if (!QFileInfo(m->ssh_command).isExecutable()) return false;
env = QString("GIT_SSH_COMMAND=\"%1\" -i \"%2\" ").arg(m->ssh_command).arg(m->ssh_key_override);
}
auto DoIt = [&](){
QString cmd = QString("\"%1\" --no-pager ").arg(gitCommand());
cmd += arg;
if (m->fn_log_writer_callback) {
QByteArray ba;
ba.append("> git ");
ba.append(arg);
ba.append('\n');
m->fn_log_writer_callback(m->callback_cookie, ba.data(), (int)ba.size());
}
if (pty) {
pty->start(cmd, env, pty->userVariant());
m->process_exit_code = 0; // バックグラウンドで実行を継続するけど、とりあえず成功したことにしておく
} else {
Process proc;
proc.start(cmd, false);
m->process_exit_code = proc.wait();
if (errout) {
m->result = proc.errbytes;
} else {
m->result = proc.outbytes;
}
m->error_message = proc.errstring();
}
return m->process_exit_code == 0;
};
bool ok = false;
if (chdir) {
if (pty) {
pty->setChangeDir(workingRepositoryDir());
ok = DoIt();
} else {
ok = chdirexec(DoIt);
}
} else {
ok = DoIt();
}
#if DEBUGLOG
qDebug() << timer.elapsed() << "ms";
#endif
return ok;
}
GitPtr Git::dup() const
{
Git *p = new Git();
p->m->git_command = m->git_command;
p->m->working_repo_dir = m->working_repo_dir;
p->m->fn_log_writer_callback = m->fn_log_writer_callback;
p->m->callback_cookie = m->callback_cookie;
return GitPtr(p);
}
bool Git::isValidWorkingCopy(QString const &dir)
{
return QFileInfo(dir).isDir() && QDir(dir / ".git").exists();
}
bool Git::isValidWorkingCopy() const
{
return isValidWorkingCopy(workingRepositoryDir());
}
QString Git::version()
{
git("--version", false);
return resultText().trimmed();
}
bool Git::init()
{
bool ok = false;
QDir cwd = QDir::current();
QString dir = workingRepositoryDir();
if (QDir::setCurrent(dir)) {
QString gitdir = dir / ".git";
if (!QFileInfo(gitdir).isDir()) {
git("init", false);
if (QFileInfo(gitdir).isDir()) {
ok = true;
}
}
QDir::setCurrent(cwd.path());
}
return ok;
}
QString Git::rev_parse(QString const &name)
{
QString cmd = "rev-parse %1";
cmd = cmd.arg(name);
if (git(cmd)) {
return resultText().trimmed();
}
return QString();
}
QList<Git::Tag> Git::tags()
{
#if 0
auto MidCmp = [](QString const &line, int i, char const *ptr){
ushort const *p = line.utf16();
for (int j = 0; ptr[j]; j++) {
if (p[i + j] != ptr[j]) return false;
}
return true;
};
QList<Git::Tag> list;
QStringList lines = refs();
for (QString const &line : lines) {
Tag tag;
if (line.isEmpty()) continue;
int i = line.indexOf(' ');
if (i == GIT_ID_LENGTH) {
tag.id = line.mid(0, i);
if (MidCmp(line, i, " refs/tags/")) {
tag.name = line.mid(i + 11).trimmed();
if (tag.name.isEmpty()) continue;
list.push_back(tag);
}
}
}
return list;
#else
return tags2();
#endif
}
QList<Git::Tag> Git::tags2()
{
QList<Tag> list;
git("show-ref");
QStringList lines = misc::splitLines(resultText());
for (QString const &line : lines) {
QStringList l = misc::splitWords(line);
if (l.size() >= 2) {
if (isValidID(l[0]) && l[1].startsWith("refs/tags/")) {
Tag t;
t.name = l[1].mid(10);
t.id = l[0];
list.push_back(t);
}
}
}
return list;
}
bool Git::tag(QString const &name, QString const &id)
{
QString cmd = "tag \"%1\" %2";
cmd = cmd.arg(name).arg(id);
return git(cmd);
}
void Git::delete_tag(QString const &name, bool remote)
{
QString cmd = "tag --delete \"%1\"";
cmd = cmd.arg(name);
git(cmd);
if (remote) {
QString cmd = "push --delete origin \"%1\"";
cmd = cmd.arg(name);
git(cmd);
}
}
QString Git::diff(QString const &old_id, QString const &new_id)
{
QString cmd = "diff --full-index -a %1 %2";
cmd = cmd.arg(old_id).arg(new_id);
git(cmd);
return resultText();
}
QString Git::diff_file(QString const &old_path, QString const &new_path)
{
QString cmd = "diff --full-index -a -- %1 %2";
cmd = cmd.arg(old_path).arg(new_path);
git(cmd);
return resultText();
}
QList<Git::DiffRaw> Git::diff_raw(QString const &old_id, QString const &new_id)
{
QList<DiffRaw> list;
QString cmd = "diff --raw --abbrev=%1 %2 %3";
cmd = cmd.arg(GIT_ID_LENGTH).arg(old_id).arg(new_id);
git(cmd);
QString text = resultText();
QStringList lines = text.split('\n', QString::SkipEmptyParts);
for (QString const &line : lines) { // raw format: e.g. ":100644 100644 08bc10d... 18f0501... M src/MainWindow.cpp"
DiffRaw item;
int colon = line.indexOf(':');
int tab = line.indexOf('\t');
if (colon >= 0 && colon < tab) {
QStringList header = line.mid(colon + 1, tab - colon - 1).split(' ', QString::SkipEmptyParts); // コロンとタブの間の文字列を空白で分割
if (header.size() >= 5) {
QStringList files = line.mid(tab + 1).split('\t', QString::SkipEmptyParts); // タブより後ろはファイルパス
if (!files.empty()) {
for (QString &file : files) {
file = Git::trimPath(file);
}
item.a.id = header[2];
item.b.id = header[3];
item.a.mode = header[0];
item.b.mode = header[1];
item.state = header[4];
item.files = files;
list.push_back(item);
}
}
}
}
return list;
}
QString Git::diff_to_file(QString const &old_id, QString const &path)
{
#if 1
QString cmd = "diff --full-index -a %1 -- \"%2\"";
cmd = cmd.arg(old_id).arg(path);
git(cmd);
return resultText();
#else
return diff_(old_id, new_id);
#endif
}
QString Git::getCurrentBranchName()
{
if (isValidWorkingCopy()) {
git("rev-parse --abbrev-ref HEAD");
QString s = resultText().trimmed();
if (!s.isEmpty() && !s.startsWith("fatal:") && s.indexOf(' ') < 0) {
return s;
}
}
return QString();
}
QStringList Git::getUntrackedFiles()
{
QStringList files;
Git::FileStatusList stats = status_s();
for (FileStatus const &s : stats) {
if (s.code() == FileStatusCode::Untracked) {
files.push_back(s.path1());
}
}
return files;
}
void Git::parseAheadBehind(QString const &s, Branch *b)
{
ushort const *begin = s.utf16();
ushort const *end = begin + s.size();
ushort const *ptr = begin;
auto NCompare = [](ushort const *a, char const *b, size_t n){
for (size_t i = 0; i < n; i++) {
if (*a < (unsigned char)*b) return false;
if (*a > (unsigned char)*b) return false;
if (!*a) break;
a++;
b++;
}
return true;
};
auto SkipSpace = [&](){
while (ptr < end && QChar(*ptr).isSpace()) {
ptr++;
}
};
auto GetNumber = [&](){
int n = 0;
while (ptr < end && QChar(*ptr).isDigit()) {
n = n * 10 + (*ptr - '0');
ptr++;
}
return n;
};
if (*ptr == '[') {
ptr++;
ushort const *e = nullptr;
int n;
for (n = 0; ptr + n < end; n++) {
if (ptr[n] == ']') {
e = ptr + n;
break;
}
}
if (e) {
end = e;
while (1) {
if (NCompare(ptr, "ahead ", 6)) {
ptr += 6;
SkipSpace();
b->ahead = GetNumber();
} else if (NCompare(ptr, "behind ", 7)) {
ptr += 7;
SkipSpace();
b->behind = GetNumber();
} else {
ushort c = 0;
if (ptr < end) {
c = *ptr;
}
if (c == 0) break;
ptr++;
}
}
}
}
}
QList<Git::Branch> Git::branches()
{
QList<Branch> branches;
git(QString("branch -vv -a --abbrev=%1").arg(GIT_ID_LENGTH));
QString s = resultText();
#if DEBUGLOG
qDebug() << s;
#endif
QStringList lines = misc::splitLines(s);
for (QString const &line : lines) {
if (line.size() > 2) {
QString name;
QString text = line.mid(2);
int pos = 0;
if (text.startsWith('(')) {
int i, paren = 1;
for (i = 1; i < text.size(); i++) {
if (text[i] == '(') {
paren++;
} else if (text[i] == ')') {
if (paren > 0) {
paren--;
if (paren == 0) {
pos = i;
break;
}
}
}
}
// pos = text.indexOf(')');
if (pos > 1) {
name = text.mid(1, pos - 1);
pos++;
}
} else {
while (pos < text.size() && !QChar::isSpace(text.utf16()[pos])) {
pos++;
}
name = text.mid(0, pos);
}
if (!name.isEmpty()) {
while (pos < text.size() && QChar::isSpace(text.utf16()[pos])) {
pos++;
}
text = text.mid(pos);
Branch b;
if (name.startsWith("HEAD detached at ")) {
b.flags |= Branch::HeadDetachedAt;
name = name.mid(17);
}
if (name.startsWith("HEAD detached from ")) {
b.flags |= Branch::HeadDetachedFrom;
name = name.mid(19);
}
if (name.startsWith("remotes/")) {
name = name.mid(8);
int i = name.indexOf('/');
if (i > 0) {
b.remote = name.mid(0, i);
name = name.mid(i + 1);
}
}
b.name = name;
if (text.startsWith("-> ")) {
b.id = ">" + text.mid(3);
} else {
int i = text.indexOf(' ');
if (i == GIT_ID_LENGTH) {
b.id = text.mid(0, GIT_ID_LENGTH);
}
while (i < text.size() && QChar::isSpace(text.utf16()[i])) {
i++;
}
text = text.mid(i);
parseAheadBehind(text, &b);
}
if (line.startsWith('*')) {
b.flags |= Branch::Current;
}
branches.push_back(b);
}
}
}
for (int i = 0; i < branches.size(); i++) {
Branch *b = &branches[i];
if (b->id.startsWith('>')) {
QString name = b->id.mid(1);
for (int j = 0; j < branches.size(); j++) {
if (j != i) {
if (branches[j].name == name) {
branches[i].id = branches[j].id;
break;
}
}
}
}
}
return branches;
}
Git::CommitItemList Git::log_all(QString const &id, int maxcount)
{
CommitItemList items;
QString text;
QString cmd = "log --pretty=format:\"commit:%H#gpg:%G?#key:%GK#parent:%P#author:%an#mail:%ae#date:%ci##%s\" --all -%1 %2";
cmd = cmd.arg(maxcount).arg(id);
git(cmd);
if (getProcessExitCode() == 0) {
text = resultText().trimmed();
QStringList lines = misc::splitLines(text);
for (QString const &line : lines) {
int i = line.indexOf("##");
if (i > 0) {
Git::CommitItem item;
QString signed_key;
item.message = line.mid(i + 2);
QStringList atts = line.mid(0, i).split('#');
for (QString const &s : atts) {
int j = s.indexOf(':');
if (j > 0) {
QString key = s.mid(0, j);
QString val = s.mid(j + 1);
if (key == "commit") {
item.commit_id = val;
} else if (key == "gpg") {
item.signature = *val.utf16();
} else if (key == "key") {
signed_key = val;
} else if (key == "parent") {
item.parent_ids = val.split(' ', QString::SkipEmptyParts);
} else if (key == "author") {
item.author = val;
} else if (key == "mail") {
item.email = val;
} else if (key == "date") {
item.commit_date = QDateTime::fromString(val, Qt::ISODate).toLocalTime();
} else if (key == "debug") {
}
}
}
if (!signed_key.isEmpty()) {
ushort const *begin = signed_key.utf16();
int n = signed_key.size();
for (int i = 0; i + 1 < n; i += 2) {
ushort c = begin[i];
ushort d = begin[i + 1];
if (c < 0x80 && isxdigit(c) && isxdigit(d)) {
char tmp[3];
tmp[0] = c;
tmp[1] = d;
tmp[2] = 0;
int v = strtol(tmp, nullptr, 16);
item.fingerprint.push_back(v);
} else {
break;
}
}
}
items.push_back(item);
}
}
}
return items;
}
Git::CommitItemList Git::log(int maxcount)
{
return log_all(QString(), maxcount);
}
bool Git::queryCommit(QString const &id, CommitItem *out)
{
if (objectType(id) == "commit") {
out->commit_id = id;
QByteArray ba;
if (cat_file(id, &ba)) {
QStringList lines = misc::splitLines(ba, [](char const *p, size_t n){
return QString::fromUtf8(p, (int)n);
});
while (!lines.empty() && lines[lines.size() - 1].isEmpty()) {
lines.pop_back();
}
int i;
for (i = 0; i < lines.size(); i++) {
QString const &line = lines[i];
if (line.isEmpty()) {
i++;
for (; i < lines.size(); i++) {
QString const &line = lines[i];
if (!out->message.isEmpty()) {
out->message.append('\n');
}
out->message.append(line);
}
break;
}
if (line.startsWith("parent ")) {
out->parent_ids.push_back(line.mid(7));
} else if (line.startsWith("author ")) {
QStringList arr = misc::splitWords(line);
int n = arr.size();
if (n > 4) {
n -= 2;
out->commit_date = QDateTime::fromTime_t(atol(arr[n].toStdString().c_str()));
n--;
out->email = arr[n];
if (out->email.startsWith('<') && out->email.endsWith('>')) {
int n = out->email.size();
out->email = out->email.mid(1, n - 2);
}
for (int i = 1; i < n; i++) {
if (!out->author.isEmpty()) {
out->author += ' ';
}
out->author += arr[i];
}
}
}
}
}
return true;
}
return false;
}
Git::CloneData Git::preclone(QString const &url, QString const &path)
{
CloneData d;
d.url = url;
if (path.endsWith('/') || path.endsWith('\\')) {
auto GitBaseName = [](QString location){
int i = location.lastIndexOf('/');
int j = location.lastIndexOf('\\');
if (i < j) i = j;
i = i < 0 ? 0 : (i + 1);
j = location.size();
if (location.endsWith(".git")) {
j -= 4;
}
if (i < j) {
location = location.mid(i, j - i);
}
return location;
};
d.basedir = path;
d.subdir = GitBaseName(url);
} else {
QFileInfo info(path);
d.basedir = info.dir().path();
d.subdir = info.fileName();
}
return d;
}
bool Git::clone(CloneData const &data, AbstractPtyProcess *pty)
{
QString clone_to = data.basedir / data.subdir;
m->working_repo_dir = misc::normalizePathSeparator(clone_to);
bool ok = false;
QDir cwd = QDir::current();
auto DoIt = [&](){
QString cmd = "clone --progress \"%1\" \"%2\"";
cmd = cmd.arg(data.url).arg(data.subdir);
ok = git(cmd, false, true, pty);
};
if (pty) {
pty->setChangeDir(data.basedir);
DoIt();
} else {
if (QDir::setCurrent(data.basedir)) {
DoIt();
QDir::setCurrent(cwd.path());
}
}
return ok;
}
std::vector<Git::Submodule> Git::submodules()
{
std::vector<Git::Submodule> mods;
git("submodule");
QString text = resultText();
ushort c = text.utf16()[0];
if (c == ' ' || c == '-') {
text = text.mid(1);
}
QStringList words = misc::splitWords(text);
if (words.size() >= 2) {
Submodule sm;
sm.id = words[0];
sm.path = words[1];
if (isValidID(sm.id)) {
if (words.size() >= 3) {
sm.refs = words[2];
if (sm.refs.startsWith('(') && sm.refs.endsWith(')')) {
sm.refs = sm.refs.mid(1, sm.refs.size() - 2);
}
}
+
mods.push_back(sm);
}
}
return mods;
}
bool Git::submodule_add(const CloneData &data, bool force, AbstractPtyProcess *pty)
{
bool ok = false;
QString cmd = "submodule add";
if (force) {
cmd += " -f";
}
cmd += " \"%1\" \"%2\"";
cmd = cmd.arg(data.url).arg(data.subdir);
ok = git(cmd, true, true, pty);
return ok;
}
bool Git::submodule_update(SubmoduleUpdateData const &data, AbstractPtyProcess *pty)
{
bool ok = false;
QString cmd = "submodule update";
if (data.init) {
cmd += " --init";
}
if (data.recursive) {
cmd += " --recursive";
}
ok = git(cmd, true, true, pty);
return ok;
}
QString Git::encodeQuotedText(QString const &str)
{
std::vector<ushort> vec;
ushort const *begin = str.utf16();
ushort const *end = begin + str.size();
ushort const *ptr = begin;
vec.push_back('\"');
while (ptr < end) {
ushort c = *ptr;
ptr++;
if (c == '\"') { // triple quotes
vec.push_back(c);
vec.push_back(c);
vec.push_back(c);
} else {
vec.push_back(c);
}
}
vec.push_back('\"');
return QString::fromUtf16(&vec[0], vec.size());
}
bool Git::commit_(QString const &msg, bool amend, bool sign, AbstractPtyProcess *pty)
{
QString cmd = "commit";
if (amend) {
cmd += " --amend";
}
if (sign) {
cmd += " -S";
}
QString text = msg.trimmed();
if (text.isEmpty()) {
text = "no message";
}
text = encodeQuotedText(text);
cmd += QString(" -m %1").arg(text);
return git(cmd, true, false, pty);
}
bool Git::commit(QString const &text, bool sign, AbstractPtyProcess *pty)
{
return commit_(text, false, sign, pty);
}
bool Git::commit_amend_m(QString const &text, bool sign, AbstractPtyProcess *pty)
{
return commit_(text, true, sign, pty);
}
bool Git::revert(QString const &id)
{
QString cmd = "revert %1";
cmd = cmd.arg(id);
return git(cmd);
}
void Git::push_u(QString const &remote, QString const &branch, AbstractPtyProcess *pty)
{
if (remote.indexOf('\"') >= 0 || branch.indexOf('\"') >= 0) {
return;
}
QString cmd = "push -u \"%1\" \"%2\"";
git(cmd.arg(remote).arg(branch), true, false, pty);
}
bool Git::push_(bool tags, AbstractPtyProcess *pty)
{
QString cmd = "push";
if (tags) {
cmd += " --tags";
}
return git(cmd, true, false, pty);
}
bool Git::push(bool tags, AbstractPtyProcess *pty)
{
return push_(tags, pty);
}
Git::FileStatusList Git::status_s_()
{
FileStatusList files;
if (git("status -s -u --porcelain")) {
QString text = resultText();
QStringList lines = misc::splitLines(text);
for (QString const &line : lines) {
if (line.size() > 3) {
FileStatus s(line);
if (s.code() != FileStatusCode::Unknown) {
files.push_back(s);
}
}
}
}
return files;
}
Git::FileStatusList Git::status_s()
{
return status_s_();
}
QString Git::objectType(QString const &id)
{
git("cat-file -t " + id);
return resultText().trimmed();
}
QByteArray Git::cat_file_(QString const &id)
{
git("cat-file -p " + id);
return toQByteArray();
}
bool Git::cat_file(QString const &id, QByteArray *out)
{
if (isValidID(id)) {
*out = cat_file_(id);
return true;
}
return false;
}
void Git::resetFile(QString const &path)
{
git("checkout -- " + path);
}
void Git::resetAllFiles()
{
git("reset --hard HEAD");
}
void Git::removeFile(QString const &path)
{
git("rm " + path);
}
void Git::stage(QString const &path)
{
git("add " + path);
}
void Git::stage(QStringList const &paths)
{
QString cmd = "add";
for (QString const &path : paths) {
cmd += ' ';
cmd += '\"';
cmd += path;
cmd += '\"';
}
git(cmd);
}
void Git::unstage(QString const &path)
{
QString cmd = "reset HEAD \"%1\"";
git(cmd.arg(path));
}
void Git::unstage(QStringList const &paths)
{
QString cmd = "reset HEAD";
for (QString const &path : paths) {
cmd += " \"";
cmd += path;
cmd += '\"';
}
git(cmd);
}
void Git::pull(AbstractPtyProcess *pty)
{
git("pull", true, false, pty);
}
void Git::fetch(AbstractPtyProcess *pty, bool prune)
{
QString cmd = "fetch --tags -f";
if (prune) {
cmd += " --prune";
}
git(cmd, true, false, pty);
}
void Git::fetch_tags_f(AbstractPtyProcess *pty)
{
QString cmd = "fetch --tags -f";
git(cmd, true, false, pty);
}
QStringList Git::make_branch_list_()
{
QStringList list;
QStringList l = misc::splitLines(resultText());
for (QString const &s : l) {
if (s.startsWith("* ")) list.push_back(s.mid(2));
if (s.startsWith(" ")) list.push_back(s.mid(2));
}
return list;
}
void Git::createBranch(QString const &name)
{
git("branch " + name);
}
void Git::checkoutBranch(QString const &name)
{
git("checkout " + name);
}
void Git::cherrypick(QString const &name)
{
git("cherry-pick " + name);
}
QString Git::getCherryPicking() const
{
QString dir = workingRepositoryDir();
QString path = dir / ".git/CHERRY_PICK_HEAD";
QFile file(path);
if (file.open(QFile::ReadOnly)) {
QString line = QString::fromLatin1(file.readLine()).trimmed();
if (isValidID(line)) {
return line;
}
}
return QString();
}
QString Git::getMessage(QString const &id)
{
git("show --no-patch --pretty=%s " + id);
return resultText().trimmed();
}
void Git::mergeBranch(QString const &name, MergeFastForward ff)
{
QString cmd = "merge ";
switch (ff) {
case MergeFastForward::No:
cmd += "--no-ff ";
break;
case MergeFastForward::Only:
cmd += "--ff-only ";
break;
default:
cmd += "--ff ";
break;
}
git(cmd + name);
}
void Git::rebaseBranch(QString const &name)
{
git("rebase " + name);
}
QStringList Git::getRemotes()
{
QStringList ret;
git("remote");
QStringList lines = misc::splitLines(resultText());
for (QString const &line: lines) {
if (!line.isEmpty()) {
ret.push_back(line);
}
}
return ret;
}
Git::User Git::getUser(Source purpose)
{
User user;
bool global = purpose == Git::Source::Global;
bool local = purpose == Git::Source::Local;
QString arg1;
if (global) arg1 = "--global";
if (local) arg1 = "--local";
bool chdir = !global;
if (git(QString("config %1 user.name").arg(arg1), chdir)) {
user.name = resultText().trimmed();
}
if (git(QString("config %1 user.email").arg(arg1), chdir)) {
user.email = resultText().trimmed();
}
return user;
}
void Git::setUser(const User &user, bool global)
{
bool chdir = !global;
git(QString("config %1 user.name %2").arg(global ? "--global" : "").arg(encodeQuotedText(user.name)), chdir);
git(QString("config %1 user.email %2").arg(global ? "--global" : "").arg(encodeQuotedText(user.email)), chdir);
}
bool Git::reset_head1()
{
return git("reset HEAD~1");
}
bool Git::reset_hard()
{
return git("reset --hard");
}
bool Git::clean_df()
{
return git("clean -df");
}
bool Git::stash()
{
return git("stash");
}
bool Git::stash_apply()
{
return git("stash apply");
}
bool Git::stash_drop()
{
return git("stash drop");
}
bool Git::rm_cached(QString const &file)
{
QString cmd = "rm --cached \"%1\"";
return git(cmd.arg(file));
}
void Git::getRemoteURLs(QList<Remote> *out)
{
out->clear();
git("remote -v");
QStringList lines = misc::splitLines(resultText());
for (QString const &line : lines) {
int i = line.indexOf('\t');
int j = line.indexOf(" (");
if (i > 0 && i < j) {
Remote r;
r.name = line.mid(0, i);
r.url = line.mid(i + 1, j - i - 1);
r.purpose = line.mid(j + 1);
r.ssh_key = m->ssh_key_override;
if (r.purpose.startsWith('(') && r.purpose.endsWith(')')) {
r.purpose = r.purpose.mid(1, r.purpose.size() - 2);
}
out->push_back(r);
}
}
}
void Git::setRemoteURL(Git::Remote const &remote)
{
QString cmd = "remote set-url %1 %2";
cmd = cmd.arg(encodeQuotedText(remote.name)).arg(encodeQuotedText(remote.url));
git(cmd);
}
void Git::addRemoteURL(Git::Remote const &remote)
{
QString cmd = "remote add \"%1\" \"%2\"";
cmd = cmd.arg(encodeQuotedText(remote.name)).arg(encodeQuotedText(remote.url));
m->ssh_key_override = remote.ssh_key;
git(cmd);
}
void Git::removeRemote(QString const &name)
{
QString cmd = "remote remove %1";
cmd = cmd.arg(encodeQuotedText(name));
git(cmd);
}
bool Git::reflog(ReflogItemList *out, int maxcount)
{
out->clear();
QString cmd = "reflog --no-abbrev --raw -n %1";
cmd = cmd.arg(maxcount);
if (!git(cmd)) return false;
QByteArray ba = toQByteArray();
if (!ba.isEmpty()) {
ReflogItem item;
char const *begin = ba.data();
char const *end = begin + ba.size();
char const *left = begin;
char const *ptr = begin;
while (1) {
int c = 0;
if (ptr < end) {
c = *ptr;
}
if (c == '\r' || c == '\n' || c == 0) {
int d = 0;
QString line = QString::fromUtf8(left, ptr - left);
if (left < ptr) {
d = *left & 0xff;
}
if (d == ':') {
// ex. ":100644 100644 bb603836fb597cca994309a1f0a52251d6b20314 d6b9798854debee375bb419f0f2ed9c8437f1932 M\tsrc/MainWindow.cpp"
int tab = line.indexOf('\t');
if (tab > 1) {
QString tmp = line.mid(1, tab - 1);
QString path = line.mid(tab + 1);
QStringList cols = misc::splitWords(tmp);
if (!path.isEmpty() && cols.size() == 5) {
ReflogItem::File file;
file.atts_a = cols[0];
file.atts_b = cols[1];
file.id_a = cols[2];
file.id_b = cols[3];
file.type = cols[4];
file.path = path;
item.files.push_back(file);
}
}
} else {
bool start = isxdigit(d);
if (start || c == 0) {
if (!item.id.isEmpty()) {
out->push_back(item);
}
}
if (start) {
// ex. "0a2a8b6b66f48bcbf985d8a2afcd14ff41676c16 HEAD@{188}: commit: message"
item = ReflogItem();
int i = line.indexOf(": ");
if (i > 0) {
int j = line.indexOf(": ", i + 2);
if (j > 2) {
item.head = line.mid(0, i);
item.command = line.mid(i + 2, j - i - 2);
item.message = line.mid(j + 2);
if (item.head.size() > GIT_ID_LENGTH) {
item.id = item.head.mid(0, GIT_ID_LENGTH);
item.head = item.head.mid(GIT_ID_LENGTH + 1);
}
}
}
}
}
if (c == 0) break;
ptr++;
left = ptr;
} else {
ptr++;
}
}
}
return true;
}
// Git::FileStatus
QString Git::trimPath(QString const &s)
{
ushort const *begin = s.utf16();
ushort const *end = begin + s.size();
ushort const *left = begin;
ushort const *right = end;
while (left < right && QChar(*left).isSpace()) left++;
while (left < right && QChar(right[-1]).isSpace()) right--;
if (left + 1 < right && *left == '\"' && right[-1] == '\"') { // if quoted ?
left++;
right--;
QByteArray ba;
ushort const *ptr = left;
while (ptr < right) {
ushort c = *ptr;
ptr++;
if (c == '\\') {
c = 0;
while (ptr < right && QChar(*ptr).isDigit()) { // decode \oct
c = c * 8 + (*ptr - '0');
ptr++;
}
}
ba.push_back(c);
}
return QString::fromUtf8(ba);
}
if (left == begin && right == end) return s;
return QString::fromUtf16(left, right - left);
}
Git::FileStatusCode Git::FileStatus::parseFileStatusCode(char x, char y)
{
if (x == ' ' && (y == 'M' || y == 'D')) return FileStatusCode::NotUpdated;
if (x == 'M' && (y == 'M' || y == 'D' || y == ' ')) return FileStatusCode::UpdatedInIndex;
if (x == 'A' && (y == 'M' || y == 'D' || y == ' ')) return FileStatusCode::AddedToIndex;
if (x == 'D' && (y == 'M' || y == ' ')) return FileStatusCode::DeletedFromIndex;
if (x == 'R' && (y == 'M' || y == 'D' || y == ' ')) return FileStatusCode::RenamedInIndex;
if (x == 'C' && (y == 'M' || y == 'D' || y == ' ')) return FileStatusCode::CopiedInIndex;
if (x == 'C' && (y == 'M' || y == 'D' || y == ' ')) return FileStatusCode::CopiedInIndex;
if (x == 'D' && y == 'D') return FileStatusCode::Unmerged_BothDeleted;
if (x == 'A' && y == 'U') return FileStatusCode::Unmerged_AddedByUs;
if (x == 'U' && y == 'D') return FileStatusCode::Unmerged_DeletedByThem;
if (x == 'U' && y == 'A') return FileStatusCode::Unmerged_AddedByThem;
if (x == 'D' && y == 'U') return FileStatusCode::Unmerged_DeletedByUs;
if (x == 'A' && y == 'A') return FileStatusCode::Unmerged_BothAdded;
if (x == 'U' && y == 'U') return FileStatusCode::Unmerged_BothModified;
if (x == '?' && y == '?') return FileStatusCode::Untracked;
if (x == '!' && y == '!') return FileStatusCode::Ignored;
return FileStatusCode::Unknown;
}
void Git::FileStatus::parse(QString const &text)
{
data = Data();
if (text.size() > 3) {
ushort const *p = text.utf16();
if (p[2] == ' ') {
data.code_x = p[0];
data.code_y = p[1];
int i = text.indexOf(" -> ", 3);
if (i > 3) {
data.rawpath1 = text.mid(3, i - 3);
data.rawpath2 = text.mid(i + 4);
data.path1 = trimPath(data.rawpath1);
data.path2 = trimPath(data.rawpath2);
} else {
data.rawpath1 = text.mid(3);
data.path1 = trimPath(data.rawpath1);
data.path2 = QString();
}
data.code = parseFileStatusCode(data.code_x, data.code_y);
}
}
}
QByteArray Git::blame(QString const &path)
{
QString cmd = "blame --porcelain --abbrev=40 \"%1\"";
cmd = cmd.arg(path);
if (git(cmd)) {
return toQByteArray();
}
return QByteArray();
}
QList<Git::RemoteInfo> Git::ls_remote()
{
QList<RemoteInfo> list;
QString cmd = "ls-remote";
if (git(cmd)) {
QStringList lines = misc::splitLines(resultText());
for (QString const &line : lines) {
QStringList words = misc::splitWords(line);
if (words.size() == 2 && isValidID(words[0])) {
RemoteInfo info;
info.commit_id = words[0];
info.name = words[1];
list.push_back(info);
}
}
}
return list;
}
QString Git::signingKey(Source purpose)
{
QString arg1;
if (purpose == Source::Global) arg1 = "--global";
if (purpose == Source::Local) arg1 = "--local";
QString cmd = "config %1 user.signingkey";
cmd = cmd.arg(arg1);
bool chdir = purpose != Source::Global;
if (git(cmd, chdir)) {
return resultText().trimmed();
}
return QString();
}
bool Git::setSigningKey(QString const &id, bool global)
{
for (auto i : id) {
if (!QChar(i).isLetterOrNumber()) return false;
}
QString cmd = "config %1 %2 user.signingkey %3";
cmd = cmd.arg(global ? "--global" : "--local").arg(id.isEmpty() ? "--unset" : "").arg(id);
return git(cmd, !global);
}
Git::SignPolicy Git::signPolicy(Source source)
{
QString arg1;
if (source == Source::Global) arg1 = "--global";
if (source == Source::Local) arg1 = "--local";
QString cmd = "config %1 commit.gpgsign";
cmd = cmd.arg(arg1);
bool chdir = source != Source::Global;
if (git(cmd, chdir)) {
QString t = resultText().trimmed();
if (t == "false") return SignPolicy::False;
if (t == "true") return SignPolicy::True;
}
return SignPolicy::Unset;
}
bool Git::setSignPolicy(Source source, SignPolicy policy)
{
QString arg1;
if (source == Source::Global) arg1 = "--global";
if (source == Source::Local) arg1 = "--local";
QString cmd = "config %1 %2 commit.gpgsign %3";
QString arg2;
QString arg3;
if (policy == SignPolicy::False) {
arg3 = "false";
} else if (policy == SignPolicy::True) {
arg3 = "true";
} else {
arg2 = "--unset";
}
cmd = cmd.arg(arg1).arg(arg2).arg(arg3);
bool chdir = source != Source::Global;
return git(cmd, chdir);
}
bool Git::configGpgProgram(QString const &path, bool global)
{
QString cmd = "config ";
if (global) {
cmd += "--global ";
}
if (path.isEmpty()) {
cmd += "--unset ";
}
cmd += "gpg.program ";
if (!path.isEmpty()) {
cmd += QString("\"%1\"").arg(path);
}
return git(cmd, false);
}
// Diff
void Git::Diff::makeForSingleFile(Git::Diff *diff, QString const &id_a, QString const &id_b, QString const &path, QString const &mode)
{
diff->diff = QString("diff --git a/%1 b/%2").arg(path).arg(path);
diff->index = QString("index %1..%2 %3").arg(id_a).arg(id_b).arg(0);
diff->blob.a_id = id_a;
diff->blob.b_id = id_b;
diff->path = path;
diff->mode = mode;
diff->type = Git::Diff::Type::Create;
}
//
void parseDiff(std::string const &s, Git::Diff const *info, Git::Diff *out)
{
std::vector<std::string> lines;
{
char const *begin = s.c_str();
char const *end = begin + s.size();
misc::splitLines(begin, end, &lines, false);
}
out->diff = QString("diff --git ") + ("a/" + info->path) + ' ' + ("b/" + info->path);
out->index = QString("index ") + info->blob.a_id + ".." + info->blob.b_id + ' ' + info->mode;
out->path = info->path;
out->blob = info->blob;
bool atat = false;
for (std::string const &line : lines) {
int c = line[0] & 0xff;
if (c == '@') {
if (strncmp(line.c_str(), "@@ ", 3) == 0) {
out->hunks.push_back(Git::Hunk());
out->hunks.back().at = line;
atat = true;
}
} else {
if (atat && c == '\\') { // e.g. \ No newline at end of file...
// ignore this line
} else {
if (atat) {
if (c == ' ' || c == '-' || c == '+') {
// nop
} else {
atat = false;
}
}
if (atat) {
if (!out->hunks.isEmpty()) {
out->hunks.back().lines.push_back(line);
}
}
}
}
}
}
+
+void parseGitSubModules(const QByteArray &ba, std::vector<Git::Submodule> *out)
+{
+ *out = {};
+ QStringList lines = misc::splitLines(QString::fromUtf8(ba));
+ Git::Submodule submod;
+ auto Push = [&](){
+ if (!submod.name.isEmpty()) {
+ out->push_back(submod);
+ }
+ submod = {};
+ };
+ for (int i = 0; i < lines.size(); i++) {
+ QString line = lines[i].trimmed();
+ if (line.startsWith('[')) {
+ Push();
+ if (line.startsWith("[submodule ") && line.endsWith(']')) {
+ int i = 11;
+ int j = line.size() - 1;
+ if (i + 1 < j && line[i] == '\"') {
+ if (line[j - 1] == '\"') {
+ i++;
+ j--;
+ }
+ }
+ submod.name = line.mid(i, j - i);
+ }
+ } else {
+ int i = line.indexOf('=');
+ if (i > 0) {
+ QString key = line.mid(0, i).trimmed();
+ QString val = line.mid(i + 1).trimmed();
+ if (key == "path") {
+ submod.path = val;
+ } else if (key == "url") {
+ submod.url = val;
+ }
+ }
+ }
+ }
+ Push();
+}
diff --git a/src/Git.h b/src/Git.h
index a635bc7..bb179fb 100644
--- a/src/Git.h
+++ b/src/Git.h
@@ -1,516 +1,521 @@
#ifndef GIT_H
#define GIT_H
#include "AbstractProcess.h"
#include <QDateTime>
#include <QObject>
#include <functional>
#include <QDebug>
#include <QMutex>
#include <memory>
#define SINGLE_THREAD 0
#define GIT_ID_LENGTH (40)
class Win32PtyProcess;
enum class LineSide {
Left,
Right,
};
struct TreeLine {
int index;
int depth;
int color_number = 0;
bool bend_early = false;
TreeLine(int index = -1, int depth = -1)
: index(index)
, depth(depth)
{
}
};
struct NamedCommitItem {
enum class Type {
None,
BranchLocal,
BranchRemote,
Tag,
};
Type type = Type::None;
QString remote;
QString name;
QString id;
};
using NamedCommitList = QList<NamedCommitItem>;
class Git;
using GitPtr = std::shared_ptr<Git>;
class Git : QObject {
public:
class Context {
public:
QString git_command;
QString ssh_command;// = "C:/Program Files/Git/usr/bin/ssh.exe";
};
struct Object {
enum class Type {
UNKNOWN = 0,
COMMIT = 1,
TREE = 2,
BLOB = 3,
TAG = 4,
UNDEFINED = 5,
OFS_DELTA = 6,
REF_DELTA = 7,
};
Type type = Type::UNKNOWN;
QByteArray content;
};
class Hunk {
public:
std::string at;
std::vector<std::string> lines;
};
class Diff {
public:
enum class Type {
Unknown,
Modify,
Copy,
Rename,
Create,
Delete,
ChType,
Unmerged,
};
Type type = Type::Unknown;
QString diff;
QString index;
QString path;
QString mode;
struct BLOB_AB_ {
QString a_id;
QString b_id;
} blob;
QList<Hunk> hunks;
Diff() = default;
Diff(QString const &id, QString const &path, QString const &mode)
{
makeForSingleFile(this, QString(GIT_ID_LENGTH, '0'), id, path, mode);
}
private:
void makeForSingleFile(Git::Diff *diff, QString const &id_a, QString const &id_b, QString const &path, QString const &mode);
};
enum class SignatureGrade {
NoSignature,
Unknown,
Good,
Dubious,
Missing,
Bad,
};
static SignatureGrade evaluateSignature(char s)
{
switch (s) {
case 'G':
return SignatureGrade::Good;
case 'U':
case 'X':
case 'Y':
return SignatureGrade::Dubious;
case 'B':
case 'R':
return SignatureGrade::Bad;
case 'E':
return SignatureGrade::Missing;
case 'N':
case ' ':
case 0:
return SignatureGrade::NoSignature;
}
return SignatureGrade::Unknown;
}
struct CommitItem {
QString commit_id;
QStringList parent_ids;
QString author;
QString email;
QString message;
QDateTime commit_date;
std::vector<TreeLine> parent_lines;
QByteArray fingerprint;
char signature = 0; // git log format:%G?
bool has_child = false;
int marker_depth = -1;
bool resolved = false;
bool strange_date = false;
};
using CommitItemList = std::vector<CommitItem>;
static bool isUncommited(CommitItem const &item)
{
return item.commit_id.isEmpty();
}
struct Branch {
QString name;
QString id;
QString remote;
int ahead = 0;
int behind = 0;
enum {
None,
Current = 0x0001,
HeadDetachedAt = 0x0002,
HeadDetachedFrom = 0x0004,
};
int flags = 0;
operator bool () const
{
if (name.isEmpty()) return false;
if (id.isEmpty()) return false;
return true;
}
bool isCurrent() const
{
return flags & Current;
}
bool isHeadDetached() const
{
return flags & HeadDetachedAt;
}
};
struct Tag {
QString name;
QString id;
};
enum class FileStatusCode : unsigned int {
Unknown,
Ignored,
Untracked,
NotUpdated = 0x10000000,
Staged_ = 0x20000000,
UpdatedInIndex,
AddedToIndex,
DeletedFromIndex,
RenamedInIndex,
CopiedInIndex,
Unmerged_ = 0x40000000,
Unmerged_BothDeleted,
Unmerged_AddedByUs,
Unmerged_DeletedByThem,
Unmerged_AddedByThem,
Unmerged_DeletedByUs,
Unmerged_BothAdded,
Unmerged_BothModified,
Tracked_ = 0xf0000000
};
enum class MergeFastForward {
Default,
No,
Only,
};
class FileStatus {
public:
struct Data {
char code_x = 0;
char code_y = 0;
FileStatusCode code = FileStatusCode::Unknown;
QString rawpath1;
QString rawpath2;
QString path1;
QString path2;
} data;
static FileStatusCode parseFileStatusCode(char x, char y);
bool isStaged() const
{
return (int)data.code & (int)FileStatusCode::Staged_;
}
bool isUnmerged() const
{
return (int)data.code & (int)FileStatusCode::Unmerged_;
}
bool isTracked() const
{
return (int)data.code & (int)FileStatusCode::Tracked_;
}
void parse(QString const &text);
FileStatus() = default;
FileStatus(QString const &text)
{
parse(text);
}
FileStatusCode code() const
{
return data.code;
}
int code_x() const
{
return data.code_x;
}
int code_y() const
{
return data.code_y;
}
bool isDeleted() const
{
return code_x() == 'D' || code_y() == 'D';
}
QString path1() const
{
return data.path1;
}
QString path2() const
{
return data.path2;
}
QString rawpath1() const
{
return data.rawpath1;
}
QString rawpath2() const
{
return data.rawpath2;
}
};
using FileStatusList = std::vector<FileStatus>;
static QString trimPath(QString const &s);
private:
struct Private;
Private *m;
QStringList make_branch_list_();
QByteArray cat_file_(QString const &id);
FileStatusList status_s_();
bool commit_(QString const &msg, bool amend, bool sign, AbstractPtyProcess *pty);
bool push_(bool tags, AbstractPtyProcess *pty);
static void parseAheadBehind(QString const &s, Branch *b);
Git();
QString encodeQuotedText(QString const &str);
public:
Git(Context const &cx, QString const &repodir, QString const &sshkey = {});
Git(Git &&r) = delete;
~Git() override;
using callback_t = bool (*)(void *, const char *, int);
void setLogCallback(callback_t func, void *cookie);
QByteArray toQByteArray() const;
void setGitCommand(QString const &gitcmd, const QString &sshcmd = {});
QString gitCommand() const;
void clearResult();
QString resultText() const;
bool chdirexec(std::function<bool ()> const &fn);
bool git(QString const &arg, bool chdir, bool errout = false, AbstractPtyProcess *pty = nullptr);
bool git(QString const &arg)
{
return git(arg, true);
}
void setWorkingRepositoryDir(QString const &repo, const QString &sshkey);
QString const &workingRepositoryDir() const;
QString const &sshKey() const;
void setSshKey(const QString &sshkey) const;
QString getCurrentBranchName();
bool isValidWorkingCopy() const;
QString version();
bool init();
QStringList getUntrackedFiles();
CommitItemList log_all(QString const &id, int maxcount);
CommitItemList log(int maxcount);
bool queryCommit(QString const &id, CommitItem *out);
struct CloneData {
QString url;
QString basedir;
QString subdir;
};
static CloneData preclone(QString const &url, QString const &path);
bool clone(CloneData const &data, AbstractPtyProcess *pty);
FileStatusList status_s();
bool cat_file(QString const &id, QByteArray *out);
void resetFile(QString const &path);
void resetAllFiles();
void removeFile(QString const &path);
void stage(QString const &path);
void stage(QStringList const &paths);
void unstage(QString const &path);
void unstage(QStringList const &paths);
void pull(AbstractPtyProcess *pty = nullptr);
void fetch(AbstractPtyProcess *pty = nullptr, bool prune = false);
void fetch_tags_f(AbstractPtyProcess *pty);
QList<Branch> branches();
int getProcessExitCode() const;
QString diff(QString const &old_id, QString const &new_id);
QString diff_file(QString const &old_path, QString const &new_path);
struct DiffRaw {
struct AB {
QString id;
QString mode;
} a, b;
QString state;
QStringList files;
};
struct Remote {
QString name;
QString url;
QString purpose;
QString ssh_key;
};
QList<DiffRaw> diff_raw(QString const &old_id, QString const &new_id);
static bool isValidID(QString const &id);
QString status();
bool commit(QString const &text, bool sign, AbstractPtyProcess *pty);
bool commit_amend_m(QString const &text, bool sign, AbstractPtyProcess *pty);
bool revert(QString const &id);
bool push(bool tags, AbstractPtyProcess *pty = nullptr);
void getRemoteURLs(QList<Remote> *out);
void createBranch(QString const &name);
void checkoutBranch(QString const &name);
void mergeBranch(QString const &name, MergeFastForward ff);
void rebaseBranch(QString const &name);
static bool isValidWorkingCopy(QString const &dir);
QString diff_to_file(QString const &old_id, QString const &path);
QString errorMessage() const;
GitPtr dup() const;
QString rev_parse(QString const &name);
QList<Tag> tags();
QList<Tag> tags2();
bool tag(QString const &name, QString const &id = QString());
void delete_tag(QString const &name, bool remote);
void setRemoteURL(const Remote &remote);
void addRemoteURL(const Remote &remote);
void removeRemote(QString const &name);
QStringList getRemotes();
struct User {
QString name;
QString email;
};
enum class Source {
Default,
Global,
Local,
};
User getUser(Source purpose);
void setUser(User const&user, bool global);
bool reset_head1();
bool reset_hard();
bool clean_df();
void push_u(QString const &remote, QString const &branch, AbstractPtyProcess *pty);
QString objectType(QString const &id);
bool rm_cached(QString const &file);
void cherrypick(QString const &name);
QString getCherryPicking() const;
QString getMessage(const QString &id);
struct ReflogItem {
QString id;
QString head;
QString command;
QString message;
struct File {
QString atts_a;
QString atts_b;
QString id_a;
QString id_b;
QString type;
QString path;
};
QList<File> files;
};
using ReflogItemList = QList<ReflogItem>;
bool reflog(ReflogItemList *out, int maxcount = 100);
QByteArray blame(QString const &path);
enum SignPolicy {
Unset,
False,
True,
};
QString signingKey(Source purpose);
bool setSigningKey(QString const &id, bool global);
SignPolicy signPolicy(Source source);
bool setSignPolicy(Source source, SignPolicy policy);
bool configGpgProgram(QString const &path, bool global);
struct RemoteInfo {
QString commit_id;
QString name;
};
QList<RemoteInfo> ls_remote();
bool stash();
bool stash_apply();
bool stash_drop();
struct Submodule {
+ QString name;
QString id;
QString path;
QString refs;
+ QString url;
};
struct SubmoduleUpdateData {
bool init = true;
bool recursive = true;
};
std::vector<Submodule> submodules();
bool submodule_add(const CloneData &data, bool force, AbstractPtyProcess *pty);
bool submodule_update(const SubmoduleUpdateData &data, AbstractPtyProcess *pty);
};
void parseDiff(std::string const &s, Git::Diff const *info, Git::Diff *out);
+void parseGitSubModules(QByteArray const &ba, std::vector<Git::Submodule> *out);
+
+
#endif // GIT_H
diff --git a/src/GitDiff.cpp b/src/GitDiff.cpp
index 3186f6a..a335664 100644
--- a/src/GitDiff.cpp
+++ b/src/GitDiff.cpp
@@ -1,458 +1,421 @@
#include "GitDiff.h"
#include "BasicMainWindow.h"
#include <QDebug>
#include <QThread>
-bool parse_tree_(GitObjectCache *objcache, QString const &commit_id, QString const &path_prefix, GitTreeItemList *out)
-{
- out->clear();
- if (!commit_id.isEmpty()) {
- Git::Object obj = objcache->catFile(commit_id);
- if (!obj.content.isEmpty()) { // 内容を取得
- QString s = QString::fromUtf8(obj.content);
- QStringList lines = misc::splitLines(s);
- for (QString const &line : lines) {
- int tab = line.indexOf('\t'); // タブより後ろにパスがある
- if (tab > 0) {
- QString stat = line.mid(0, tab); // タブの手前まで
- QStringList vals = misc::splitWords(stat); // 空白で分割
- if (vals.size() >= 3) {
- GitTreeItem data;
- data.mode = vals[0]; // ファイルモード
- data.id = vals[2]; // id(ハッシュ値)
- QString type = vals[1]; // 種類(tree/blob)
- QString path = line.mid(tab + 1); // パス
- path = Git::trimPath(path);
- data.name = path_prefix.isEmpty() ? path : misc::joinWithSlash(path_prefix, path);
- if (type == "tree") {
- data.type = GitTreeItem::TREE;
- out->push_back(data);
- } else if (type == "blob") {
- data.type = GitTreeItem::BLOB;
- out->push_back(data);
- }
- }
- }
- }
- return true;
- }
- }
- return false;
-}
-
// PathToIdMap
class GitDiff::LookupTable {
private:
public:
std::map<QString, QString> path_to_id_map;
std::map<QString, QString> id_to_path_map;
public:
using const_iterator = std::map<QString, QString>::const_iterator;
void store(QString const &path, QString const &id)
{
path_to_id_map[path] = id;
id_to_path_map[id] = path;
}
void store(GitTreeItemList const &files)
{
for (GitTreeItem const &cd : files) {
store(cd.name, cd.id);
}
}
const_iterator find_path(QString const &path) const
{
return path_to_id_map.find(path);
}
const_iterator end_path() const
{
return path_to_id_map.end();
}
};
// GitDiff
GitPtr GitDiff::git()
{
return objcache->git()->dup();
}
QString GitDiff::makeKey(QString const &a_id, QString const &b_id)
{
return a_id + ".." + b_id;
}
QString GitDiff::makeKey(Git::Diff const &diff)
{
return makeKey(diff.blob.a_id, diff.blob.b_id);
}
QString GitDiff::prependPathPrefix(QString const &path)
{
return PATH_PREFIX + path;
}
QString GitDiff::diffObjects(GitPtr const &g, QString const &a_id, QString const &b_id)
{
QString path_prefix = PATH_PREFIX;
if (b_id.startsWith(path_prefix)) {
QString path = b_id.mid(path_prefix.size());
return g->diff_to_file(a_id, path);
} else {
return g->diff(a_id, b_id);
}
}
QString GitDiff::diffFiles(GitPtr const &g, QString const &a_path, QString const &b_path)
{
return g->diff_file(a_path, b_path);
}
void GitDiff::parseDiff(std::string const &s, Git::Diff const *info, Git::Diff *out)
{
std::vector<std::string> lines;
{
char const *begin = s.c_str();
char const *end = begin + s.size();
misc::splitLines(begin, end, &lines, false);
}
out->diff = QString("diff --git ") + ("a/" + info->path) + ' ' + ("b/" + info->path);
out->index = QString("index ") + info->blob.a_id + ".." + info->blob.b_id + ' ' + info->mode;
out->path = info->path;
out->blob = info->blob;
bool atat = false;
for (std::string const &line : lines) {
int c = line[0] & 0xff;
if (c == '@') {
if (strncmp(line.c_str(), "@@ ", 3) == 0) {
out->hunks.push_back(Git::Hunk());
out->hunks.back().at = line;
atat = true;
}
} else {
if (atat && c == '\\') { // e.g. \ No newline at end of file...
// ignore this line
} else {
if (atat) {
if (c == ' ' || c == '-' || c == '+') {
// nop
} else {
atat = false;
}
}
if (atat) {
if (!out->hunks.isEmpty()) {
out->hunks.back().lines.push_back(line);
}
}
}
}
}
}
void GitDiff::retrieveCompleteTree(QString const &dir, GitTreeItemList const *files, std::map<QString, GitTreeItem> *out)
{
for (GitTreeItem const &d : *files) {
QString path = misc::joinWithSlash(dir, d.name);
if (d.type == GitTreeItem::BLOB) {
(*out)[path] = d;
} else if (d.type == GitTreeItem::TREE) {
GitTreeItemList files2;
- parse_tree_(objcache, d.id, QString(), &files2);
+ parseGitTreeObject(objcache, d.id, QString(), &files2);
retrieveCompleteTree(path, &files2, out);
}
}
}
void GitDiff::retrieveCompleteTree(QString const &dir, GitTreeItemList const *files)
{
for (GitTreeItem const &d : *files) {
QString path = misc::joinWithSlash(dir, d.name);
if (d.type == GitTreeItem::BLOB) {
Git::Diff diff(d.id, path, d.mode);
diffs.push_back(diff);
} else if (d.type == GitTreeItem::TREE) {
GitTreeItemList files2;
- parse_tree_(objcache, d.id, QString(), &files2);
+ parseGitTreeObject(objcache, d.id, QString(), &files2);
retrieveCompleteTree(path, &files2);
}
}
}
bool GitDiff::diff(QString const &id, QList<Git::Diff> *out)
{
out->clear();
diffs.clear();
try {
if (Git::isValidID(id)) { // 有効なID
{ // diff_raw
GitTreeItemList files;
GitCommit newer_commit;
- newer_commit.parseCommit(objcache, id);
- parse_tree_(objcache, newer_commit.tree_id, QString(), &files);
+ GitCommit::parseCommit(objcache, id, &newer_commit);
+ parseGitTreeObject(objcache, newer_commit.tree_id, QString(), &files);
if (newer_commit.parents.isEmpty()) { // 親がないなら最古のコミット
retrieveCompleteTree(QString(), &files); // ツリー全体を取得
} else {
std::map<QString, Git::Diff> diffmap;
std::set<QString> deleted_set;
std::set<QString> renamed_set;
QList<Git::DiffRaw> list;
for (QString const &parent : newer_commit.parents) {
QList<Git::DiffRaw> l = git()->diff_raw(parent, id);
for (Git::DiffRaw const &item : l) {
if (item.state.startsWith('D')) {
deleted_set.insert(item.a.id);
}
list.push_back(item);
}
}
for (Git::DiffRaw const &item : list) {
if (item.state.startsWith('A') || item.state.startsWith('C')) { // 追加されたファイル
auto it = deleted_set.find(item.b.id); // 同じオブジェクトIDが削除リストに載っているなら
if (it != deleted_set.end()) {
renamed_set.insert(item.b.id); // 名前変更とみなす
}
} else if (item.state.startsWith('R')) { // 名前変更されたファイル
renamed_set.insert(item.b.id);
}
}
for (Git::DiffRaw const &item : list) {
QString file;
if (!item.files.isEmpty()) {
file = item.files.back(); // 名前変更された場合はリストの最後が新しい名前
}
Git::Diff diff;
diff.diff = QString("diff --git a/%1 b/%2").arg(file).arg(file);
diff.index = QString("index %1..%2 %3").arg(item.a.id).arg(item.b.id).arg(item.b.mode);
diff.path = file;
diff.mode = item.b.mode;
if (Git::isValidID(item.a.id)) diff.blob.a_id = item.a.id;
if (Git::isValidID(item.b.id)) diff.blob.b_id = item.b.id;
#if 0
if (!diff.blob.a_id.isEmpty()) {
if (!diff.blob.b_id.isEmpty()) {
if (renamed_set.find(diff.blob.b_id) != renamed_set.end()) {
diff.type = Git::Diff::Type::Rename;
} else {
diff.type = Git::Diff::Type::Modify;
}
} else {
if (renamed_set.find(diff.blob.a_id) != renamed_set.end()) { // 名前変更されたオブジェクトなら
diff.type = Git::Diff::Type::Unknown; // マップに追加しない
} else {
diff.type = Git::Diff::Type::Delete; // 削除されたオブジェクト
}
}
} else if (!diff.blob.b_id.isEmpty()) {
if (renamed_set.find(diff.blob.b_id) != renamed_set.end()) {
diff.type = Git::Diff::Type::Rename;
} else {
diff.type = Git::Diff::Type::Create;
}
}
#else
diff.type = Git::Diff::Type::Unknown;
int state = item.state.utf16()[0];
switch (state) {
case 'A': diff.type = Git::Diff::Type::Create; break;
case 'C': diff.type = Git::Diff::Type::Copy; break;
case 'D': diff.type = Git::Diff::Type::Delete; break;
case 'M': diff.type = Git::Diff::Type::Modify; break;
case 'R': diff.type = Git::Diff::Type::Rename; break;
case 'T': diff.type = Git::Diff::Type::ChType; break;
case 'U': diff.type = Git::Diff::Type::Unmerged; break;
}
#endif
if (diff.type != Git::Diff::Type::Unknown) {
if (diffmap.find(diff.path) == diffmap.end()) {
diffmap[diff.path] = diff;
}
}
}
for (auto const &pair : diffmap) {
diffs.push_back(pair.second);
}
}
}
} else { // 無効なIDなら、HEADと作業コピーのdiff
GitPtr g = objcache->git();
QString head_id = objcache->revParse("HEAD");
Git::FileStatusList stats = g->status_s(); // git status
GitCommitTree head_tree(objcache);
head_tree.parseCommit(head_id); // HEADが親
QString zeros(GIT_ID_LENGTH, '0');
for (Git::FileStatus const &fs : stats) {
QString path = fs.path1();
Git::Diff item;
GitTreeItem treeitem;
if (head_tree.lookup(path, &treeitem)) {
item.blob.a_id = treeitem.id; // HEADにおけるこのファイルのID
if (fs.isDeleted()) { // 削除されてる
item.blob.b_id = zeros; // 削除された
} else {
item.blob.b_id = prependPathPrefix(path); // IDの代わりに実在するファイルパスを入れる
}
item.mode = treeitem.mode;
} else {
item.blob.a_id = zeros;
item.blob.b_id = prependPathPrefix(path); // 実在するファイルパス
}
item.diff = QString("diff --git a/%1 b/%2").arg(path).arg(path);
item.index = QString("index %1..%2 %3").arg(item.blob.a_id).arg(zeros).arg(item.mode);
item.path = path;
diffs.push_back(item);
}
}
std::sort(diffs.begin(), diffs.end(), [](Git::Diff const &left, Git::Diff const &right){
return left.path.compare(right.path, Qt::CaseInsensitive) < 0;
});
*out = std::move(diffs);
return true;
} catch (Interrupted &) {
out->clear();
}
return false;
}
bool GitDiff::diff_uncommited(QList<Git::Diff> *out)
{
return diff(QString(), out);
}
// GitCommitTree
GitCommitTree::GitCommitTree(GitObjectCache *objcache)
: objcache(objcache)
{
}
GitPtr GitCommitTree::git()
{
return objcache->git();
}
QString GitCommitTree::lookup_(QString const &file, GitTreeItem *out)
{
int i = file.lastIndexOf('/');
if (i >= 0) {
QString subdir = file.mid(0, i);
QString name = file.mid(i + 1);
QString tree_id;
{
auto it = tree_id_map.find(subdir);
if (it != tree_id_map.end()) {
tree_id = it->second;
} else {
tree_id = lookup_(subdir, out);
}
}
GitTreeItemList list;
- if (parse_tree_(objcache, tree_id, QString(), &list)) {
+ if (parseGitTreeObject(objcache, tree_id, QString(), &list)) {
QString return_id;
for (GitTreeItem const &d : list) {
if (d.name == name) {
return_id = d.id;
}
QString path = misc::joinWithSlash(subdir, d.name);
if (d.type == GitTreeItem::BLOB) {
if (out && d.name == name) {
*out = d;
}
blob_map[path] = d;
} else if (d.type == GitTreeItem::TREE) {
tree_id_map[path] = d.id;
}
}
return return_id;
}
} else {
QString return_id;
for (GitTreeItem const &d : root_item_list) {
if (d.name == file) {
return_id = d.id;
}
if (d.type == GitTreeItem::BLOB) {
if (out && d.name == file) {
*out = d;
}
blob_map[d.name] = d;
} else if (d.type == GitTreeItem::TREE) {
tree_id_map[d.name] = d.id;
}
}
return return_id;
}
return QString();
}
QString GitCommitTree::lookup(QString const &file)
{
auto it = blob_map.find(file);
if (it != blob_map.end()) {
return it->second.id;
}
return lookup_(file, nullptr);
}
bool GitCommitTree::lookup(QString const &file, GitTreeItem *out)
{
*out = GitTreeItem();
auto it = blob_map.find(file);
if (it != blob_map.end()) {
*out = it->second;
return true;
}
return !lookup_(file, out).isEmpty();
}
void GitCommitTree::parseTree(QString const &tree_id)
{
- parse_tree_(objcache, tree_id, QString(), &root_item_list);
+ parseGitTreeObject(objcache, tree_id, QString(), &root_item_list);
}
QString GitCommitTree::parseCommit(QString const &commit_id)
{
GitCommit commit;
- commit.parseCommit(objcache, commit_id);
+ GitCommit::parseCommit(objcache, commit_id, &commit);
parseTree(commit.tree_id);
return commit.tree_id;
}
//
QString lookupFileID(GitObjectCache *objcache, QString const &commit_id, QString const &file)
// 指定されたコミットに属するファイルのIDを求める
{
GitCommitTree commit_tree(objcache);
commit_tree.parseCommit(commit_id);
QString id = commit_tree.lookup(file);
return id;
}
diff --git a/src/GitObjectManager.cpp b/src/GitObjectManager.cpp
index 45c6d09..2118c7e 100644
--- a/src/GitObjectManager.cpp
+++ b/src/GitObjectManager.cpp
@@ -1,393 +1,438 @@
#include "GitObjectManager.h"
#include "Git.h"
#include "common/joinpath.h"
#include "common/misc.h"
#include <QBuffer>
#include <QDebug>
#include <QDirIterator>
#include <QFile>
#include <memory>
GitObjectManager::GitObjectManager()
{
subdir_git_objects = ".git/objects";
subdir_git_objects_pack = subdir_git_objects / "pack";
}
void GitObjectManager::setup(GitPtr const &g)
{
this->g = g;
clearIndexes();
}
QString GitObjectManager::workingDir()
{
return g->workingRepositoryDir();
}
void GitObjectManager::loadIndexes()
{
QMutexLocker lock(&mutex);
if (git_idx_list.empty()) {
QString path = workingDir() / subdir_git_objects_pack;
QDirIterator it(path, { "pack-*.idx" }, QDir::Files | QDir::Readable);
while (it.hasNext()) {
it.next();
GitPackIdxPtr idx = std::make_shared<GitPackIdxV2>();
idx->basename = it.fileInfo().baseName();
idx->parse(it.filePath());
git_idx_list.push_back(idx);
}
}
}
void GitObjectManager::clearIndexes()
{
git_idx_list.clear();
}
void GitObjectManager::applyDelta(QByteArray const *base_obj, QByteArray const *delta_obj, QByteArray *out)
{
if (delta_obj->size() > 0) {
uint8_t const *begin = (uint8_t const *)delta_obj->data();
uint8_t const *end = begin + delta_obj->size();
uint8_t const *ptr = begin;
auto ReadNumber = [&](){
uint64_t n = 0;
int shift = 0;
while (ptr < end) {
uint64_t c = *ptr;
ptr++;
n |= (c & 0x7f) << shift;
shift += 7;
if (!(c & 0x80)) break;
}
return n;
};
uint64_t a = ReadNumber(); // older file size
uint64_t b = ReadNumber(); // newer file size
(void)a;
(void)b;
// cf. https://github.com/github/git-msysgit/blob/master/patch-delta.c
while (ptr < end) {
uint8_t op = *ptr;
ptr++;
if (op & 0x80) { // copy operation
uint32_t offset = 0;
uint32_t length = 0;
if (op & 0x01) offset = *ptr++;
if (op & 0x02) offset |= (*ptr++ << 8);
if (op & 0x04) offset |= (*ptr++ << 16);
if (op & 0x08) offset |= ((unsigned) *ptr++ << 24);
if (op & 0x10) length = *ptr++;
if (op & 0x20) length |= (*ptr++ << 8);
if (op & 0x40) length |= (*ptr++ << 16);
if (length == 0) length = 0x10000;
if (offset + length > (uint32_t)base_obj->size()) {
qDebug() << Q_FUNC_INFO << "base-file or delta-file is corrupted";
out->clear();
return;
}
out->append(base_obj->data() + offset, length);
} else if (op > 0) { // insert operation
int length = op & 0x7f;
if (ptr + length <= end) {
out->append((char const *)ptr, length);
ptr += length;
}
} else {
qDebug() << Q_FUNC_INFO << "unexpected delta opcode 0";
}
}
}
}
bool GitObjectManager::loadPackedObject(GitPackIdxPtr const &idx, QIODevice *packfile, GitPackIdxItem const *item, GitPack::Object *out)
{
GitPack::Info info;
if (GitPack::seekPackedObject(packfile, item, &info)) {
GitPackIdxItem const *source_item = nullptr;
if (info.type == Git::Object::Type::OFS_DELTA) {
source_item = idx->item(item->offset - info.offset);
} else if (info.type == Git::Object::Type::REF_DELTA) {
source_item = idx->item(info.ref_id);
}
if (source_item) { // if deltified object
GitPack::Object source;
if (source_item && loadPackedObject(idx, packfile, source_item, &source)) {
GitPack::Object delta;
if (GitPack::load(packfile, item, &delta)) {
if (delta.checksum != item->checksum) {
qDebug() << "crc checksum incorrect";
return false;
}
QByteArray ba;
applyDelta(&source.content, &delta.content, &ba);
*out = GitPack::Object();
out->type = source.type;
out->content = std::move(ba);
out->expanded_size = out->content.size();
return true;
}
}
qDebug() << Q_FUNC_INFO << "failed";
return false;
}
}
return GitPack::load(packfile, item, out);
}
bool GitObjectManager::extractObjectFromPackFile(GitPackIdxPtr const &idx, GitPackIdxItem const *item, GitPack::Object *out)
{
*out = GitPack::Object();
QString packfilepath = workingDir() / subdir_git_objects_pack / (idx->basename + ".pack");
QFile packfile(packfilepath);
if (packfile.open(QFile::ReadOnly)) {
if (loadPackedObject(idx, &packfile, item, out)) {
if (out->type == Git::Object::Type::TREE) {
GitPack::decodeTree(&out->content);
}
return true;
}
}
return false;
}
bool GitObjectManager::extractObjectFromPackFile(QString const &id, QByteArray *out, Git::Object::Type *type)
{
loadIndexes();
for (GitPackIdxPtr const &idx : git_idx_list) {
GitPackIdxItem const *item = idx->item(id);
if (item) {
GitPack::Object obj;
if (extractObjectFromPackFile(idx, item, &obj)) {
*out = std::move(obj.content);
*type = obj.type;
return true;
}
qDebug() << Q_FUNC_INFO << "failed";
return false;
}
}
return false;
}
QString GitObjectManager::findObjectPath(QString const &id)
{
if (Git::isValidID(id)) {
int count = 0;
QString absolute_path;
QString xx = id.mid(0, 2); // leading two xdigits
QString name = id.mid(2); // remaining xdigits
QString dir = workingDir() / subdir_git_objects / xx; // e.g. /home/user/myproject/.git/objects/5a
QDirIterator it(dir, QDir::Files);
while (it.hasNext()) {
it.next();
if (it.fileName().startsWith(name)) {
QString id = xx + it.fileName(); // complete id
if (id.size() == GIT_ID_LENGTH && Git::isValidID(id)) {
absolute_path = dir / it.fileName();
count++;
}
}
}
if (count == 1) return absolute_path;
if (count > 1) qDebug() << Q_FUNC_INFO << "ambiguous id" << id;
}
return QString(); // not found
}
bool GitObjectManager::loadObject(QString const &id, QByteArray *out, Git::Object::Type *type)
{
QString path = findObjectPath(id);
if (!path.isEmpty()) {
QFile file(path);
if (file.open(QFile::ReadOnly)) {
if (GitPack::decompress(&file, 1000000000, out)) {
*type = GitPack::stripHeader(out);
if (*type == Git::Object::Type::TREE) {
GitPack::decodeTree(out);
}
return true;
}
}
}
return false;
}
bool GitObjectManager::catFile(QString const &id, QByteArray *out, Git::Object::Type *type)
{
*type = Git::Object::Type::UNKNOWN;
if (loadObject(id, out, type)) return true;
if (extractObjectFromPackFile(id, out, type)) return true;
return false;
}
//
size_t GitObjectCache::size() const
{
size_t size = 0;
for (ItemPtr const &item : items) {
size += item->ba.size();
}
return size;
}
void GitObjectCache::setup(GitPtr const &g)
{
items.clear();
revparsemap.clear();
if (g) {
object_manager.setup(g->dup());
}
}
QString GitObjectCache::revParse(QString const &name)
{
GitPtr g = git();
if (!g) return QString();
{
QMutexLocker lock(&object_manager.mutex);
auto it = revparsemap.find(name);
if (it != revparsemap.end()) {
return it->second;
}
}
QString id = g->rev_parse(name);
{
QMutexLocker lock(&object_manager.mutex);
revparsemap[name] = id;
return id;
}
}
Git::Object GitObjectCache::catFile(QString const &id)
{
{
QMutexLocker lock(&object_manager.mutex);
size_t n = items.size();
size_t i = n;
while (i > 0) {
i--;
if (items[i]->id == id) {
ItemPtr item = items[i];
if (i + 1 < n) {
items.erase(items.begin() + i);
items.push_back(item);
}
Git::Object obj;
obj.type = item->type;
obj.content = item->ba;
return obj;
}
}
while (size() > 100000000) { // 100MB
items.erase(items.begin());
}
}
QByteArray ba;
Git::Object::Type type = Git::Object::Type::UNKNOWN;
auto Store = [&](){
QMutexLocker lock(&object_manager.mutex);
Item *item = new Item();
item->id = id;
item->ba = std::move(ba);
item->type = type;
items.push_back(ItemPtr(item));
Git::Object obj;
obj.type = item->type;
obj.content = item->ba;
return obj;
};
if (object_manager.catFile(id, &ba, &type)) { // 独自実装のファイル取得
return Store();
}
if (true) {
if (git()->cat_file(id, &ba)) { // 外部コマンド起動の git cat-file -p を試してみる
// 上の独自実装のファイル取得が正しく動作していれば、ここには来ないはず
- qDebug() << __LINE__ << __FILE__ << Q_FUNC_INFO << id;
+ qDebug() << __FILE__ << __LINE__ << Q_FUNC_INFO << id;
return Store();
}
}
qDebug() << "failed to cat file: " << id;
return Git::Object();
}
QString GitObjectCache::getCommitIdFromTag(QString const &tag)
{
QString commit_id;
GitPtr g = git();
if (g && g->isValidWorkingCopy()) {
QString id = g->rev_parse(tag);
Git::Object obj = catFile(id);
switch (obj.type) {
case Git::Object::Type::COMMIT:
commit_id = id;
break;
case Git::Object::Type::TAG:
if (!obj.content.isEmpty()) {
misc::splitLines(obj.content, [&](char const *ptr, size_t len){
if (commit_id.isEmpty()) {
if (len >= 7 + GIT_ID_LENGTH && strncmp(ptr, "object ", 7) == 0) {
QString id = QString::fromUtf8(ptr + 7, len - 7).trimmed();
if (Git::isValidID(id)) {
commit_id = id;
}
}
}
return QString();
});
}
break;
}
}
return commit_id;
}
-bool GitCommit::parseCommit(GitObjectCache *objcache, QString const &id)
+bool GitCommit::parseCommit(GitObjectCache *objcache, QString const &id, GitCommit *out)
{
- parents.clear();
+ *out = {};
if (!id.isEmpty()) {
QStringList parents;
{
Git::Object obj = objcache->catFile(id);
if (!obj.content.isEmpty()) {
QStringList lines = misc::splitLines(QString::fromUtf8(obj.content));
for (QString const &line : lines) {
int i = line.indexOf(' ');
if (i < 1) break;
QString key = line.mid(0, i);
QString val = line.mid(i + 1).trimmed();
if (key == "tree") {
- tree_id = val;
+ out->tree_id = val;
} else if (key == "parent") {
parents.push_back(val);
}
}
}
}
- if (!tree_id.isEmpty()) { // サブディレクトリ
- this->parents.append(parents);
+ if (!out->tree_id.isEmpty()) { // サブディレクトリ
+ out->parents.append(parents);
return true;
}
}
return false;
}
+void parseGitTreeObject(QByteArray const &ba, const QString &path_prefix, GitTreeItemList *out)
+{
+ *out = {};
+ QString s = QString::fromUtf8(ba);
+ QStringList lines = misc::splitLines(s);
+ for (QString const &line : lines) {
+ int tab = line.indexOf('\t'); // タブより後ろにパスがある
+ if (tab > 0) {
+ QString stat = line.mid(0, tab); // タブの手前まで
+ QStringList vals = misc::splitWords(stat); // 空白で分割
+ if (vals.size() >= 3) {
+ GitTreeItem data;
+ data.mode = vals[0]; // ファイルモード
+ data.id = vals[2]; // id(ハッシュ値)
+ QString type = vals[1]; // 種類(tree/blob)
+ QString path = line.mid(tab + 1); // パス
+ path = Git::trimPath(path);
+ data.name = path_prefix.isEmpty() ? path : misc::joinWithSlash(path_prefix, path);
+ if (type == "tree") {
+ data.type = GitTreeItem::TREE;
+ } else if (type == "blob") {
+ data.type = GitTreeItem::BLOB;
+ } else if (type == "commit") {
+ data.type = GitTreeItem::COMMIT;
+ }
+ if (data.type != GitTreeItem::UNKNOWN) {
+ out->push_back(data);
+ }
+ }
+ }
+ }
+}
+
+bool parseGitTreeObject(GitObjectCache *objcache, const QString &commit_id, const QString &path_prefix, GitTreeItemList *out)
+{
+ out->clear();
+ if (!commit_id.isEmpty()) {
+ Git::Object obj = objcache->catFile(commit_id);
+ if (!obj.content.isEmpty()) { // 内容を取得
+ parseGitTreeObject(obj.content, path_prefix, out);
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/src/GitObjectManager.h b/src/GitObjectManager.h
index 99b23fc..0d849a3 100644
--- a/src/GitObjectManager.h
+++ b/src/GitObjectManager.h
@@ -1,129 +1,134 @@
#ifndef GITOBJECTMANAGER_H
#define GITOBJECTMANAGER_H
#include <QMutex>
#include <QString>
#include "GitPack.h"
#include "GitPackIdxV2.h"
#include <map>
class GitPackIdxV2;
class Git;
using GitPtr = std::shared_ptr<Git>;
class GitObjectManager {
friend class GitObjectCache;
private:
GitPtr g;
QMutex mutex;
QString subdir_git_objects;
QString subdir_git_objects_pack;
std::vector<GitPackIdxPtr> git_idx_list;
QString workingDir();
static void applyDelta(QByteArray const *base, QByteArray const *delta, QByteArray *out);
static bool loadPackedObject(GitPackIdxPtr const &idx, QIODevice *packfile, GitPackIdxItem const *item, GitPack::Object *out);
bool extractObjectFromPackFile(GitPackIdxPtr const &idx, GitPackIdxItem const *item, GitPack::Object *out);
bool extractObjectFromPackFile(QString const &id, QByteArray *out, Git::Object::Type *type);
void loadIndexes();
QString findObjectPath(QString const &id);
bool loadObject(QString const &id, QByteArray *out, Git::Object::Type *type);
GitPtr git()
{
return g;
}
public:
GitObjectManager();
void setup(const GitPtr &g);
bool catFile(QString const &id, QByteArray *out, Git::Object::Type *type);
void clearIndexes();
};
class GitObjectCache {
public:
struct Item {
QString id;
QByteArray ba;
Git::Object::Type type;
};
private:
GitObjectManager object_manager;
using ItemPtr = std::shared_ptr<Item>;
std::vector<ItemPtr> items;
std::map<QString, QString> revparsemap;
size_t size() const;
public:
GitPtr git()
{
return object_manager.git();
}
void setup(const GitPtr &g);
QString revParse(QString const &name);
Git::Object catFile(QString const &id);
QString getCommitIdFromTag(QString const &tag);
};
class GitCommit {
public:
QString tree_id;
QStringList parents;
- bool parseCommit(GitObjectCache *objcache, QString const &id);
+ static bool parseCommit(GitObjectCache *objcache, QString const &id, GitCommit *out);
};
struct GitTreeItem {
enum Type {
UNKNOWN,
TREE,
BLOB,
+ COMMIT,
};
Type type = UNKNOWN;
QString name;
QString id;
QString mode;
QString to_string_() const
{
QString t;
switch (type) {
case TREE: t = "TREE"; break;
case BLOB: t = "BLOB"; break;
}
return QString("GitTreeItem:{ %1 %2 %3 %4 }").arg(t).arg(id).arg(mode).arg(name);
}
};
using GitTreeItemList = QList<GitTreeItem>;
class GitCommitTree {
private:
GitObjectCache *objcache;
GitTreeItemList root_item_list;
std::map<QString, GitTreeItem> blob_map;
std::map<QString, QString> tree_id_map;
GitPtr git();
QString lookup_(QString const &file, GitTreeItem *out);
public:
GitCommitTree(GitObjectCache *objcache);
QString lookup(QString const &file);
bool lookup(QString const &file, GitTreeItem *out);
void parseTree(QString const &tree_id);
QString parseCommit(QString const &commit_id);
GitTreeItemList const *treelist() const
{
return &root_item_list;
}
};
QString lookupFileID(GitObjectCache *objcache, QString const &commit_id, QString const &file);
+void parseGitTreeObject(QByteArray const &ba, const QString &path_prefix, GitTreeItemList *out);
+bool parseGitTreeObject(GitObjectCache *objcache, QString const &commit_id, QString const &path_prefix, GitTreeItemList *out);
+
+
#endif // GITOBJECTMANAGER_H
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index cdb87ed..0393c96 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -1,3109 +1,3098 @@
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "AboutDialog.h"
#include "ApplicationGlobal.h"
#include "AreYouSureYouWantToContinueConnectingDialog.h"
#include "AvatarLoader.h"
#include "BlameWindow.h"
#include "CloneFromGitHubDialog.h"
#include "CommitPropertyDialog.h"
#include "DeleteBranchDialog.h"
#include "EditGitIgnoreDialog.h"
#include "EditTagsDialog.h"
#include "FileDiffWidget.h"
#include "FindCommitDialog.h"
#include "GitDiff.h"
#include "JumpDialog.h"
#include "LineEditDialog.h"
#include "MySettings.h"
#include "ObjectBrowserDialog.h"
#include "ReflogWindow.h"
#include "RemoteWatcher.h"
#include "SetGpgSigningDialog.h"
#include "SettingsDialog.h"
#include "StatusLabel.h"
#include "TextEditDialog.h"
#include "UserEvent.h"
#include "common/joinpath.h"
#include "common/misc.h"
#include "CherryPickDialog.h"
#include "CloneDialog.h"
#include "MergeDialog.h"
#include "SubmoduleUpdateDialog.h"
#include "SubmodulesDialog.h"
#include "platform.h"
#include "webclient.h"
#include <QClipboard>
#include <QDirIterator>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QFileIconProvider>
#include <QMessageBox>
#include <QMimeData>
#include <QPainter>
#include <QStandardPaths>
#include <QTimer>
FileDiffWidget::DrawData::DrawData()
{
bgcolor_text = QColor(255, 255, 255);
bgcolor_gray = QColor(224, 224, 224);
bgcolor_add = QColor(192, 240, 192);
bgcolor_del = QColor(255, 224, 224);
bgcolor_add_dark = QColor(64, 192, 64);
bgcolor_del_dark = QColor(240, 64, 64);
}
struct MainWindow::Private {
bool is_online_mode = true;
QTimer interval_10ms_timer;
QImage graph_color;
QPixmap digits;
StatusLabel *status_bar_label;
QObject *last_focused_file_list = nullptr;
QListWidgetItem *last_selected_file_item = nullptr;
bool searching = false;
QString search_text;
RemoteWatcher remote_watcher;
int repos_panel_width = 0;
std::set<QString> ancestors;
QWidget *focused_widget = nullptr;
QList<int> splitter_h_sizes;
};
MainWindow::MainWindow(QWidget *parent)
: BasicMainWindow(parent)
, ui(new Ui::MainWindow)
, m(new Private)
{
ui->setupUi(this);
#ifdef Q_OS_MACX
ui->action_about->setText("About Guitar...");
ui->action_edit_settings->setText("Settings...");
#endif
ui->splitter_v->setSizes({100, 400});
ui->splitter_h->setSizes({200, 100, 200});
m->status_bar_label = new StatusLabel(this);
ui->statusBar->addWidget(m->status_bar_label);
ui->widget_diff_view->bind(this);
qApp->installEventFilter(this);
setShowLabels(appsettings()->show_labels, false);
ui->widget_log->setupForLogWidget(ui->verticalScrollBar_log, ui->horizontalScrollBar_log, themeForTextEditor());
onLogVisibilityChanged();
initNetworking();
showFileList(FilesListType::SingleList);
m->digits.load(":/image/digits.png");
m->graph_color = global->theme->graphColorMap();
prepareLogTableWidget();
#ifdef Q_OS_WIN
{
QFont font;
font = ui->label_repo_name->font();
font.setFamily("Meiryo");
ui->label_repo_name->setFont(font);
font = ui->label_branch_name->font();
font.setFamily("Meiryo");
ui->label_branch_name->setFont(font);
}
#endif
connect(this, &BasicMainWindow::signalWriteLog, this, &BasicMainWindow::writeLog_);
connect(ui->dockWidget_log, &QDockWidget::visibilityChanged, this, &MainWindow::onLogVisibilityChanged);
connect(ui->widget_log, &TextEditorWidget::idle, this, &MainWindow::onLogIdle);
connect(ui->treeWidget_repos, &RepositoriesTreeWidget::dropped, this, &MainWindow::onRepositoriesTreeDropped);
connect((AbstractPtyProcess *)getPtyProcess(), &AbstractPtyProcess::completed, this, &MainWindow::onPtyProcessCompleted);
connect(this, &BasicMainWindow::remoteInfoChanged, [&](){
ui->lineEdit_remote->setText(currentRemoteName());
});
connect(this, &MainWindow::signalSetRemoteChanged, [&](bool f){
setRemoteChanged(f);
updateButton();
});
//
QString path = getBookmarksFilePath();
*getReposPtr() = RepositoryBookmark::load(path);
updateRepositoriesList();
webContext()->set_keep_alive_enabled(true);
getAvatarLoader()->start(this);
connect(getAvatarLoader(), &AvatarLoader::updated, this, &MainWindow::onAvatarUpdated);
*ptrUpdateFilesListCounter() = 0;
connect(ui->widget_diff_view, &FileDiffWidget::textcodecChanged, [&](){ updateDiffView(); });
if (!global->start_with_shift_key && appsettings()->remember_and_restore_window_position) {
Qt::WindowStates state = windowState();
MySettings settings;
settings.beginGroup("MainWindow");
bool maximized = settings.value("Maximized").toBool();
restoreGeometry(settings.value("Geometry").toByteArray());
settings.endGroup();
if (maximized) {
state |= Qt::WindowMaximized;
setWindowState(state);
}
}
ui->action_sidebar->setChecked(true);
startTimers();
}
MainWindow::~MainWindow()
{
stopPtyProcess();
getAvatarLoader()->stop();
m->remote_watcher.quit();
m->remote_watcher.wait();
delete m;
delete ui;
}
void MainWindow::notifyRemoteChanged(bool f)
{
postUserFunctionEvent([&](QVariant const &v){
setRemoteChanged(v.toBool());
updateButton();
}, QVariant(f));
}
void MainWindow::postStartEvent()
{
QTimer::singleShot(100, [&](){
QApplication::postEvent(this, new StartEvent);
});
}
bool MainWindow::shown()
{
m->repos_panel_width = ui->stackedWidget_leftpanel->width();
ui->stackedWidget_leftpanel->setCurrentWidget(ui->page_repos);
ui->action_repositories_panel->setChecked(true);
{
MySettings settings;
{
settings.beginGroup("Remote");
bool f = settings.value("Online", true).toBool();
settings.endGroup();
setRemoteOnline(f, false);
}
{
settings.beginGroup("MainWindow");
int n = settings.value("FirstColumnWidth", 50).toInt();
if (n < 10) n = 50;
ui->tableWidget_log->setColumnWidth(0, n);
settings.endGroup();
}
}
updateUI();
postStartEvent(); // 開始イベント
return true;
}
bool MainWindow::isUninitialized()
{
return !misc::isExecutable(appsettings()->git_command) || !misc::isExecutable(appsettings()->file_command);
}
void MainWindow::onStartEvent()
{
if (isUninitialized()) { // gitコマンドの有効性チェック
if (!execWelcomeWizardDialog()) { // ようこそダイアログを表示
close(); // キャンセルされたらプログラム終了
}
}
if (isUninitialized()) { // 正しく初期設定されたか
postStartEvent(); // 初期設定されなかったら、もういちどようこそダイアログを出す。
} else {
// 外部コマンド登録
setGitCommand(appsettings()->git_command, false);
setFileCommand(appsettings()->file_command, false);
setGpgCommand(appsettings()->gpg_command, false);
setSshCommand(appsettings()->ssh_command, false);
// メインウィンドウのタイトルを設定
updateWindowTitle(git());
// プログラムバーション表示
writeLog(AboutDialog::appVersion() + '\n');
// gitコマンドバージョン表示
logGitVersion();
}
}
void MainWindow::startTimers()
{
// interval 10ms
connect(&m->interval_10ms_timer, &QTimer::timeout, [&](){
const int ms = 10;
auto *p1 = ptrUpdateCommitTableCounter();
if (*p1 > 0) {
if (*p1 > ms) {
*p1 -= ms;
} else {
*p1 = 0;
ui->tableWidget_log->viewport()->update();
}
}
auto *p2 = ptrUpdateFilesListCounter();
if (*p2 > 0) {
if (*p2 > ms) {
*p2 -= ms;
} else {
*p2 = 0;
updateCurrentFilesList();
}
}
});
m->interval_10ms_timer.setInterval(10);
m->interval_10ms_timer.start();
startTimer(10);
}
void MainWindow::setCurrentLogRow(int row)
{
if (row >= 0 && row < ui->tableWidget_log->rowCount()) {
ui->tableWidget_log->setCurrentCell(row, 2);
ui->tableWidget_log->setFocus();
updateStatusBarText();
}
}
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
QEvent::Type et = event->type();
if (et == QEvent::KeyPress) {
if (QApplication::activeModalWidget()) {
// thru
} else {
auto *e = dynamic_cast<QKeyEvent *>(event);
Q_ASSERT(e);
int k = e->key();
if (k == Qt::Key_Escape) {
if (centralWidget()->isAncestorOf(qApp->focusWidget())) {
ui->treeWidget_repos->setFocus();
return true;
}
}
if (e->modifiers() & Qt::ControlModifier) {
if (k == Qt::Key_Up || k == Qt::Key_Down) {
int rows = ui->tableWidget_log->rowCount();
int row = ui->tableWidget_log->currentRow();
if (k == Qt::Key_Up) {
if (row > 0) {
row--;
}
} else if (k == Qt::Key_Down) {
if (row + 1 < rows) {
row++;
}
}
ui->tableWidget_log->setCurrentCell(row, 0);
return true;
}
}
if (watched == ui->treeWidget_repos) {
if (k == Qt::Key_Enter || k == Qt::Key_Return) {
openSelectedRepository();
return true;
}
if (!(e->modifiers() & Qt::ControlModifier)) {
if (k >= 0 && k < 128 && QChar((uchar)k).isLetterOrNumber()) {
appendCharToRepoFilter(k);
return true;
}
if (k == Qt::Key_Backspace) {
backspaceRepoFilter();
return true;
}
if (k == Qt::Key_Escape) {
clearRepoFilter();
return true;
}
}
} else if (watched == ui->tableWidget_log) {
if (k == Qt::Key_Home) {
setCurrentLogRow(0);
return true;
}
if (k == Qt::Key_Escape) {
ui->treeWidget_repos->setFocus();
return true;
}
} else if (watched == ui->listWidget_files || watched == ui->listWidget_unstaged || watched == ui->listWidget_staged) {
if (k == Qt::Key_Escape) {
ui->tableWidget_log->setFocus();
return true;
}
}
}
} else if (et == QEvent::FocusIn) {
auto SelectItem = [](QListWidget *w){
int row = w->currentRow();
if (row < 0) {
row = 0;
w->setCurrentRow(row);
}
w->setItemSelected(w->item(row), true);
w->viewport()->update();
};
// ファイルリストがフォーカスを得たとき、diffビューを更新する。(コンテキストメニュー対応)
if (watched == ui->listWidget_unstaged) {
m->last_focused_file_list = watched;
updateStatusBarText();
updateUnstagedFileCurrentItem();
SelectItem(ui->listWidget_unstaged);
return true;
}
if (watched == ui->listWidget_staged) {
m->last_focused_file_list = watched;
updateStatusBarText();
updateStagedFileCurrentItem();
SelectItem(ui->listWidget_staged);
return true;
}
if (watched == ui->listWidget_files) {
m->last_focused_file_list = watched;
SelectItem(ui->listWidget_files);
return true;
}
}
return false;
}
bool MainWindow::event(QEvent *event)
{
QEvent::Type et = event->type();
if (et == QEvent::KeyPress) {
auto *e = dynamic_cast<QKeyEvent *>(event);
Q_ASSERT(e);
int k = e->key();
if (k == Qt::Key_Escape) {
emit onEscapeKeyPressed();
} else if (k == Qt::Key_Delete) {
if (qApp->focusWidget() == ui->treeWidget_repos) {
removeSelectedRepositoryFromBookmark(true);
return true;
}
}
}
return BasicMainWindow::event(event);
}
void MainWindow::customEvent(QEvent *e)
{
if (e->type() == (QEvent::Type)UserEvent::Start) {
onStartEvent();
return;
}
}
void MainWindow::closeEvent(QCloseEvent *event)
{
MySettings settings;
if (appsettings()->remember_and_restore_window_position) {
setWindowOpacity(0);
Qt::WindowStates state = windowState();
bool maximized = (state & Qt::WindowMaximized) != 0;
if (maximized) {
state &= ~Qt::WindowMaximized;
setWindowState(state);
}
{
settings.beginGroup("MainWindow");
settings.setValue("Maximized", maximized);
settings.setValue("Geometry", saveGeometry());
settings.endGroup();
}
}
{
settings.beginGroup("MainWindow");
settings.setValue("FirstColumnWidth", ui->tableWidget_log->columnWidth(0));
settings.endGroup();
}
QMainWindow::closeEvent(event);
}
void MainWindow::setStatusBarText(QString const &text)
{
m->status_bar_label->setText(text);
}
void MainWindow::clearStatusBarText()
{
setStatusBarText(QString());
}
void MainWindow::onLogVisibilityChanged()
{
ui->action_window_log->setChecked(ui->dockWidget_log->isVisible());
}
void MainWindow::internalWriteLog(char const *ptr, int len)
{
ui->widget_log->logicalMoveToBottom();
ui->widget_log->write(ptr, len, false);
ui->widget_log->setChanged(false);
setInteractionCanceled(false);
}
void MainWindow::buildRepoTree(QString const &group, QTreeWidgetItem *item, QList<RepositoryItem> *repos)
{
QString name = item->text(0);
if (isGroupItem(item)) {
int n = item->childCount();
for (int i = 0; i < n; i++) {
QTreeWidgetItem *child = item->child(i);
QString sub = group / name;
buildRepoTree(sub, child, repos);
}
} else {
RepositoryItem const *repo = repositoryItem(item);
if (repo) {
RepositoryItem newrepo = *repo;
newrepo.name = name;
newrepo.group = group;
item->setData(0, IndexRole, repos->size());
repos->push_back(newrepo);
}
}
}
void MainWindow::refrectRepositories()
{
QList<RepositoryItem> newrepos;
int n = ui->treeWidget_repos->topLevelItemCount();
for (int i = 0; i < n; i++) {
QTreeWidgetItem *item = ui->treeWidget_repos->topLevelItem(i);
buildRepoTree(QString(), item, &newrepos);
}
*getReposPtr() = std::move(newrepos);
saveRepositoryBookmarks();
}
void MainWindow::onRepositoriesTreeDropped()
{
refrectRepositories();
QTreeWidgetItem *item = ui->treeWidget_repos->currentItem();
if (item) item->setExpanded(true);
}
const QPixmap &MainWindow::digitsPixmap() const
{
return m->digits;
}
int MainWindow::digitWidth() const
{
return 5;
}
int MainWindow::digitHeight() const
{
return 7;
}
void MainWindow::drawDigit(QPainter *pr, int x, int y, int n) const
{
int w = digitWidth();
int h = digitHeight();
pr->drawPixmap(x, y, w, h, m->digits, n * w, 0, w, h);
}
QString BasicMainWindow::defaultWorkingDir() const
{
return appsettings()->default_working_dir;
}
QColor MainWindow::color(unsigned int i)
{
unsigned int n = m->graph_color.width();
if (n > 0) {
n--;
if (i > n) i = n;
QRgb const *p = (QRgb const *)m->graph_color.scanLine(0);
return QColor(qRed(p[i]), qGreen(p[i]), qBlue(p[i]));
}
return Qt::black;
}
//QString MainWindow::currentWorkingCopyDir() const
//{
// QString workdir = BasicMainWindow::currentWorkingCopyDir();
// if (workdir.isEmpty()) {
// RepositoryItem const *repo = selectedRepositoryItem();
// if (repo) {
// workdir = repo->local_dir;
// return workdir;
// }
// }
// return workdir;
//}
RepositoryItem const *BasicMainWindow::findRegisteredRepository(QString *workdir) const
{
*workdir = QDir(*workdir).absolutePath();
workdir->replace('\\', '/');
if (Git::isValidWorkingCopy(*workdir)) {
for (RepositoryItem const &item : getRepos()) {
Qt::CaseSensitivity cs = Qt::CaseSensitive;
#ifdef Q_OS_WIN
cs = Qt::CaseInsensitive;
#endif
if (workdir->compare(item.local_dir, cs) == 0) {
return &item;
}
}
}
return nullptr;
}
int MainWindow::repositoryIndex_(QTreeWidgetItem const *item) const
{
if (item) {
int i = item->data(0, IndexRole).toInt();
if (i >= 0 && i < getRepos().size()) {
return i;
}
}
return -1;
}
RepositoryItem const *MainWindow::repositoryItem(QTreeWidgetItem const *item) const
{
int row = repositoryIndex_(item);
QList<RepositoryItem> const &repos = getRepos();
return (row >= 0 && row < repos.size()) ? &repos[row] : nullptr;
}
RepositoryItem const *MainWindow::selectedRepositoryItem() const
{
return repositoryItem(ui->treeWidget_repos->currentItem());
}
static QTreeWidgetItem *newQTreeWidgetItem()
{
auto *item = new QTreeWidgetItem;
item->setSizeHint(0, QSize(20, 20));
return item;
}
QTreeWidgetItem *MainWindow::newQTreeWidgetFolderItem(QString const &name)
{
QTreeWidgetItem *item = newQTreeWidgetItem();
item->setText(0, name);
item->setData(0, IndexRole, GroupItem);
item->setIcon(0, getFolderIcon());
item->setFlags(item->flags() | Qt::ItemIsEditable);
return item;
}
void MainWindow::updateRepositoriesList()
{
QString path = getBookmarksFilePath();
auto *repos = getReposPtr();
*repos = RepositoryBookmark::load(path);
QString filter = getRepositoryFilterText();
ui->treeWidget_repos->clear();
std::map<QString, QTreeWidgetItem *> parentmap;
for (int i = 0; i < repos->size(); i++) {
RepositoryItem const &repo = repos->at(i);
if (!filter.isEmpty() && repo.name.indexOf(filter, 0, Qt::CaseInsensitive) < 0) {
continue;
}
QTreeWidgetItem *parent = nullptr;
{
QString group = repo.group;
if (group.startsWith('/')) {
group = group.mid(1);
}
auto it = parentmap.find(group);
if (it != parentmap.end()) {
parent = it->second;
}
if (!parent) {
QStringList list = group.split('/', QString::SkipEmptyParts);
if (list.isEmpty()) {
list.push_back(tr("Default"));
}
for (QString const &name : list) {
if (name.isEmpty()) continue;
if (!parent) {
auto it = parentmap.find(name);
if (it != parentmap.end()) {
parent = it->second;
} else {
parent = newQTreeWidgetFolderItem(name);
ui->treeWidget_repos->addTopLevelItem(parent);
}
} else {
QTreeWidgetItem *child = newQTreeWidgetFolderItem(name);
parent->addChild(child);
parent = child;
}
parent->setExpanded(true);
}
Q_ASSERT(parent);
parentmap[group] = parent;
}
parent->setData(0, FilePathRole, "");
}
QTreeWidgetItem *child = newQTreeWidgetItem();
child->setText(0, repo.name);
child->setData(0, IndexRole, i);
child->setIcon(0, getRepositoryIcon());
child->setFlags(child->flags() & ~Qt::ItemIsDropEnabled);
parent->addChild(child);
parent->setExpanded(true);
}
}
void MainWindow::showFileList(FilesListType files_list_type)
{
switch (files_list_type) {
case FilesListType::SingleList:
ui->stackedWidget_filelist->setCurrentWidget(ui->page_files);
break;
case FilesListType::SideBySide:
ui->stackedWidget_filelist->setCurrentWidget(ui->page_uncommited);
break;
}
}
void MainWindow::clearFileList()
{
showFileList(FilesListType::SingleList);
ui->listWidget_unstaged->clear();
ui->listWidget_staged->clear();
ui->listWidget_files->clear();
}
void MainWindow::clearDiffView()
{
ui->widget_diff_view->clearDiffView();
}
void MainWindow::clearRepositoryInfo()
{
internalClearRepositoryInfo();
ui->label_repo_name->setText(QString());
ui->label_branch_name->setText(QString());
}
void MainWindow::setRepositoryInfo(QString const &reponame, QString const &brname)
{
ui->label_repo_name->setText(reponame);
ui->label_branch_name->setText(brname);
}
-
void MainWindow::updateFilesList(QString id, bool wait)
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
if (!wait) return;
clearFileList();
// wip submodule support
-#if 0
+#if 1
+ std::vector<Git::Submodule> submodules;
{
+ GitTreeItemList list;
GitObjectCache objcache;
objcache.setup(g);
GitCommit tree;
- tree.parseCommit(&objcache, id);
- Git::Object obj = objcache.catFile(tree.tree_id);
- if (obj.type == Git::Object::Type::TREE) {
- QStringList lines = misc::splitLines(QString::fromUtf8(obj.content));
- for (QString const &line : lines) {
- int i = line.indexOf('\t');
- if (i > 0) {
- struct FileItem {
- int attr = 0;
- enum class Type {
- unknown,
- blob,
- commit,
- };
- Type type = Type::unknown;
- QString id;
- QString name;
- };
- FileItem item;
- item.name = line.mid(i + 1);
- QStringList list = misc::splitWords(line);
- if (list.size() >= 3) {
- item.attr = list[0].toInt();
- QString type = list[1];
- if (type == "blob") {
- item.type = FileItem::Type::blob;
- } else if (type == "commit") {
- item.type = FileItem::Type::commit;
- }
- item.id = list[2];
+ GitCommit::parseCommit(&objcache, id, &tree);
+ parseGitTreeObject(&objcache, tree.tree_id, {}, &list);
+ for (GitTreeItem const &item : list) {
+ if (item.type == GitTreeItem::Type::BLOB && item.name == ".gitmodules") {
+ Git::Object obj = objcache.catFile(item.id);
+ if (!obj.content.isEmpty()) {
+ parseGitSubModules(obj.content, &submodules);
+ }
+ }
+ }
+ for (GitTreeItem const &item : list) {
+ if (item.type == GitTreeItem::Type::BLOB && item.mode == "160000") {
+ for (int i = 0; i < submodules.size(); i++) {
+ if (submodules[i].path == item.name) {
+ submodules[i].id = item.id;
+ break;
}
- qDebug() << item.attr << (int)item.type << item.id << item.name;
}
}
}
- qDebug() << tree.tree_id;
}
+ for (Git::Submodule const &m : submodules) {
+ qDebug() << m.name << m.path << m.url << m.id;
+ }
+
#endif
Git::FileStatusList stats = g->status_s();
setUncommitedChanges(!stats.empty());
FilesListType files_list_type = FilesListType::SingleList;
bool staged = false;
auto AddItem = [&](ObjectData const &data){
QString header = data.header;
if (header.isEmpty()) {
header = "(??\?) "; // damn trigraph
}
QString text = data.path;
if (data.submod) {
text += " : ";
text += data.submod->id.mid(0, 7);
}
QListWidgetItem *item = new QListWidgetItem(text);
item->setSizeHint(QSize(item->sizeHint().width(), 18));
item->setData(FilePathRole, data.path);
item->setData(DiffIndexRole, data.idiff);
item->setData(HunkIndexRole, -1);
item->setData(HeaderRole, header);
switch (files_list_type) {
case FilesListType::SingleList:
ui->listWidget_files->addItem(item);
break;
case FilesListType::SideBySide:
if (staged) {
ui->listWidget_staged->addItem(item);
} else {
ui->listWidget_unstaged->addItem(item);
}
break;
}
};
if (id.isEmpty()) {
bool uncommited = isThereUncommitedChanges();
if (uncommited) {
files_list_type = FilesListType::SideBySide;
}
if (!makeDiff(uncommited ? QString() : id, diffResult())) {
return;
}
std::map<QString, int> diffmap;
for (int idiff = 0; idiff < diffResult()->size(); idiff++) {
Git::Diff const &diff = diffResult()->at(idiff);
QString filename = diff.path;
if (!filename.isEmpty()) {
diffmap[filename] = idiff;
}
}
showFileList(files_list_type);
for (Git::FileStatus const &s : stats) {
staged = (s.isStaged() && s.code_y() == ' ');
int idiff = -1;
QString header;
auto it = diffmap.find(s.path1());
if (it != diffmap.end()) {
idiff = it->second;
}
QString path = s.path1();
if (s.code() == Git::FileStatusCode::Unknown) {
qDebug() << "something wrong...";
} else if (s.code() == Git::FileStatusCode::Untracked) {
// nop
} else if (s.isUnmerged()) {
header += "(unmerged) ";
} else if (s.code() == Git::FileStatusCode::AddedToIndex) {
header = "(add) ";
} else if (s.code_x() == 'D' || s.code_y() == 'D' || s.code() == Git::FileStatusCode::DeletedFromIndex) {
header = "(del) ";
} else if (s.code_x() == 'R' || s.code() == Git::FileStatusCode::RenamedInIndex) {
header = "(ren) ";
path = s.path2(); // renamed newer path
} else if (s.code_x() == 'M' || s.code_y() == 'M') {
header = "(chg) ";
}
ObjectData data;
data.path = path;
data.header = header;
data.idiff = idiff;
AddItem(data);
}
} else {
if (!makeDiff(id, diffResult())) {
return;
}
showFileList(files_list_type);
addDiffItems(diffResult(), AddItem);
}
for (Git::Diff const &diff : *diffResult()) {
QString key = GitDiff::makeKey(diff);
(*getDiffCacheMap())[key] = diff;
}
}
void MainWindow::updateFilesList(Git::CommitItem const &commit, bool wait)
{
QString id;
if (Git::isUncommited(commit)) {
// empty id for uncommited changes
} else {
id = commit.commit_id;
}
updateFilesList(id, wait);
}
void MainWindow::updateCurrentFilesList()
{
auto logs = getLogs();
QTableWidgetItem *item = ui->tableWidget_log->item(selectedLogIndex(), 0);
if (!item) return;
int index = item->data(IndexRole).toInt();
int count = (int)logs.size();
if (index < count) {
updateFilesList(logs[index], true);
}
}
void MainWindow::prepareLogTableWidget()
{
QStringList cols = {
tr("Graph"),
tr("Commit"),
tr("Date"),
tr("Author"),
tr("Message"),
};
int n = cols.size();
ui->tableWidget_log->setColumnCount(n);
ui->tableWidget_log->setRowCount(0);
for (int i = 0; i < n; i++) {
QString const &text = cols[i];
auto *item = new QTableWidgetItem(text);
ui->tableWidget_log->setHorizontalHeaderItem(i, item);
}
updateCommitGraph(); // コミットグラフを更新
}
void MainWindow::detectGitServerType(GitPtr const &g)
{
setServerType(ServerType::Standard);
*ptrGitHub() = GitHubRepositoryInfo();
QString push_url;
QList<Git::Remote> remotes;
g->getRemoteURLs(&remotes);
for (Git::Remote const &r : remotes) {
if (r.purpose == "push") {
push_url = r.url;
}
}
auto Check = [&](QString const &s){
int i = push_url.indexOf(s);
if (i > 0) return i + s.size();
return 0;
};
// check GitHub
int pos = Check("@github.com:");
if (pos == 0) {
pos = Check("://github.com/");
}
if (pos > 0) {
int end = push_url.size();
{
QString s = ".git";
if (push_url.endsWith(s)) {
end -= s.size();
}
}
QString s = push_url.mid(pos, end - pos);
int i = s.indexOf('/');
if (i > 0) {
auto *p = ptrGitHub();
QString user = s.mid(0, i);
QString repo = s.mid(i + 1);
p->owner_account_name = user;
p->repository_name = repo;
}
setServerType(ServerType::GitHub);
}
}
void MainWindow::clearLog()
{
clearLogs();
clearLabelMap();
setUncommitedChanges(false);
ui->tableWidget_log->clearContents();
ui->tableWidget_log->scrollToTop();
}
void MainWindow::openRepository_(GitPtr g, bool keep_selection)
{
getObjCache()->setup(g);
int scroll_pos = -1;
int select_row = -1;
if (keep_selection) {
scroll_pos = ui->tableWidget_log->verticalScrollBar()->value();
select_row = ui->tableWidget_log->currentRow();
}
if (isValidWorkingCopy(g)) {
bool do_fetch = isOnlineMode() && (getForceFetch() || appsettings()->automatically_fetch_when_opening_the_repository);
setForceFetch(false);
if (do_fetch) {
if (!fetch(g, false)) {
return;
}
}
clearLog();
clearRepositoryInfo();
detectGitServerType(g);
updateFilesList(QString(), true);
bool canceled = false;
ui->tableWidget_log->setEnabled(false);
// ログを取得
setLogs(retrieveCommitLog(g));
// ブランチを取得
queryBranches(g);
// タグを取得
ptrTagMap()->clear();
QList<Git::Tag> tags = g->tags();
for (Git::Tag const &tag : tags) {
Git::Tag t = tag;
t.id = getObjCache()->getCommitIdFromTag(t.id);
(*ptrTagMap())[t.id].push_back(t);
}
ui->tableWidget_log->setEnabled(true);
updateCommitTableLater();
if (canceled) return;
QString branch_name;
if (currentBranch().flags & Git::Branch::HeadDetachedAt) {
branch_name += QString("(HEAD detached at %1)").arg(currentBranchName());
}
if (currentBranch().flags & Git::Branch::HeadDetachedFrom) {
branch_name += QString("(HEAD detached from %1)").arg(currentBranchName());
}
if (branch_name.isEmpty()) {
branch_name = currentBranchName();
}
QString repo_name = currentRepositoryName();
setRepositoryInfo(repo_name, branch_name);
} else {
clearLog();
clearRepositoryInfo();
}
if (!g) return;
updateRemoteInfo();
updateWindowTitle(g);
setHeadId(getObjCache()->revParse("HEAD"));
if (isThereUncommitedChanges()) {
Git::CommitItem item;
item.parent_ids.push_back(currentBranch().id);
item.message = tr("Uncommited changes");
auto p = getLogsPtr();
p->insert(p->begin(), item);
}
prepareLogTableWidget();
auto const &logs = getLogs();
const int count = logs.size();
ui->tableWidget_log->setRowCount(count);
int selrow = 0;
for (int row = 0; row < count; row++) {
Git::CommitItem const *commit = &logs[row];
{
auto *item = new QTableWidgetItem;
item->setData(IndexRole, row);
ui->tableWidget_log->setItem(row, 0, item);
}
int col = 1; // カラム0はコミットグラフなので、その次から
auto AddColumn = [&](QString const &text, bool bold, QString const &tooltip){
auto *item = new QTableWidgetItem(text);
if (!tooltip.isEmpty()) {
QString tt = tooltip;
tt.replace('\n', ' ');
tt = tt.toHtmlEscaped();
tt = "<p style='white-space: pre'>" + tt + "</p>";
item->setToolTip(tt);
}
if (bold) {
QFont font = item->font();
font.setBold(true);
item->setFont(font);
}
ui->tableWidget_log->setItem(row, col, item);
col++;
};
QString commit_id;
QString datetime;
QString author;
QString message;
QString message_ex;
bool isHEAD = (commit->commit_id == getHeadId());
bool bold = false;
{
if (Git::isUncommited(*commit)) { // 未コミットの時
bold = true; // 太字
selrow = row;
} else {
if (isHEAD && !isThereUncommitedChanges()) { // HEADで、未コミットがないとき
bold = true; // 太字
selrow = row;
}
commit_id = abbrevCommitID(*commit);
}
datetime = misc::makeDateTimeString(commit->commit_date);
author = commit->author;
message = commit->message;
message_ex = makeCommitInfoText(row, &(*getLabelMap())[row]);
}
AddColumn(commit_id, false, QString());
AddColumn(datetime, false, QString());
AddColumn(author, false, QString());
AddColumn(message, bold, message + message_ex);
ui->tableWidget_log->setRowHeight(row, 24);
}
int t = ui->tableWidget_log->columnWidth(0);
ui->tableWidget_log->resizeColumnsToContents();
ui->tableWidget_log->setColumnWidth(0, t);
ui->tableWidget_log->horizontalHeader()->setStretchLastSection(false);
ui->tableWidget_log->horizontalHeader()->setStretchLastSection(true);
m->last_focused_file_list = nullptr;
ui->tableWidget_log->setFocus();
if (select_row < 0) {
setCurrentLogRow(selrow);
} else {
setCurrentLogRow(select_row);
ui->tableWidget_log->verticalScrollBar()->setValue(scroll_pos >= 0 ? scroll_pos : 0);
}
m->remote_watcher.setCurrent(currentRemoteName(), currentBranchName());
updateUI();
}
void MainWindow::removeSelectedRepositoryFromBookmark(bool ask)
{
int i = indexOfRepository(ui->treeWidget_repos->currentItem());
removeRepositoryFromBookmark(i, ask);
}
void MainWindow::setNetworkingCommandsEnabled(bool enabled)
{
ui->action_clone->setEnabled(enabled);
ui->toolButton_clone->setEnabled(enabled);
if (!Git::isValidWorkingCopy(currentWorkingCopyDir())) {
enabled = false;
}
bool opened = !currentRepository().name.isEmpty();
ui->action_fetch->setEnabled(enabled || opened);
ui->toolButton_fetch->setEnabled(enabled || opened);
if (isOnlineMode()) {
ui->action_fetch->setText(tr("Fetch"));
ui->toolButton_fetch->setText(tr("Fetch"));
} else {
ui->action_fetch->setText(tr("Update"));
ui->toolButton_fetch->setText(tr("Update"));
}
ui->action_fetch_prune->setEnabled(enabled);
ui->action_pull->setEnabled(enabled);
ui->action_push->setEnabled(enabled);
ui->action_push_u->setEnabled(enabled);
ui->action_push_all_tags->setEnabled(enabled);
ui->toolButton_pull->setEnabled(enabled);
ui->toolButton_push->setEnabled(enabled);
}
void MainWindow::updateUI()
{
setNetworkingCommandsEnabled(isOnlineMode());
ui->toolButton_fetch->setDot(getRemoteChanged());
Git::Branch b = currentBranch();
ui->toolButton_push->setNumber(b.ahead > 0 ? b.ahead : -1);
ui->toolButton_pull->setNumber(b.behind > 0 ? b.behind : -1);
{
bool f = isRepositoryOpened();
ui->toolButton_status->setEnabled(f);
ui->toolButton_terminal->setEnabled(f);
ui->toolButton_explorer->setEnabled(f);
ui->action_repository_status->setEnabled(f);
ui->action_terminal->setEnabled(f);
ui->action_explorer->setEnabled(f);
}
}
void MainWindow::updateStatusBarText()
{
QString text;
QWidget *w = qApp->focusWidget();
if (w == ui->treeWidget_repos) {
RepositoryItem const *repo = selectedRepositoryItem();
if (repo) {
text = QString("%1 : %2")
.arg(repo->name)
.arg(misc::normalizePathSeparator(repo->local_dir))
;
}
} else if (w == ui->tableWidget_log) {
QTableWidgetItem *item = ui->tableWidget_log->item(selectedLogIndex(), 0);
if (item) {
auto const &logs = getLogs();
int row = item->data(IndexRole).toInt();
if (row < (int)logs.size()) {
Git::CommitItem const &commit = logs[row];
if (Git::isUncommited(commit)) {
text = tr("Uncommited changes");
} else {
QString id = commit.commit_id;
text = QString("%1 : %2%3")
.arg(id.mid(0, 7))
.arg(commit.message)
.arg(makeCommitInfoText(row, nullptr))
;
}
}
}
}
setStatusBarText(text);
}
void MainWindow::mergeBranch(QString const &commit, Git::MergeFastForward ff)
{
if (commit.isEmpty()) return;
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
g->mergeBranch(commit, ff);
openRepository(true);
}
void MainWindow::mergeBranch(Git::CommitItem const *commit, Git::MergeFastForward ff)
{
if (!commit) return;
mergeBranch(commit->commit_id, ff);
}
void MainWindow::rebaseBranch(Git::CommitItem const *commit)
{
if (!commit) return;
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
QString text = tr("Are you sure you want to rebase the commit ?");
text += "\n\n";
text += "> git rebase " + commit->commit_id;
int r = QMessageBox::information(this, tr("Rebase"), text, QMessageBox::Ok, QMessageBox::Cancel);
if (r == QMessageBox::Ok) {
g->rebaseBranch(commit->commit_id);
openRepository(true);
}
}
void MainWindow::cherrypick(Git::CommitItem const *commit)
{
if (!commit) return;
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
int n = commit->parent_ids.size();
if (n == 1) {
g->cherrypick(commit->commit_id);
} else if (n > 1) {
Git::CommitItem head;
Git::CommitItem pick;
g->queryCommit(g->rev_parse("HEAD"), &head);
g->queryCommit(commit->commit_id, &pick);
QList<Git::CommitItem> parents;
for (int i = 0; i < n; i++) {
QString id = commit->commit_id + QString("^%1").arg(i + 1);
id = g->rev_parse(id);
Git::CommitItem item;
g->queryCommit(id, &item);
parents.push_back(item);
}
CherryPickDialog dlg(this, head, pick, parents);
if (dlg.exec() == QDialog::Accepted) {
QString cmd = "-m %1 ";
cmd = cmd.arg(dlg.number());
if (dlg.allowEmpty()) {
cmd += "--allow-empty ";
}
cmd += commit->commit_id;
g->cherrypick(cmd);
} else {
return;
}
}
openRepository(true);
}
void MainWindow::merge(Git::CommitItem const *commit)
{
if (isThereUncommitedChanges()) return;
if (!commit) {
int row = selectedLogIndex();
commit = commitItem(row);
if (!commit) return;
}
if (!Git::isValidID(commit->commit_id)) return;
static const char MergeFastForward[] = "MergeFastForward";
QString fastforward;
{
MySettings s;
s.beginGroup("Behavior");
fastforward = s.value(MergeFastForward).toString();
s.endGroup();
}
std::vector<QString> labels;
{
int row = selectedLogIndex();
QList<Label> const *v = label(row);
for (Label const &label : *v) {
if (label.kind == Label::LocalBranch || label.kind == Label::Tag) {
labels.push_back(label.text);
}
}
std::sort(labels.begin(), labels.end());
labels.erase(std::unique(labels.begin(), labels.end()), labels.end());
}
labels.push_back(commit->commit_id);
QString branch_name = currentBranchName();
MergeDialog dlg(fastforward, labels, branch_name, this);
if (dlg.exec() == QDialog::Accepted) {
fastforward = dlg.getFastForwardPolicy();
{
MySettings s;
s.beginGroup("Behavior");
s.setValue(MergeFastForward, fastforward);
s.endGroup();
}
QString from = dlg.mergeFrom();
mergeBranch(from, MergeDialog::ff(fastforward));
}
}
void MainWindow::showStatus()
{
auto g = git();
if (!g->isValidWorkingCopy()) {
msgNoRepositorySelected();
return;
}
QString s = g->status();
TextEditDialog dlg(this);
dlg.setWindowTitle(tr("Status"));
dlg.setText(s, true);
dlg.exec();
}
void MainWindow::on_action_commit_triggered()
{
commit();
}
void MainWindow::on_action_fetch_triggered()
{
if (isOnlineMode()) {
reopenRepository(true, [&](GitPtr g){
fetch(g, false);
});
} else {
updateRepository();
}
}
void MainWindow::on_action_fetch_prune_triggered()
{
if (!isOnlineMode()) return;
reopenRepository(true, [&](GitPtr g){
fetch(g, true);
});
}
void MainWindow::on_action_push_triggered()
{
push();
}
void MainWindow::on_action_pull_triggered()
{
if (!isOnlineMode()) return;
reopenRepository(true, [&](GitPtr g){
setPtyCondition(PtyCondition::Pull);
setPtyProcessOk(true);
g->pull(getPtyProcess());
while (1) {
if (getPtyProcess()->wait(1)) break;
QApplication::processEvents();
}
});
}
void MainWindow::on_toolButton_push_clicked()
{
ui->action_push->trigger();
}
void MainWindow::on_toolButton_pull_clicked()
{
ui->action_pull->trigger();
}
void MainWindow::on_toolButton_status_clicked()
{
showStatus();
}
void MainWindow::on_action_repository_status_triggered()
{
showStatus();
}
void MainWindow::on_treeWidget_repos_currentItemChanged(QTreeWidgetItem * /*current*/, QTreeWidgetItem * /*previous*/)
{
updateStatusBarText();
}
void MainWindow::on_treeWidget_repos_itemDoubleClicked(QTreeWidgetItem * /*item*/, int /*column*/)
{
openSelectedRepository();
}
void BasicMainWindow::execCommitPropertyDialog(QWidget *parent, Git::CommitItem const *commit)
{
CommitPropertyDialog dlg(parent, this, commit);
dlg.exec();
}
int MainWindow::indexOfRepository(QTreeWidgetItem const *treeitem) const
{
if (!treeitem) return -1;
return treeitem->data(0, IndexRole).toInt();
}
void MainWindow::on_treeWidget_repos_customContextMenuRequested(const QPoint &pos)
{
QTreeWidgetItem *treeitem = ui->treeWidget_repos->currentItem();
if (!treeitem) return;
RepositoryItem const *repo = repositoryItem(treeitem);
int index = indexOfRepository(treeitem);
if (isGroupItem(treeitem)) { // group item
QMenu menu;
QAction *a_add_new_group = menu.addAction(tr("&Add new group"));
QAction *a_delete_group = menu.addAction(tr("&Delete group"));
QAction *a_rename_group = menu.addAction(tr("&Rename group"));
QPoint pt = ui->treeWidget_repos->mapToGlobal(pos);
QAction *a = menu.exec(pt + QPoint(8, -8));
if (a) {
if (a == a_add_new_group) {
QTreeWidgetItem *child = newQTreeWidgetFolderItem(tr("New group"));
treeitem->addChild(child);
child->setFlags(child->flags() | Qt::ItemIsEditable);
ui->treeWidget_repos->setCurrentItem(child);
return;
}
if (a == a_delete_group) {
QTreeWidgetItem *parent = treeitem->parent();
if (parent) {
int i = parent->indexOfChild(treeitem);
delete parent->takeChild(i);
} else {
int i = ui->treeWidget_repos->indexOfTopLevelItem(treeitem);
delete ui->treeWidget_repos->takeTopLevelItem(i);
}
refrectRepositories();
return;
}
if (a == a_rename_group) {
ui->treeWidget_repos->editItem(treeitem);
return;
}
}
} else if (repo) { // repository item
QString open_terminal = tr("Open &terminal");
QString open_commandprompt = tr("Open command promp&t");
QMenu menu;
QAction *a_open = menu.addAction(tr("&Open"));
menu.addSeparator();
#ifdef Q_OS_WIN
QAction *a_open_terminal = menu.addAction(open_commandprompt);
(void)open_terminal;
#else
QAction *a_open_terminal = menu.addAction(open_terminal);
(void)open_commandprompt;
#endif
a_open_terminal->setIcon(QIcon(":/image/terminal.svg"));
QAction *a_open_folder = menu.addAction(tr("Open &folder"));
a_open_folder->setIcon(QIcon(":/image/explorer.svg"));
menu.addSeparator();
QAction *a_remove = menu.addAction(tr("&Remove"));
menu.addSeparator();
QAction *a_properties = addMenuActionProperty(&menu);
QPoint pt = ui->treeWidget_repos->mapToGlobal(pos);
QAction *a = menu.exec(pt + QPoint(8, -8));
if (a) {
if (a == a_open) {
openSelectedRepository();
return;
}
if (a == a_open_folder) {
openExplorer(repo);
return;
}
if (a == a_open_terminal) {
openTerminal(repo);
return;
}
if (a == a_remove) {
removeRepositoryFromBookmark(index, true);
return;
}
if (a == a_properties) {
execRepositoryPropertyDialog(*repo);
return;
}
}
}
}
void MainWindow::on_tableWidget_log_customContextMenuRequested(const QPoint &pos)
{
int row = selectedLogIndex();
Git::CommitItem const *commit = commitItem(row);
if (commit) {
bool is_valid_commit_id = Git::isValidID(commit->commit_id);
QMenu menu;
QAction *a_copy_id_7letters = is_valid_commit_id ? menu.addAction(tr("Copy commit id (7 letters)")) : nullptr;
QAction *a_copy_id_complete = is_valid_commit_id ? menu.addAction(tr("Copy commit id (completely)")) : nullptr;
std::set<QAction *> copy_label_actions;
{
QList<BasicMainWindow::Label> v = sortedLabels(row);
if (!v.isEmpty()) {
auto *copy_lebel_menu = menu.addMenu("Copy label");
for (BasicMainWindow::Label const &l : v) {
QAction *a = copy_lebel_menu->addAction(l.text);
copy_label_actions.insert(copy_label_actions.end(), a);
}
}
}
menu.addSeparator();
QAction *a_checkout = menu.addAction(tr("Checkout/Branch..."));
menu.addSeparator();
QAction *a_edit_message = nullptr;
auto canEditMessage = [&](){
if (commit->has_child) return false; // 子がないこと
if (Git::isUncommited(*commit)) return false; // 未コミットがないこと
bool is_head = false;
bool has_remote_branch = false;
QList<Label> const *labels = label(row);
for (const Label &label : *labels) {
if (label.kind == Label::Head) {
is_head = true;
} else if (label.kind == Label::RemoteBranch) {
has_remote_branch = true;
}
}
return is_head && !has_remote_branch; // HEAD && リモートブランチ無し
};
if (canEditMessage()) {
a_edit_message = menu.addAction(tr("Edit message..."));
}
QAction *a_merge = is_valid_commit_id ? menu.addAction(tr("Merge")) : nullptr;
QAction *a_rebase = is_valid_commit_id ? menu.addAction(tr("Rebase")) : nullptr;
QAction *a_cherrypick = is_valid_commit_id ? menu.addAction(tr("Cherry-pick")) : nullptr;
QAction *a_edit_tags = is_valid_commit_id ? menu.addAction(tr("Edit tags...")) : nullptr;
QAction *a_revert = is_valid_commit_id ? menu.addAction(tr("Revert")) : nullptr;
menu.addSeparator();
QAction *a_delbranch = is_valid_commit_id ? menu.addAction(tr("Delete branch...")) : nullptr;
QAction *a_delrembranch = remoteBranches(commit->commit_id, nullptr).isEmpty() ? nullptr : menu.addAction(tr("Delete remote branch..."));
menu.addSeparator();
QAction *a_explore = is_valid_commit_id ? menu.addAction(tr("Explore")) : nullptr;
QAction *a_properties = addMenuActionProperty(&menu);
QAction *a = menu.exec(ui->tableWidget_log->viewport()->mapToGlobal(pos) + QPoint(8, -8));
if (a) {
if (a == a_copy_id_7letters) {
qApp->clipboard()->setText(commit->commit_id.mid(0, 7));
return;
}
if (a == a_copy_id_complete) {
qApp->clipboard()->setText(commit->commit_id);
return;
}
if (a == a_properties) {
execCommitPropertyDialog(this, commit);
return;
}
if (a == a_edit_message) {
commitAmend();
return;
}
if (a == a_checkout) {
checkout(this, commit);
return;
}
if (a == a_delbranch) {
deleteBranch(commit);
return;
}
if (a == a_delrembranch) {
deleteRemoteBranch(commit);
return;
}
if (a == a_merge) {
merge(commit);
return;
}
if (a == a_rebase) {
rebaseBranch(commit);
return;
}
if (a == a_cherrypick) {
cherrypick(commit);
return;
}
if (a == a_edit_tags) {
ui->action_edit_tags->trigger();
return;
}
if (a == a_revert) {
revertCommit();
return;
}
if (a == a_explore) {
execCommitExploreWindow(this, commit);
return;
}
if (copy_label_actions.find(a) != copy_label_actions.end()) {
QString text = a->text();
QApplication::clipboard()->setText(text);
return;
}
}
}
}
void MainWindow::on_listWidget_files_customContextMenuRequested(const QPoint &pos)
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
QMenu menu;
QAction *a_delete = menu.addAction(tr("Delete"));
QAction *a_untrack = menu.addAction(tr("Untrack"));
QAction *a_history = menu.addAction(tr("History"));
QAction *a_blame = menu.addAction(tr("Blame"));
QAction *a_properties = addMenuActionProperty(&menu);
QPoint pt = ui->listWidget_files->mapToGlobal(pos) + QPoint(8, -8);
QAction *a = menu.exec(pt);
if (a) {
QListWidgetItem *item = ui->listWidget_files->currentItem();
if (a == a_delete) {
if (askAreYouSureYouWantToRun("Delete", tr("Delete selected files."))) {
for_each_selected_files([&](QString const &path){
g->removeFile(path);
g->chdirexec([&](){
QFile(path).remove();
return true;
});
});
openRepository(false);
}
} else if (a == a_untrack) {
if (askAreYouSureYouWantToRun("Untrack", tr("rm --cached files"))) {
for_each_selected_files([&](QString const &path){
g->rm_cached(path);
});
openRepository(false);
}
} else if (a == a_history) {
execFileHistory(item);
} else if (a == a_blame) {
blame(item);
} else if (a == a_properties) {
execFilePropertyDialog(item);
}
}
}
void MainWindow::on_listWidget_unstaged_customContextMenuRequested(const QPoint &pos)
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
QList<QListWidgetItem *> items = ui->listWidget_unstaged->selectedItems();
if (!items.isEmpty()) {
QMenu menu;
QAction *a_stage = menu.addAction(tr("Stage"));
QAction *a_reset_file = menu.addAction(tr("Reset"));
QAction *a_ignore = menu.addAction(tr("Ignore"));
QAction *a_delete = menu.addAction(tr("Delete"));
QAction *a_untrack = menu.addAction(tr("Untrack"));
QAction *a_history = menu.addAction(tr("History"));
QAction *a_blame = menu.addAction(tr("Blame"));
QAction *a_properties = addMenuActionProperty(&menu);
QPoint pt = ui->listWidget_unstaged->mapToGlobal(pos) + QPoint(8, -8);
QAction *a = menu.exec(pt);
if (a) {
QListWidgetItem *item = ui->listWidget_unstaged->currentItem();
if (a == a_stage) {
for_each_selected_files([&](QString const &path){
g->stage(path);
});
updateCurrentFilesList();
return;
}
if (a == a_reset_file) {
QStringList paths;
for_each_selected_files([&](QString const &path){
paths.push_back(path);
});
resetFile(paths);
return;
}
if (a == a_ignore) {
QString gitignore_path = currentWorkingCopyDir() / ".gitignore";
if (items.size() == 1) {
QString file = getFilePath(items[0]);
EditGitIgnoreDialog dlg(this, gitignore_path, file);
if (dlg.exec() == QDialog::Accepted) {
QString appending = dlg.text();
if (!appending.isEmpty()) {
QString text;
QString path = gitignore_path;
path.replace('/', QDir::separator());
{
QFile file(path);
if (file.open(QFile::ReadOnly)) {
text += QString::fromUtf8(file.readAll());
}
}
size_t n = text.size();
if (n > 0 && text[(int)n - 1] != '\n') {
text += '\n'; // 最後に改行を追加
}
text += appending + '\n';
{
QFile file(path);
if (file.open(QFile::WriteOnly)) {
file.write(text.toUtf8());
}
}
updateCurrentFilesList();
return;
}
} else {
return;
}
}
QString append;
for_each_selected_files([&](QString const &path){
if (path == ".gitignore") {
// skip
} else {
append += path + '\n';
}
});
if (TextEditDialog::editFile(this, gitignore_path, ".gitignore", append)) {
updateCurrentFilesList();
}
return;
}
if (a == a_delete) {
if (askAreYouSureYouWantToRun("Delete", "Delete selected files.")) {
for_each_selected_files([&](QString const &path){
g->removeFile(path);
g->chdirexec([&](){
QFile(path).remove();
return true;
});
});
openRepository(false);
}
return;
}
if (a == a_untrack) {
if (askAreYouSureYouWantToRun("Untrack", "rm --cached")) {
for_each_selected_files([&](QString const &path){
g->rm_cached(path);
});
openRepository(false);
}
return;
}
if (a == a_history) {
execFileHistory(item);
return;
}
if (a == a_blame) {
blame(item);
return;
}
if (a == a_properties) {
execFilePropertyDialog(item);
return;
}
}
}
}
void MainWindow::on_listWidget_staged_customContextMenuRequested(const QPoint &pos)
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
QListWidgetItem *item = ui->listWidget_staged->currentItem();
if (item) {
QString path = getFilePath(item);
QString fullpath = currentWorkingCopyDir() / path;
if (QFileInfo(fullpath).isFile()) {
QMenu menu;
QAction *a_unstage = menu.addAction(tr("Unstage"));
QAction *a_history = menu.addAction(tr("History"));
QAction *a_blame = menu.addAction(tr("Blame"));
QAction *a_properties = addMenuActionProperty(&menu);
QPoint pt = ui->listWidget_staged->mapToGlobal(pos) + QPoint(8, -8);
QAction *a = menu.exec(pt);
if (a) {
QListWidgetItem *item = ui->listWidget_unstaged->currentItem();
if (a == a_unstage) {
g->unstage(path);
openRepository(false);
} else if (a == a_history) {
execFileHistory(item);
} else if (a == a_blame) {
blame(item);
} else if (a == a_properties) {
execFilePropertyDialog(item);
}
}
}
}
}
QStringList MainWindow::selectedFiles_(QListWidget *listwidget) const
{
QStringList list;
QList<QListWidgetItem *> items = listwidget->selectedItems();
for (QListWidgetItem *item : items) {
QString path = getFilePath(item);
list.push_back(path);
}
return list;
}
QStringList MainWindow::selectedFiles() const
{
if (m->last_focused_file_list == ui->listWidget_files) return selectedFiles_(ui->listWidget_files);
if (m->last_focused_file_list == ui->listWidget_staged) return selectedFiles_(ui->listWidget_staged);
if (m->last_focused_file_list == ui->listWidget_unstaged) return selectedFiles_(ui->listWidget_unstaged);
return QStringList();
}
void MainWindow::for_each_selected_files(std::function<void(QString const&)> const &fn)
{
for (QString const &path : selectedFiles()) {
fn(path);
}
}
void BasicMainWindow::execFileHistory(QListWidgetItem *item)
{
if (item) {
QString path = getFilePath(item);
if (!path.isEmpty()) {
execFileHistory(path);
}
}
}
void MainWindow::doLogCurrentItemChanged()
{
clearFileList();
int row = selectedLogIndex();
QTableWidgetItem *item = ui->tableWidget_log->item(row, 0);
if (item) {
auto const &logs = getLogs();
int index = item->data(IndexRole).toInt();
if (index < (int)logs.size()) {
updateStatusBarText();
*ptrUpdateFilesListCounter() = 200;
}
} else {
row = -1;
}
updateAncestorCommitMap();
ui->tableWidget_log->viewport()->update();
}
void MainWindow::findNext()
{
if (m->search_text.isEmpty()) {
return;
}
auto const &logs = getLogs();
for (int pass = 0; pass < 2; pass++) {
int row = 0;
if (pass == 0) {
row = selectedLogIndex();
if (row < 0) {
row = 0;
} else if (m->searching) {
row++;
}
}
while (row < (int)logs.size()) {
Git::CommitItem const commit = logs[row];
if (!Git::isUncommited(commit)) {
if (commit.message.indexOf(m->search_text, 0, Qt::CaseInsensitive) >= 0) {
bool b = ui->tableWidget_log->blockSignals(true);
setCurrentLogRow(row);
ui->tableWidget_log->blockSignals(b);
m->searching = true;
return;
}
}
row++;
}
}
}
void MainWindow::findText(QString const &text)
{
m->search_text = text;
}
bool MainWindow::isAncestorCommit(QString const &id)
{
auto it = m->ancestors.find(id);
return it != m->ancestors.end();
}
void MainWindow::updateAncestorCommitMap()
{
m->ancestors.clear();
auto const &logs = getLogs();
const size_t LogCount = logs.size();
const size_t index = selectedLogIndex();
if (index < LogCount) {
// ok
} else {
return;
}
auto *logsp = getLogsPtr();
auto LogItem = [&](size_t i)->Git::CommitItem &{ return logsp->at(i); };
std::map<QString, size_t> commit_to_index_map;
size_t end = LogCount;
if (index < end) {
for (size_t i = index; i < end; i++) {
Git::CommitItem const &commit = LogItem(i);
commit_to_index_map[commit.commit_id] = i;
auto *item = ui->tableWidget_log->item(i, 0);
QRect r = ui->tableWidget_log->visualItemRect(item);
if (r.y() >= ui->tableWidget_log->height()) {
end = i + 1;
break;
}
}
}
Git::CommitItem *item = &LogItem(index);
if (item) {
m->ancestors.insert(m->ancestors.end(), item->commit_id);
}
for (size_t i = index; i < end; i++) {
Git::CommitItem *item = &LogItem(i);
if (isAncestorCommit(item->commit_id)) {
for (QString const &parent : item->parent_ids) {
m->ancestors.insert(m->ancestors.end(), parent);
}
}
}
}
void MainWindow::on_action_open_existing_working_copy_triggered()
{
QString dir = defaultWorkingDir();
dir = QFileDialog::getExistingDirectory(this, tr("Add existing working copy"), dir);
addWorkingCopyDir(dir, false);
}
void MainWindow::on_action_view_refresh_triggered()
{
openRepository(true);
}
void MainWindow::on_tableWidget_log_currentItemChanged(QTableWidgetItem * /*current*/, QTableWidgetItem * /*previous*/)
{
doLogCurrentItemChanged();
m->searching = false;
}
void MainWindow::on_toolButton_stage_clicked()
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
g->stage(selectedFiles());
updateCurrentFilesList();
}
void MainWindow::on_toolButton_unstage_clicked()
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
g->unstage(selectedFiles());
updateCurrentFilesList();
}
void MainWindow::on_toolButton_select_all_clicked()
{
if (ui->listWidget_unstaged->count() > 0) {
ui->listWidget_unstaged->setFocus();
ui->listWidget_unstaged->selectAll();
} else if (ui->listWidget_staged->count() > 0) {
ui->listWidget_staged->setFocus();
ui->listWidget_staged->selectAll();
}
}
void MainWindow::on_toolButton_commit_clicked()
{
ui->action_commit->trigger();
}
void MainWindow::on_action_edit_global_gitconfig_triggered()
{
QString dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
QString path = dir / ".gitconfig";
editFile(path, ".gitconfig");
}
void MainWindow::on_action_edit_git_config_triggered()
{
QString dir = currentWorkingCopyDir();
QString path = dir / ".git/config";
editFile(path, ".git/config");
}
void MainWindow::on_action_edit_gitignore_triggered()
{
QString dir = currentWorkingCopyDir();
QString path = dir / ".gitignore";
if (editFile(path, ".gitignore")) {
updateCurrentFilesList();
}
}
int MainWindow::selectedLogIndex() const
{
auto const &logs = getLogs();
int i = ui->tableWidget_log->currentRow();
if (i >= 0 && i < (int)logs.size()) {
return i;
}
return -1;
}
void MainWindow::updateDiffView(QListWidgetItem *item)
{
clearDiffView();
m->last_selected_file_item = item;
if (!item) return;
int idiff = indexOfDiff(item);
if (idiff >= 0 && idiff < diffResult()->size()) {
Git::Diff const &diff = diffResult()->at(idiff);
QString key = GitDiff::makeKey(diff);
auto it = getDiffCacheMap()->find(key);
if (it != getDiffCacheMap()->end()) {
auto const &logs = getLogs();
int row = ui->tableWidget_log->currentRow();
bool uncommited = (row >= 0 && row < (int)logs.size() && Git::isUncommited(logs[row]));
ui->widget_diff_view->updateDiffView(it->second, uncommited);
}
}
}
void MainWindow::updateDiffView()
{
updateDiffView(m->last_selected_file_item);
}
void MainWindow::updateUnstagedFileCurrentItem()
{
updateDiffView(ui->listWidget_unstaged->currentItem());
}
void MainWindow::updateStagedFileCurrentItem()
{
updateDiffView(ui->listWidget_staged->currentItem());
}
void MainWindow::on_listWidget_unstaged_currentRowChanged(int /*currentRow*/)
{
updateUnstagedFileCurrentItem();
}
void MainWindow::on_listWidget_staged_currentRowChanged(int /*currentRow*/)
{
updateStagedFileCurrentItem();
}
void MainWindow::on_listWidget_files_currentRowChanged(int /*currentRow*/)
{
updateDiffView(ui->listWidget_files->currentItem());
}
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
if (QApplication::modalWindow()) return;
if (event->mimeData()->hasUrls()) {
event->acceptProposedAction();
event->accept();
}
}
void MainWindow::timerEvent(QTimerEvent *)
{
bool running = getPtyProcess()->isRunning();
if (ui->toolButton_stop_process->isEnabled() != running) {
ui->toolButton_stop_process->setEnabled(running);
ui->action_stop_process->setEnabled(running);
setNetworkingCommandsEnabled(!running);
}
if (!running) {
setInteractionMode(InteractionMode::None);
}
while (1) {
char tmp[1024];
int len = getPtyProcess()->readOutput(tmp, sizeof(tmp));
if (len < 1) break;
writeLog(tmp, len);
}
}
void MainWindow::keyPressEvent(QKeyEvent *event)
{
int c = event->key();
if (c == Qt::Key_T && (event->modifiers() & Qt::ControlModifier)) {
test();
return;
}
if (QApplication::focusWidget() == ui->widget_log) {
auto write_char = [&](char c){
if (getPtyProcess()->isRunning()) {
getPtyProcess()->writeInput(&c, 1);
}
};
auto write_text = [&](QString const &str){
std::string s = str.toStdString();
for (char i : s) {
write_char(i);
}
};
if (c == Qt::Key_Return || c == Qt::Key_Enter) {
write_char('\n');
} else {
QString text = event->text();
write_text(text);
}
}
}
void MainWindow::on_action_edit_settings_triggered()
{
SettingsDialog dlg(this);
if (dlg.exec() == QDialog::Accepted) {
ApplicationSettings const &newsettings = dlg.settings();
setAppSettings(newsettings);
setGitCommand(appsettings()->git_command, false);
setFileCommand(appsettings()->file_command, false);
setGpgCommand(appsettings()->gpg_command, false);
setRemoteMonitoringEnabled(true);
}
}
void MainWindow::onCloneCompleted(bool success, QVariant const &userdata)
{
if (success) {
RepositoryItem r = userdata.value<RepositoryItem>();
saveRepositoryBookmark(r);
setCurrentRepository(r, false);
openRepository(true);
}
}
void MainWindow::onPtyProcessCompleted(bool /*ok*/, QVariant const &userdata)
{
switch (getPtyCondition()) {
case PtyCondition::Clone:
onCloneCompleted(getPtyProcessOk(), userdata);
break;
}
setPtyCondition(PtyCondition::None);
}
void MainWindow::on_action_clone_triggered()
{
clone();
}
void MainWindow::on_action_about_triggered()
{
AboutDialog dlg(this);
dlg.exec();
}
void MainWindow::on_toolButton_clone_clicked()
{
ui->action_clone->trigger();
}
void MainWindow::on_toolButton_fetch_clicked()
{
ui->action_fetch->trigger();
}
void MainWindow::clearRepoFilter()
{
ui->lineEdit_filter->clear();
}
void MainWindow::appendCharToRepoFilter(ushort c)
{
if (QChar(c).isLetter()) {
c = QChar(c).toLower().unicode();
}
ui->lineEdit_filter->setText(getRepositoryFilterText() + c);
}
void MainWindow::backspaceRepoFilter()
{
QString s = getRepositoryFilterText();
int n = s.size();
if (n > 0) {
s = s.mid(0, n - 1);
}
ui->lineEdit_filter->setText(s);
}
void MainWindow::on_lineEdit_filter_textChanged(QString const &text)
{
setRepositoryFilterText(text);
updateRepositoriesList();
}
void MainWindow::on_toolButton_erase_filter_clicked()
{
clearRepoFilter();
ui->lineEdit_filter->setFocus();
}
void MainWindow::deleteTags(QStringList const &tagnames)
{
int row = ui->tableWidget_log->currentRow();
internalDeleteTags(tagnames);
ui->tableWidget_log->selectRow(row);
}
void MainWindow::revertCommit()
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
Git::CommitItem const *commit = selectedCommitItem();
if (commit) {
g->revert(commit->commit_id);
openRepository(false);
}
}
bool MainWindow::addTag(QString const &name)
{
int row = ui->tableWidget_log->currentRow();
bool ok = internalAddTag(name);
ui->tableWidget_log->selectRow(row);
return ok;
}
void MainWindow::on_action_push_all_tags_triggered()
{
reopenRepository(false, [&](GitPtr g){
g->push(true);
});
}
void MainWindow::on_tableWidget_log_itemDoubleClicked(QTableWidgetItem *)
{
Git::CommitItem const *commit = selectedCommitItem();
if (commit) {
execCommitPropertyDialog(this, commit);
}
}
void MainWindow::on_listWidget_unstaged_itemDoubleClicked(QListWidgetItem * item)
{
execFilePropertyDialog(item);
}
void MainWindow::on_listWidget_staged_itemDoubleClicked(QListWidgetItem *item)
{
execFilePropertyDialog(item);
}
void MainWindow::on_listWidget_files_itemDoubleClicked(QListWidgetItem *item)
{
execFilePropertyDialog(item);
}
QListWidgetItem *MainWindow::currentFileItem() const
{
QListWidget *listwidget = nullptr;
if (ui->stackedWidget_filelist->currentWidget() == ui->page_uncommited) {
QWidget *w = qApp->focusWidget();
if (w == ui->listWidget_unstaged) {
listwidget = ui->listWidget_unstaged;
} else if (w == ui->listWidget_staged) {
listwidget = ui->listWidget_staged;
}
} else {
listwidget = ui->listWidget_files;
}
if (listwidget) {
return listwidget->currentItem();
}
return nullptr;
}
void MainWindow::on_action_set_config_user_triggered()
{
Git::User global_user;
Git::User repo_user;
GitPtr g = git();
if (isValidWorkingCopy(g)) {
repo_user = g->getUser(Git::Source::Local);
}
global_user = g->getUser(Git::Source::Global);
execSetUserDialog(global_user, repo_user, currentRepositoryName());
}
void MainWindow::showLogWindow(bool show)
{
ui->dockWidget_log->setVisible(show);
}
void MainWindow::on_action_window_log_triggered(bool checked)
{
showLogWindow(checked);
}
void MainWindow::on_action_repo_jump_triggered()
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
NamedCommitList items = namedCommitItems(Branches | Tags | Remotes);
{
NamedCommitItem head;
head.name = "HEAD";
head.id = getHeadId();
items.push_front(head);
}
JumpDialog dlg(this, items);
if (dlg.exec() == QDialog::Accepted) {
QString text = dlg.text();
if (text.isEmpty()) return;
QString id = g->rev_parse(text);
if (id.isEmpty() && Git::isValidID(text)) {
QStringList list = findGitObject(text);
if (list.isEmpty()) {
QMessageBox::warning(this, tr("Jump"), QString("%1\n\n").arg(text) + tr("No such commit"));
return;
}
ObjectBrowserDialog dlg2(this, list);
if (dlg2.exec() == QDialog::Accepted) {
id = dlg2.text();
if (id.isEmpty()) return;
}
}
if (g->objectType(id) == "tag") {
id = getObjCache()->getCommitIdFromTag(id);
}
int row = rowFromCommitId(id);
if (row < 0) {
QMessageBox::warning(this, tr("Jump"), QString("%1\n(%2)\n\n").arg(text).arg(id) + tr("No such commit"));
} else {
setCurrentLogRow(row);
}
}
}
void MainWindow::on_action_repo_checkout_triggered()
{
checkout();
}
void MainWindow::on_action_delete_branch_triggered()
{
deleteBranch();
}
void MainWindow::on_toolButton_terminal_clicked()
{
openTerminal(nullptr);
}
void MainWindow::on_toolButton_explorer_clicked()
{
openExplorer(nullptr);
}
void MainWindow::on_action_reset_HEAD_1_triggered()
{
GitPtr g = git();
if (!isValidWorkingCopy(g)) return;
g->reset_head1();
openRepository(false);
}
void MainWindow::on_action_create_a_repository_triggered()
{
createRepository(QString());
}
bool MainWindow::isOnlineMode() const
{
return m->is_online_mode;
}
void MainWindow::setRemoteOnline(bool f, bool save)
{
m->is_online_mode = f;
{
QRadioButton *rb = nullptr;
rb = f ? ui->radioButton_remote_online : ui->radioButton_remote_offline;
rb->blockSignals(true);
rb->click();
rb->blockSignals(false);
ui->action_online->setCheckable(true);
ui->action_offline->setCheckable(true);
ui->action_online->setChecked(f);
ui->action_offline->setChecked(!f);
setNetworkingCommandsEnabled(f);
}
if (save) {
MySettings s;
s.beginGroup("Remote");
s.setValue("Online", f);
s.endGroup();
}
}
void MainWindow::on_radioButton_remote_online_clicked()
{
setRemoteOnline(true, true);
}
void MainWindow::on_radioButton_remote_offline_clicked()
{
setRemoteOnline(false, true);
}
void MainWindow::on_verticalScrollBar_log_valueChanged(int)
{
ui->widget_log->refrectScrollBar();
}
void MainWindow::on_horizontalScrollBar_log_valueChanged(int)
{
ui->widget_log->refrectScrollBar();
}
void MainWindow::on_toolButton_stop_process_clicked()
{
abortPtyProcess();
}
void MainWindow::on_action_stop_process_triggered()
{
abortPtyProcess();
}
void MainWindow::on_action_exit_triggered()
{
close();
}
void MainWindow::on_action_reflog_triggered()
{
GitPtr g = git();
Git::ReflogItemList reflog;
g->reflog(&reflog);
ReflogWindow dlg(this, this, reflog);
dlg.exec();
}
void MainWindow::blame(QListWidgetItem *item)
{
QList<BlameItem> list;
QString path = getFilePath(item);
{
GitPtr g = git();
QByteArray ba = g->blame(path);
if (!ba.isEmpty()) {
char const *begin = ba.data();
char const *end = begin + ba.size();
list = BlameWindow::parseBlame(begin, end);
}
}
if (!list.isEmpty()) {
qApp->setOverrideCursor(Qt::WaitCursor);
BlameWindow win(this, path, list);
qApp->restoreOverrideCursor();
win.exec();
}
}
void MainWindow::blame()
{
blame(currentFileItem());
}
void MainWindow::on_action_repository_property_triggered()
{
execRepositoryPropertyDialog(currentRepository());
}
void MainWindow::on_action_set_gpg_signing_triggered()
{
GitPtr g = git();
QString global_key_id = g->signingKey(Git::Source::Global);
QString repository_key_id;
if (g->isValidWorkingCopy()) {
repository_key_id = g->signingKey(Git::Source::Local);
}
SetGpgSigningDialog dlg(this, currentRepositoryName(), global_key_id, repository_key_id);
if (dlg.exec() == QDialog::Accepted) {
g->setSigningKey(dlg.id(), dlg.isGlobalChecked());
}
}
void MainWindow::execAreYouSureYouWantToContinueConnectingDialog()
{
using TheDlg = AreYouSureYouWantToContinueConnectingDialog;
setInteractionMode(InteractionMode::Busy);
QApplication::restoreOverrideCursor();
TheDlg dlg(this);
if (dlg.exec() == QDialog::Accepted) {
TheDlg::Result r = dlg.result();
if (r == TheDlg::Result::Yes) {
getPtyProcess()->writeInput("yes\n", 4);
} else {
setPtyProcessOk(false); // abort
getPtyProcess()->writeInput("no\n", 3);
QThread::msleep(300);
stopPtyProcess();
}
} else {
ui->widget_log->setFocus();
setInteractionCanceled(true);
}
setInteractionMode(InteractionMode::Busy);
}
void MainWindow::onLogIdle()
{
if (interactionCanceled()) return;
if (interactionMode() != InteractionMode::None) return;
static char const are_you_sure_you_want_to_continue_connecting[] = "Are you sure you want to continue connecting (yes/no)?";
static char const enter_passphrase[] = "Enter passphrase: ";
static char const enter_passphrase_for_key[] = "Enter passphrase for key '";
static char const fatal_authentication_failed_for[] = "fatal: Authentication failed for '";
std::vector<char> vec;
ui->widget_log->retrieveLastText(&vec, 100);
if (!vec.empty()) {
std::string line;
int n = (int)vec.size();
int i = n;
while (i > 0) {
i--;
if (i + 1 < n && vec[i] == '\n') {
i++;
line.assign(&vec[i], n - i);
break;
}
}
if (!line.empty()) {
auto ExecLineEditDialog = [&](QWidget *parent, QString const &title, QString const &prompt, QString const &val, bool password){
LineEditDialog dlg(parent, title, prompt, val, password);
if (dlg.exec() == QDialog::Accepted) {
std::string ret = dlg.text().toStdString();
std::string str = ret + '\n';
getPtyProcess()->writeInput(str.c_str(), str.size());
return ret;
}
abortPtyProcess();
return std::string();
};
auto Match = [&](char const *str){
int n = strlen(str);
if (strncmp(line.c_str(), str, n) == 0) {
char const *p = line.c_str() + n;
while (1) {
if (!*p) return true;
if (!isspace((unsigned char)*p)) break;
p++;
}
}
return false;
};
auto StartsWith = [&](char const *str){
char const *p = line.c_str();
while (*str) {
if (*p != *str) return false;
str++;
p++;
}
return true;
};
if (Match(are_you_sure_you_want_to_continue_connecting)) {
execAreYouSureYouWantToContinueConnectingDialog();
return;
}
if (line == enter_passphrase) {
ExecLineEditDialog(this, "Passphrase", QString::fromStdString(line), QString(), true);
return;
}
if (StartsWith(enter_passphrase_for_key)) {
std::string keyfile;
{
int i = strlen(enter_passphrase_for_key);
char const *p = line.c_str() + i;
char const *q = strrchr(p, ':');
if (q && p + 2 < q && q[-1] == '\'') {
keyfile.assign(p, q - 1);
}
}
if (!keyfile.empty()) {
if (keyfile == sshPassphraseUser() && !sshPassphrasePass().empty()) {
std::string text = sshPassphrasePass() + '\n';
getPtyProcess()->writeInput(text.c_str(), text.size());
} else {
std::string secret = ExecLineEditDialog(this, "Passphrase for key", QString::fromStdString(line), QString(), true);
sshSetPassphrase(keyfile, secret);
}
return;
}
}
char const *begin = line.c_str();
char const *end = line.c_str() + line.size();
auto Input = [&](QString const &title, bool password, std::string *value){
Q_ASSERT(value);
std::string header = QString("%1 for '").arg(title).toStdString();
if (strncmp(begin, header.c_str(), header.size()) == 0) {
QString msg;
if (memcmp(end - 2, "':", 2) == 0) {
msg = QString::fromUtf8(begin, end - begin - 1);
} else if (memcmp(end - 3, "': ", 3) == 0) {
msg = QString::fromUtf8(begin, end - begin - 2);
}
if (!msg.isEmpty()) {
std::string s = ExecLineEditDialog(this, title, msg, value ? QString::fromStdString(*value) : QString(), password);
*value = s;
return true;
}
}
return false;
};
std::string uid = httpAuthenticationUser();
std::string pwd = httpAuthenticationPass();
bool ok = false;
if (Input("Username", false, &uid)) ok = true;
if (Input("Password", true, &pwd)) ok = true;
if (ok) {
httpSetAuthentication(uid, pwd);
return;
}
if (StartsWith(fatal_authentication_failed_for)) {
QMessageBox::critical(this, tr("Authentication Failed"), QString::fromStdString(line));
abortPtyProcess();
return;
}
}
}
}
void MainWindow::on_action_edit_tags_triggered()
{
Git::CommitItem const *commit = selectedCommitItem();
if (commit && Git::isValidID(commit->commit_id)) {
EditTagsDialog dlg(this, commit);
dlg.exec();
}
}
void MainWindow::on_action_push_u_triggered()
{
pushSetUpstream(false);
}
void MainWindow::on_action_delete_remote_branch_triggered()
{
deleteRemoteBranch(selectedCommitItem());
}
void MainWindow::on_action_terminal_triggered()
{
auto const *repo = &currentRepository();
openTerminal(repo);
}
void MainWindow::on_action_explorer_triggered()
{
auto const *repo = &currentRepository();
openExplorer(repo);
}
void MainWindow::on_action_reset_hard_triggered()
{
doGitCommand([&](GitPtr g){
g->reset_hard();
});
}
void MainWindow::on_action_clean_df_triggered()
{
doGitCommand([&](GitPtr g){
g->clean_df();
});
}
void MainWindow::postOpenRepositoryFromGitHub(QString const &username, QString const &reponame)
{
QVariantList list;
list.push_back(username);
list.push_back(reponame);
postUserFunctionEvent([&](QVariant const &v){
QVariantList l = v.toList();
QString uname = l[0].toString();
QString rname = l[1].toString();
CloneFromGitHubDialog dlg(this, uname, rname);
if (dlg.exec() == QDialog::Accepted) {
clone(dlg.url());
}
}, QVariant(list));
}
void MainWindow::on_action_stash_triggered()
{
doGitCommand([&](GitPtr g){
g->stash();
});
}
void MainWindow::on_action_stash_apply_triggered()
{
doGitCommand([&](GitPtr g){
g->stash_apply();
});
}
void MainWindow::on_action_stash_drop_triggered()
{
doGitCommand([&](GitPtr g){
g->stash_drop();
});
}
void MainWindow::on_action_online_triggered()
{
ui->radioButton_remote_online->click();
}
void MainWindow::on_action_offline_triggered()
{
ui->radioButton_remote_offline->click();
}
void MainWindow::on_action_repositories_panel_triggered()
{
bool checked = ui->action_repositories_panel->isChecked();
ui->stackedWidget_leftpanel->setCurrentWidget(checked ? ui->page_repos : ui->page_collapsed);
if (checked) {
ui->stackedWidget_leftpanel->setFixedWidth(m->repos_panel_width);
ui->stackedWidget_leftpanel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
ui->stackedWidget_leftpanel->setMinimumWidth(QWIDGETSIZE_MAX);
ui->stackedWidget_leftpanel->setMaximumWidth(QWIDGETSIZE_MAX);
} else {
m->repos_panel_width = ui->stackedWidget_leftpanel->width();
ui->stackedWidget_leftpanel->setFixedWidth(24);
}
}
void MainWindow::on_action_find_triggered()
{
m->searching = false;
if (getLogs().empty()) {
return;
}
FindCommitDialog dlg(this, m->search_text);
if (dlg.exec() == QDialog::Accepted) {
m->search_text = dlg.text();
ui->tableWidget_log->setFocus();
findNext();
}
}
void MainWindow::on_action_find_next_triggered()
{
if (m->search_text.isEmpty()) {
on_action_find_triggered();
} else {
findNext();
}
}
void MainWindow::on_action_repo_jump_to_head_triggered()
{
QString name = "HEAD";
GitPtr g = git();
QString id = g->rev_parse(name);
int row = rowFromCommitId(id);
if (row < 0) {
qDebug() << "No such commit";
} else {
setCurrentLogRow(row);
}
}
void MainWindow::on_action_repo_merge_triggered()
{
merge();
}
void MainWindow::on_action_expand_commit_log_triggered()
{
ui->splitter_h->setSizes({10000, 1, 1});
}
void MainWindow::on_action_expand_file_list_triggered()
{
ui->splitter_h->setSizes({1, 10000, 1});
}
void MainWindow::on_action_expand_diff_view_triggered()
{
ui->splitter_h->setSizes({1, 1, 10000});
}
void MainWindow::on_action_sidebar_triggered()
{
bool f = ui->stackedWidget_leftpanel->isVisible();
f = !f;
ui->stackedWidget_leftpanel->setVisible(f);
ui->action_sidebar->setChecked(f);
}
#if 0
void MainWindow::on_action_wide_triggered()
{
QWidget *w = focusWidget();
if (w == m->focused_widget) {
ui->splitter_h->setSizes(m->splitter_h_sizes);
m->focused_widget = nullptr;
} else {
m->focused_widget = w;
m->splitter_h_sizes = ui->splitter_h->sizes();
if (w == ui->tableWidget_log) {
ui->splitter_h->setSizes({10000, 1, 1});
} else if (ui->stackedWidget_filelist->isAncestorOf(w)) {
ui->splitter_h->setSizes({1, 10000, 1});
} else if (ui->frame_diff_view->isAncestorOf(w)) {
ui->splitter_h->setSizes({1, 1, 10000});
}
}
}
#endif
void MainWindow::setShowLabels(bool show, bool save)
{
ApplicationSettings *as = appsettings();
as->show_labels = show;
bool b = ui->action_show_labels->blockSignals(true);
ui->action_show_labels->setChecked(show);
ui->action_show_labels->blockSignals(b);
if (save) {
saveApplicationSettings();
}
}
bool MainWindow::isLabelsVisible() const
{
return appsettings()->show_labels;
}
void MainWindow::on_action_show_labels_triggered()
{
bool f = ui->action_show_labels->isChecked();
setShowLabels(f, true);
ui->tableWidget_log->viewport()->update();
}
void MainWindow::test()
{
QElapsedTimer t;
t.start();
{
QPixmap pm(1, 1);
QPainter pr(&pm);
pr.setFont(QFont("MS Gothic", 30));
char tmp[2];
for (int i = 0x20; i < 0x80; i++) {
tmp[0] = i;
tmp[1] = 0;
QString s = tmp;
int w = pr.fontMetrics().size(0, s).width();
qDebug() << w;
}
}
qDebug() << QString("%1ms").arg(t.elapsed());
}
void MainWindow::on_action_submodules_triggered()
{
GitPtr g = git();
std::vector<Git::Submodule> mods = g->submodules();
std::vector<SubmodulesDialog::Submodule> mods2;
mods2.resize(mods.size());
for (size_t i = 0; i < mods.size(); i++) {
const Git::Submodule mod = mods[i];
mods2[i].submodule = mod;
GitPtr g2 = git(g->workingRepositoryDir() / mod.path, g->sshKey());
g2->queryCommit(mod.id, &mods2[i].head);
}
SubmodulesDialog dlg(this, mods2);
dlg.exec();
}
void MainWindow::on_action_submodule_add_triggered()
{
QString dir = currentRepository().local_dir;
submodule_add({}, dir);
}
void MainWindow::on_action_submodule_update_triggered()
{
SubmoduleUpdateDialog dlg(this);
if (dlg.exec() == QDialog::Accepted) {
GitPtr g = git();
Git::SubmoduleUpdateData data;
data.init = dlg.isInit();
data.recursive = dlg.isRecursive();
g->submodule_update(data, getPtyProcess());
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Feb 7, 2:42 PM (10 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55943
Default Alt Text
(162 KB)

Event Timeline