#include "ide.h" struct RepoDiff : DiffDlg { FrameTop pane; DropList r, branch; Button copy_hash, copy_log; Button github; String repo_dir; int kind; bool modal = false; int line = 0; void LoadGit(); void Load(); void Set(const String& f); void SyncTitle(); typedef RepoDiff CLASSNAME; RepoDiff(); }; void RepoDiff::SyncTitle() { String h = GetFileName(editfile); String m = ~r.GetValue(); int q = m.FindAfter("@b \1"); if(q >= 0) { m = m.Mid(q); q = m.Find('\1'); if(q >= 0) h << " " << m.Mid(0, q); } Title(h); } void RepoDiff::Set(const String& f) { repo_dir = f; kind = GetRepo(repo_dir); editfile = f; if(kind == SVN_DIR) { pane << r.SizePos(); WaitCursor wait; String log = HostSys("svn log " + f); if(log.IsVoid()) { Exclamation("Error executing 'svn log'"); return; } StringStream ss(log); while(!ss.IsEof()) { String l = ss.GetLine(); if(l[0] == 'r') { Vector h = Split(l, '|'); if(h.GetCount() > 3) { String rev = TrimBoth(h[0]); String s = rev; Vector t = Split(h[2], ' '); if(t.GetCount() > 1) s << ' ' << t[0]; s << ' ' << TrimBoth(h[1]); while(!ss.IsEof()) { l = ss.GetLine(); if(l.GetCount()) { if(l[0] != '-') s << ": " << l; break; } } r.Add(rev, s); } } } if(r.GetCount() == 0) { Exclamation("No parsable history for the file"); return; } if(r.GetCount()) r.SetIndex(0); Load(); } if(kind == GIT_DIR) { pane << branch.LeftPos(0, Zx(100)).VSizePos() << r.HSizePos(Zx(100) + DPI(2), 2 * Zx(80) + DPI(4) + DPI(20)).VSizePos() << copy_hash.RightPos(0, Zx(79)).VSizePos() << copy_log.RightPos(Zx(80), Zx(79)).VSizePos() << github.RightPos(2 * Zx(80) + DPI(2), DPI(20)).VSizePos(); copy_hash.SetLabel("Copy Hash"); auto GetHash = [=] { String h = ~~r; String commit, path; SplitTo(h, ':', commit, path); return commit; }; copy_hash << [=] { WriteClipboardText(GetHash()); }; copy_log.SetLabel("Copy Log"); copy_log << [=] { CopyGitRevisions(r); }; github.SetImage(IdeImg::GitHub()); String origin = HostSys("git -C " + GetFileFolder(f) + " remote get-url origin"); if(origin.StartsWith("https://github.com/")) { github.Enable(); origin.TrimEnd("\n"); origin.TrimEnd("\r"); origin.TrimEnd(".git"); github << [=] { LaunchWebBrowser(origin + "/commit/" + GetHash()); }; } else github.Disable(); LoadBranches(branch, GetFileFolder(f)); LoadGit(); } DiffDlg::Set(f); SyncTitle(); } void LoadBranches(DropList& branch, const String& dir) { branch.Clear(); String branches = GitCmd(dir, "branch --all"); StringStream ss(branches); String author, date, commit; int ci = -1; auto Add = [&](const String& l) { String s = l.Mid(2); int q = s.ReverseFind('/'); if(q >= 0) branch.Add(s, s.Mid(q)); else branch.Add(s); }; while(!ss.IsEof()) { String l = ss.GetLine(); if(l.StartsWith("* ")) { ci = branch.GetCount(); Add(l); } if(l.StartsWith(" ")) Add(l); } if(ci >= 0) branch.SetIndex(ci); } void CopyGitRevisions(const DropList& dl) { String qtf; for(int i = 0; i < dl.GetCount(); i++) { String s = ~dl.GetValue(i); s.TrimStart("\1"); MergeWith(qtf, "&", s); } RichText txt = ParseQTF(qtf); ClearClipboard(); AppendClipboard(pick(txt)); } void LoadGitRevisions(DropList& r, const String& dir, const String& branch, const String& file) { String gitcmd = "log --format=medium --date=short --name-only "; if(file.GetCount()) gitcmd << " --follow "; gitcmd << branch; if(file.GetCount()) gitcmd << " -- " << GetFileName(file); String log = GitCmd(dir, gitcmd); StringStream ss(log); String author, date, commit, path, msg; r.ClearList(); auto AddCommit = [&] { if(commit.GetCount()) { String h = commit; if(h.GetCount() > 6) h.Trim(6); r.Add(IsNull(file) ? commit : commit + ":" + path, "\1[g [@b \1" + date + "\1] [@g \1" + h + "\1] [@r \1" + author + "\1]: " "[* \1" + Join(Split(msg, CharFilterWhitespace), " ") + "\1]"); } date = commit = author = msg = Null; }; while(!ss.IsEof()) { String l = TrimBoth(ss.GetLine()); if(l.GetCount() == 0 && msg.GetCount() == 0) { while(!ss.IsEof()) { l = ss.GetLine(); if(l.GetCount()) break; } msg = l; while(!ss.IsEof()) { l = ss.GetLine(); if(l.GetCount() == 0) break; msg << l; } } else if(l.TrimStart("Author:")) { int q = l.Find('<'); author = TrimBoth(q >= 0 ? l.Mid(0, q) : l); } else if(l.TrimStart("Date:")) date = TrimBoth(l); else if(l.TrimStart("commit")) { AddCommit(); commit = TrimBoth(l); } else if(l.GetCount()) path = l; } AddCommit(); if(r.GetCount()) r.SetIndex(0); } void RepoDiff::LoadGit() { LoadGitRevisions(r, GetFileFolder(editfile), ~~branch, editfile); Load(); } void RepoDiff::Load() { if(IsNull(r)) return; if(kind == SVN_DIR) extfile = HostSys("svn cat " + editfile + '@' + AsString(~r)); Vector bl; if(kind == GIT_DIR) { String h = ~~r; String commit, path; SplitTo(h, ':', commit, path); if(path.GetCount()) extfile = GitCmd(repo_dir, "show " + h); else extfile = GitCmd(GetFileFolder(editfile), "show " + commit + ":./" + GetFileName(editfile)); extfile.Clear(); StringStream ss(GitCmd(GetFileFolder(editfile), "blame -p " + GetFileName(editfile) + " " + commit)); VectorMap blame; while(!ss.IsEof()) { String ln = ss.GetLine(); Vector h = Split(ln, ' '); if(h.GetCount() == 0) break; String hash = h[0]; TextCompareCtrl::Blame& b = blame.GetAdd(hash); while(!ss.IsEof()) { String ln = ss.GetLine(); if(ln.TrimStart("\t")) { auto& m = bl.Add(); m = b; m.hash = hash; extfile << ln << "\n"; break; } if(ln.TrimStart("author ")) b.author = ln; else if(ln.TrimStart("summary ")) b.summary = ln; else if(ln.TrimStart("author-time ")) b.time = Time(1970, 1, 1) + Atoi(ln); } } } diff.Set(backup = LoadFile(editfile), extfile); if(bl.GetCount()) { diff.SetPos(4700); diff.right.WhenBlame = [=](const String& hash) { auto FindHash = [&](RepoDiff& d) { for(int i = 0; i < d.r.GetCount(); i++) { String h = d.r.GetKey(i); String commit, path; SplitTo(h, ':', commit, path); if(commit == hash) { d.r.SetIndex(i); d.Load(); break; } } }; if(modal) FindHash(*this); else { RepoDiff *rd = TheIde()->RunRepoDiff(editfile, line); if(rd) FindHash(*rd); } }; } diff.right.PickBlame(pick(bl)); SyncTitle(); } RepoDiff::RepoDiff() { pane.Height(EditField::GetStdHeight()); r.SetDropLines(32); branch.SetDropLines(32); Icon(IdeImg::SvnDiff()); diff.InsertFrameRight(pane); r << [=] { Load(); }; branch << [=] { LoadGit(); }; Sizeable().Zoomable(); serialize_placement = false; Rect r = TheIde()->GetWorkArea(); r.Deflate(DPI(30), DPI(40)); SetRect(r); } void RunRepoDiff(const String& filepath, int line) { if(IsNull(filepath)) return; RepoDiff dlg; dlg.modal = true; dlg.Set(filepath); if(line >= 0) dlg.diff.left.SetCursor(line + 1); dlg.Execute(); } RepoDiff *Ide::RunRepoDiff(const String& filepath, int line) { if(IsNull(filepath)) return nullptr; RepoDiff& dlg = CreateNewWindow(); dlg.line = line; dlg.Set(filepath); if(line >= 0) dlg.diff.left.SetCursor(line + 1); dlg.diff.WhenRightLine = dlg.diff.WhenLeftLine = [=](int line) { EditFile(filepath); editor.SetCursor(editor.GetPos64(line)); editor.SetFocus(); }; dlg.OpenMain(); return &dlg; }