diff --git a/lvc/resources/Lvc.lr8 b/lvc/resources/Lvc.lr8
--- a/lvc/resources/Lvc.lr8
+++ b/lvc/resources/Lvc.lr8
@@ -1,347 +1,421 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lvc/resources/resdefs.h b/lvc/resources/resdefs.h
--- a/lvc/resources/resdefs.h
+++ b/lvc/resources/resdefs.h
@@ -1,76 +1,82 @@
// Generated by LgiRes
// This file generated by LgiRes
#define IDC_COMMIT_AND_PUSH 3
#define IDC_BRANCH 11
#define IDC_BRANCH_DROPDOWN 14
#define IDC_MSG 16
#define IDD_OPTIONS 22
#define IDC_OPTIONS_TABLE 23
#define IDC_SVN 25
#define IDC_SVN_BROWSE 26
#define IDC_GIT 28
#define IDC_GIT_BROWSE 29
#define IDC_HG 32
#define IDC_CVS 33
#define IDC_HG_BROWSE 36
#define IDC_CVS_BROWSE 37
#define IDC_UNTRACKED 38
#define IDC_PULL_ALL 39
#define IDC_STATUS 40
#define IDC_UPDATE 41
#define IDC_BRANCHES 45
#define IDC_BUILD_FIX 47
#define IDD_REMOTE_FOLDER 61
#define IDC_62 62
#define IDC_HOSTS 64
#define IDC_65 65
#define IDC_HOSTNAME 67
#define IDC_USER 68
#define IDC_PASS 69
#define IDC_DELETE 70
#define IDC_REMOTE_PATH 74
#define IDC_SVN_LIMIT 79
#define IDC_GIT_LIMIT 80
#define IDC_HG_LIMIT 81
#define IDC_CVS_LIMIT 82
#define IDS_CREATE_NEW_BRANCH 84
#define IDS_ERR_NO_IMPL_FOR_TYPE 85
#define IDS_ERR_MERGE_FAILED 86
#define IDS_ERR_NO_VCS_FOUND 87
#define IDS_ERR_ADD_FAILED 88
#define IDS_ERR_REVERT_FAILED 89
#define IDC_AUTHOR 91
#define IDC_EDIT_AUTHOR 92
+#define IDD_AUTHOR 93
+#define IDD_ 94
+#define IDC_LOCAL_NAME 95
+#define IDC_LOCAL_EMAIL 104
+#define IDC_GLOBAL_NAME 105
+#define IDC_GLOBAL_EMAIL 106
#define IDC_TABLE 500
#define IDC_COMMIT 501
#define IDC_PULL 502
#define IDC_PUSH 503
#define IDC_COMMIT_TABLE 504
#define IDM_PATCH_VIEWER 505
#define IDM_MENU_506 506
#define IDD_COMMIT 507
#define IDD_TOOLBAR 508
#define IDC_OPEN 509
#define IDM_FILE 510
#define IDM_OPTIONS 511
#define IDM_HELP 512
#define IDM_ABOUT 513
#define IDM_VIEW_MENU 514
#define IDM_EDIT_MENU 515
#define IDM_FIND 516
#define IDM_REFRESH 517
#define IDM_UNTRACKED 518
#define IDM_MENU_519 519
#define IDM_EXIT 520
#define IDM_REPO 521
#define IDM_PULL 522
#define IDM_PUSH 523
#define IDM_STATUS 524
#define IDM_MENU_525 525
#define IDM_UPDATE_SUBS 526
#define IDM_OPEN_LOCAL 527
#define IDM_OPEN_REMOTE 528
#define IDM_MENU_529 529
#define IDM_OPEN_DIFF 530
diff --git a/lvc/src/Main.cpp b/lvc/src/Main.cpp
--- a/lvc/src/Main.cpp
+++ b/lvc/src/Main.cpp
@@ -1,1987 +1,2045 @@
#include "lgi/common/Lgi.h"
#include "lgi/common/TableLayout.h"
#include "lgi/common/TextLog.h"
#include "lgi/common/Button.h"
#include "lgi/common/XmlTreeUi.h"
#include "lgi/common/Tree.h"
#include "lgi/common/FileSelect.h"
#include "lgi/common/StructuredLog.h"
#include "lgi/common/PopupNotification.h"
#include "Lvc.h"
#include "resdefs.h"
#ifdef WINDOWS
#include "resource.h"
#endif
//////////////////////////////////////////////////////////////////
const char *AppName = "Lvc";
#define DEFAULT_BUILD_FIX_MSG "Build fix."
#define OPT_Hosts "Hosts"
#define OPT_Host "Host"
AppPriv::~AppPriv()
{
if (CurFolder)
CurFolder->Empty();
}
#if HAS_LIBSSH
SshConnection *AppPriv::GetConnection(const char *Uri, bool Create)
{
LUri u(Uri);
u.sPath.Empty();
auto s = u.ToString();
auto Conn = Connections.Find(s);
if (!Conn && Create)
Connections.Add(s, Conn = new SshConnection(Log, s, "matthew@*$ "));
return Conn;
}
#endif
VersionCtrl AppPriv::DetectVcs(VcFolder *Fld)
{
char p[MAX_PATH_LEN];
LUri u = Fld->GetUri();
if (!u.IsFile() || !u.sPath)
{
#if HAS_LIBSSH
auto c = GetConnection(u.ToString());
if (!c)
return VcError;
auto type = c->Types.Find(u.sPath);
if (type)
return type;
c->DetectVcs(Fld);
Fld->GetCss(true)->Color(LColour::Blue);
Fld->Update();
return VcPending;
#else
return VcError;
#endif
}
auto Path = u.sPath.Get();
#ifdef WINDOWS
if (*Path == '/')
Path++;
#endif
if (LMakePath(p, sizeof(p), Path, ".git") &&
LDirExists(p))
return VcGit;
if (LMakePath(p, sizeof(p), Path, ".svn") &&
LDirExists(p))
return VcSvn;
if (LMakePath(p, sizeof(p), Path, ".hg") &&
LDirExists(p))
return VcHg;
if (LMakePath(p, sizeof(p), Path, "CVS") &&
LDirExists(p))
return VcCvs;
return VcNone;
}
class DiffView : public LTextLog
{
public:
DiffView(int id) : LTextLog(id)
{
}
void PourStyle(size_t Start, ssize_t Length)
{
for (auto ln : LTextView3::Line)
{
if (!ln->c.IsValid())
{
char16 *t = Text + ln->Start;
if (*t == '+')
{
ln->c = LColour::Green;
ln->Back.Rgb(245, 255, 245);
}
else if (*t == '-')
{
ln->c = LColour::Red;
ln->Back.Rgb(255, 245, 245);
}
else if (*t == '@')
{
ln->c.Rgb(128, 128, 128);
ln->Back.Rgb(235, 235, 235);
}
else
ln->c = LColour(L_TEXT);
}
}
}
};
+class EditAuthor : public LDialog
+{
+ VcFolder *folder = NULL;
+
+public:
+ EditAuthor(VcFolder *f) : folder(f)
+ {
+ SetParent(folder->GetTree());
+ LoadFromResource(IDD_AUTHOR);
+
+ f->GetAuthor(true, [this](auto name, auto email)
+ {
+ SetCtrlName(IDC_LOCAL_NAME, name);
+ SetCtrlName(IDC_LOCAL_EMAIL, email);
+ });
+
+ f->GetAuthor(false, [this](auto name, auto email)
+ {
+ SetCtrlName(IDC_GLOBAL_NAME, name);
+ SetCtrlName(IDC_GLOBAL_EMAIL, email);
+ });
+ }
+
+ int OnNotify(LViewI *Ctrl, LNotification n)
+ {
+ switch (Ctrl->GetId())
+ {
+ case IDOK:
+ {
+ folder->SetAuthor(true, GetCtrlName(IDC_LOCAL_NAME), GetCtrlName(IDC_LOCAL_EMAIL));
+ folder->SetAuthor(false, GetCtrlName(IDC_GLOBAL_NAME), GetCtrlName(IDC_GLOBAL_EMAIL));
+ // fall through
+ }
+ case IDCANCEL:
+ {
+ EndModal(Ctrl->GetId() == IDOK);
+ break;
+ }
+ }
+
+ return 0;
+ }
+};
+
class ToolBar : public LLayout, public LResourceLoad
{
public:
ToolBar()
{
LString Name;
LRect Pos;
if (LoadFromResource(IDD_TOOLBAR, this, &Pos, &Name))
{
OnPosChange();
}
else LAssert(!"Missing toolbar resource");
}
void OnCreate()
{
AttachChildren();
}
void OnPosChange()
{
LRect Cli = GetClient();
LTableLayout *v;
if (GetViewById(IDC_TABLE, v))
{
v->SetPos(Cli);
v->OnPosChange();
auto r = v->GetUsedArea();
if (r.Y() <= 1)
r.Set(0, 0, 30, 30);
// printf("Used = %s\n", r.GetStr());
LCss::Len NewSz(LCss::LenPx, (float)r.Y()+3);
auto OldSz = GetCss(true)->Height();
if (OldSz != NewSz)
{
GetCss(true)->Height(NewSz);
SendNotify(LNotifyTableLayoutRefresh);
}
}
else LAssert(!"Missing table ctrl");
}
void OnPaint(LSurface *pDC)
{
pDC->Colour(LColour(L_MED));
pDC->Rectangle();
}
};
class CommitCtrls : public LLayout, public LResourceLoad
{
public:
CommitCtrls()
{
LString Name;
LRect Pos;
if (LoadFromResource(IDD_COMMIT, this, &Pos, &Name))
{
LTableLayout *v;
if (GetViewById(IDC_COMMIT_TABLE, v))
{
v->GetCss(true)->PaddingRight("8px");
LRect r = v->GetPos();
r.Offset(-r.x1, -r.y1);
r.x2++;
v->SetPos(r);
v->OnPosChange();
r = v->GetUsedArea();
if (r.Y() <= 1)
r.Set(0, 0, 30, 30);
GetCss(true)->Height(LCss::Len(LCss::LenPx, (float)r.Y()));
}
else LAssert(!"Missing table ctrl");
}
else LAssert(!"Missing toolbar resource");
}
void OnPosChange()
{
LTableLayout *v;
if (GetViewById(IDC_COMMIT_TABLE, v))
v->SetPos(GetClient());
}
void OnCreate()
{
AttachChildren();
}
};
LString::Array GetProgramsInPath(const char *Program)
{
LString::Array Bin;
LString Prog = Program;
#ifdef WINDOWS
Prog += LGI_EXECUTABLE_EXT;
#endif
LString::Array a = LGetPath();
for (auto p : a)
{
LFile::Path c(p, Prog);
if (c.Exists())
Bin.New() = c.GetFull();
}
return Bin;
}
class OptionsDlg : public LDialog, public LXmlTreeUi
{
LOptionsFile &Opts;
public:
OptionsDlg(LViewI *Parent, LOptionsFile &opts) : Opts(opts)
{
SetParent(Parent);
Map("svn-path", IDC_SVN, GV_STRING);
Map("svn-limit", IDC_SVN_LIMIT);
Map("git-path", IDC_GIT, GV_STRING);
Map("git-limit", IDC_GIT_LIMIT);
Map("hg-path", IDC_HG, GV_STRING);
Map("hg-limit", IDC_HG_LIMIT);
Map("cvs-path", IDC_CVS, GV_STRING);
Map("cvs-limit", IDC_CVS_LIMIT);
if (LoadFromResource(IDD_OPTIONS))
{
MoveSameScreen(Parent);
Convert(&Opts, this, true);
}
}
void Browse(int EditId)
{
auto s = new LFileSelect;
s->Parent(this);
s->Open([this, EditId](auto s, auto status)
{
if (status)
SetCtrlName(EditId, s->Name());
delete s;
});
}
void BrowseFiles(LViewI *Ctrl, const char *Bin, int EditId)
{
LRect Pos = Ctrl->GetPos();
LPoint Pt(Pos.x1, Pos.y2 + 1);
PointToScreen(Pt);
LSubMenu s;
LString::Array Bins = GetProgramsInPath(Bin);
for (unsigned i=0; i= 1000)
{
LString Bin = Bins[Cmd - 1000];
if (Bin)
SetCtrlName(EditId, Bin);
}
break;
}
}
}
int OnNotify(LViewI *Ctrl, LNotification n)
{
switch (Ctrl->GetId())
{
case IDC_SVN_BROWSE:
BrowseFiles(Ctrl, "svn", IDC_SVN);
break;
case IDC_GIT_BROWSE:
BrowseFiles(Ctrl, "git", IDC_GIT);
break;
case IDC_HG_BROWSE:
BrowseFiles(Ctrl, "hg", IDC_HG);
break;
case IDC_CVS_BROWSE:
BrowseFiles(Ctrl, "cvs", IDC_CVS);
break;
case IDOK:
Convert(&Opts, this, false);
// fall
case IDCANCEL:
{
EndModal(Ctrl->GetId() == IDOK);
break;
}
}
return LDialog::OnNotify(Ctrl, n);
}
};
int CommitDataCmp(VcCommit **_a, VcCommit **_b)
{
auto a = *_a;
auto b = *_b;
return a->GetTs().Compare(&b->GetTs());
}
LString::Array AppPriv::GetCommitRange()
{
LString::Array r;
if (Commits)
{
LArray Sel;
Commits->GetSelection(Sel);
if (Sel.Length() > 1)
{
Sel.Sort(CommitDataCmp);
r.Add(Sel[0]->GetRev());
r.Add(Sel.Last()->GetRev());
}
else if (Sel.Length() == 1)
{
r.Add(Sel[0]->GetRev());
}
}
else LAssert(!"No commit list ptr");
return r;
}
LArray AppPriv::GetRevs(LString::Array &Revs)
{
LArray a;
for (auto i = Commits->begin(); i != Commits->end(); i++)
{
VcCommit *c = dynamic_cast(*i);
if (c)
{
for (auto r: Revs)
{
if (r.Equals(c->GetRev()))
{
a.Add(c);
break;
}
}
}
}
return a;
}
class CommitList : public LList
{
public:
CommitList(int id) : LList(id, 0, 0, 200, 200)
{
}
void SelectRevisions(LString::Array &Revs, const char *BranchHint = NULL)
{
VcCommit *Scroll = NULL;
LArray Matches;
for (auto i: *this)
{
VcCommit *item = dynamic_cast(i);
if (!item)
continue;
bool IsMatch = false;
for (auto r: Revs)
{
if (item->IsRev(r))
{
IsMatch = true;
break;
}
}
if (IsMatch)
Matches.Add(item);
else if (i->Select())
i->Select(false);
}
for (auto item: Matches)
{
auto b = item->GetBranch();
if (BranchHint)
{
if (!b || Stricmp(b, BranchHint))
continue;
}
else if (b)
{
continue;
}
if (!Scroll)
Scroll = item;
item->Select(true);
}
if (!Scroll && Matches.Length() > 0)
{
Scroll = Matches[0];
Scroll->Select(true);
}
if (Scroll)
Scroll->ScrollTo();
}
bool OnKey(LKey &k)
{
switch (k.c16)
{
case 'p':
case 'P':
{
if (k.Down())
{
LArray Sel;
GetSelection(Sel);
if (Sel.Length())
{
auto first = Sel[0];
auto branch = first->GetBranch();
auto p = first->GetParents();
if (p->Length() == 0)
break;
for (auto c:Sel)
c->Select(false);
SelectRevisions(*p, branch);
}
}
return true;
}
case 'c':
case 'C':
{
if (k.Down())
{
LArray Sel;
GetSelection(Sel);
if (Sel.Length())
{
LHashTbl,VcCommit*> Map;
for (auto s:Sel)
Map.Add(s->GetRev(), s);
LString::Array n;
for (auto it = begin(); it != end(); it++)
{
VcCommit *c = dynamic_cast(*it);
if (c)
{
for (auto r:*c->GetParents())
{
if (Map.Find(r))
{
n.Add(c->GetRev());
break;
}
}
}
}
for (auto c:Sel)
c->Select(false);
SelectRevisions(n, Sel[0]->GetBranch());
}
}
return true;
}
}
return LList::OnKey(k);
}
};
int LstCmp(LListItem *a, LListItem *b, int Col)
{
VcCommit *A = dynamic_cast(a);
VcCommit *B = dynamic_cast(b);
if (A == NULL || B == NULL)
{
return (A ? 1 : -1) - (B ? 1 : -1);
}
auto f = A->GetFolder();
auto flds = f->GetFields();
if (!flds.Length())
{
LgiTrace("%s:%i - No fields?\n", _FL);
return 0;
}
auto fld = flds[Col];
switch (fld)
{
case LGraph:
case LIndex:
case LParents:
case LRevision:
default:
return (int) (B->GetIndex() - A->GetIndex());
case LBranch:
case LAuthor:
case LMessageTxt:
return Stricmp(A->GetFieldText(fld), B->GetFieldText(fld));
case LTime:
return B->GetTs().Compare(&A->GetTs());
}
return 0;
}
struct TestThread : public LThread
{
public:
TestThread() : LThread("test")
{
Run();
}
int Main()
{
auto Path = LGetPath();
LSubProcess p("python", "/Users/matthew/CodeLib/test.py");
auto t = LString(LGI_PATH_SEPARATOR).Join(Path);
for (auto s: Path)
printf("s: %s\n", s.Get());
p.SetEnvironment("PATH", t);
if (p.Start())
{
LStringPipe s;
p.Communicate(&s);
printf("Test: %s\n", s.NewLStr().Get());
}
return 0;
}
};
class RemoteFolderDlg : public LDialog
{
class App *app;
LTree *tree;
struct SshHost *root, *newhost;
LXmlTreeUi Ui;
public:
LString Uri;
RemoteFolderDlg(App *application);
~RemoteFolderDlg();
int OnNotify(LViewI *Ctrl, LNotification n);
};
class VcDiffFile : public LTreeItem
{
AppPriv *d;
LString File;
uint64 modTime = 0;
public:
VcDiffFile(AppPriv *priv, LString file) : d(priv), File(file)
{
}
const char *GetText(int i = 0) override
{
return i ? NULL : File.Get();
}
LString StripFirst(LString s)
{
return s.Replace("\\","/").SplitDelimit("/", 1).Last();
}
void Reload()
{
Select(true);
}
void OnPulse()
{
LDirectory dir;
if (dir.First(File))
{
if (modTime)
{
if (modTime != dir.GetLastWriteTime())
{
modTime = dir.GetLastWriteTime();
Reload();
}
}
else
{
modTime = dir.GetLastWriteTime();
}
}
// else LgiTrace("%s:%i couldn't get stat for '%s'\n", _FL, File.Get());
}
void OnMouseClick(LMouse &m)
{
if (m.IsContextMenu())
{
LSubMenu s;
s.AppendItem("Remove", IDM_DELETE);
switch (s.Float(GetTree(), m))
{
case IDM_DELETE:
{
if (LTreeItem::Select())
d->Files->Empty();
delete this;
return;
}
}
}
}
void Select(bool selected) override
{
LTreeItem::Select(selected);
if (!selected)
return;
d->Files->Empty();
d->Diff->Name(NULL);
LFile in(File, O_READ);
LString s = in.Read();
if (!s)
return;
LString NewLine("\n");
LString::Array a = s.Replace("\r").Split("\n");
LArray index;
LString oldName, newName;
LString::Array Diff;
VcFile *f = NULL;
bool InPreamble = false;
bool InDiff = false;
for (unsigned i=0; iSetDiff(NewLine.Join(Diff));
f->Select(false);
}
Diff.Empty();
oldName.Empty();
newName.Empty();
InDiff = false;
InPreamble = true;
}
else if (!Strnicmp(Ln, "Index", 5))
{
if (InPreamble)
index = a[i].SplitDelimit(": ", 1).Slice(1);
}
else if (!strncmp(Ln, "--- ", 4))
{
auto p = a[i].SplitDelimit(" \t", 1);
if (p.Length() > 1)
oldName = p[1];
}
else if (!strncmp(Ln, "+++ ", 4))
{
auto p = a[i].SplitDelimit(" \t", 1);
if (p.Length() > 1)
newName = p[1];
if (oldName && newName)
{
InDiff = true;
InPreamble = false;
f = d->FindFile(newName);
if (!f)
f = new VcFile(d, NULL, LString(), false);
const char *nullFn = "dev/null";
auto Path = StripFirst(oldName);
f->SetUri(LString("file:///") + Path);
if (newName.Find(nullFn) >= 0)
{
// Delete
f->SetText(Path, COL_FILENAME);
f->SetText("D", COL_STATE);
}
else
{
f->SetText(Path, COL_FILENAME);
if (oldName.Find(nullFn) >= 0)
// Add
f->SetText("A", COL_STATE);
else
// Modify
f->SetText("M", COL_STATE);
}
f->GetStatus();
d->Files->Insert(f);
}
}
else if (!_strnicmp(Ln, "------", 6))
{
InPreamble = !InPreamble;
}
else if (!_strnicmp(Ln, "======", 6))
{
InPreamble = false;
InDiff = true;
}
else if (InDiff)
{
Diff.Add(a[i]);
}
}
if (f && Diff.Length())
{
f->SetDiff(NewLine.Join(Diff));
Diff.Empty();
}
}
};
#ifdef HAIKU
enum PipeIndexes
{
READ_END,
WRITE_END,
};
#include
int peekPipe(int fd, char *buf, size_t sz)
{
int bytesAvailable = 0;
int r = ioctl(fd, FIONREAD, &bytesAvailable);
// printf("ioctl=%i %i\n", r, bytesAvailable);
if (r)
return 0;
// printf("starting read\n");
auto rd = read(fd, buf, MIN(bytesAvailable, sz));
// printf("read=%i\n", (int)rd);
return rd;
}
LString RunProcess(const char *exe, const char *args)
{
LStringPipe r;
int fd[2];
pipe(fd);
printf("Running %s...\n", exe);
auto pid = fork();
if (pid == 0)
{
// Child...
dup2(fd[WRITE_END], STDOUT_FILENO);
close(fd[READ_END]);
close(fd[WRITE_END]);
execlp(exe, exe, args, (char*) NULL);
fprintf(stderr, "Failed to execute '%s'\n", exe);
exit(1);
}
else
{
// Parent...
int status;
pid_t result;
#if 0
// More basic...
close(fd[READ_END]);
close(fd[WRITE_END]);
result = waitpid(pid, &status, 0);
printf("waitpid=%i\n", result);
#else
// Read the output
do
{
result = waitpid(pid, &status, WNOHANG);
char buf[256];
auto rd = peekPipe(fd[READ_END], buf, sizeof(buf));
if (rd > 0)
r.Write(buf, rd);
//printf("waitpid=%i rd=%i\n", result, rd);
}
while (result == 0);
close(fd[READ_END]);
close(fd[WRITE_END]);
#endif
}
printf("RunProcess done.\n");
return r.NewLStr();
}
#endif
class GetVcsVersions : public LThread
{
AppPriv *d = NULL;
public:
GetVcsVersions(AppPriv *priv) : LThread("GetVcsVersions")
{
d = priv;
Run();
}
bool ParseVersion(int Result, VersionCtrl type, LString s)
{
if (Result)
return false;
// printf("s=%s\n", s.Get());
auto p = s.SplitDelimit();
switch (type)
{
case VcGit:
{
if (p.Length() > 2)
{
ToolVersion[type] = Ver2Int(p[2]);
d->Log->Print("Git version: %s\n", p[2].Get());
}
break;
}
case VcSvn:
{
if (p.Length() > 2)
{
ToolVersion[type] = Ver2Int(p[2]);
d->Log->Print("Svn version: %s\n", p[2].Get());
}
break;
}
case VcHg:
{
if (p.Length() >= 5)
{
auto Ver = p[4].Strip("()");
ToolVersion[type] = Ver2Int(Ver);
d->Log->Print("Hg version: %s\n", Ver.Get());
}
break;
}
case VcCvs:
{
if (p.Length() > 1)
{
auto Ver = p[2];
ToolVersion[type] = Ver2Int(Ver);
d->Log->Print("Cvs version: %s\n", Ver.Get());
}
break;
}
default:
break;
}
return false;
}
int Main()
{
VersionCtrl types[] = {
#ifndef HAIKU
// Enabling these causes lock error in the parent process...
VcCvs,
VcSvn,
#endif
VcHg,
VcGit
};
for (int i=0; iGetVcName(types[i]);
#ifdef HAIKU
// Something funky is going on with launching subprocesses on Haiku...
// So lets do an absolute minimal example of fork/exec to test whether it's
// something in the LSubProcess classes?
auto result = RunProcess(Exe, "--version");
ParseVersion(0, types[i], result);
#else
LSubProcess sub(Exe, "--version");
if (sub.Start())
{
LStringPipe p;
auto result = sub.Communicate(&p);
ParseVersion(result, types[i], p.NewLStr());
}
#endif
}
printf("GetVcsVersions finished.\n");
return 0;
}
};
class App : public LWindow, public AppPriv
{
LAutoPtr ImgLst;
LBox *FoldersBox = NULL;
bool CallMethod(const char *MethodName, LScriptArguments &Args)
{
if (!Stricmp(MethodName, METHOD_GetContext))
{
*Args.GetReturn() = (AppPriv*)this;
return true;
}
return false;
}
public:
App()
{
LString AppRev;
AppRev.Printf("%s v%s", AppName, APP_VERSION);
Name(AppRev);
LRect r(0, 0, 1400, 800);
SetPos(r);
MoveToCenter();
SetQuitOnClose(true);
Opts.SerializeFile(false);
SerializeState(&Opts, "WndPos", true);
#ifdef WINDOWS
SetIcon(MAKEINTRESOURCEA(IDI_ICON1));
#else
SetIcon("icon32.png");
#endif
ImgLst.Reset(LLoadImageList("image-list.png", 16, 16));
if (Attach(0))
{
SetPulse(200);
DropTarget(true);
Visible(true);
}
}
~App()
{
SerializeState(&Opts, "WndPos", false);
SaveFolders();
}
void OnCreate()
{
if ((Menu = new LMenu))
{
Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT);
Menu->Attach(this);
Menu->Load(this, "IDM_MENU");
}
auto ToolsBox = new LBox(IDC_TOOLS_BOX, true, "ToolsBox");
FoldersBox = new LBox(IDC_FOLDERS_BOX, false, "FoldersBox");
auto CommitsBox = new LBox(IDC_COMMITS_BOX, true, "CommitsBox");
auto Tools = new ToolBar;
ToolsBox->Attach(this);
Tools->Attach(ToolsBox);
FoldersBox->Attach(ToolsBox);
auto FolderLayout = new LTableLayout(IDC_FOLDER_TBL);
auto c = FolderLayout->GetCell(0, 0, true, 2);
Tree = new LTree(IDC_TREE, 0, 0, 320, 200);
Tree->SetImageList(ImgLst, false);
Tree->ColumnHeaders(true);
Tree->AddColumn("Folder", 250);
Tree->AddColumn("Counts", 50);
c->Add(Tree);
c = FolderLayout->GetCell(0, 1);
c->Add(new LEdit(IDC_FILTER_FOLDERS, 0, 0, -1, -1));
c = FolderLayout->GetCell(1, 1);
c->Add(new LButton(IDC_CLEAR_FILTER_FOLDERS, 0, 0, -1, -1, "x"));
FolderLayout->Attach(FoldersBox);
CommitsBox->Attach(FoldersBox);
auto CommitsLayout = new LTableLayout(IDC_COMMITS_TBL);
c = CommitsLayout->GetCell(0, 0, true, 2);
Commits = new CommitList(IDC_LIST);
c->Add(Commits);
c = CommitsLayout->GetCell(0, 1);
c->Add(new LEdit(IDC_FILTER_COMMITS, 0, 0, -1, -1));
c = CommitsLayout->GetCell(1, 1);
c->Add(new LButton(IDC_CLEAR_FILTER_COMMITS, 0, 0, -1, -1, "x"));
CommitsLayout->Attach(CommitsBox);
CommitsLayout->GetCss(true)->Height("40%");
LBox *FilesBox = new LBox(IDC_FILES_BOX, false);
FilesBox->Attach(CommitsBox);
auto FilesLayout = new LTableLayout(IDC_FILES_TBL);
c = FilesLayout->GetCell(0, 0, true, 2);
Files = new LList(IDC_FILES, 0, 0, 200, 200);
Files->AddColumn("[ ]", 30);
Files->AddColumn("State", 100);
Files->AddColumn("Name", 400);
c->Add(Files);
c = FilesLayout->GetCell(0, 1);
c->Add(new LEdit(IDC_FILTER_FILES, 0, 0, -1, -1));
c = FilesLayout->GetCell(1, 1);
c->Add(new LButton(IDC_CLEAR_FILTER_FILES, 0, 0, -1, -1, "x"));
FilesLayout->GetCss(true)->Width("35%");
FilesLayout->Attach(FilesBox);
auto MsgBox = new LBox(IDC_MSG_BOX, true);
MsgBox->Attach(FilesBox);
auto Commit = new CommitCtrls;
Commit->Attach(MsgBox);
Commit->GetCss(true)->Height("30%");
if (Commit->GetViewById(IDC_MSG, Msg))
{
auto Tv = dynamic_cast(Msg);
if (Tv)
{
Tv->Sunken(true);
Tv->SetWrapType(L_WRAP_NONE);
}
}
else LAssert(!"No ctrl?");
Tabs = new LTabView(IDC_TAB_VIEW);
Tabs->Attach(MsgBox);
auto Style = "Padding: 0px 8px 8px 0px";
Tabs->GetCss(true)->Parse(Style);
auto p = Tabs->Append("Diff");
p->Append(Diff = new DiffView(IDC_TXT));
// Diff->Sunken(true);
Diff->SetWrapType(L_WRAP_NONE);
p = Tabs->Append("Log");
p->Append(Log = new LTextLog(IDC_LOG));
// Log->Sunken(true);
Log->SetWrapType(L_WRAP_NONE);
SetCtrlValue(IDC_UPDATE, true);
AttachChildren();
auto f = Opts.LockTag(OPT_Folders, _FL);
if (!f)
{
Opts.CreateTag(OPT_Folders);
f = Opts.LockTag(OPT_Folders, _FL);
}
if (f)
{
new GetVcsVersions(this);
for (auto c: f->Children)
{
if (c->IsTag(OPT_Folder))
{
auto f = new VcFolder(this, c);
Tree->Insert(f);
}
}
Opts.Unlock();
LRect Large(0, 0, 2000, 200);
Tree->SetPos(Large);
Tree->ResizeColumnsToContent();
LItemColumn *c;
int i = 0, px = 0;
while ((c = Tree->ColumnAt(i++)))
{
px += c->Width();
}
FoldersBox->Value(MAX(320, px + 20));
// new TestThread();
}
}
void SaveFolders()
{
LXmlTag *f = Opts.LockTag(OPT_Folders, _FL);
if (!f)
return;
f->EmptyChildren();
for (auto i: *Tree)
{
VcFolder *vcf = dynamic_cast(i);
if (vcf)
f->InsertTag(vcf->Save());
}
Opts.Unlock();
Opts.SerializeFile(true);
}
LMessage::Result OnEvent(LMessage *Msg)
{
switch (Msg->Msg())
{
#if HAS_LIBSSH
case M_RESPONSE:
{
SshConnection::HandleMsg(Msg);
break;
}
#endif
case M_HANDLE_CALLBACK:
{
LAutoPtr Pc((ProcessCallback*)Msg->A());
if (Pc)
Pc->OnComplete();
break;
}
}
return LWindow::OnEvent(Msg);
}
void OnReceiveFiles(LArray &Files)
{
for (auto f : Files)
{
if (LDirExists(f))
OpenLocalFolder(f);
}
}
int OnCommand(int Cmd, int Event, OsView Wnd)
{
switch (Cmd)
{
case IDM_PATCH_VIEWER:
{
OpenPatchViewer(this, &Opts);
break;
}
case IDM_OPEN_LOCAL:
{
OpenLocalFolder();
break;
}
case IDM_OPEN_REMOTE:
{
OpenRemoteFolder();
break;
}
case IDM_OPEN_DIFF:
{
auto s = new LFileSelect;
s->Parent(this);
s->Open([this](auto dlg, auto status)
{
if (status)
OpenDiff(dlg->Name());
delete dlg;
});
break;
}
case IDM_OPTIONS:
{
auto Dlg = new OptionsDlg(this, Opts);
Dlg->DoModal([](auto dlg, auto ctrlId)
{
delete dlg;
});
break;
}
case IDM_FIND:
{
auto i = new LInput(this, "", "Search string:");
i->DoModal([this, i](auto dlg, auto ctrlId)
{
if (ctrlId == IDOK)
{
LString::Array Revs;
Revs.Add(i->GetStr());
CommitList *cl;
if (GetViewById(IDC_LIST, cl))
cl->SelectRevisions(Revs);
}
delete dlg;
});
break;
}
case IDM_UNTRACKED:
{
auto mi = GetMenu()->FindItem(IDM_UNTRACKED);
if (!mi)
break;
mi->Checked(!mi->Checked());
LArray Flds;
Tree->GetSelection(Flds);
for (auto f : Flds)
{
f->Refresh();
}
break;
}
case IDM_REFRESH:
{
LArray Flds;
Tree->GetSelection(Flds);
for (auto f: Flds)
f->Refresh();
break;
}
case IDM_PULL:
{
LArray Flds;
Tree->GetSelection(Flds);
for (auto f: Flds)
f->Pull();
break;
}
case IDM_PUSH:
{
LArray Flds;
Tree->GetSelection(Flds);
for (auto f: Flds)
f->Push();
break;
}
case IDM_STATUS:
{
LArray Flds;
Tree->GetSelection(Flds);
for (auto f: Flds)
f->FolderStatus();
break;
}
case IDM_UPDATE_SUBS:
{
LArray Flds;
Tree->GetSelection(Flds);
for (auto f: Flds)
f->UpdateSubs();
break;
break;
}
case IDM_EXIT:
{
LCloseApp();
break;
}
}
return 0;
}
void OnPulse()
{
for (auto i: *Tree)
i->OnPulse();
}
void OpenLocalFolder(const char *Fld = NULL)
{
auto Load = [this](const char *Fld)
{
// Check the folder isn't already loaded...
VcFolder *Has = NULL;
LArray Folders;
Tree->GetAll(Folders);
for (auto f: Folders)
{
if (f->GetUri().IsFile() &&
!Stricmp(f->LocalPath(), Fld))
{
Has = f;
break;
}
}
if (Has)
{
Has->Select(true);
LPopupNotification::Message(this, LString::Fmt("'%s' is already open", Has->LocalPath()));
}
else
{
LUri u;
u.SetFile(Fld);
auto f = new VcFolder(this, u.ToString());
if (f)
{
Tree->Insert(f);
SaveFolders();
}
}
};
if (!Fld)
{
auto s = new LFileSelect;
s->Parent(this);
s->OpenFolder([this, Load](auto s, auto status)
{
if (status)
Load(s->Name());
delete s;
});
}
else Load(Fld);
}
void OpenRemoteFolder()
{
auto Dlg = new RemoteFolderDlg(this);
Dlg->DoModal([this, Dlg](auto dlg, auto status)
{
if (status)
{
Tree->Insert(new VcFolder(this, Dlg->Uri));
SaveFolders();
}
delete dlg;
});
}
void OpenDiff(const char *File)
{
Tree->Insert(new VcDiffFile(this, File));
}
void OnFilterFolders()
{
if (!Tree)
return;
for (auto i = Tree->GetChild(); i; i = i->GetNext())
{
auto n = i->GetText();
bool vis = !FolderFilter || Stristr(n, FolderFilter.Get()) != NULL;
i->GetCss(true)->Display(vis ? LCss::DispBlock : LCss::DispNone);
}
Tree->UpdateAllItems();
Tree->Invalidate();
}
void OnFilterCommits()
{
if (!Commits || !CurFolder)
return;
if (CommitFilter)
{
// Filtered logging:
CurFolder->LogFilter(CommitFilter);
}
else
{
// Regular full logging:
CurFolder->ClearLog();
CurFolder->Select(true);
}
}
void OnFilterFiles()
{
VcFolder *f = dynamic_cast(Tree->Selection());
if (f)
f->FilterCurrentFiles();
}
+ VcFolder *GetCurrent()
+ {
+ if (!Tree)
+ return NULL;
+ return dynamic_cast(Tree->Selection());
+ }
+
int OnNotify(LViewI *c, LNotification n)
{
switch (c->GetId())
{
case IDC_CLEAR_FILTER_FOLDERS:
{
SetCtrlName(IDC_FILTER_FOLDERS, NULL);
// Fall through
}
case IDC_FILTER_FOLDERS:
{
if (n.Type == LNotifyEscapeKey)
SetCtrlName(IDC_FILTER_FOLDERS, NULL);
LString n = GetCtrlName(IDC_FILTER_FOLDERS);
if (n != FolderFilter)
{
FolderFilter = n;
OnFilterFolders();
}
break;
}
case IDC_CLEAR_FILTER_COMMITS:
{
SetCtrlName(IDC_FILTER_COMMITS, NULL);
// Fall through
}
case IDC_FILTER_COMMITS:
{
if (n.Type == LNotifyEscapeKey)
SetCtrlName(IDC_FILTER_COMMITS, NULL);
LString n = GetCtrlName(IDC_FILTER_COMMITS);
if (n != CommitFilter)
{
CommitFilter = n;
OnFilterCommits();
}
break;
}
case IDC_CLEAR_FILTER_FILES:
{
SetCtrlName(IDC_FILTER_FILES, NULL);
// Fall through
}
case IDC_FILTER_FILES:
{
if (n.Type == LNotifyEscapeKey)
SetCtrlName(IDC_FILTER_FILES, NULL);
LString n = GetCtrlName(IDC_FILTER_FILES);
if (n != FileFilter)
{
FileFilter = n;
OnFilterFiles();
}
break;
}
case IDC_FILES:
{
switch (n.Type)
{
case LNotifyItemColumnClicked:
{
int Col = -1;
LMouse m;
if (Files->GetColumnClickInfo(Col, m))
{
if (Col == 0)
{
// Select / deselect all check boxes..
List n;
if (Files->GetAll(n))
{
bool Checked = false;
for (auto f: n)
Checked |= f->Checked() > 0;
for (auto f: n)
f->Checked(Checked ? 0 : 1);
}
}
}
break;
}
default:
break;
}
break;
}
case IDC_OPEN:
{
OpenLocalFolder();
break;
}
case IDC_TREE:
{
switch (n.Type)
{
case LNotifyContainerClick:
{
LMouse m;
c->GetMouse(m);
if (m.Right())
{
LSubMenu s;
s.AppendItem("Add Local", IDM_ADD_LOCAL);
s.AppendItem("Add Remote", IDM_ADD_REMOTE);
s.AppendItem("Add Diff File", IDM_ADD_DIFF_FILE);
int Cmd = s.Float(c->GetGView(), m);
switch (Cmd)
{
case IDM_ADD_LOCAL:
{
OpenLocalFolder();
break;
}
case IDM_ADD_REMOTE:
{
OpenRemoteFolder();
break;
}
case IDM_ADD_DIFF_FILE:
{
auto s = new LFileSelect;
s->Parent(this);
s->Open([this](auto dlg, auto status)
{
if (status)
OpenDiff(dlg->Name());
delete dlg;
});
break;
}
}
}
break;
}
case (LNotifyType)LvcCommandStart:
{
SetCtrlEnabled(IDC_PUSH, false);
SetCtrlEnabled(IDC_PULL, false);
SetCtrlEnabled(IDC_PULL_ALL, false);
break;
}
case (LNotifyType)LvcCommandEnd:
{
SetCtrlEnabled(IDC_PUSH, true);
SetCtrlEnabled(IDC_PULL, true);
SetCtrlEnabled(IDC_PULL_ALL, true);
break;
}
default:
break;
}
break;
}
case IDC_COMMIT_AND_PUSH:
case IDC_COMMIT:
{
auto BuildFix = GetCtrlValue(IDC_BUILD_FIX);
const char *Msg = GetCtrlName(IDC_MSG);
if (BuildFix || ValidStr(Msg))
{
auto Sel = Tree->Selection();
if (Sel)
{
VcFolder *f = dynamic_cast(Sel);
if (!f)
{
for (auto p = Sel->GetParent(); p; p = p->GetParent())
{
f = dynamic_cast(p);
if (f)
break;
}
}
if (f)
{
auto Branch = GetCtrlName(IDC_BRANCH);
bool AndPush = c->GetId() == IDC_COMMIT_AND_PUSH;
f->Commit(BuildFix ? DEFAULT_BUILD_FIX_MSG : Msg, ValidStr(Branch) ? Branch : NULL, AndPush);
}
}
}
else LgiMsg(this, "No message for commit.", AppName);
break;
}
+ case IDC_EDIT_AUTHOR:
+ {
+ if (auto f = GetCurrent())
+ {
+ auto dlg = new EditAuthor(f);
+ dlg->DoModal(NULL);
+ }
+ break;
+ }
case IDC_PUSH:
{
- VcFolder *f = dynamic_cast(Tree->Selection());
- if (f)
+ if (auto f = GetCurrent())
f->Push();
break;
}
case IDC_PULL:
{
- VcFolder *f = dynamic_cast(Tree->Selection());
- if (f)
+ if (auto f = GetCurrent())
f->Pull();
break;
}
case IDC_PULL_ALL:
{
LArray Folders;
Tree->GetAll(Folders);
bool AndUpdate = GetCtrlValue(IDC_UPDATE) != 0;
for (auto f : Folders)
{
f->Pull(AndUpdate, LogSilo);
}
break;
}
case IDC_STATUS:
{
LArray Folders;
Tree->GetAll(Folders);
for (auto f : Folders)
{
f->FolderStatus();
}
break;
}
case IDC_BRANCHES:
{
if (n.Type == LNotifyValueChanged)
{
VcFolder *f = dynamic_cast(Tree->Selection());
auto branch = c->Name();
if (!f || !branch)
{
Log->Print("%s:%i - Missing param: %p %p\n", _FL, f, branch);
break;
}
if (f->GetCurrentBranch() != branch)
f->Checkout(branch, true);
}
break;
}
case IDC_LIST:
{
switch (n.Type)
{
case LNotifyItemColumnClicked:
{
int Col = -1;
LMouse Ms;
Commits->GetColumnClickInfo(Col, Ms);
Commits->Sort(LstCmp, Col);
break;
}
case LNotifyItemDoubleClick:
{
VcFolder *f = dynamic_cast(Tree->Selection());
if (!f)
break;
LArray s;
if (Commits->GetSelection(s) && s.Length() == 1)
f->Checkout(s[0]->GetRev(), false);
break;
}
default:
break;
}
break;
}
}
return 0;
}
};
struct SshHost : public LTreeItem
{
LXmlTag *t;
LString Host, User, Pass;
SshHost(LXmlTag *tag = NULL)
{
t = tag;
if (t)
{
Serialize(false);
SetText(Host);
}
}
void Serialize(bool WriteToTag)
{
if (WriteToTag)
{
LUri u;
u.sProtocol = "ssh";
u.sHost = Host;
u.sUser = User;
u.sPass = Pass;
t->SetContent(u.ToString());
}
else
{
LUri u(t->GetContent());
if (!Stricmp(u.sProtocol.Get(), "ssh"))
{
Host = u.sHost;
User = u.sUser;
Pass = u.sPass;
}
}
}
};
RemoteFolderDlg::RemoteFolderDlg(App *application) : app(application), root(NULL), newhost(NULL), tree(NULL)
{
SetParent(app);
LoadFromResource(IDD_REMOTE_FOLDER);
if (GetViewById(IDC_HOSTS, tree))
{
printf("tree=%p\n", tree);
tree->Insert(root = new SshHost());
root->SetText("Ssh Hosts");
}
else return;
LViewI *v;
if (GetViewById(IDC_HOSTNAME, v))
v->Focus(true);
Ui.Map("Host", IDC_HOSTNAME);
Ui.Map("User", IDC_USER);
Ui.Map("Password", IDC_PASS);
LXmlTag *hosts = app->Opts.LockTag(OPT_Hosts, _FL);
if (hosts)
{
SshHost *h;
for (auto c: hosts->Children)
if (c->IsTag(OPT_Host) && (h = new SshHost(c)))
root->Insert(h);
app->Opts.Unlock();
}
root->Insert(newhost = new SshHost());
newhost->SetText("New Host");
root->Expanded(true);
newhost->Select(true);
}
RemoteFolderDlg::~RemoteFolderDlg()
{
}
int RemoteFolderDlg::OnNotify(LViewI *Ctrl, LNotification n)
{
SshHost *cur = tree ? dynamic_cast(tree->Selection()) : NULL;
#define CHECK_SPECIAL() \
if (cur == newhost) \
{ \
root->Insert(cur = new SshHost()); \
cur->Select(true); \
} \
if (cur == root) \
break;
switch (Ctrl->GetId())
{
case IDC_HOSTS:
{
switch (n.Type)
{
case LNotifyItemSelect:
{
bool isRoot = cur == root;
SetCtrlEnabled(IDC_HOSTNAME, !isRoot);
SetCtrlEnabled(IDC_USER, !isRoot);
SetCtrlEnabled(IDC_PASS, !isRoot);
SetCtrlEnabled(IDC_DELETE, !isRoot && !(cur == newhost));
SetCtrlName(IDC_HOSTNAME, cur ? cur->Host.Get() : NULL);
SetCtrlName(IDC_USER, cur ? cur->User.Get() : NULL);
SetCtrlName(IDC_PASS, cur ? cur->Pass.Get() : NULL);
break;
}
default:
break;
}
break;
}
case IDC_HOSTNAME:
{
CHECK_SPECIAL()
if (cur)
{
cur->Host = Ctrl->Name();
cur->SetText(cur->Host ? cur->Host : "");
}
break;
}
case IDC_DELETE:
{
auto sel = tree ? dynamic_cast(tree->Selection()) : NULL;
if (!sel)
break;
LXmlTag *hosts = app->Opts.LockTag(OPT_Hosts, _FL);
if (!hosts)
{
LAssert(!"Couldn't lock tag.");
break;
}
if (hosts->Children.HasItem(sel->t))
{
sel->t->RemoveTag();
DeleteObj(sel->t);
delete sel;
}
app->Opts.Unlock();
break;
}
case IDC_USER:
{
CHECK_SPECIAL()
if (cur)
cur->User = Ctrl->Name();
break;
}
case IDC_PASS:
{
CHECK_SPECIAL()
if (cur)
cur->Pass = Ctrl->Name();
break;
}
case IDOK:
{
LXmlTag *hosts;
if (!(hosts = app->Opts.LockTag(OPT_Hosts, _FL)))
{
if (!(app->Opts.CreateTag(OPT_Hosts) && (hosts = app->Opts.LockTag(OPT_Hosts, _FL))))
break;
}
LAssert(hosts != NULL);
for (auto i = root->GetChild(); i; i = i->GetNext())
{
SshHost *h = dynamic_cast(i);
if (!h || h == newhost)
continue;
if (h->t)
;
else if ((h->t = new LXmlTag(OPT_Host)))
hosts->InsertTag(cur->t);
else
return false;
h->Serialize(true);
}
app->Opts.Unlock();
LUri u;
u.sProtocol = "ssh";
u.sHost = GetCtrlName(IDC_HOSTNAME);
u.sUser = GetCtrlName(IDC_USER);
u.sPass = GetCtrlName(IDC_PASS);
u.sPath = GetCtrlName(IDC_REMOTE_PATH);
Uri = u.ToString();
// Fall through
}
case IDCANCEL:
{
EndModal(Ctrl->GetId() == IDOK);
break;
}
}
return 0;
}
const char* toString(VersionCtrl v)
{
switch (v)
{
case VcCvs: return "VcCvs";
case VcSvn: return "VcSvn";
case VcGit: return "VcGit";
case VcHg: return "VcHg";
case VcPending: return "VcPending";
case VcError: return "VcError";
}
return "VcNone";
}
//////////////////////////////////////////////////////////////////
int LgiMain(OsAppArguments &AppArgs)
{
LApp a(AppArgs, AppName);
if (a.IsOk())
{
// LStructuredLog::UnitTest();
a.AppWnd = new App;
a.Run();
DeleteObj(a.AppWnd);
}
LAssert(VcCommit::Instances == 0);
return 0;
}
diff --git a/lvc/src/VcFolder.cpp b/lvc/src/VcFolder.cpp
--- a/lvc/src/VcFolder.cpp
+++ b/lvc/src/VcFolder.cpp
@@ -1,5069 +1,5174 @@
#include "Lvc.h"
#include "lgi/common/Combo.h"
#include "lgi/common/ClipBoard.h"
#include "lgi/common/Json.h"
#include "lgi/common/ProgressDlg.h"
#include "resdefs.h"
#ifndef CALL_MEMBER_FN
#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))
#endif
#define MAX_AUTO_RESIZE_ITEMS 2000
#define PROFILE_FN 0
#if PROFILE_FN
#define PROF(s) Prof.Add(s)
#else
#define PROF(s)
#endif
class TmpFile : public LFile
{
int Status;
LString Hint;
public:
TmpFile(const char *hint = NULL)
{
Status = 0;
if (hint)
Hint = hint;
else
Hint = "_lvc";
}
LFile &Create()
{
LFile::Path p(LSP_TEMP);
p += Hint;
do
{
char s[256];
sprintf_s(s, sizeof(s), "../%s%i.tmp", Hint.Get(), LRand());
p += s;
}
while (p.Exists());
Status = LFile::Open(p.GetFull(), O_READWRITE);
return *this;
}
};
bool TerminalAt(LString Path)
{
#if defined(MAC)
const char *Locations[] = {
"/System/Applications/Utilities/Terminal.app",
"/Applications/Utilities/Terminal.app",
NULL
};
for (size_t i=0; Locations[i]; i++)
{
if (LFileExists(Locations[i]))
{
LString term;
term.Printf("%s/Contents/MacOS/Terminal", Locations[i]);
return LExecute(term, Path);
}
}
#elif defined(WINDOWS)
TCHAR w[MAX_PATH_LEN];
auto r = GetWindowsDirectory(w, CountOf(w));
if (r > 0)
{
LFile::Path p = LString(w);
p += "system32\\cmd.exe";
FileDev->SetCurrentFolder(Path);
return LExecute(p);
}
#elif defined(LINUX)
LExecute("gnome-terminal", NULL, Path);
#endif
return false;
}
int Ver2Int(LString v)
{
auto p = v.Split(".");
int i = 0;
for (auto s : p)
{
auto Int = s.Int();
if (Int < 256)
{
i <<= 8;
i |= (uint8_t)Int;
}
else
{
LAssert(0);
return 0;
}
}
return i;
}
int ToolVersion[VcMax] = {0};
#define DEBUG_READER_THREAD 0
#if DEBUG_READER_THREAD
#define LOG_READER(...) printf(__VA_ARGS__)
#else
#define LOG_READER(...)
#endif
ReaderThread::ReaderThread(VersionCtrl vcs, LAutoPtr p, LStream *out) : LThread("ReaderThread")
{
Vcs = vcs;
Process = p;
Out = out;
Result = -1;
FilterCount = 0;
// We don't start this thread immediately... because the number of threads is scaled to the system
// resources, particularly CPU cores.
}
ReaderThread::~ReaderThread()
{
Out = NULL;
while (!IsExited())
LSleep(1);
}
const char *HgFilter = "We\'re removing Mercurial support";
const char *CvsKill = "No such file or directory";
int ReaderThread::OnLine(char *s, ssize_t len)
{
switch (Vcs)
{
case VcHg:
{
if (strnistr(s, HgFilter, len))
FilterCount = 4;
if (FilterCount > 0)
{
FilterCount--;
return 0;
}
else if (LString(s, len).Strip().Equals("remote:"))
{
return 0;
}
break;
}
case VcCvs:
{
if (strnistr(s, CvsKill, len))
return -1;
break;
}
default:
break;
}
return 1;
}
bool ReaderThread::OnData(char *Buf, ssize_t &r)
{
LOG_READER("OnData %i\n", (int)r);
#if 1
char *Start = Buf;
for (char *c = Buf; c < Buf + r;)
{
bool nl = *c == '\n';
c++;
if (nl)
{
int Result = OnLine(Start, c - Start);
if (Result < 0)
{
// Kill process and exit thread.
Process->Kill();
return false;
}
if (Result == 0)
{
ssize_t LineLen = c - Start;
ssize_t NextLine = c - Buf;
ssize_t Remain = r - NextLine;
if (Remain > 0)
memmove(Start, Buf + NextLine, Remain);
r -= LineLen;
c = Start;
}
else Start = c;
}
}
#endif
Out->Write(Buf, r);
return true;
}
int ReaderThread::Main()
{
bool b = Process->Start(true, false);
if (!b)
{
LString s("Process->Start failed.\n");
Out->Write(s.Get(), s.Length(), ErrSubProcessFailed);
return ErrSubProcessFailed;
}
char Buf[1024];
ssize_t r;
LOG_READER("%s:%i - starting reader loop, pid=%i\n", _FL, Process->Handle());
while (Process->IsRunning())
{
if (Out)
{
LOG_READER("%s:%i - starting read.\n", _FL);
r = Process->Read(Buf, sizeof(Buf));
LOG_READER("%s:%i - read=%i.\n", _FL, (int)r);
if (r > 0)
{
if (!OnData(Buf, r))
return -1;
}
}
else
{
Process->Kill();
return -1;
break;
}
}
LOG_READER("%s:%i - process loop done.\n", _FL);
if (Out)
{
while ((r = Process->Read(Buf, sizeof(Buf))) > 0)
OnData(Buf, r);
}
LOG_READER("%s:%i - loop done.\n", _FL);
Result = (int) Process->GetExitValue();
#if _DEBUG
if (Result)
printf("%s:%i - Process err: %i 0x%x\n", _FL, Result, Result);
#endif
return Result;
}
/////////////////////////////////////////////////////////////////////////////////////////////
int VcFolder::CmdMaxThreads = 0;
int VcFolder::CmdActiveThreads = 0;
void VcFolder::Init(AppPriv *priv)
{
if (!CmdMaxThreads)
CmdMaxThreads = LAppInst->GetCpuCount();
d = priv;
Expanded(false);
Insert(Tmp = new LTreeItem);
Tmp->SetText("Loading...");
LAssert(d != NULL);
}
VcFolder::VcFolder(AppPriv *priv, const char *uri)
{
Init(priv);
Uri.Set(uri);
GetType();
}
VcFolder::VcFolder(AppPriv *priv, LXmlTag *t)
{
Init(priv);
Serialize(t, false);
}
VcFolder::~VcFolder()
{
if (d->CurFolder == this)
d->CurFolder = NULL;
Log.DeleteObjects();
}
VersionCtrl VcFolder::GetType()
{
if (Type == VcNone)
Type = d->DetectVcs(this);
return Type;
}
bool VcFolder::IsLocal()
{
return Uri.IsProtocol("file");
}
const char *VcFolder::LocalPath()
{
if (!Uri.IsProtocol("file") || Uri.sPath.IsEmpty())
{
LAssert(!"Shouldn't call this if not a file path.");
return NULL;
}
auto c = Uri.sPath.Get();
#ifdef WINDOWS
if (*c == '/')
c++;
#endif
return c;
}
const char *VcFolder::GetText(int Col)
{
switch (Col)
{
case 0:
{
if (Uri.IsFile())
Cache = LocalPath();
else
Cache.Printf("%s%s", Uri.sHost.Get(), Uri.sPath.Get());
if (Cmds.Length())
Cache += " (...)";
return Cache;
}
case 1:
{
CountCache.Printf("%i/%i", Unpulled, Unpushed);
CountCache = CountCache.Replace("-1", "--");
return CountCache;
}
}
return NULL;
}
bool VcFolder::Serialize(LXmlTag *t, bool Write)
{
if (Write)
t->SetContent(Uri.ToString());
else
{
LString s = t->GetContent();
bool isUri = s.Find("://") >= 0;
if (isUri)
Uri.Set(s);
else
Uri.SetFile(s);
}
return true;
}
LXmlTag *VcFolder::Save()
{
LXmlTag *t = new LXmlTag(OPT_Folder);
if (t)
Serialize(t, true);
return t;
}
const char *VcFolder::GetVcName()
{
if (!VcCmd)
VcCmd = d->GetVcName(GetType());
return VcCmd;
}
char VcFolder::GetPathSep()
{
if (Uri.IsFile())
return DIR_CHAR;
return '/'; // FIXME: Assumption is that the remote system is unix based.
}
bool VcFolder::RunCmd(const char *Args, LoggingType Logging, std::function Callback)
{
Result Ret;
Ret.Code = -1;
const char *Exe = GetVcName();
if (!Exe || CmdErrors > 2)
return false;
if (Uri.IsFile())
{
new ProcessCallback(Exe,
Args,
LocalPath(),
Logging == LogNone ? d->Log : NULL,
GetTree()->GetWindow(),
Callback);
}
else
{
LAssert(!"Impl me.");
return false;
}
return true;
}
#if HAS_LIBSSH
SshConnection::LoggingType Convert(LoggingType t)
{
switch (t)
{
case LogNormal:
case LogSilo:
return SshConnection::LogInfo;
case LogDebug:
return SshConnection::LogDebug;
}
return SshConnection::LogNone;
}
#endif
bool VcFolder::StartCmd(const char *Args, ParseFn Parser, ParseParams *Params, LoggingType Logging)
{
const char *Exe = GetVcName();
if (!Exe)
return false;
if (CmdErrors > 2)
return false;
if (Uri.IsFile())
{
if (d->Log && Logging != LogSilo)
d->Log->Print("%s %s\n", Exe, Args);
LAutoPtr Process(new LSubProcess(Exe, Args));
if (!Process)
return false;
Process->SetInitFolder(Params && Params->AltInitPath ? Params->AltInitPath.Get() : LocalPath());
#if 0//def MAC
// Mac GUI apps don't share the terminal path, so this overrides that and make it work
auto Path = LGetPath();
if (Path.Length())
{
LString Tmp = LString(LGI_PATH_SEPARATOR).Join(Path);
printf("Tmp='%s'\n", Tmp.Get());
Process->SetEnvironment("PATH", Tmp);
}
#endif
LString::Array Ctx;
Ctx.SetFixedLength(false);
Ctx.Add(LocalPath());
Ctx.Add(Exe);
Ctx.Add(Args);
LAutoPtr c(new Cmd(Ctx, Logging, d->Log));
if (!c)
return false;
c->PostOp = Parser;
c->Params.Reset(Params);
c->Rd.Reset(new ReaderThread(GetType(), Process, c));
Cmds.Add(c.Release());
}
else
{
#if HAS_LIBSSH
auto c = d->GetConnection(Uri.ToString());
if (!c)
return false;
if (!c->Command(this, Exe, Args, Parser, Params, Convert(Logging)))
return false;
#endif
}
Update();
return true;
}
int LogDateCmp(LListItem *a, LListItem *b, NativeInt Data)
{
VcCommit *A = dynamic_cast(a);
VcCommit *B = dynamic_cast(b);
if ((A != NULL) ^ (B != NULL))
{
// This handles keeping the "working folder" list item at the top
return (A != NULL) - (B != NULL);
}
// Sort the by date from most recent to least
return -A->GetTs().Compare(&B->GetTs());
}
void VcFolder::AddGitName(LString Hash, LString Name)
{
if (!Hash || !Name)
{
LAssert(!"Param error");
return;
}
LString Existing = GitNames.Find(Hash);
if (Existing)
GitNames.Add(Hash, Existing + "," + Name);
else
GitNames.Add(Hash, Name);
}
LString VcFolder::GetGitNames(LString Hash)
{
LString Short = Hash(0, 11);
return GitNames.Find(Short);
}
bool VcFolder::ParseBranches(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcGit:
{
LString::Array a = s.SplitDelimit("\r\n");
for (auto &l: a)
{
LString::Array c;
char *s = l.Get();
while (*s && IsWhite(*s))
s++;
bool IsCur = *s == '*';
if (IsCur)
s++;
while (*s && IsWhite(*s))
s++;
if (*s == '(')
{
s++;
auto e = strchr(s, ')');
if (e)
{
c.New().Set(s, e - s);
e++;
c += LString(e).SplitDelimit(" \t");
}
}
else
{
c = LString(s).SplitDelimit(" \t");
}
if (c.Length() < 1)
{
d->Log->Print("%s:%i - Too few parts in line '%s'\n", _FL, l.Get());
continue;
}
if (IsCur)
SetCurrentBranch(c[0]);
AddGitName(c[1], c[0]);
Branches.Add(c[0], new VcBranch(c[0], c[1]));
}
break;
}
case VcHg:
{
auto a = s.SplitDelimit("\r\n");
for (auto b: a)
{
if (b.Find("inactive") > 0)
continue;
auto name = b(0, 28).Strip();
auto refs = b(28, -1).SplitDelimit()[0].SplitDelimit(":");
auto branch = Branches.Find(name);
if (branch)
branch->Hash = refs.Last();
else
Branches.Add(name, new VcBranch(name, refs.Last()));
}
if (Params && Params->Str.Equals("CountToTip"))
CountToTip();
break;
}
default:
{
break;
}
}
IsBranches = Result ? StatusError : StatusNone;
OnBranchesChange();
return false;
}
void VcFolder::GetRemoteUrl(std::function Callback)
{
LAutoPtr p(new ParseParams);
p->Callback = Callback;
switch (GetType())
{
case VcGit:
{
StartCmd("config --get remote.origin.url", NULL, p.Release());
break;
}
case VcSvn:
{
StartCmd("info --show-item=url", NULL, p.Release());
break;
}
case VcHg:
{
StartCmd("paths default", NULL, p.Release());
break;
}
default:
break;
}
}
void VcFolder::SelectCommit(LWindow *Parent, LString Commit, LString Path)
{
bool requireFullMatch = true;
if (GetType() == VcGit)
requireFullMatch = false;
// This function find the given commit and selects it such that the diffs are displayed in the file list
VcCommit *ExistingMatch = NULL;
for (auto c: Log)
{
char *rev = c->GetRev();
bool match = requireFullMatch ? Commit.Equals(rev) : Strstr(rev, Commit.Get()) != NULL;
if (match)
{
ExistingMatch = c;
break;
}
}
FileToSelect = Path;
if (ExistingMatch)
{
ExistingMatch->Select(true);
}
else
{
// If the commit isn't there, it's likely that the log item limit was reached before the commit was
// found. In which case we should go get just that commit and add it:
d->Files->Empty();
// Diff just that ref:
LString a;
switch (GetType())
{
case VcGit:
{
a.Printf("diff %s~ %s", Commit.Get(), Commit.Get());
StartCmd(a, &VcFolder::ParseSelectCommit);
break;
}
case VcHg:
{
a.Printf("log -p -r %s", Commit.Get());
StartCmd(a, &VcFolder::ParseSelectCommit);
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
// if (Parent) LgiMsg(Parent, "The commit '%s' wasn't found", AppName, MB_OK, Commit.Get());
}
}
bool VcFolder::ParseSelectCommit(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcGit:
case VcHg:
case VcSvn:
case VcCvs:
{
ParseDiff(Result, s, Params);
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
return false;
}
void VcFolder::OnBranchesChange()
{
auto *w = d->Tree->GetWindow();
if (!w || !LTreeItem::Select())
return;
if (Branches.Length())
{
// Set the colours up
LString Default;
for (auto b: Branches)
{
if (!stricmp(b.key, "default") ||
!stricmp(b.key, "trunk"))
Default = b.key;
/*
else
printf("Other=%s\n", b.key);
*/
}
int Idx = 1;
for (auto b: Branches)
{
if (!b.value->Colour.IsValid())
{
if (Default && !stricmp(b.key, Default))
b.value->Colour = GetPaletteColour(0);
else
b.value->Colour = GetPaletteColour(Idx++);
}
}
}
UpdateBranchUi();
}
void VcFolder::DefaultFields()
{
if (Fields.Length() == 0)
{
switch (GetType())
{
case VcHg:
{
Fields.Add(LGraph);
Fields.Add(LIndex);
Fields.Add(LRevision);
Fields.Add(LBranch);
Fields.Add(LAuthor);
Fields.Add(LTime);
Fields.Add(LMessageTxt);
break;
}
case VcGit:
{
Fields.Add(LGraph);
Fields.Add(LRevision);
Fields.Add(LBranch);
Fields.Add(LAuthor);
Fields.Add(LTime);
Fields.Add(LMessageTxt);
break;
}
default:
{
Fields.Add(LGraph);
Fields.Add(LRevision);
Fields.Add(LAuthor);
Fields.Add(LTime);
Fields.Add(LMessageTxt);
break;
}
}
}
}
int VcFolder::IndexOfCommitField(CommitField fld)
{
return (int)Fields.IndexOf(fld);
}
void VcFolder::UpdateColumns(LList *lst)
{
if (!lst)
lst = d->Commits;
lst->EmptyColumns();
for (auto c: Fields)
{
switch (c)
{
case LGraph: lst->AddColumn("---", 60); break;
case LIndex: lst->AddColumn("Index", 60); break;
case LBranch: lst->AddColumn("Branch", 60); break;
case LRevision: lst->AddColumn("Revision", 60); break;
case LAuthor: lst->AddColumn("Author", 240); break;
case LTime: lst->AddColumn("Date", 130); break;
case LMessageTxt: lst->AddColumn("Message", 700); break;
default: LAssert(0); break;
}
}
}
void VcFolder::FilterCurrentFiles()
{
LArray All;
d->Files->GetAll(All);
// Update the display property
for (auto i: All)
{
auto fn = i->GetText(COL_FILENAME);
bool vis = !d->FileFilter || Stristr(fn, d->FileFilter.Get());
i->GetCss(true)->Display(vis ? LCss::DispBlock : LCss::DispNone);
// LgiTrace("Filter '%s' by '%s' = %i\n", fn, d->FileFilter.Get(), vis);
}
d->Files->Sort(0);
d->Files->UpdateAllItems();
d->Files->ResizeColumnsToContent();
}
void VcFolder::UpdateAuthorUi()
{
if (AuthorEmail && AuthorName)
{
auto author = LString::Fmt("%s <%s>", AuthorName.Get(), AuthorEmail.Get());
d->Wnd()->SetCtrlName(IDC_AUTHOR, author);
}
}
-bool VcFolder::GetRepoAuthor()
+LString VcFolder::GetConfigFile(bool local)
{
switch (GetType())
{
+ case VcHg:
+ {
+ LFile::Path p;
+ if (local)
+ {
+ p = LFile::Path(LocalPath()) / ".hg" / "hgrc";
+ }
+ else
+ {
+ p = LFile::Path(LSP_HOME) / ".hgrc";
+ if (!p.Exists())
+ p = LFile::Path(LSP_HOME) / "mercurial.ini";
+ }
+
+ d->Log->Print("%s: %i\n", p.GetFull().Get(), p.Exists());
+ if (p.Exists())
+ return p.GetFull();
+ break;
+ }
+ default:
+ {
+ NoImplementation(_FL);
+ return false;
+ }
+ }
+
+ return LString();
+}
+
+LString GetIni(LString::Array &lines, LString section, LString var)
+{
+ LString curSection;
+ LString sect = section.Lower();
+ for (auto &ln: lines)
+ {
+ auto s = ln.LStrip();
+ if (s(0) == '[')
+ {
+ auto end = s.Find("]", 1);
+ if (end > 0)
+ curSection = s(1, end).Strip().Lower();
+ }
+ else if (curSection == sect)
+ {
+ auto p = ln.SplitDelimit("=", 1);
+ if (p.Length() == 2)
+ {
+ if (p[0].Strip() == var)
+ return p[1].Strip();
+ }
+ }
+ }
+
+ return LString();
+}
+
+bool VcFolder::GetAuthor(bool local, std::function callback)
+{
+ auto scope = local ? "--local" : "--global";
+
+ switch (GetType())
+ {
case VcGit:
{
- if (IsGettingAuthor)
- return true;
-
auto params = new ParseParams;
- params->Callback = [this](auto code, auto s)
+ params->Callback = [this, callback](auto code, auto s)
{
for (auto ln: s.Strip().SplitDelimit("\r\n"))
{
auto parts = ln.SplitDelimit("=", 1);
if (parts.Length() == 2)
{
if (parts[0].Equals("user.email"))
AuthorEmail = parts[1];
else if (parts[0].Equals("user.name"))
AuthorName = parts[1];
}
}
IsGettingAuthor = false;
- UpdateAuthorUi();
+ callback(AuthorName, AuthorEmail);
};
- IsGettingAuthor = StartCmd("-P config -l", NULL, params);
+
+ auto args = LString::Fmt("-P config -l %s", scope);
+ StartCmd(args, NULL, params);
break;
}
case VcHg:
{
- if (IsGettingAuthor)
- return true;
-
- auto params = new ParseParams;
- params->Callback = [this](auto code, auto s)
+ auto config = GetConfigFile(local);
+ if (!config)
+ return false;
+
+ auto data = LReadFile(config);
+ if (!data)
+ return false;
+
+ auto lines = data.SplitDelimit("\r\n");
+ auto author = GetIni(lines, "ui", "username");
+
+ auto start = author.Find("<");
+ auto end = author.Find(">", start);
+ if (start >= 0 &&
+ end >= start)
{
- s = s.Strip();
- auto start = s.Find("<");
- auto end = s.Find(">", start);
- if (start >= 0 &&
- end >= start)
- {
- AuthorName = s(0, start).Strip();
- AuthorEmail = s(start + 1, end).Strip();
- }
-
- IsGettingAuthor = false;
- UpdateAuthorUi();
- };
- IsGettingAuthor = StartCmd("config ui.username", NULL, params);
+ AuthorName = author(0, start).Strip();
+ AuthorEmail = author(start + 1, end).Strip();
+ }
+
+ IsGettingAuthor = false;
+ callback(AuthorName, AuthorEmail);
+ break;
+ }
+ default:
+ {
+ NoImplementation(_FL);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool VcFolder::SetAuthor(bool local, LString name, LString email)
+{
+ auto scope = local ? "--local" : "--global";
+
+ if (local)
+ {
+ AuthorName = name;
+ AuthorEmail = email;
+ }
+
+ switch (GetType())
+ {
+ case VcGit:
+ {
+ auto args = LString::Fmt("config %s user.name \"%s\"", scope, name.Get());
+ StartCmd(args);
+
+ args = LString::Fmt("config %s user.email \"%s\"", scope, email.Get());
+ StartCmd(args);
+ break;
+ }
+ case VcHg:
+ {
+ auto config = GetConfigFile(local);
+ if (!config)
+ return false;
+
break;
}
default:
{
NoImplementation(_FL);
return false;
}
}
return true;
}
void VcFolder::Select(bool b)
{
#if PROFILE_FN
LProfile Prof("Select");
#endif
if (!b)
{
auto *w = d->Tree->GetWindow();
w->SetCtrlName(IDC_BRANCH, NULL);
}
PROF("Parent.Select");
LTreeItem::Select(b);
if (b)
{
if (Uri.IsFile() && !LDirExists(LocalPath()))
return;
PROF("DefaultFields");
DefaultFields();
if (AuthorEmail)
UpdateAuthorUi();
else
- GetRepoAuthor();
+ GetAuthor(true, [this](auto name, auto email)
+ {
+ UpdateAuthorUi();
+ });
PROF("Type Change");
if (GetType() != d->PrevType)
{
d->PrevType = GetType();
UpdateColumns();
}
PROF("UpdateCommitList");
if ((Log.Length() == 0 || CommitListDirty) && !IsLogging)
{
switch (GetType())
{
case VcGit:
{
LVariant Limit;
d->Opts.GetValue("git-limit", Limit);
LString cmd = "rev-list --all --header --timestamp --author-date-order", s;
if (Limit.CastInt32() > 0)
{
s.Printf(" -n %i", Limit.CastInt32());
cmd += s;
}
IsLogging = StartCmd(cmd, &VcFolder::ParseRevList);
break;
}
case VcSvn:
{
LVariant Limit;
d->Opts.GetValue("svn-limit", Limit);
if (CommitListDirty)
{
IsLogging = StartCmd("up", &VcFolder::ParsePull, new ParseParams("log"));
break;
}
LString s;
if (Limit.CastInt32() > 0)
s.Printf("log --limit %i", Limit.CastInt32());
else
s = "log";
IsLogging = StartCmd(s, &VcFolder::ParseLog);
break;
}
case VcHg:
{
IsLogging = StartCmd("log", &VcFolder::ParseLog);
StartCmd("resolve -l", &VcFolder::ParseResolveList);
break;
}
case VcPending:
{
break;
}
default:
{
IsLogging = StartCmd("log", &VcFolder::ParseLog);
break;
}
}
CommitListDirty = false;
}
PROF("GetBranches");
if (GetBranches())
OnBranchesChange();
if (d->CurFolder != this)
{
PROF("RemoveAll");
d->CurFolder = this;
d->Commits->RemoveAll();
}
PROF("Uncommit");
if (!Uncommit)
Uncommit.Reset(new UncommitedItem(d));
d->Commits->Insert(Uncommit, 0);
PROF("Log Loop");
int64 CurRev = Atoi(CurrentCommit.Get());
List Ls;
for (auto l: Log)
{
if (CurrentCommit &&
l->GetRev())
{
switch (GetType())
{
case VcSvn:
{
int64 LogRev = Atoi(l->GetRev());
if (CurRev >= 0 && CurRev >= LogRev)
{
CurRev = -1;
l->SetCurrent(true);
}
else
{
l->SetCurrent(false);
}
break;
}
default:
l->SetCurrent(!_stricmp(CurrentCommit, l->GetRev()));
break;
}
}
LList *CurOwner = l->GetList();
if (!CurOwner)
Ls.Insert(l);
}
PROF("Ls Ins");
d->Commits->Insert(Ls);
if (d->Resort >= 0)
{
PROF("Resort");
d->Commits->Sort(LstCmp, d->Resort);
d->Resort = -1;
}
PROF("ColSizing");
if (d->Commits->Length() > MAX_AUTO_RESIZE_ITEMS)
{
int i = 0;
if (GetType() == VcHg && d->Commits->GetColumns() >= 7)
{
d->Commits->ColumnAt(i++)->Width(60); // LGraph
d->Commits->ColumnAt(i++)->Width(40); // LIndex
d->Commits->ColumnAt(i++)->Width(100); // LRevision
d->Commits->ColumnAt(i++)->Width(60); // LBranch
d->Commits->ColumnAt(i++)->Width(240); // LAuthor
d->Commits->ColumnAt(i++)->Width(130); // LTimeStamp
d->Commits->ColumnAt(i++)->Width(400); // LMessage
}
else if (d->Commits->GetColumns() >= 5)
{
d->Commits->ColumnAt(i++)->Width(40); // LGraph
d->Commits->ColumnAt(i++)->Width(270); // LRevision
d->Commits->ColumnAt(i++)->Width(240); // LAuthor
d->Commits->ColumnAt(i++)->Width(130); // LTimeStamp
d->Commits->ColumnAt(i++)->Width(400); // LMessage
}
}
else d->Commits->ResizeColumnsToContent();
PROF("UpdateAll");
d->Commits->UpdateAllItems();
PROF("GetCur");
GetCurrentRevision();
}
}
int CommitRevCmp(VcCommit **a, VcCommit **b)
{
int64 arev = Atoi((*a)->GetRev());
int64 brev = Atoi((*b)->GetRev());
int64 diff = (int64)brev - arev;
if (diff < 0) return -1;
return (diff > 0) ? 1 : 0;
}
int CommitIndexCmp(VcCommit **a, VcCommit **b)
{
auto ai = (*a)->GetIndex();
auto bi = (*b)->GetIndex();
auto diff = (int64)bi - ai;
if (diff < 0) return -1;
return (diff > 0) ? 1 : 0;
}
int CommitDateCmp(VcCommit **a, VcCommit **b)
{
LTimeStamp ats, bts;
(*a)->GetTs().Get(ats);
(*b)->GetTs().Get(bts);
int64 diff = (int64)bts.Get() - ats.Get();
if (diff < 0) return -1;
return (diff > 0) ? 1 : 0;
}
void VcFolder::GetCurrentRevision(ParseParams *Params)
{
if (CurrentCommit || IsIdent != StatusNone)
return;
switch (GetType())
{
case VcGit:
if (StartCmd("rev-parse HEAD", &VcFolder::ParseInfo, Params))
IsIdent = StatusActive;
break;
case VcSvn:
if (StartCmd("info", &VcFolder::ParseInfo, Params))
IsIdent = StatusActive;
break;
case VcHg:
if (StartCmd("id -i -n", &VcFolder::ParseInfo, Params))
IsIdent = StatusActive;
break;
case VcCvs:
break;
default:
break;
}
}
bool VcFolder::GetBranches(ParseParams *Params)
{
if (Branches.Length() > 0 || IsBranches != StatusNone)
return true;
switch (GetType())
{
case VcGit:
if (StartCmd("-P branch -v", &VcFolder::ParseBranches, Params))
IsBranches = StatusActive;
break;
case VcSvn:
Branches.Add("trunk", new VcBranch("trunk"));
OnBranchesChange();
break;
case VcHg:
{
if (StartCmd("branches", &VcFolder::ParseBranches, Params))
IsBranches = StatusActive;
auto p = new ParseParams;
p->Callback = [this](auto code, auto str)
{
SetCurrentBranch(str.Strip());
};
StartCmd("branch", NULL, p);
break;
}
case VcCvs:
break;
default:
break;
}
return false;
}
bool VcFolder::ParseRevList(int Result, LString s, ParseParams *Params)
{
Log.DeleteObjects();
int Errors = 0;
switch (GetType())
{
case VcGit:
{
LString::Array Commits;
Commits.SetFixedLength(false);
// Split on the NULL chars...
char *c = s.Get();
char *e = c + s.Length();
while (c < e)
{
char *nul = c;
while (nul < e && *nul) nul++;
if (nul <= c) break;
Commits.New().Set(c, nul-c);
if (nul >= e) break;
c = nul + 1;
}
for (auto Commit: Commits)
{
LAutoPtr Rev(new VcCommit(d, this));
if (Rev->GitParse(Commit, true))
{
Log.Add(Rev.Release());
}
else
{
// LAssert(!"Parse failed.");
LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, Commit.Get());
Errors++;
}
}
LinkParents();
break;
}
default:
LAssert(!"Impl me.");
break;
}
IsLogging = false;
return Errors == 0;
}
LString VcFolder::GetFilePart(const char *uri)
{
LUri u(uri);
LString File = u.IsFile() ?
u.DecodeStr(u.LocalPath()) :
u.sPath(Uri.sPath.Length(), -1).LStrip("/");
return File;
}
void VcFolder::ClearLog()
{
Uncommit.Reset();
Log.DeleteObjects();
}
void VcFolder::LogFilter(const char *Filter)
{
if (!Filter)
{
LAssert(!"No filter.");
return;
}
switch (GetType())
{
case VcGit:
{
// See if 'Filter' is a commit id?
LString args;
args.Printf("-P show %s", Filter);
ParseParams *params = new ParseParams;
params->Callback = [this, Filter=LString(Filter)](auto code, auto str)
{
ClearLog();
if (code == 0 && str.Find(Filter) >= 0)
{
// Found the commit...
d->Commits->Empty();
CurrentCommit.Empty();
ParseLog(code, str, NULL);
d->Commits->Insert(Log);
}
else
{
// Not a commit ref...?
LString args;
args.Printf("log --grep \"%s\"", Filter.Get());
IsLogging = StartCmd(args, &VcFolder::ParseLog);
}
};
StartCmd(args, NULL, params);
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
}
void VcFolder::LogFile(const char *uri)
{
LString Args;
if (IsLogging)
{
d->Log->Print("%s:%i - already logging.\n", _FL);
return;
}
const char *Page = "";
switch (GetType())
{
case VcGit:
Page = "-P ";
// fall through
case VcSvn:
case VcHg:
{
FileToSelect = GetFilePart(uri);
if (IsLocal() && !LFileExists(FileToSelect))
{
LFile::Path Abs(LocalPath());
Abs += FileToSelect;
if (Abs.Exists())
FileToSelect = Abs;
}
ParseParams *Params = new ParseParams(uri);
Args.Printf("%slog \"%s\"", Page, FileToSelect.Get());
IsLogging = StartCmd(Args, &VcFolder::ParseLog, Params, LogNormal);
break;
}
default:
NoImplementation(_FL);
break;
}
}
VcLeaf *VcFolder::FindLeaf(const char *Path, bool OpenTree)
{
VcLeaf *r = NULL;
if (OpenTree)
DoExpand();
for (auto n = GetChild(); !r && n; n = n->GetNext())
{
auto l = dynamic_cast(n);
if (l)
r = l->FindLeaf(Path, OpenTree);
}
return r;
}
bool VcFolder::ParseLog(int Result, LString s, ParseParams *Params)
{
int Skipped = 0, Errors = 0;
bool LoggingFile = Params ? Params->Str != NULL : false;
VcLeaf *File = LoggingFile ? FindLeaf(Params->Str, true) : NULL; // This may be NULL even if we are logging a file...
LArray *Out, BrowseLog;
if (File)
Out = &File->Log;
else if (LoggingFile)
Out = &BrowseLog;
else
Out = &Log;
LHashTbl, VcCommit*> Map;
for (auto pc: *Out)
Map.Add(pc->GetRev(), pc);
if (File)
{
for (auto Leaf = File; Leaf; Leaf = dynamic_cast(Leaf->GetParent()))
Leaf->OnExpand(true);
File->Select(true);
File->ScrollTo();
}
switch (GetType())
{
case VcGit:
{
LString::Array c;
c.SetFixedLength(false);
char *prev = s.Get();
#if 0
LFile::Path outPath("~/code/dump.txt");
LFile out(outPath.Absolute(), O_WRITE);
out.Write(s);
#endif
if (!s)
{
OnCmdError(s, "No output from command.");
return false;
}
char *i = s.Get();
while (*i)
{
if (!strnicmp(i, "commit ", 7))
{
if (i > prev)
{
c.New().Set(prev, i - prev);
// LgiTrace("commit=%i\n", (int)(i - prev));
}
prev = i;
}
while (*i)
{
if (*i++ == '\n')
break;
}
}
if (prev && i > prev)
{
// Last one...
c.New().Set(prev, i - prev);
}
for (auto txt: c)
{
LAutoPtr Rev(new VcCommit(d, this));
if (Rev->GitParse(txt, false))
{
if (!Map.Find(Rev->GetRev()))
Out->Add(Rev.Release());
else
Skipped++;
}
else
{
LgiTrace("%s:%i - Failed:\n%s\n\n", _FL, txt.Get());
Errors++;
}
}
Out->Sort(CommitDateCmp);
break;
}
case VcSvn:
{
LString::Array c = s.Split("------------------------------------------------------------------------");
for (unsigned i=0; i Rev(new VcCommit(d, this));
LString Raw = c[i].Strip();
if (Rev->SvnParse(Raw))
{
if (File || !Map.Find(Rev->GetRev()))
Out->Add(Rev.Release());
else
Skipped++;
}
else if (Raw)
{
OnCmdError(Raw, "ParseLog Failed");
Errors++;
}
}
Out->Sort(CommitRevCmp);
break;
}
case VcHg:
{
LString::Array c = s.Split("\n\n");
LHashTbl, VcCommit*> Idx;
for (auto &Commit: c)
{
LAutoPtr Rev(new VcCommit(d, this));
if (Rev->HgParse(Commit))
{
auto Existing = File ? NULL : Map.Find(Rev->GetRev());
if (!Existing)
Out->Add(Existing = Rev.Release());
if (Existing->GetIndex() >= 0)
Idx.Add(Existing->GetIndex(), Existing);
}
}
if (!File)
{
// Patch all the trivial parents...
for (auto c: Log)
{
if (c->GetParents()->Length() > 0)
continue;
auto CIdx = c->GetIndex();
if (CIdx <= 0)
continue;
auto Par = Idx.Find(CIdx - 1);
if (Par)
c->GetParents()->Add(Par->GetRev());
}
}
Out->Sort(CommitIndexCmp);
if (!File)
LinkParents();
d->Resort = 1;
break;
}
case VcCvs:
{
if (Result)
{
OnCmdError(s, "Cvs command failed.");
break;
}
LHashTbl, VcCommit*> Map;
LString::Array c = s.Split("=============================================================================");
for (auto &Commit: c)
{
if (Commit.Strip().Length())
{
LString Head, File;
LString::Array Versions = Commit.Split("----------------------------");
LString::Array Lines = Versions[0].SplitDelimit("\r\n");
for (auto &Line: Lines)
{
LString::Array p = Line.Split(":", 1);
if (p.Length() == 2)
{
// LgiTrace("Line: %s\n", Line->Get());
LString Var = p[0].Strip().Lower();
LString Val = p[1].Strip();
if (Var.Equals("branch"))
{
if (Val.Length())
Branches.Add(Val, new VcBranch(Val));
}
else if (Var.Equals("head"))
{
Head = Val;
}
else if (Var.Equals("rcs file"))
{
LString::Array f = Val.SplitDelimit(",");
File = f.First();
}
}
}
// LgiTrace("%s\n", Commit->Get());
for (unsigned i=1; i= 3)
{
LString Ver = Lines[0].Split(" ").Last();
LString::Array a = Lines[1].SplitDelimit(";");
LString Date = a[0].Split(":", 1).Last().Strip();
LString Author = a[1].Split(":", 1).Last().Strip();
LString Id = a[2].Split(":", 1).Last().Strip();
LString Msg = Lines[2];
LDateTime Dt;
if (Dt.Parse(Date))
{
LTimeStamp Ts;
if (Dt.Get(Ts))
{
VcCommit *Cc = Map.Find(Ts.Get());
if (!Cc)
{
Map.Add(Ts.Get(), Cc = new VcCommit(d, this));
Out->Add(Cc);
Cc->CvsParse(Dt, Author, Msg);
}
Cc->Files.Add(File.Get());
}
else LAssert(!"NO ts for date.");
}
else LAssert(!"Date parsing failed.");
}
}
}
}
break;
}
default:
LAssert(!"Impl me.");
break;
}
if (File)
{
File->ShowLog();
}
else if (LoggingFile)
{
if (auto ui = new BrowseUi(BrowseUi::TLog, d, this, Params->Str))
ui->ParseLog(BrowseLog, s);
}
// LgiTrace("%s:%i - ParseLog: Skip=%i, Error=%i\n", _FL, Skipped, Errors);
IsLogging = false;
return !Result;
}
void VcFolder::LinkParents()
{
#if PROFILE_FN
LProfile Prof("LinkParents");
#endif
LHashTbl,VcCommit*> Map;
// Index all the commits
int i = 0;
for (auto c:Log)
{
c->Idx = i++;
c->NodeIdx = -1;
Map.Add(c->GetRev(), c);
}
// Create all the edges...
PROF("Create edges.");
for (auto c:Log)
{
auto *Par = c->GetParents();
for (auto &pRev : *Par)
{
auto *p = Map.Find(pRev);
if (p)
new VcEdge(p, c);
#if 0
else
return;
#endif
}
}
// Map the edges to positions
PROF("Map edges.");
typedef LArray EdgeArr;
LArray Active;
for (auto c:Log)
{
for (unsigned i=0; c->NodeIdx<0 && iParent == c)
{
c->NodeIdx = i;
break;
}
}
}
// Add starting edges to active set
for (auto e:c->Edges)
{
if (e->Child == c)
{
if (c->NodeIdx < 0)
c->NodeIdx = (int)Active.Length();
e->Idx = c->NodeIdx;
c->Pos.Add(e, e->Idx);
Active[e->Idx].Add(e);
}
}
// Now for all active edges... assign positions
for (unsigned i=0; iLength(); n++)
{
LAssert(Active.PtrCheck(Edges));
VcEdge *e = (*Edges)[n];
if (c == e->Child || c == e->Parent)
{
LAssert(c->NodeIdx >= 0);
c->Pos.Add(e, c->NodeIdx);
}
else
{
// May need to untangle edges with different parents here
bool Diff = false;
for (auto edge: *Edges)
{
if (edge != e &&
edge->Child != c &&
edge->Parent != e->Parent)
{
Diff = true;
break;
}
}
if (Diff)
{
int NewIndex = -1;
// Look through existing indexes for a parent match
for (unsigned ii=0; iiParent?
bool Match = true;
for (auto ee: Active[ii])
{
if (ee->Parent != e->Parent)
{
Match = false;
break;
}
}
if (Match)
NewIndex = ii;
}
if (NewIndex < 0)
// Create new index for this parent
NewIndex = (int)Active.Length();
Edges->Delete(e);
auto &NewEdges = Active[NewIndex];
NewEdges.Add(e);
Edges = &Active[i]; // The 'Add' above can invalidate the object 'Edges' refers to
e->Idx = NewIndex;
c->Pos.Add(e, NewIndex);
n--;
}
else
{
LAssert(e->Idx == i);
c->Pos.Add(e, i);
}
}
}
}
// Process terminating edges
for (auto e: c->Edges)
{
if (e->Parent == c)
{
if (e->Idx < 0)
{
// This happens with out of order commits..?
continue;
}
int i = e->Idx;
if (c->NodeIdx < 0)
c->NodeIdx = i;
if (Active[i].HasItem(e))
Active[i].Delete(e);
else
LgiTrace("%s:%i - Warning: Active doesn't have 'e'.\n", _FL);
}
}
// Collapse any empty active columns
for (unsigned i=0; iIdx > 0);
edge->Idx--;
c->Pos.Add(edge, edge->Idx);
}
}
i--;
}
}
}
}
void VcFolder::UpdateBranchUi()
{
auto w = d->Wnd();
DropDownBtn *dd;
if (w->GetViewById(IDC_BRANCH_DROPDOWN, dd))
{
LString::Array a;
for (auto b: Branches)
a.Add(b.key);
dd->SetList(IDC_BRANCH, a);
}
LViewI *b;
if (Branches.Length() > 0 &&
w->GetViewById(IDC_BRANCH, b))
{
if (CurrentBranch)
{
b->Name(CurrentBranch);
}
else
{
auto it = Branches.begin();
if (it != Branches.end())
b->Name((*it).key);
}
}
LCombo *Cbo;
if (w->GetViewById(IDC_BRANCHES, Cbo))
{
Cbo->Empty();
int64 select = -1;
for (auto b: Branches)
{
if (CurrentBranch && CurrentBranch == b.key)
select = Cbo->Length();
Cbo->Insert(b.key);
}
if (select >= 0)
Cbo->Value(select);
Cbo->SendNotify(LNotifyTableLayoutRefresh);
// LgiTrace("%s:%i - Branches len=%i->%i\n", _FL, (int)Branches.Length(), (int)Cbo->Length());
}
}
VcFile *AppPriv::FindFile(const char *Path)
{
if (!Path)
return NULL;
LArray files;
if (Files->GetAll(files))
{
LString p = Path;
p = p.Replace(DIR_STR, "/");
for (auto f : files)
{
auto Fn = f->GetFileName();
if (p.Equals(Fn))
return f;
}
}
return NULL;
}
VcFile *VcFolder::FindFile(const char *Path)
{
return d->FindFile(Path);
}
void VcFolder::NoImplementation(const char* file, int line)
{
LString s;
s.Printf("%s, uri=%s, type=%s (%s:%i)",
LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE),
Uri.ToString().Get(),
toString(GetType()),
file, line);
OnCmdError(LString(), s);
}
void VcFolder::OnCmdError(LString Output, const char *Msg)
{
if (!CmdErrors)
{
if (Output.Length())
d->Log->Write(Output, Output.Length());
auto vc_name = GetVcName();
if (vc_name)
{
LString::Array a = GetProgramsInPath(GetVcName());
d->Log->Print("'%s' executables in the path:\n", GetVcName());
for (auto Bin : a)
d->Log->Print(" %s\n", Bin.Get());
}
else if (Msg)
{
d->Log->Print("%s\n", Msg);
}
}
CmdErrors++;
d->Tabs->Value(1);
GetCss(true)->Color(LColour::Red);
Update();
}
void VcFolder::ClearError()
{
GetCss(true)->Color(LCss::ColorInherit);
}
bool VcFolder::ParseInfo(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcGit:
case VcHg:
{
auto p = s.Strip().SplitDelimit();
CurrentCommit = p[0].Strip(" \t\r\n+");
if (p.Length() > 1)
CurrentCommitIdx = p[1].Int();
else
CurrentCommitIdx = -1;
if (Params && Params->Str.Equals("CountToTip"))
CountToTip();
break;
}
case VcSvn:
{
if (s.Find("client is too old") >= 0)
{
OnCmdError(s, "Client too old");
break;
}
LString::Array c = s.Split("\n");
for (unsigned i=0; iStr.Equals("Branch"))
SetCurrentBranch(NewRev);
else
CurrentCommit = NewRev;
}
NewRev.Empty();
IsUpdate = false;
return true;
}
bool VcFolder::ParseWorking(int Result, LString s, ParseParams *Params)
{
IsListingWorking = false;
switch (GetType())
{
case VcSvn:
case VcHg:
{
ParseParams Local;
if (!Params) Params = &Local;
Params->IsWorking = true;
ParseStatus(Result, s, Params);
break;
}
case VcCvs:
{
bool Untracked = d->IsMenuChecked(IDM_UNTRACKED);
if (Untracked)
{
auto Lines = s.SplitDelimit("\n");
for (auto Ln: Lines)
{
auto p = Ln.SplitDelimit(" \t", 1);
if (p.Length() > 1)
{
auto f = new VcFile(d, this, LString(), true);
f->SetText(p[0], COL_STATE);
f->SetText(p[1], COL_FILENAME);
f->GetStatus();
d->Files->Insert(f);
}
}
}
// else fall thru
}
default:
{
ParseDiffs(s, LString(), true);
break;
}
}
FilterCurrentFiles();
d->Files->ResizeColumnsToContent();
if (GetType() == VcSvn)
{
Unpushed = d->Files->Length() > 0 ? 1 : 0;
Update();
}
return false;
}
void VcFolder::DiffRange(const char *FromRev, const char *ToRev)
{
if (!FromRev || !ToRev)
return;
switch (GetType())
{
case VcSvn:
{
ParseParams *p = new ParseParams;
p->IsWorking = false;
p->Str = LString(FromRev) + ":" + ToRev;
LString a;
a.Printf("diff -r%s:%s", FromRev, ToRev);
StartCmd(a, &VcFolder::ParseDiff, p);
break;
}
case VcGit:
{
ParseParams *p = new ParseParams;
p->IsWorking = false;
p->Str = LString(FromRev) + ":" + ToRev;
LString a;
a.Printf("-P diff %s..%s", FromRev, ToRev);
StartCmd(a, &VcFolder::ParseDiff, p);
break;
}
case VcCvs:
case VcHg:
default:
LAssert(!"Impl me.");
break;
}
}
bool VcFolder::ParseDiff(int Result, LString s, ParseParams *Params)
{
if (Params)
ParseDiffs(s, Params->Str, Params->IsWorking);
else
ParseDiffs(s, LString(), true);
return false;
}
void VcFolder::Diff(VcFile *file)
{
auto Fn = file->GetFileName();
if (!Fn ||
!Stricmp(Fn, ".") ||
!Stricmp(Fn, ".."))
return;
const char *Prefix = "";
switch (GetType())
{
case VcGit:
Prefix = "-P ";
// fall through
case VcHg:
{
LString a;
auto rev = file->GetRevision();
if (rev)
a.Printf("%sdiff %s \"%s\"", Prefix, rev, Fn);
else
a.Printf("%sdiff \"%s\"", Prefix, Fn);
StartCmd(a, &VcFolder::ParseDiff);
break;
}
case VcSvn:
{
LString a;
if (file->GetRevision())
a.Printf("diff -r %s \"%s\"", file->GetRevision(), Fn);
else
a.Printf("diff \"%s\"", Fn);
StartCmd(a, &VcFolder::ParseDiff);
break;
}
case VcCvs:
break;
default:
LAssert(!"Impl me.");
break;
}
}
void VcFolder::InsertFiles(List &files)
{
d->Files->Insert(files);
if (FileToSelect)
{
LListItem *scroll = NULL;
for (auto f: files)
{
// Convert to an absolute path:
bool match = false;
auto relPath = f->GetText(COL_FILENAME);
if (IsLocal())
{
LFile::Path p(LocalPath());
p += relPath;
match = p.GetFull().Equals(FileToSelect);
}
else
{
match = !Stricmp(FileToSelect.Get(), relPath);
}
f->Select(match);
if (match)
scroll = f;
}
if (scroll)
scroll->ScrollTo();
}
}
bool VcFolder::ParseDiffs(LString s, LString Rev, bool IsWorking)
{
LAssert(IsWorking || Rev.Get() != NULL);
switch (GetType())
{
case VcGit:
{
List Files;
LString::Array a = s.Split("\n");
LString Diff;
VcFile *f = NULL;
for (unsigned i=0; iSetDiff(Diff);
Diff.Empty();
auto Bits = a[i].SplitDelimit();
LString Fn, State = "M";
if (Bits[1].Equals("--cc"))
{
Fn = Bits.Last();
State = "C";
}
else
Fn = Bits.Last()(2,-1);
// LgiTrace("%s\n", a[i].Get());
f = FindFile(Fn);
if (!f)
f = new VcFile(d, this, Rev, IsWorking);
f->SetText(State, COL_STATE);
f->SetText(Fn.Replace("\\","/"), COL_FILENAME);
f->GetStatus();
Files.Insert(f);
}
else if (!_strnicmp(Ln, "new file", 8))
{
if (f)
f->SetText("A", COL_STATE);
}
else if (!_strnicmp(Ln, "deleted file", 12))
{
if (f)
f->SetText("D", COL_STATE);
}
else if (!_strnicmp(Ln, "index", 5) ||
!_strnicmp(Ln, "commit", 6) ||
!_strnicmp(Ln, "Author:", 7) ||
!_strnicmp(Ln, "Date:", 5) ||
!_strnicmp(Ln, "+++", 3) ||
!_strnicmp(Ln, "---", 3))
{
// Ignore
}
else
{
if (Diff) Diff += "\n";
Diff += a[i];
}
}
if (f && Diff)
{
f->SetDiff(Diff);
Diff.Empty();
}
InsertFiles(Files);
break;
}
case VcHg:
{
LString Sep("\n");
LString::Array a = s.Split(Sep);
LString::Array Diffs;
VcFile *f = NULL;
List Files;
LProgressDlg Prog(GetTree(), 1000);
Prog.SetDescription("Reading diff lines...");
Prog.SetRange(a.Length());
// Prog.SetYieldTime(300);
for (unsigned i=0; iSetDiff(Sep.Join(Diffs));
Diffs.Empty();
auto MainParts = a[i].Split(" -r ");
auto FileParts = MainParts.Last().Split(" ",1);
LString Fn = FileParts.Last();
f = FindFile(Fn);
if (!f)
f = new VcFile(d, this, Rev, IsWorking);
f->SetText(Fn.Replace("\\","/"), COL_FILENAME);
// f->SetText(Status, COL_STATE);
Files.Insert(f);
}
else if (!_strnicmp(Ln, "index", 5) ||
!_strnicmp(Ln, "commit", 6) ||
!_strnicmp(Ln, "Author:", 7) ||
!_strnicmp(Ln, "Date:", 5) ||
!_strnicmp(Ln, "+++", 3) ||
!_strnicmp(Ln, "---", 3))
{
// Ignore
}
else
{
Diffs.Add(a[i]);
}
Prog.Value(i);
if (Prog.IsCancelled())
break;
}
if (f && Diffs.Length())
{
f->SetDiff(Sep.Join(Diffs));
Diffs.Empty();
}
InsertFiles(Files);
break;
}
case VcSvn:
{
List Files;
LString::Array a = s.Replace("\r").Split("\n");
LString Diff;
VcFile *f = NULL;
bool InPreamble = false;
bool InDiff = false;
for (unsigned i=0; iSetDiff(Diff);
f->Select(false);
}
Diff.Empty();
InDiff = false;
InPreamble = false;
LString Fn = a[i].Split(":", 1).Last().Strip();
f = FindFile(Fn);
if (!f)
f = new VcFile(d, this, Rev, IsWorking);
f->SetText(Fn.Replace("\\","/"), COL_FILENAME);
f->SetText("M", COL_STATE);
f->GetStatus();
Files.Insert(f);
}
else if (!_strnicmp(Ln, "------", 6))
{
InPreamble = !InPreamble;
}
else if (!_strnicmp(Ln, "======", 6))
{
InPreamble = false;
InDiff = true;
}
else if (InDiff)
{
if (!strncmp(Ln, "--- ", 4) ||
!strncmp(Ln, "+++ ", 4))
{
}
else
{
if (Diff) Diff += "\n";
Diff += a[i];
}
}
}
InsertFiles(Files);
if (f && Diff)
{
f->SetDiff(Diff);
Diff.Empty();
}
break;
}
case VcCvs:
{
break;
}
default:
{
LAssert(!"Impl me.");
break;
}
}
FilterCurrentFiles();
return true;
}
bool VcFolder::ParseFiles(int Result, LString s, ParseParams *Params)
{
d->ClearFiles();
ParseDiffs(s, Params->Str, false);
IsFilesCmd = false;
FilterCurrentFiles();
return false;
}
#if HAS_LIBSSH
void VcFolder::OnSshCmd(SshParams *p)
{
if (!p || !p->f)
{
LAssert(!"Param error.");
return;
}
LString s = p->Output;
int Result = p->ExitCode;
if (Result == ErrSubProcessFailed)
{
CmdErrors++;
}
else if (p->Parser)
{
bool Reselect = CALL_MEMBER_FN(*this, p->Parser)(Result, s, p->Params);
if (Reselect)
{
if (LTreeItem::Select())
Select(true);
}
}
if (p->Params &&
p->Params->Callback)
{
p->Params->Callback(Result, s);
}
}
#endif
void VcFolder::OnPulse()
{
bool Reselect = false, CmdsChanged = false;
static bool Processing = false;
if (!Processing)
{
Processing = true; // Lock out processing, if it puts up a dialog or something...
// bad things happen if we try and re-process something.
// printf("Cmds.Len=%i\n", (int)Cmds.Length());
for (unsigned i=0; iRd->GetState());
if (c->Rd->GetState() == LThread::THREAD_INIT)
{
if (CmdActiveThreads < CmdMaxThreads)
{
c->Rd->Run();
CmdActiveThreads++;
// printf("CmdActiveThreads++ = %i\n", CmdActiveThreads);
}
// else printf("Too many active threads.\n");
}
else if (c->Rd->IsExited())
{
CmdActiveThreads--;
// printf("CmdActiveThreads-- = %i\n", CmdActiveThreads);
LString s = c->GetBuf();
int Result = c->Rd->ExitCode();
if (Result == ErrSubProcessFailed)
{
if (!CmdErrors)
d->Log->Print("Error: Can't run '%s'\n", GetVcName());
CmdErrors++;
}
else if (c->PostOp)
{
if (s.Length() == 18 &&
s.Equals("GSUBPROCESS_ERROR\n"))
{
OnCmdError(s, "Sub process failed.");
}
else
{
Reselect |= CALL_MEMBER_FN(*this, c->PostOp)(Result, s, c->Params);
}
}
if (c->Params &&
c->Params->Callback)
{
c->Params->Callback(Result, s);
}
Cmds.DeleteAt(i--, true);
delete c;
CmdsChanged = true;
}
// else printf("Not exited.\n");
}
Processing = false;
}
if (Reselect)
{
if (LTreeItem::Select())
Select(true);
}
if (CmdsChanged)
{
Update();
}
if (CmdErrors)
{
d->Tabs->Value(1);
CmdErrors = false;
}
}
void VcFolder::OnRemove()
{
LXmlTag *t = d->Opts.LockTag(OPT_Folders, _FL);
if (t)
{
Uncommit.Reset();
if (LTreeItem::Select())
{
d->Files->Empty();
d->Commits->RemoveAll();
}
bool Found = false;
auto u = Uri.ToString();
for (auto c: t->Children)
{
if (!c->IsTag(OPT_Folder))
printf("%s:%i - Wrong tag: %s, %s\n", _FL, c->GetTag(), OPT_Folder);
else if (!c->GetContent())
printf("%s:%i - No content.\n", _FL);
else
{
auto Content = c->GetContent();
if (!_stricmp(Content, u))
{
c->RemoveTag();
delete c;
Found = true;
break;
}
}
}
LAssert(Found);
d->Opts.Unlock();
}
}
void VcFolder::Empty()
{
Type = VcNone;
IsCommit = false;
IsLogging = false;
IsUpdate = false;
IsFilesCmd = false;
CommitListDirty = false;
IsUpdatingCounts = false;
IsBranches = StatusNone;
IsIdent = StatusNone;
Unpushed = Unpulled = -1;
CmdErrors = 0;
CurrentCommitIdx = -1;
CurrentCommit.Empty();
RepoUrl.Empty();
VcCmd.Empty();
Uncommit.Reset();
Log.DeleteObjects();
d->Commits->Empty();
d->Files->Empty();
if (!Uri.IsFile())
GetCss(true)->Color(LColour::Blue);
}
void VcFolder::OnMouseClick(LMouse &m)
{
if (m.IsContextMenu())
{
LSubMenu s;
s.AppendItem("Browse To", IDM_BROWSE_FOLDER, Uri.IsFile());
s.AppendItem(
#ifdef WINDOWS
"Command Prompt At",
#else
"Terminal At",
#endif
IDM_TERMINAL, Uri.IsFile());
s.AppendItem("Clean", IDM_CLEAN);
s.AppendSeparator();
s.AppendItem("Pull", IDM_PULL);
s.AppendItem("Status", IDM_STATUS);
s.AppendItem("Push", IDM_PUSH);
s.AppendItem("Update Subs", IDM_UPDATE_SUBS, GetType() == VcGit);
s.AppendSeparator();
s.AppendItem("Remove", IDM_REMOVE);
s.AppendItem("Remote URL", IDM_REMOTE_URL);
if (!Uri.IsFile())
{
s.AppendSeparator();
s.AppendItem("Edit Location", IDM_EDIT);
}
int Cmd = s.Float(GetTree(), m);
switch (Cmd)
{
case IDM_BROWSE_FOLDER:
{
LBrowseToFile(LocalPath());
break;
}
case IDM_TERMINAL:
{
TerminalAt(LocalPath());
break;
}
case IDM_CLEAN:
{
Clean();
break;
}
case IDM_PULL:
{
Pull();
break;
}
case IDM_STATUS:
{
FolderStatus();
break;
}
case IDM_PUSH:
{
Push();
break;
}
case IDM_UPDATE_SUBS:
{
UpdateSubs();
break;
}
case IDM_REMOVE:
{
OnRemove();
delete this;
break;
}
case IDM_EDIT:
{
auto Dlg = new LInput(GetTree(), Uri.ToString(), "URI:", "Remote Folder Location");
Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId)
{
if (ctrlId)
{
Uri.Set(Dlg->GetStr());
Empty();
Select(true);
}
delete dlg;
});
break;
}
case IDM_REMOTE_URL:
{
GetRemoteUrl([this](auto code, auto str)
{
LString Url = str.Strip();
if (Url)
{
auto a = new LAlert(GetTree(), "Remote Url", Url, "Copy", "Ok");
a->DoModal([this, Url](auto dlg, auto code)
{
if (code == 1)
{
LClipBoard c(GetTree());
c.Text(Url);
}
delete dlg;
});
}
});
break;
}
default:
break;
}
}
}
LString &VcFolder::GetCurrentBranch()
{
return CurrentBranch;
}
void VcFolder::SetCurrentBranch(LString name)
{
if (CurrentBranch != name)
{
CurrentBranch = name;
UpdateBranchUi();
}
}
void VcFolder::Checkout(const char *Rev, bool isBranch)
{
if (!Rev || IsUpdate)
return;
LString Args;
LAutoPtr params(new ParseParams(isBranch ? "Branch" : "Rev"));
NewRev = Rev;
switch (GetType())
{
case VcGit:
Args.Printf("checkout %s", Rev);
IsUpdate = StartCmd(Args, &VcFolder::ParseCheckout, params.Release(), LogNormal);
break;
case VcSvn:
Args.Printf("up -r %s", Rev);
IsUpdate = StartCmd(Args, &VcFolder::ParseCheckout, params.Release(), LogNormal);
break;
case VcHg:
Args.Printf("update -r %s", Rev);
IsUpdate = StartCmd(Args, &VcFolder::ParseCheckout, params.Release(), LogNormal);
break;
default:
{
NoImplementation(_FL);
break;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////
int FolderCompare(LTreeItem *a, LTreeItem *b, NativeInt UserData)
{
VcLeaf *A = dynamic_cast(a);
VcLeaf *B = dynamic_cast(b);
if (!A || !B)
return 0;
return A->Compare(B);
}
struct SshFindEntry
{
LString Flags, Name, User, Group;
uint64_t Size;
LDateTime Modified, Access;
SshFindEntry &operator =(const LString &s)
{
auto p = s.SplitDelimit("/");
if (p.Length() == 7)
{
Flags = p[0];
Group = p[1];
User = p[2];
Access.Set((uint64_t) p[3].Int());
Modified.Set((uint64_t) p[4].Int());
Size = p[5].Int();
Name = p[6];
}
return *this;
}
bool IsDir() { return Flags(0) == 'd'; }
bool IsHidden() { return Name(0) == '.'; }
const char *GetName() { return Name; }
static int Compare(SshFindEntry *a, SshFindEntry *b) { return Stricmp(a->Name.Get(), b->Name.Get()); }
};
bool VcFolder::ParseRemoteFind(int Result, LString s, ParseParams *Params)
{
if (!Params || !s)
return false;
auto Parent = Params->Leaf ? static_cast(Params->Leaf) : static_cast(this);
LUri u(Params->Str);
auto Lines = s.SplitDelimit("\r\n");
LArray Entries;
for (size_t i=1; iStr, Dir.GetName(), true);
}
}
else if (!Dir.IsHidden())
{
char *Ext = LGetExtension(Dir.GetName());
if (!Ext) continue;
if (!stricmp(Ext, "c") ||
!stricmp(Ext, "cpp") ||
!stricmp(Ext, "h"))
{
LUri Path = u;
Path += Dir.GetName();
new VcLeaf(this, Parent, Params->Str, Dir.GetName(), false);
}
}
}
return false;
}
void VcFolder::ReadDir(LTreeItem *Parent, const char *ReadUri)
{
LUri u(ReadUri);
if (u.IsFile())
{
// Read child items
LDirectory Dir;
for (int b = Dir.First(u.LocalPath()); b; b = Dir.Next())
{
auto name = Dir.GetName();
if (Dir.IsHidden())
continue;
LUri Path = u;
Path += name;
new VcLeaf(this, Parent, u.ToString(), name, Dir.IsDir());
}
}
#if HAS_LIBSSH
else
{
auto c = d->GetConnection(ReadUri);
if (!c)
return;
LString Path = u.sPath(Uri.sPath.Length(), -1).LStrip("/");
LString Args;
Args.Printf("\"%s\" -maxdepth 1 -printf \"%%M/%%g/%%u/%%A@/%%T@/%%s/%%P\n\"", Path ? Path.Get() : ".");
auto *Params = new ParseParams(ReadUri);
Params->Leaf = dynamic_cast(Parent);
c->Command(this, "find", Args, &VcFolder::ParseRemoteFind, Params, SshConnection::LogNone);
return;
}
#endif
Parent->Sort(FolderCompare);
}
void VcFolder::OnVcsType(LString errorMsg)
{
if (!d)
{
LAssert(!"No priv instance");
return;
}
#if HAS_LIBSSH
auto c = d->GetConnection(Uri.ToString(), false);
if (c)
{
auto NewType = c->Types.Find(Uri.sPath);
if (NewType && NewType != Type)
{
if (NewType == VcError)
{
OnCmdError(LString(), errorMsg);
}
else
{
Type = NewType;
ClearError();
Update();
if (LTreeItem::Select())
Select(true);
for (auto &e: OnVcsTypeEvents)
e();
OnVcsTypeEvents.Empty();
}
}
}
#endif
}
void VcFolder::DoExpand()
{
if (Tmp)
{
Tmp->Remove();
DeleteObj(Tmp);
ReadDir(this, Uri.ToString());
}
}
void VcFolder::OnExpand(bool b)
{
if (b)
DoExpand();
}
void VcFolder::ListCommit(VcCommit *c)
{
if (!IsFilesCmd)
{
LString Args;
switch (GetType())
{
case VcGit:
Args.Printf("-P show %s^..%s", c->GetRev(), c->GetRev());
IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev()));
break;
case VcSvn:
Args.Printf("log --verbose --diff -r %s", c->GetRev());
IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev()));
break;
case VcCvs:
{
d->ClearFiles();
for (unsigned i=0; iFiles.Length(); i++)
{
VcFile *f = new VcFile(d, this, c->GetRev(), false);
if (f)
{
f->SetText(c->Files[i], COL_FILENAME);
d->Files->Insert(f);
}
}
FilterCurrentFiles();
break;
}
case VcHg:
{
Args.Printf("diff --change %s", c->GetRev());
IsFilesCmd = StartCmd(Args, &VcFolder::ParseFiles, new ParseParams(c->GetRev()));
break;
}
default:
LAssert(!"Impl me.");
break;
}
if (IsFilesCmd)
d->ClearFiles();
}
}
LString ConvertUPlus(LString s)
{
LArray c;
LUtf8Ptr p(s);
int32 ch;
while ((ch = p))
{
if (ch == '{')
{
auto n = p.GetPtr();
if (n[1] == 'U' &&
n[2] == '+')
{
// Convert unicode code point
p += 3;
ch = (int32)htoi(p.GetPtr());
c.Add(ch);
while ((ch = p) != '}')
p++;
}
else c.Add(ch);
}
else c.Add(ch);
p++;
}
c.Add(0);
#ifdef LINUX
return LString((char16*)c.AddressOf());
#else
return LString(c.AddressOf());
#endif
}
bool VcFolder::ParseStatus(int Result, LString s, ParseParams *Params)
{
bool ShowUntracked = d->Wnd()->GetCtrlValue(IDC_UNTRACKED) != 0;
bool IsWorking = Params ? Params->IsWorking : false;
List Ins;
switch (GetType())
{
case VcCvs:
{
LHashTbl,VcFile*> Map;
for (auto i: *d->Files)
{
VcFile *f = dynamic_cast(i);
if (f)
Map.Add(f->GetText(COL_FILENAME), f);
}
#if 0
LFile Tmp("C:\\tmp\\output.txt", O_WRITE);
Tmp.Write(s);
Tmp.Close();
#endif
LString::Array a = s.Split("===================================================================");
for (auto i : a)
{
LString::Array Lines = i.SplitDelimit("\r\n");
if (Lines.Length() == 0)
continue;
LString f = Lines[0].Strip();
if (f.Find("File:") == 0)
{
LString::Array Parts = f.SplitDelimit("\t");
LString File = Parts[0].Split(": ").Last().Strip();
LString Status = Parts[1].Split(": ").Last();
LString WorkingRev;
for (auto l : Lines)
{
LString::Array p = l.Strip().Split(":", 1);
if (p.Length() > 1 &&
p[0].Strip().Equals("Working revision"))
{
WorkingRev = p[1].Strip();
}
}
VcFile *f = Map.Find(File);
if (!f)
{
if ((f = new VcFile(d, this, WorkingRev, IsWorking)))
Ins.Insert(f);
}
if (f)
{
f->SetText(Status, COL_STATE);
f->SetText(File, COL_FILENAME);
f->Update();
}
}
else if (f(0) == '?' &&
ShowUntracked)
{
LString File = f(2, -1);
VcFile *f = Map.Find(File);
if (!f)
{
if ((f = new VcFile(d, this, LString(), IsWorking)))
Ins.Insert(f);
}
if (f)
{
f->SetText("?", COL_STATE);
f->SetText(File, COL_FILENAME);
f->Update();
}
}
}
for (auto i: *d->Files)
{
VcFile *f = dynamic_cast(i);
if (f)
{
if (f->GetStatus() == VcFile::SUnknown)
f->SetStatus(VcFile::SUntracked);
}
}
break;
}
case VcGit:
{
auto Lines = s.SplitDelimit("\r\n");
int Fmt = ToolVersion[VcGit] >= Ver2Int("2.8.0") ? 2 : 1;
for (auto Ln : Lines)
{
auto Type = Ln(0);
if (Ln.Lower().Find("error:") >= 0)
{
}
else if (Ln.Find("usage: git") >= 0)
{
// It's probably complaining about the --porcelain=2 parameter
OnCmdError(s, "Args error");
}
else if (Type != '?')
{
VcFile *f = NULL;
if (Fmt == 2)
{
LString::Array p = Ln.SplitDelimit(" ", 8);
if (p.Length() < 7)
d->Log->Print("%s:%i - Error: not enough tokens: '%s'\n", _FL, Ln.Get());
else
{
auto path = p[6];
f = new VcFile(d, this, path, IsWorking);
auto state = p[1].Strip(".");
auto pos = p[1].Find(state);
d->Log->Print("%s state='%s' pos=%i\n", path.Get(), state.Get(), (int)pos);
f->SetText(state, COL_STATE);
f->SetText(p.Last(), COL_FILENAME);
f->SetStaged(pos == 0);
}
}
else if (Fmt == 1)
{
LString::Array p = Ln.SplitDelimit(" ");
f = new VcFile(d, this, LString(), IsWorking);
f->SetText(p[0], COL_STATE);
f->SetText(p.Last(), COL_FILENAME);
}
if (f)
Ins.Insert(f);
}
else if (ShowUntracked)
{
VcFile *f = new VcFile(d, this, LString(), IsWorking);
f->SetText("?", COL_STATE);
f->SetText(Ln(2,-1), COL_FILENAME);
Ins.Insert(f);
}
}
break;
}
case VcHg:
case VcSvn:
{
if (s.Find("failed to import") >= 0)
{
OnCmdError(s, "Tool error.");
return false;
}
LString::Array Lines = s.SplitDelimit("\r\n");
for (auto Ln : Lines)
{
char Type = Ln(0);
if (Ln.Lower().Find("error:") >= 0)
{
}
else if (Ln.Find("client is too old") >= 0)
{
OnCmdError(s, "Client too old.");
return false;
}
else if (Strchr(" \t", Type) ||
Ln.Find("Summary of conflicts") >= 0)
{
// Ignore
}
else if (Type != '?')
{
LString::Array p = Ln.SplitDelimit(" ", 1);
if (p.Length() == 2)
{
LString File;
if (GetType() == VcSvn)
File = ConvertUPlus(p.Last());
else
File = p.Last();
if (GetType() == VcSvn &&
File.Find("+ ") == 0)
{
File = File(5, -1);
}
VcFile *f = new VcFile(d, this, LString(), IsWorking);
f->SetText(p[0], COL_STATE);
f->SetText(File.Replace("\\","/"), COL_FILENAME);
f->GetStatus();
Ins.Insert(f);
}
else LAssert(!"What happen?");
}
else if (ShowUntracked)
{
VcFile *f = new VcFile(d, this, LString(), IsWorking);
f->SetText("?", COL_STATE);
f->SetText(Ln(2,-1), COL_FILENAME);
Ins.Insert(f);
}
}
break;
}
default:
{
LAssert(!"Impl me.");
break;
}
}
if ((Unpushed = Ins.Length() > 0))
{
if (CmdErrors == 0)
GetCss(true)->Color(LColour(255, 128, 0));
}
else if (Unpulled == 0)
{
GetCss(true)->Color(LCss::ColorInherit);
}
Update();
if (LTreeItem::Select())
{
d->Files->Insert(Ins);
FilterCurrentFiles();
}
else
{
Ins.DeleteObjects();
}
if (Params && Params->Leaf)
Params->Leaf->AfterBrowse();
return false; // Don't refresh list
}
// Clone/checkout any sub-repositries.
bool VcFolder::UpdateSubs()
{
LString Arg;
switch (GetType())
{
default:
case VcSvn:
case VcHg:
case VcCvs:
return false;
case VcGit:
Arg = "submodule update --init --recursive";
break;
}
return StartCmd(Arg, &VcFolder::ParseUpdateSubs, NULL, LogNormal);
}
bool VcFolder::ParseUpdateSubs(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
default:
case VcSvn:
case VcHg:
case VcCvs:
return false;
case VcGit:
break;
}
return false;
}
void VcFolder::FolderStatus(const char *uri, VcLeaf *Notify)
{
LUri Uri(uri);
if (Uri.IsFile() && Uri.sPath)
{
LFile::Path p(Uri.sPath(1,-1));
if (!p.IsFolder())
{
LAssert(!"Needs to be a folder.");
return;
}
}
if (LTreeItem::Select())
d->ClearFiles();
LString Arg;
switch (GetType())
{
case VcSvn:
case VcHg:
Arg = "status";
break;
case VcCvs:
Arg = "status -l";
break;
case VcGit:
if (!ToolVersion[VcGit])
LAssert(!"Where is the version?");
// What version did =2 become available? It's definitely not in v2.5.4
// Not in v2.7.4 either...
if (ToolVersion[VcGit] >= Ver2Int("2.8.0"))
Arg = "-P status --porcelain=2";
else
Arg = "-P status --porcelain";
break;
default:
return;
}
ParseParams *p = new ParseParams;
if (uri && Notify)
{
p->AltInitPath = uri;
p->Leaf = Notify;
}
else
{
p->IsWorking = true;
}
StartCmd(Arg, &VcFolder::ParseStatus, p);
switch (GetType())
{
case VcHg:
CountToTip();
break;
default:
break;
}
}
void VcFolder::CountToTip()
{
// if (Path.Equals("C:\\Users\\matthew\\Code\\Lgi\\trunk"))
{
// LgiTrace("%s: CountToTip, br=%s, idx=%i\n", Path.Get(), CurrentBranch.Get(), (int)CurrentCommitIdx);
if (!CurrentBranch)
GetBranches(new ParseParams("CountToTip"));
else if (CurrentCommitIdx < 0)
GetCurrentRevision(new ParseParams("CountToTip"));
else
{
LString Arg;
Arg.Printf("id -n -r %s", CurrentBranch.Get());
StartCmd(Arg, &VcFolder::ParseCountToTip);
}
}
}
bool VcFolder::ParseCountToTip(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcHg:
if (CurrentCommitIdx >= 0)
{
auto p = s.Strip();
auto idx = p.Int();
if (idx >= CurrentCommitIdx)
{
Unpulled = (int) (idx - CurrentCommitIdx);
Update();
}
}
break;
default:
break;
}
return false;
}
void VcFolder::ListWorkingFolder()
{
if (IsListingWorking)
return;
d->ClearFiles();
bool Untracked = d->IsMenuChecked(IDM_UNTRACKED);
LString Arg;
switch (GetType())
{
case VcPending:
OnVcsTypeEvents.Add([this]()
{
ListWorkingFolder();
});
break;
case VcCvs:
if (Untracked)
Arg = "-qn update";
else
Arg = "-q diff --brief";
break;
case VcSvn:
Arg = "status";
break;
case VcGit:
#if 1
Arg = "-P status -vv";
#else
Arg = "-P diff --diff-filter=CMRTU --cached";
#endif
break;
case VcHg:
Arg = "status -mard";
break;
default:
return;
}
IsListingWorking = StartCmd(Arg, &VcFolder::ParseWorking);
}
void VcFolder::GitAdd()
{
if (!PostAdd)
return;
LString Args;
if (PostAdd->Files.Length() == 0)
{
LString m(PostAdd->Msg);
m = m.Replace("\"", "\\\"");
Args.Printf("commit -m \"%s\"", m.Get());
IsCommit = StartCmd(Args, &VcFolder::ParseCommit, PostAdd->Param, LogNormal);
PostAdd.Reset();
}
else
{
char NativeSep[] = {GetPathSep(), 0};
LString Last = PostAdd->Files.Last();
Args.Printf("add \"%s\"", Last.Replace("\"", "\\\"").Replace("/", NativeSep).Get());
PostAdd->Files.PopLast();
StartCmd(Args, &VcFolder::ParseGitAdd, NULL, LogNormal);
}
}
bool VcFolder::ParseGitAdd(int Result, LString s, ParseParams *Params)
{
if (Result)
{
OnCmdError(s, "add failed.");
}
else
{
GitAdd();
}
return false;
}
bool VcFolder::ParseCommit(int Result, LString s, ParseParams *Params)
{
if (LTreeItem::Select())
Select(true);
CommitListDirty = Result == 0;
CurrentCommit.Empty();
IsCommit = false;
if (Result)
{
switch (GetType())
{
case VcGit:
{
if (s.Find("Please tell me who you are") >= 0)
{
auto i = new LInput(GetTree(), "", "Git user name:", AppName);
i->DoModal([this, i](auto dlg, auto ctrlId)
{
if (ctrlId)
{
LString Args;
Args.Printf("config --global user.name \"%s\"", i->GetStr().Get());
StartCmd(Args);
auto inp = new LInput(GetTree(), "", "Git user email:", AppName);
i->DoModal([this, inp](auto dlg, auto ctrlId)
{
if (ctrlId)
{
LString Args;
Args.Printf("config --global user.email \"%s\"", inp->GetStr().Get());
StartCmd(Args);
}
delete dlg;
});
}
delete dlg;
});
}
break;
}
default:
break;
}
return false;
}
if (Result == 0 && LTreeItem::Select())
{
d->ClearFiles();
auto *w = d->Diff ? d->Diff->GetWindow() : NULL;
if (w)
w->SetCtrlName(IDC_MSG, NULL);
}
switch (GetType())
{
case VcGit:
{
Unpushed++;
CommitListDirty = true;
Update();
if (Params && Params->Str.Find("Push") >= 0)
Push();
break;
}
case VcSvn:
{
CurrentCommit.Empty();
CommitListDirty = true;
GetTree()->SendNotify((LNotifyType)LvcCommandEnd);
if (!Result)
{
Unpushed = 0;
Update();
GetCss(true)->Color(LColour::Green);
}
break;
}
case VcHg:
{
CurrentCommit.Empty();
CommitListDirty = true;
GetTree()->SendNotify((LNotifyType)LvcCommandEnd);
if (!Result)
{
Unpushed = 0;
Update();
if (Params && Params->Str.Find("Push") >= 0)
Push();
else
GetCss(true)->Color(LColour::Green);
}
break;
}
case VcCvs:
{
CurrentCommit.Empty();
CommitListDirty = true;
GetTree()->SendNotify((LNotifyType)LvcCommandEnd);
if (!Result)
{
Unpushed = 0;
Update();
GetCss(true)->Color(LColour::Green);
}
break;
}
default:
{
LAssert(!"Impl me.");
break;
}
}
return true;
}
void VcFolder::Commit(const char *Msg, const char *Branch, bool AndPush)
{
LArray Add;
bool Partial = false;
for (auto fp: *d->Files)
{
VcFile *f = dynamic_cast(fp);
if (f)
{
int c = f->Checked();
if (c > 0)
Add.Add(f);
else
Partial = true;
}
}
if (CurrentBranch && Branch &&
!CurrentBranch.Equals(Branch))
{
int Response = LgiMsg(GetTree(), "Do you want to start a new branch?", AppName, MB_YESNO);
if (Response != IDYES)
return;
LJson j;
j.Set("Command", "commit");
j.Set("Msg", Msg);
j.Set("AndPush", (int64_t)AndPush);
StartBranch(Branch, j.GetJson());
return;
}
if (!IsCommit)
{
LString Args;
ParseParams *Param = AndPush ? new ParseParams("Push") : NULL;
switch (GetType())
{
case VcGit:
{
if (Add.Length() == 0)
{
break;
}
else if (Partial)
{
if (PostAdd.Reset(new GitCommit))
{
PostAdd->Files.SetFixedLength(false);
for (auto f : Add)
PostAdd->Files.Add(f->GetFileName());
PostAdd->Msg = Msg;
PostAdd->Branch = Branch;
PostAdd->Param = Param;
GitAdd();
}
}
else
{
LString m(Msg);
m = m.Replace("\"", "\\\"");
Args.Printf("commit -am \"%s\"", m.Get());
IsCommit = StartCmd(Args, &VcFolder::ParseCommit, Param, LogNormal);
}
break;
}
case VcSvn:
{
LString::Array a;
a.New().Printf("commit -m \"%s\"", Msg);
for (auto pf: Add)
{
LString s = pf->GetFileName();
if (s.Find(" ") >= 0)
a.New().Printf("\"%s\"", s.Get());
else
a.New() = s;
}
Args = LString(" ").Join(a);
IsCommit = StartCmd(Args, &VcFolder::ParseCommit, Param, LogNormal);
if (d->Tabs && IsCommit)
{
d->Tabs->Value(1);
GetTree()->SendNotify((LNotifyType)LvcCommandStart);
}
break;
}
case VcHg:
{
LString::Array a;
LString CommitMsg = Msg;
TmpFile Tmp;
if (CommitMsg.Find("\n") >= 0)
{
Tmp.Create().Write(Msg);
a.New().Printf("commit -l \"%s\"", Tmp.GetName());
}
else
{
a.New().Printf("commit -m \"%s\"", Msg);
}
if (Partial)
{
for (auto pf: Add)
{
LString s = pf->GetFileName();
if (s.Find(" ") >= 0)
a.New().Printf("\"%s\"", s.Get());
else
a.New() = s;
}
}
Args = LString(" ").Join(a);
IsCommit = StartCmd(Args, &VcFolder::ParseCommit, Param, LogNormal);
if (d->Tabs && IsCommit)
{
d->Tabs->Value(1);
GetTree()->SendNotify((LNotifyType)LvcCommandStart);
}
break;
}
case VcCvs:
{
LString a;
a.Printf("commit -m \"%s\"", Msg);
IsCommit = StartCmd(a, &VcFolder::ParseCommit, NULL, LogNormal);
break;
}
default:
{
OnCmdError(LString(), "No commit impl for type.");
break;
}
}
}
}
bool VcFolder::ParseStartBranch(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcHg:
{
if (Result == 0 && Params && Params->Str)
{
LJson j(Params->Str);
auto cmd = j.Get("Command");
if (cmd.Equals("commit"))
{
auto Msg = j.Get("Msg");
auto AndPush = j.Get("AndPush").Int();
if (Msg)
{
Commit(Msg, NULL, AndPush > 0);
}
}
}
break;
}
default:
{
OnCmdError(LString(), "No commit impl for type.");
break;
}
}
return true;
}
void VcFolder::StartBranch(const char *BranchName, const char *OnCreated)
{
if (!BranchName)
return;
switch (GetType())
{
case VcHg:
{
LString a;
a.Printf("branch \"%s\"", BranchName);
StartCmd(a, &VcFolder::ParseStartBranch, OnCreated ? new ParseParams(OnCreated) : NULL);
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
}
void VcFolder::Push(bool NewBranchOk)
{
LString Args;
bool Working = false;
switch (GetType())
{
case VcHg:
{
auto args = NewBranchOk ? "push --new-branch" : "push";
Working = StartCmd(args, &VcFolder::ParsePush, NULL, LogNormal);
break;
}
case VcGit:
{
LString args;
if (NewBranchOk)
{
if (CurrentBranch)
{
args.Printf("push --set-upstream origin %s", CurrentBranch.Get());
}
else
{
OnCmdError(LString(), "Don't have the current branch?");
return;
}
}
else
{
args = "push";
}
Working = StartCmd(args, &VcFolder::ParsePush, NULL, LogNormal);
break;
}
case VcSvn:
{
// Nothing to do here.. the commit pushed the data already
break;
}
default:
{
OnCmdError(LString(), "No push impl for type.");
break;
}
}
if (d->Tabs && Working)
{
d->Tabs->Value(1);
GetTree()->SendNotify((LNotifyType)LvcCommandStart);
}
}
bool VcFolder::ParsePush(int Result, LString s, ParseParams *Params)
{
bool Status = false;
if (Result)
{
bool needsNewBranchPerm = false;
switch (GetType())
{
case VcHg:
{
needsNewBranchPerm = s.Find("push creates new remote branches") >= 0;
break;
}
case VcGit:
{
needsNewBranchPerm = s.Find("The current branch") >= 0 &&
s.Find("has no upstream branch") >= 0;
break;
}
}
if (needsNewBranchPerm &&
LgiMsg(GetTree(), LLoadString(IDS_CREATE_NEW_BRANCH), AppName, MB_YESNO) == IDYES)
{
Push(true);
return false;
}
OnCmdError(s, "Push failed.");
}
else
{
switch (GetType())
{
case VcGit:
break;
case VcSvn:
break;
default:
break;
}
Unpushed = 0;
GetCss(true)->Color(LColour::Green);
Update();
Status = true;
}
GetTree()->SendNotify((LNotifyType)LvcCommandEnd);
return Status; // no reselect
}
void VcFolder::Pull(int AndUpdate, LoggingType Logging)
{
bool Status = false;
if (AndUpdate < 0)
AndUpdate = GetTree()->GetWindow()->GetCtrlValue(IDC_UPDATE) != 0;
switch (GetType())
{
case VcNone:
return;
case VcHg:
Status = StartCmd(AndUpdate ? "pull -u" : "pull", &VcFolder::ParsePull, NULL, Logging);
break;
case VcGit:
Status = StartCmd(AndUpdate ? "pull" : "fetch", &VcFolder::ParsePull, NULL, Logging);
break;
case VcSvn:
Status = StartCmd("up", &VcFolder::ParsePull, NULL, Logging);
break;
case VcPending:
OnVcsTypeEvents.New() = [this, AndUpdate, Logging]()
{
Pull(AndUpdate, Logging);
};
break;
default:
NoImplementation(_FL);
break;
}
if (d->Tabs && Status)
{
d->Tabs->Value(1);
GetTree()->SendNotify((LNotifyType)LvcCommandStart);
}
}
bool VcFolder::ParsePull(int Result, LString s, ParseParams *Params)
{
GetTree()->SendNotify((LNotifyType)LvcCommandEnd);
if (Result)
{
OnCmdError(s, "Pull failed.");
return false;
}
else ClearError();
switch (GetType())
{
case VcGit:
{
// Git does a merge by default, so the current commit changes...
CurrentCommit.Empty();
break;
}
case VcHg:
{
CurrentCommit.Empty();
auto Lines = s.SplitDelimit("\n");
bool HasUpdates = false;
for (auto Ln: Lines)
{
if (Ln.Find("files updated") < 0)
continue;
auto Parts = Ln.Split(",");
for (auto p: Parts)
{
auto n = p.Strip().Split(" ", 1);
if (n.Length() == 2)
{
if (n[0].Int() > 0)
HasUpdates = true;
}
}
}
if (HasUpdates)
GetCss(true)->Color(LColour::Green);
else
GetCss(true)->Color(LCss::ColorInherit);
break;
}
case VcSvn:
{
// Svn also does a merge by default and can update our current position...
CurrentCommit.Empty();
LString::Array a = s.SplitDelimit("\r\n");
for (auto &Ln: a)
{
if (Ln.Find("At revision") >= 0)
{
LString::Array p = Ln.SplitDelimit(" .");
CurrentCommit = p.Last();
break;
}
else if (Ln.Find("svn cleanup") >= 0)
{
OnCmdError(s, "Needs cleanup");
break;
}
}
if (Params && Params->Str.Equals("log"))
{
LVariant Limit;
d->Opts.GetValue("svn-limit", Limit);
LString Args;
if (Limit.CastInt32() > 0)
Args.Printf("log --limit %i", Limit.CastInt32());
else
Args = "log";
IsLogging = StartCmd(Args, &VcFolder::ParseLog);
return false;
}
break;
}
default:
break;
}
CommitListDirty = true;
return true; // Yes - reselect and update
}
void VcFolder::MergeToLocal(LString Rev)
{
switch (GetType())
{
case VcGit:
{
LString Args;
Args.Printf("merge -m \"Merge with %s\" %s", Rev.Get(), Rev.Get());
StartCmd(Args, &VcFolder::ParseMerge, NULL, LogNormal);
break;
}
case VcHg:
{
LString Args;
Args.Printf("merge -r %s", Rev.Get());
StartCmd(Args, &VcFolder::ParseMerge, NULL, LogNormal);
break;
}
default:
LgiMsg(GetTree(), LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE), AppName);
break;
}
}
bool VcFolder::ParseMerge(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcGit:
case VcHg:
if (Result == 0)
CommitListDirty = true;
else
OnCmdError(s, LLoadString(IDS_ERR_MERGE_FAILED));
break;
default:
LAssert(!"Impl me.");
break;
}
return true;
}
void VcFolder::Refresh()
{
CommitListDirty = true;
CurrentCommit.Empty();
GitNames.Empty();
Branches.DeleteObjects();
if (Uncommit && Uncommit->LListItem::Select())
Uncommit->Select(true);
Select(true);
}
void VcFolder::Clean()
{
switch (GetType())
{
case VcSvn:
StartCmd("cleanup", &VcFolder::ParseClean, NULL, LogNormal);
break;
default:
LgiMsg(GetTree(), LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE), AppName);
break;
}
}
bool VcFolder::ParseClean(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcSvn:
if (Result == 0)
GetCss(true)->Color(LCss::ColorInherit);
break;
default:
LAssert(!"Impl me.");
break;
}
return false;
}
LColour VcFolder::BranchColour(const char *Name)
{
if (!Name)
return GetPaletteColour(0);
auto b = Branches.Find(Name);
if (!b) // Must be a new one?
{
int i = 1;
for (auto b: Branches)
{
auto &v = b.value;
if (!v->Colour.IsValid())
{
if (v->Default)
v->Colour = GetPaletteColour(0);
else
v->Colour = GetPaletteColour(i++);
}
}
Branches.Add(Name, b = new VcBranch(Name));
b->Colour = GetPaletteColour((int)Branches.Length());
}
return b ? b->Colour : GetPaletteColour(0);
}
void VcFolder::CurrentRev(std::function Callback)
{
LString Cmd;
Cmd.Printf("id -i");
RunCmd(Cmd, LogNormal, [Callback](auto r)
{
if (r.Code == 0)
Callback(r.Out.Strip());
});
}
bool VcFolder::RenameBranch(LString NewName, LArray &Revs)
{
switch (GetType())
{
case VcHg:
{
// Update to the ancestor of the commits
LHashTbl,int> Refs(0, -1);
for (auto c: Revs)
{
for (auto p:*c->GetParents())
if (Refs.Find(p) < 0)
Refs.Add(p, 0);
if (Refs.Find(c->GetRev()) >= 0)
Refs.Add(c->GetRev(), 1);
}
LString::Array Ans;
for (auto i:Refs)
{
if (i.value == 0)
Ans.Add(i.key);
}
LArray Ancestors = d->GetRevs(Ans);
if (Ans.Length() != 1)
{
// We should only have one ancestor
LString s, m;
s.Printf("Wrong number of ancestors: " LPrintfInt64 ".\n", Ans.Length());
for (auto i: Ancestors)
{
m.Printf("\t%s\n", i->GetRev());
s += m;
}
LgiMsg(GetTree(), s, AppName, MB_OK);
break;
}
LArray Top;
for (auto c:Revs)
{
for (auto p:*c->GetParents())
if (Refs.Find(p) == 0)
Top.Add(c);
}
if (Top.Length() != 1)
{
d->Log->Print("Error: Can't find top most commit. (%s:%i)\n", _FL);
return false;
}
// Create the new branch...
auto First = Ancestors.First();
LString Cmd;
Cmd.Printf("update -r " LPrintfInt64, First->GetIndex());
RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, &Top](auto r)
{
if (r.Code)
{
d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL);
return;
}
Cmd.Printf("branch \"%s\"", NewName.Get());
RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, &Top](auto r)
{
if (r.Code)
{
d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL);
return;
}
// Commit it to get a revision point to rebase to
Cmd.Printf("commit -m \"Branch: %s\"", NewName.Get());
RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, &Top](auto r)
{
if (r.Code)
{
d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL);
return;
}
CurrentRev([this, &Cmd, NewName, &Top](auto BranchNode)
{
// Rebase the old tree to this point
Cmd.Printf("rebase -s %s -d %s", Top.First()->GetRev(), BranchNode.Get());
RunCmd(Cmd, LogNormal, [this, &Cmd, NewName, Top](auto r)
{
if (r.Code)
{
d->Log->Print("Error: Cmd '%s' failed. (%s:%i)\n", Cmd.Get(), _FL);
return;
}
CommitListDirty = true;
d->Log->Print("Finished rename.\n", _FL);
});
});
});
});
});
break;
}
default:
{
LgiMsg(GetTree(), LLoadString(IDS_ERR_NO_IMPL_FOR_TYPE), AppName);
break;
}
}
return true;
}
bool VcFolder::ParseAddFile(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcCvs:
{
if (Result)
{
d->Tabs->Value(1);
OnCmdError(s, LLoadString(IDS_ERR_ADD_FAILED));
}
else ClearError();
break;
}
default:
break;
}
return false;
}
bool VcFolder::AddFile(const char *Path, bool AsBinary)
{
if (!Path)
return false;
switch (GetType())
{
case VcCvs:
{
auto p = LString(Path).RSplit(DIR_STR, 1);
ParseParams *params = NULL;
if (p.Length() >= 2)
{
if ((params = new ParseParams))
params->AltInitPath = p[0];
}
LString a;
a.Printf("add%s \"%s\"", AsBinary ? " -kb" : "", p.Length() > 1 ? p.Last().Get() : Path);
return StartCmd(a, &VcFolder::ParseAddFile, params);
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
return false;
}
bool VcFolder::ParseRevert(int Result, LString s, ParseParams *Params)
{
if (GetType() == VcSvn)
{
if (s.Find("Skipped ") >= 0)
Result = 1; // Stupid svn... *sigh*
}
if (Result)
{
OnCmdError(s, LLoadString(IDS_ERR_REVERT_FAILED));
}
ListWorkingFolder();
return false;
}
bool VcFolder::Revert(LString::Array &Uris, const char *Revision)
{
if (Uris.Length() == 0)
return false;
switch (GetType())
{
case VcGit:
{
LStringPipe cmd, paths;
LAutoPtr params;
if (Revision)
{
cmd.Print("checkout %s", Revision);
}
else
{
// Unstage the file...
cmd.Print("reset");
}
for (auto u: Uris)
{
auto Path = GetFilePart(u);
paths.Print(" \"%s\"", Path.Get());
}
auto p = paths.NewLStr();
cmd.Write(p);
if (!Revision)
{
if (params.Reset(new ParseParams))
{
params->Callback = [this, p](auto code, auto str)
{
LString c;
c.Printf("checkout %s", p.Get());
StartCmd(c, &VcFolder::ParseRevert);
};
}
}
return StartCmd(cmd.NewLStr(), &VcFolder::ParseRevert, params.Release());
break;
}
case VcHg:
case VcSvn:
{
LStringPipe p;
if (Revision)
p.Print("up -r %s", Revision);
else
p.Print("revert");
for (auto u: Uris)
{
auto Path = GetFilePart(u);
p.Print(" \"%s\"", Path.Get());
}
auto a = p.NewLStr();
return StartCmd(a, &VcFolder::ParseRevert);
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
return false;
}
bool VcFolder::ParseResolveList(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcHg:
{
auto lines = s.Replace("\r").Split("\n");
for (auto &ln: lines)
{
auto p = ln.Split(" ", 1);
if (p.Length() == 2)
{
if (p[0].Equals("U"))
{
auto f = new VcFile(d, this, LString(), true);
f->SetText(p[0], COL_STATE);
f->SetText(p[1], COL_FILENAME);
f->GetStatus();
d->Files->Insert(f);
}
}
}
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
return true;
}
bool VcFolder::ParseResolve(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcGit:
{
break;
}
case VcHg:
{
d->Log->Print("Resolve: %s\n", s.Get());
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
return true;
}
bool VcFolder::Resolve(const char *Path, LvcResolve Type)
{
if (!Path)
return false;
switch (GetType())
{
case VcGit:
{
LString a;
auto local = GetFilePart(Path);
LAutoPtr params(new ParseParams(Path));
switch (Type)
{
case ResolveIncoming:
a.Printf("checkout --theirs \"%s\"", local.Get());
break;
case ResolveLocal:
a.Printf("checkout --ours \"%s\"", local.Get());
break;
case ResolveMark:
a.Printf("add \"%s\"", local.Get());
break;
default:
OnCmdError(Path, "No resolve type implemented.");
return false;
}
if (Type == ResolveIncoming ||
Type == ResolveLocal)
{
// Add the file after the resolution:
params->Callback = [this, local](auto code, auto str)
{
LString a;
a.Printf("add \"%s\"", local.Get());
StartCmd(a, &VcFolder::ParseAddFile);
Refresh();
};
}
return StartCmd(a, &VcFolder::ParseResolve, params.Release());
}
case VcHg:
{
LString a;
auto local = GetFilePart(Path);
switch (Type)
{
case ResolveMark:
a.Printf("resolve -m \"%s\"", local.Get());
break;
case ResolveUnmark:
a.Printf("resolve -u \"%s\"", local.Get());
break;
case ResolveLocal:
a.Printf("resolve -t internal:local \"%s\"", local.Get());
break;
case ResolveIncoming:
a.Printf("resolve -t internal:other \"%s\"", local.Get());
break;
default:
break;
}
if (a)
return StartCmd(a, &VcFolder::ParseResolve, new ParseParams(Path));
break;
}
case VcSvn:
case VcCvs:
default:
{
NoImplementation(_FL);
break;
}
}
return false;
}
bool BlameLine::Parse(VersionCtrl type, LArray &out, LString in)
{
auto lines = in.SplitDelimit("\n", -1, false);
switch (type)
{
case VcGit:
{
for (auto &ln: lines)
{
auto s = ln.Get();
auto open = ln.Find("(");
auto close = ln.Find(")", open);
if (open > 0 && close > open)
{
auto eRef = ln(0, open-1);
auto fields = ln(open + 1, close);
auto parts = fields.SplitDelimit();
auto &o = out.New();
o.ref = eRef;
o.line = parts.Last();
parts.PopLast();
LString::Array name;
LDateTime dt;
for (auto p: parts)
{
auto first = p(0);
if (IsDigit(first))
{
if (p.Find("-") > 0)
dt.SetDate(p);
else if (p.Find(":") > 0)
dt.SetTime(p);
}
else if (first == '+')
dt.SetTimeZone((int)p.Int(), false);
else
name.Add(p);
}
o.user = LString(" ").Join(name);
o.date = dt.Get();
o.src = ln(close + 1, -1);
}
else if (ln.Length() > 0)
{
int asd=0;
}
}
break;
}
case VcHg:
{
for (auto &ln: lines)
{
auto s = ln.Get();
auto eUser = strchr(s, ' ');
if (!eUser)
continue;
auto eRef = strchr(eUser, ':');
if (!eRef)
continue;
auto &o = out.New();
o.user.Set(s, eUser++ - s);
o.ref.Set(eUser, eRef - eUser);
o.src = eRef + 1;
}
break;
}
/*
case VcSvn:
{
break;
}
*/
default:
{
LAssert(0);
return false;
}
}
return true;
}
bool VcFolder::ParseBlame(int Result, LString s, ParseParams *Params)
{
if (!Params)
{
LAssert(!"Need the path in the params.");
return false;
}
LArray lines;
if (BlameLine::Parse(GetType(), lines, s))
{
if (auto ui = new BrowseUi(BrowseUi::TBlame, d, this, Params->Str))
ui->ParseBlame(lines, s);
}
else NoImplementation(_FL);
return false;
}
bool VcFolder::Blame(const char *Path)
{
if (!Path)
return false;
auto file = GetFilePart(Path);
LAutoPtr Params(new ParseParams(file));
LUri u(Path);
switch (GetType())
{
case VcGit:
{
LString a;
a.Printf("-P blame \"%s\"", file.Get());
return StartCmd(a, &VcFolder::ParseBlame, Params.Release());
break;
}
case VcHg:
{
LString a;
a.Printf("annotate -un \"%s\"", file.Get());
return StartCmd(a, &VcFolder::ParseBlame, Params.Release());
break;
}
case VcSvn:
{
LString a;
a.Printf("blame \"%s\"", file.Get());
return StartCmd(a, &VcFolder::ParseBlame, Params.Release());
break;
}
default:
{
NoImplementation(_FL);
break;
}
}
return true;
}
bool VcFolder::SaveFileAs(const char *Path, const char *Revision)
{
if (!Path || !Revision)
return false;
return true;
}
bool VcFolder::ParseSaveAs(int Result, LString s, ParseParams *Params)
{
return false;
}
bool VcFolder::ParseCounts(int Result, LString s, ParseParams *Params)
{
switch (GetType())
{
case VcGit:
{
Unpushed = (int) s.Strip().Split("\n").Length();
break;
}
case VcSvn:
{
int64 ServerRev = 0;
bool HasUpdate = false;
LString::Array c = s.Split("\n");
for (unsigned i=0; i 1 && a[0].Equals("Status"))
ServerRev = a.Last().Int();
else if (a[0].Equals("*"))
HasUpdate = true;
}
if (ServerRev > 0 && HasUpdate)
{
int64 CurRev = CurrentCommit.Int();
Unpulled = (int) (ServerRev - CurRev);
}
else Unpulled = 0;
Update();
break;
}
default:
{
LAssert(!"Impl me.");
break;
}
}
IsUpdatingCounts = false;
Update();
return false; // No re-select
}
void VcFolder::SetEol(const char *Path, int Type)
{
if (!Path) return;
switch (Type)
{
case IDM_EOL_LF:
{
ConvertEol(Path, false);
break;
}
case IDM_EOL_CRLF:
{
ConvertEol(Path, true);
break;
}
case IDM_EOL_AUTO:
{
#ifdef WINDOWS
ConvertEol(Path, true);
#else
ConvertEol(Path, false);
#endif
break;
}
}
}
void VcFolder::UncommitedItem::Select(bool b)
{
LListItem::Select(b);
if (b)
{
LTreeItem *i = d->Tree->Selection();
VcFolder *f = dynamic_cast(i);
if (f)
f->ListWorkingFolder();
if (d->Msg)
{
d->Msg->Name(NULL);
auto *w = d->Msg->GetWindow();
if (w)
{
w->SetCtrlEnabled(IDC_COMMIT, true);
w->SetCtrlEnabled(IDC_COMMIT_AND_PUSH, true);
}
}
}
}
void VcFolder::UncommitedItem::OnPaint(LItem::ItemPaintCtx &Ctx)
{
LFont *f = GetList()->GetFont();
f->Transparent(false);
f->Colour(Ctx.Fore, Ctx.Back);
LDisplayString ds(f, "(working folder)");
ds.Draw(Ctx.pDC, Ctx.x1 + ((Ctx.X() - ds.X()) / 2), Ctx.y1 + ((Ctx.Y() - ds.Y()) / 2), &Ctx);
}
//////////////////////////////////////////////////////////////////////////////////////////
VcLeaf::VcLeaf(VcFolder *parent, LTreeItem *Item, LString uri, LString leaf, bool folder)
{
Parent = parent;
d = Parent->GetPriv();
LAssert(uri.Find("://") >= 0); // Is URI
Uri.Set(uri);
LAssert(Uri);
Leaf = leaf;
Folder = folder;
Item->Insert(this);
if (Folder)
{
Insert(Tmp = new LTreeItem);
Tmp->SetText("Loading...");
}
}
VcLeaf::~VcLeaf()
{
for (auto l: Log)
{
if (!l->GetList())
delete l;
}
}
LString VcLeaf::Full()
{
LUri u = Uri;
u += Leaf;
return u.ToString();
}
void VcLeaf::OnBrowse()
{
auto full = Full();
LList *Files = d->Files;
Files->Empty();
LDirectory Dir;
for (int b = Dir.First(full); b; b = Dir.Next())
{
if (Dir.IsDir())
continue;
VcFile *f = new VcFile(d, Parent, LString(), true);
if (f)
{
f->SetUri(LString("file://") + full);
f->SetText(Dir.GetName(), COL_FILENAME);
Files->Insert(f);
}
}
Files->ResizeColumnsToContent();
if (Folder)
Parent->FolderStatus(full, this);
}
void VcLeaf::AfterBrowse()
{
}
VcLeaf *VcLeaf::FindLeaf(const char *Path, bool OpenTree)
{
if (!Stricmp(Path, Full().Get()))
return this;
if (OpenTree)
DoExpand();
VcLeaf *r = NULL;
for (auto n = GetChild(); !r && n; n = n->GetNext())
{
auto l = dynamic_cast(n);
if (l)
r = l->FindLeaf(Path, OpenTree);
}
return r;
}
void VcLeaf::DoExpand()
{
if (Tmp)
{
Tmp->Remove();
DeleteObj(Tmp);
Parent->ReadDir(this, Full());
}
}
void VcLeaf::OnExpand(bool b)
{
if (b)
DoExpand();
}
const char *VcLeaf::GetText(int Col)
{
if (Col == 0)
return Leaf;
return NULL;
}
int VcLeaf::GetImage(int Flags)
{
return Folder ? IcoFolder : IcoFile;
}
int VcLeaf::Compare(VcLeaf *b)
{
// Sort folders to the top...
if (Folder ^ b->Folder)
return (int)b->Folder - (int)Folder;
// Then alphabetical
return Stricmp(Leaf.Get(), b->Leaf.Get());
}
bool VcLeaf::Select()
{
return LTreeItem::Select();
}
void VcLeaf::Select(bool b)
{
LTreeItem::Select(b);
if (b)
{
d->Commits->RemoveAll();
OnBrowse();
ShowLog();
}
}
void VcLeaf::ShowLog()
{
if (!Log.Length())
return;
d->Commits->RemoveAll();
Parent->DefaultFields();
Parent->UpdateColumns();
for (auto i: Log)
// We make a copy of the commit here so that the LList owns the copied object,
// and this object still owns 'i'.
d->Commits->Insert(new VcCommit(*i), -1, false);
d->Commits->UpdateAllItems();
d->Commits->ResizeColumnsToContent();
}
void VcLeaf::OnMouseClick(LMouse &m)
{
if (m.IsContextMenu())
{
LSubMenu s;
s.AppendItem("Log", IDM_LOG);
s.AppendItem("Blame", IDM_BLAME, !Folder);
s.AppendSeparator();
s.AppendItem("Browse To", IDM_BROWSE_FOLDER);
s.AppendItem("Terminal At", IDM_TERMINAL);
int Cmd = s.Float(GetTree(), m - _ScrollPos());
switch (Cmd)
{
case IDM_LOG:
{
Parent->LogFile(Full());
break;
}
case IDM_BLAME:
{
Parent->Blame(Full());
break;
}
case IDM_BROWSE_FOLDER:
{
LBrowseToFile(Full());
break;
}
case IDM_TERMINAL:
{
TerminalAt(Full());
break;
}
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////
ProcessCallback::ProcessCallback(LString exe,
LString args,
LString localPath,
LTextLog *log,
LView *view,
std::function callback) :
Log(log),
View(view),
Callback(callback),
LThread("ProcessCallback.Thread"),
LSubProcess(exe, args)
{
SetInitFolder(localPath);
if (Log)
Log->Print("%s %s\n", exe.Get(), args.Get());
Run();
}
int ProcessCallback::Main()
{
if (!Start())
{
Ret.Out.Printf("Process failed with %i", GetErrorCode());
Callback(Ret);
}
else
{
while (IsRunning())
{
auto Rd = Read();
if (Rd.Length())
{
Ret.Out += Rd;
if (Log)
Log->Write(Rd.Get(), Rd.Length());
}
}
auto Rd = Read();
if (Rd.Length())
{
Ret.Out += Rd;
if (Log)
Log->Write(Rd.Get(), Rd.Length());
}
Ret.Code = GetExitValue();
}
View->PostEvent(M_HANDLE_CALLBACK, (LMessage::Param)this);
return 0;
}
void ProcessCallback::OnComplete() // Called in the GUI thread...
{
Callback(Ret);
}
diff --git a/lvc/src/VcFolder.h b/lvc/src/VcFolder.h
--- a/lvc/src/VcFolder.h
+++ b/lvc/src/VcFolder.h
@@ -1,405 +1,407 @@
#ifndef _VcFolder_h_
#define _VcFolder_h_
#include
#include "lgi/common/SubProcess.h"
#include "lgi/common/Uri.h"
#include "SshConnection.h"
class VcLeaf;
enum LoggingType
{
LogNone, // No output from cmd
LogNormal, // Output appears as it's available
LogSilo, // Output appears after cmd finished (keeps it non-interleaved with other log msgs)
LogDebug, // Debug log level
};
enum LvcError
{
ErrNone,
ErrSubProcessFailed = GSUBPROCESS_ERROR,
};
enum LvcStatus
{
StatusNone,
StatusActive,
StatusError,
};
enum LvcResolve
{
ResolveNone,
//---
ResolveMark,
ResolveUnmark,
//---
ResolveLocal,
ResolveIncoming,
ResolveTool,
};
class ReaderThread : public LThread
{
VersionCtrl Vcs;
LStream *Out;
LAutoPtr Process;
int FilterCount;
int OnLine(char *s, ssize_t len);
bool OnData(char *Buf, ssize_t &r);
public:
int Result;
ReaderThread(VersionCtrl vcs, LAutoPtr p, LStream *out);
~ReaderThread();
int Main();
};
extern int Ver2Int(LString v);
extern int ToolVersion[VcMax];
extern int LstCmp(LListItem *a, LListItem *b, int Col);
const char *GetVcName(VersionCtrl type);
struct Result
{
int Code;
LString Out;
};
struct ProcessCallback : public LThread, public LSubProcess
{
Result Ret;
LView *View = NULL;
LTextLog *Log = NULL;
std::function Callback;
public:
ProcessCallback(LString exe,
LString args,
LString localPath,
LTextLog *log,
LView *view,
std::function callback);
int Main();
void OnComplete();
};
struct VcBranch : public LString
{
bool Default;
LColour Colour;
LString Hash;
VcBranch(LString name, LString hash = LString())
{
Default = name.Equals("default") ||
name.Equals("trunk") ||
name.Equals("main");
Set(name);
if (hash)
Hash = hash;
}
};
#if HAS_LIBSSH
struct SshParams
{
SshConnection *c = NULL;
SshConnection::LoggingType LogType = SshConnection::LogNone;
VcFolder *f = NULL;
LString Exe, Args, Path, Output;
VersionCtrl Vcs = VcNone;
ParseFn Parser = NULL;
ParseParams *Params = NULL;
int ExitCode = -1;
SshParams(SshConnection *con) : c(con)
{
}
};
#endif
class VcFolder : public LTreeItem
{
friend class VcCommit;
class Cmd : public LStream
{
LString::Array Context;
LStringPipe Buf;
public:
LoggingType Logging;
LStream *Log;
LAutoPtr Rd;
ParseFn PostOp;
LAutoPtr Params;
LvcError Err;
Cmd(LString::Array &context, LoggingType logging, LStream *log)
{
Context = context;
Logging = logging;
Log = log;
Err = ErrNone;
}
~Cmd()
{
}
LString GetBuf()
{
LString s = Buf.NewLStr();
if (Log && Logging == LogSilo)
{
LString m;
m.Printf("=== %s ===\n\t%s %s\n",
Context[0].Get(),
Context[1].Get(), Context[2].Get());
Log->Write(m.Get(), m.Length());
auto Lines = s.Split("\n");
for (auto Ln : Lines)
Log->Print("\t%s\n", Ln.Get());
}
return s;
}
ssize_t Write(const void *Ptr, ssize_t Size, int Flags = 0)
{
ssize_t Wr = Buf.Write(Ptr, Size, Flags);
if (Log && Logging == LogNormal)
Log->Write(Ptr, Size, Flags);
if (Flags)
Err = (LvcError) Flags;
return Wr;
}
};
class UncommitedItem : public LListItem
{
AppPriv *d;
public:
UncommitedItem(AppPriv *priv)
{
d = priv;
}
void OnPaint(LItem::ItemPaintCtx &Ctx);
void Select(bool b);
};
AppPriv *d;
VersionCtrl Type = VcNone;
LUri Uri;
LString CurrentCommit, RepoUrl, VcCmd;
int64 CurrentCommitIdx = -1;
LArray Log;
LString CurrentBranch;
LHashTbl,VcBranch*> Branches;
LAutoPtr Uncommit;
LString Cache, NewRev;
bool CommitListDirty = false;
int Unpushed = -1, Unpulled = -1;
LString CountCache;
LTreeItem *Tmp = NULL;
int CmdErrors = 0;
LArray Fields;
LArray> OnVcsTypeEvents;
// Author name/email
LString AuthorName, AuthorEmail;
bool IsGettingAuthor = false;
// This is set when a blame or log is looking at a particular file,
// and wants it selected after the file list is populated
LString FileToSelect;
void InsertFiles(List &files);
// Git specific
LHashTbl,LString> GitNames;
void AddGitName(LString Hash, LString Name);
LString GetGitNames(LString Hash);
static int CmdMaxThreads;
static int CmdActiveThreads;
struct GitCommit
{
LString::Array Files;
LString Msg, Branch;
ParseParams *Param;
GitCommit()
{
Param = NULL;
}
};
LAutoPtr PostAdd;
void GitAdd();
LArray Cmds;
bool IsLogging = false, IsUpdate = false, IsFilesCmd = false;
bool IsCommit = false, IsUpdatingCounts = false;
bool IsListingWorking = false;
LvcStatus IsBranches = StatusNone, IsIdent = StatusNone;
void Init(AppPriv *priv);
const char *GetVcName();
char GetPathSep();
bool StartCmd(const char *Args, ParseFn Parser = NULL, ParseParams *Params = NULL, LoggingType Logging = LogNone);
bool RunCmd(const char *Args, LoggingType Logging, std::function Callback);
void OnBranchesChange();
void NoImplementation(const char *file, int line);
void OnCmdError(LString Output, const char *Msg);
void ClearError();
VcFile *FindFile(const char *Path);
void LinkParents();
void CurrentRev(std::function Callback);
LColour BranchColour(const char *Name);
void UpdateBranchUi();
bool ParseDiffs(LString s, LString Rev, bool IsWorking);
bool ParseRevList(int Result, LString s, ParseParams *Params);
bool ParseLog(int Result, LString s, ParseParams *Params);
bool ParseInfo(int Result, LString s, ParseParams *Params);
bool ParseFiles(int Result, LString s, ParseParams *Params);
bool ParseWorking(int Result, LString s, ParseParams *Params);
bool ParseCheckout(int Result, LString s, ParseParams *Params);
bool ParseCommit(int Result, LString s, ParseParams *Params);
bool ParseGitAdd(int Result, LString s, ParseParams *Params);
bool ParsePush(int Result, LString s, ParseParams *Params);
bool ParsePull(int Result, LString s, ParseParams *Params);
bool ParseCounts(int Result, LString s, ParseParams *Params);
bool ParseRevert(int Result, LString s, ParseParams *Params);
bool ParseResolveList(int Result, LString s, ParseParams *Params);
bool ParseResolve(int Result, LString s, ParseParams *Params);
bool ParseBlame(int Result, LString s, ParseParams *Params);
bool ParseSaveAs(int Result, LString s, ParseParams *Params);
bool ParseBranches(int Result, LString s, ParseParams *Params);
bool ParseStatus(int Result, LString s, ParseParams *Params);
bool ParseAddFile(int Result, LString s, ParseParams *Params);
bool ParseVersion(int Result, LString s, ParseParams *Params);
bool ParseClean(int Result, LString s, ParseParams *Params);
bool ParseDiff(int Result, LString s, ParseParams *Params);
bool ParseMerge(int Result, LString s, ParseParams *Params);
bool ParseCountToTip(int Result, LString s, ParseParams *Params);
bool ParseUpdateSubs(int Result, LString s, ParseParams *Params);
bool ParseRemoteFind(int Result, LString s, ParseParams *Params);
bool ParseStartBranch(int Result, LString s, ParseParams *Params);
bool ParseSelectCommit(int Result, LString s, ParseParams *Params);
void DoExpand();
public:
VcFolder(AppPriv *priv, const char *uri);
VcFolder(AppPriv *priv, LXmlTag *t);
~VcFolder();
VersionCtrl GetType();
AppPriv *GetPriv() { return d; }
bool IsLocal();
const char *LocalPath();
LUri GetUri() { return Uri; }
VcLeaf *FindLeaf(const char *Path, bool OpenTree);
void DefaultFields();
void UpdateColumns(LList *lst = NULL);
int IndexOfCommitField(CommitField fld);
const char *GetText(int Col);
LArray &GetFields() { return Fields; }
bool Serialize(LXmlTag *t, bool Write);
LString &GetCurrentBranch();
void SetCurrentBranch(LString name);
LXmlTag *Save();
- bool GetRepoAuthor();
+ LString GetConfigFile(bool local);
+ bool GetAuthor(bool local, std::function callback);
+ bool SetAuthor(bool local, LString name, LString email);
void UpdateAuthorUi();
void Empty();
void Select(bool b);
void ListCommit(VcCommit *c);
void ListWorkingFolder();
void FolderStatus(const char *Path = NULL, VcLeaf *Notify = NULL);
void Commit(const char *Msg, const char *Branch, bool AndPush);
void StartBranch(const char *BranchName, const char *OnCreated = NULL);
void Push(bool NewBranchOk = false);
void Pull(int AndUpdate = -1, LoggingType Logging = LogNormal);
void Clean();
bool Revert(LString::Array &uris, const char *Revision = NULL);
bool Resolve(const char *Path, LvcResolve Type);
bool AddFile(const char *Path, bool AsBinary = true);
bool Blame(const char *Path);
bool SaveFileAs(const char *Path, const char *Revision);
void ReadDir(LTreeItem *Parent, const char *Uri);
void SetEol(const char *Path, int Type);
void GetVersion();
void Diff(VcFile *file);
void DiffRange(const char *FromRev, const char *ToRev);
void MergeToLocal(LString Rev);
bool RenameBranch(LString NewName, LArray &Revs);
void Refresh();
bool GetBranches(ParseParams *Params = NULL);
void GetCurrentRevision(ParseParams *Params = NULL);
void CountToTip();
bool UpdateSubs(); // Clone/checkout any sub-repositories.
void LogFilter(const char *Filter);
void LogFile(const char *Path);
void ClearLog();
LString GetFilePart(const char *uri);
void FilterCurrentFiles();
void GetRemoteUrl(std::function Callback);
void SelectCommit(LWindow *Parent, LString Commit, LString Path);
void Checkout(const char *Rev, bool isBranch);
void OnPulse();
void OnMouseClick(LMouse &m);
void OnRemove();
void OnExpand(bool b);
void OnVcsType(LString errorMsg);
#if HAS_LIBSSH
void OnSshCmd(SshParams *p);
#endif
};
class VcLeaf : public LTreeItem
{
AppPriv *d = NULL;
VcFolder *Parent = NULL;
bool Folder = false;
LUri Uri;
LString Leaf;
LTreeItem *Tmp = NULL;
void DoExpand();
public:
LArray Log;
VcLeaf(VcFolder *parent, LTreeItem *Item, LString uri, LString leaf, bool folder);
~VcLeaf();
LString Full();
VcLeaf *FindLeaf(const char *Path, bool OpenTree);
void OnBrowse();
void AfterBrowse();
void OnExpand(bool b);
const char *GetText(int Col);
int GetImage(int Flags);
int Compare(VcLeaf *b);
bool Select();
void Select(bool b);
void OnMouseClick(LMouse &m);
void ShowLog();
};
#endif