ultimatepp/uppsrc/ide/RepoSync.cpp

598 lines
15 KiB
C++

#include "ide.h"
String RepoCfgFile(const String& path)
{
return ConfigFile("cfg/repo-" + SHA1String(path));
}
String GetGitUrl(const String& repo_dir)
{
Vector<String> ln = Split(GitCmd(repo_dir, "config --get remote.origin.url"), CharFilterCrLf);
return ln.GetCount() ? ln[0] : String();
}
String GetSvnUrl(const String& repo_dir)
{
Vector<String> ln = Split(RepoSys("svn info --show-item repos-root-url " + repo_dir), CharFilterCrLf);
return ln.GetCount() ? ln[0] : String();
}
int UrepoConsole::Git(const char *dir, const char *command, bool pwd)
{
String h = GetCurrentDirectory();
SetCurrentDirectory(dir);
list.Add(AttrText(String("cd ") + dir).SetFont(font().Bold().Italic()).Ink(SLtBlue()));
String cmd = String() << "git " << command;
if(pwd) {
String url = GetGitUrl(dir);
String username, password;
if(url.StartsWith("https://") && GetCredentials(url, dir, username, password)) {
String https = "https://";
cmd << ' ' << https;
if(username.GetCount())
cmd << UrlEncode(username) + ":";
int p = cmd.GetCount();
cmd << UrlEncode(password);
HidePassword(p, cmd.GetCount());
cmd << "@" << url.Mid(https.GetCount());
}
}
int code = CheckSystem(cmd);
SetCurrentDirectory(h);
return code;
}
String RepoSync::SvnCmd(UrepoConsole& sys, const char *svncmd, const String& dir)
{
String cmd;
cmd << "svn " << svncmd << " --non-interactive ";
String username, password;
if(GetCredentials(GetSvnUrl(dir), dir, username, password)) {
cmd << "--username " << username << " --password ";
int i0 = cmd.GetCount();
cmd << password;
sys.HidePassword(i0, cmd.GetCount());
cmd << " ";
}
return cmd;
}
RepoSync::RepoSync()
{
CtrlLayoutOKCancel(*this, "Version control repository synchronize");
list.AddIndex();
list.AddIndex();
list.AddColumn("Action");
list.AddColumn("Path");
list.AddColumn("Changes");
list.ColumnWidths("220 500 100");
list.NoCursor().EvenRowColor();
list.SetLineCy(max(Draw::GetStdFontCy() + Zy(4), Zy(20)));
Sizeable().Zoomable();
BackPaint();
credentials << [=] {
Index<String> hint;
for(const auto& w : ~work) {
String path = w.key;
String s = decode(w.value, SVN_DIR, GetSvnUrl(path), GIT_DIR, GetGitUrl(path), Null);
if(s.GetCount())
hint.FindAdd(s);
if(path.GetCount())
hint.FindAdd(path);
}
EditCredentials(hint.PickKeys());
};
}
RepoSync::~RepoSync()
{
int repoi = 0;
for(int i = 0; i < list.GetCount(); i++) {
SvnOptions *svn = dynamic_cast<SvnOptions *>(list.GetCtrl(i, 0));
GitOptions *git = dynamic_cast<GitOptions *>(list.GetCtrl(i, 0));
if(svn || git) {
String s;
if(svn) {
if(svn->commit)
s << "commit;";
if(svn->update)
s << "update;";
}
if(git) {
if(git->pull)
s << "pull;";
if(git->commit)
s << "commit;";
if(git->push)
s << "push;";
}
SaveChangedFile(RepoCfgFile(work.GetKey(repoi++)), s);
}
}
}
int CharFilterSvnMsgRepo(int c)
{
return c >= 32 && c < 128 && c != '\"' ? c : 0;
}
bool IsConflictFile(String path)
{
String ext = GetFileExt(path);
if(*ext == '.') {
ext = ext.Mid(1);
if(findarg(ext, "mine", "theirs", "working") >= 0 || *ext == 'r' && IsDigit(ext[1])) {
for(int i = 0; i < 3; i++) {
int q = path.ReverseFind('.');
if(q < 0)
return false;
path.Trim(q);
if(FileExists(path))
return true;
}
}
}
return false;
}
bool RepoSync::ListSvn(const String& path)
{
Vector<String> ln = Split(RepoSys("svn status " + path), CharFilterCrLf);
bool actions = false;
for(int pass = 0; pass < 2; pass++)
for(int i = 0; i < ln.GetCount(); i++) {
String h = ln[i];
if(h.GetCount() > 7) {
String file = GetFullPath(TrimLeft(h.Mid(7)));
if(IsFullPath(file)) {
actions = true;
h.Trim(7);
bool simple = h.Mid(1, 6) == " ";
int action = simple ? String("MC?!~").Find(h[0]) : SVN_IGNORE;
if(h == " S ")
action = REPLACE;
String an;
Color color;
if(action == SVN_IGNORE) {
color = Black;
if(simple && h[0] == 'A') {
an = "svn add";
action = SVN_ACTION;
}
else
if(simple && h[0] == 'D') {
an = "svn delete";
action = SVN_ACTION;
}
else {
an = h.Mid(0, 7);
color = Gray;
}
}
else {
if(action == ADD && IsConflictFile(file)) {
action = DELETEC;
an = "Delete (conflict resolved)";
color = AdjustIfDark(Black());
}
else {
static const char *as[] = {
"Modify", "Resolved", "Add", "Remove", "Replace"
};
static Color c[] = { LtBlue, Magenta, Green, LtRed, LtMagenta };
an = as[action];
color = AdjustIfDark(c[action]);
}
}
if(pass == action < 0 && action != DELETEC) {
int ii = list.GetCount();
list.Add(action, file, Null,
AttrText(action < 0 ? ln[i] : " " + file.Mid(path.GetCount() + 1)).Ink(color));
if(action >= 0) {
list.SetCtrl(ii, 0, revert.Add().SetLabel(an + (action == ADD ? "\nSkip" : "\nRevert")).NoWantFocus());
revert.Top() <<= 0;
Ctrl& b = diff.Add().SetLabel("Changes..").SizePos().NoWantFocus();
b << [=] { DoDiff(ii); };
list.SetCtrl(ii, 2, b);
}
}
}
}
}
return actions;
}
String GitCmd(const char *dir, const char *command)
{
LOG("GitCmd " << dir << ", " << command);
String h = GetCurrentDirectory();
SetCurrentDirectory(dir);
String r = RepoSys(String() << "git " << command);
SetCurrentDirectory(h);
return r;
}
bool RepoSync::ListGit(const String& path)
{
Vector<String> ln = Split(GitCmd(path, "status --porcelain ."), CharFilterCrLf);
bool actions = false;
for(int i = 0; i < ln.GetCount(); i++) {
String h = ln[i];
if(h.GetCount() > 3) {
if(!h.TrimEnd("/"))
h.TrimEnd("\\");
String file = AppendFileName(path, h.Mid(3));
actions = true;
int action = String("M.?DR").Find(h[1]);
if(action < 0 || h[0] != '?' && h[0] != ' ')
action = SVN_IGNORE;
String an;
Color color;
if(action == SVN_IGNORE) {
an = h;
color = Gray;
}
else {
static const char *as[] = {
"Modify", "Resolved", "Add", "Remove", "Rename"
};
static Color c[] = { LtBlue, Magenta, Green, LtRed, LtMagenta };
an = as[action];
color = AdjustIfDark(c[action]);
}
int ii = list.GetCount();
list.Add(action, file, Null, AttrText(action < 0 ? h : file).Ink(color));
if(action >= 0) {
list.SetCtrl(ii, 0, revert.Add().SetLabel(an + (action == ADD ? "\nSkip" : "\nRevert")).NoWantFocus());
revert.Top() <<= 0;
Ctrl& b = diff.Add().SetLabel("Changes..").SizePos().NoWantFocus();
b <<= THISBACK1(DoDiff, ii);
list.SetCtrl(ii, 2, b);
}
}
}
return actions;
}
void RepoSync::SyncCommits()
{
bool commit = true;
for(int i = 0; i < list.GetCount(); i++) {
if(SvnOptions *o = dynamic_cast<SvnOptions *>(list.GetCtrl(i, 0)))
commit = o->commit.IsEnabled() && o->commit;
else
if(GitOptions *o = dynamic_cast<GitOptions *>(list.GetCtrl(i, 0)))
commit = o->commit.IsEnabled() && o->commit;
else {
for(int j = 0; j < 2; j++) {
Ctrl *ctrl = list.GetCtrl(i, j);
if(ctrl)
ctrl->Enable(commit);
}
}
}
}
void RepoSync::SyncList()
{
list.Clear();
credentials.Show();
svndir.Clear();
for(const auto& w : ~work) {
String path = GetFullPath(w.key);
String cfg = LoadFile(RepoCfgFile(path));
auto Default = [&](const char *s) {
if(cfg.IsVoid())
return true;
return cfg.Find(s) >= 0;
};
int hi = list.GetCount();
Color bk = AdjustIfDark(LtYellow());
list.Add(REPOSITORY, path,
AttrText().Paper(bk),
AttrText(path).SetFont(ArialZ(20).Bold()).Paper(bk),
AttrText().Paper(bk));
list.SetLineCy(hi, Zy(26));
bool actions = false;
if(w.value == SVN_DIR) {
auto& o = list.CreateCtrl<SvnOptions>(hi, 0, false);
o.SizePos();
o.commit = Default("commit");
o.commit << [=] { SyncCommits(); };
o.update = Default("update");
actions = ListSvn(path);
if(!actions) {
o.commit.Disable();
}
credentials.Show();
svndir.FindAdd(GetSvnDir(w.key));
}
if(w.value == GIT_DIR) {
auto& o = list.CreateCtrl<GitOptions>(hi, 0, false);
o.SizePos();
o.commit = Default("commit");
o.commit << [=] { SyncCommits(); };
o.push = Default("push");
o.pull = Default("pull");
actions = ListGit(path);
if(!actions) {
o.push = false;
o.commit.Disable();
}
}
if(actions) {
list.Add(MESSAGE, Null, AttrText("Commit message:").SetFont(StdFont().Bold()));
list.SetLineCy(list.GetCount() - 1, (3 * EditField::GetStdHeight()) + 4);
list.SetCtrl(list.GetCount() - 1, 1, message.Add().SetFilter(CharFilterSvnMsgRepo).VSizePos(2, 2).HSizePos());
int q = msgmap.Find(w.key);
if(q >= 0) {
message.Top() <<= msgmap[q];
msgmap.Unlink(q);
}
}
else
list.Add(-1, Null, "", AttrText("Nothing to do").SetFont(StdFont().Italic()));
}
}
void RepoSync::DoDiff(int ii)
{
String f = list.Get(ii, 1);
if(!IsNull(f))
RunRepoDiff(f);
}
#ifdef PLATFORM_WIN32
void sRepoDeleteFolderDeep(const char *dir)
{
{
FindFile ff(AppendFileName(dir, "*.*"));
while(ff) {
String name = ff.GetName();
String p = AppendFileName(dir, name);
if(ff.IsFile()) {
SetFileAttributes(p, GetFileAttributes(p) & ~FILE_ATTRIBUTE_READONLY);
FileDelete(p);
}
else
if(ff.IsFolder())
sRepoDeleteFolderDeep(p);
ff.Next();
}
}
DirectoryDelete(dir);
}
#else
void sRepoDeleteFolderDeep(const char *path)
{
DeleteFolderDeep(path);
}
#endif
void RepoSvnDel(const char *path)
{
FindFile ff(AppendFileName(path, "*.*"));
while(ff) {
if(ff.IsFolder()) {
String dir = AppendFileName(path, ff.GetName());
if(ff.GetName() == ".svn")
sRepoDeleteFolderDeep(dir);
else
RepoSvnDel(dir);
}
ff.Next();
}
}
void RepoSync::Dir(const char *dir)
{
String d = dir;
int kind = GetRepo(d);
if(kind)
work.GetAdd(kind == GIT_DIR ? d : String(dir)) = kind;
}
void RepoMoveSvn(const String& path, const String& tp)
{
FindFile ff(AppendFileName(path, "*.*"));
while(ff) {
String nm = ff.GetName();
String s = AppendFileName(path, nm);
String t = AppendFileName(tp, nm);
if(ff.IsFolder()) {
if(nm == ".svn")
FileMove(s, t);
else
RepoMoveSvn(s, t);
}
ff.Next();
}
}
void RepoSync::DoSync()
{
SyncList();
msgmap.Sweep();
again:
Enable();
if(Run() != IDOK || list.GetCount() == 0) {
int repoi = 0;
for(int i = 0; i < list.GetCount(); i++)
if(list.Get(i, 0) == MESSAGE)
msgmap.GetAdd(work.GetKey(repoi++)) = list.Get(i, 3);
return;
}
Disable();
bool changes = false;
for(int i = 0; i < list.GetCount(); i++) {
int action = list.Get(i, 0);
if(action == MESSAGE) {
if(changes && IsNull(list.Get(i, 3)) && list.GetCtrl(i, 1)->IsEnabled()
&& !PromptYesNo("Commit message is empty.&Do you want to continue?"))
goto again;
changes = false;
}
else if(action != REPOSITORY && list.Get(i, 2) == 0)
changes = true;
}
UrepoConsole sys;
int repoi = 0;
int l = 0;
while(l < list.GetCount()) {
SvnOptions *svn = dynamic_cast<SvnOptions *>(list.GetCtrl(l, 0));
GitOptions *git = dynamic_cast<GitOptions *>(list.GetCtrl(l, 0));
String repo_dir = work.GetKey(repoi++);
String url;
if(git) {
url = GetGitUrl(repo_dir);
if(url.GetCount())
sys.Log("git origin url: " + url, Gray());
}
if(svn) {
url = GetSvnUrl(repo_dir);
if(url.GetCount())
sys.Log("svn repository url: " + url, Gray());
}
l++;
String message;
String filelist; // <-- list of files to update
if(git && git->pull)
if(sys.Git(repo_dir, "pull --ff-only", true)) {
while(l < list.GetCount()) {
int action = list.Get(l, 0);
if(action == REPOSITORY)
break;
if(action == MESSAGE)
msgmap.GetAdd(repo_dir) = list.Get(l, 3);
l++;
}
continue;
}
bool commit = false;
while(l < list.GetCount()) {
int action = list.Get(l, 0);
if(action == REPOSITORY)
break;
String path = list.Get(l, 1);
bool revert = list.Get(l, 2) == 1;
if(svn && svn->commit.IsEnabled() && svn->commit) {
if(action == MESSAGE && commit) {
String msg = list.Get(l, 3);
if(sys.CheckSystem(SvnCmd(sys, "commit", repo_dir) << filelist << " -m \"" << msg << "\""))
msgmap.GetAdd(repo_dir) = msg;
l++;
break;
}
if(SvnFile(sys, filelist, action, path, revert))
commit = true;
}
if(git && git->commit.IsEnabled() && git->commit) {
if(action == MESSAGE && commit) {
String msg = list.Get(l, 3);
if(sys.Git(repo_dir, "commit -a -m \"" << msg << "\""))
msgmap.GetAdd(repo_dir) = msg;
l++;
break;
}
if(GitFile(sys, action, path, revert))
commit = true;
}
l++;
}
if(svn && svn->update)
sys.CheckSystem(SvnCmd(sys, "update", repo_dir).Cat() << repo_dir);
if(git && git->push)
sys.Git(repo_dir, "push", true);
}
sys.Log("Done", Gray());
ResetBlitz();
sys.Perform();
}
bool RepoSync::GitFile(UrepoConsole& sys, int action, const String& path, bool revert)
{
String repo_dir = GetFileFolder(path);
String file = GetFileName(path);
if(revert) {
if(action != ADD)
sys.Git(repo_dir, "checkout \"" + file + "\"");
return false;
}
if(action == ADD)
sys.Git(repo_dir, "add \"" + file + "\"");
return true;
}
bool RepoSync::SvnFile(UrepoConsole& sys, String& filelist, int action, const String& path, bool revert)
{
if(revert) {
if(action == REPLACE)
DeleteFolderDeep(path);
if(action != ADD)
sys.CheckSystem("svn revert \"" + path + "\"");
return false;
}
if(action >= 0 || action == SVN_ACTION)
filelist << " \"" << path << "\""; // <-- add the file to the list
switch(action) {
case ADD:
RepoSvnDel(path);
sys.CheckSystem("svn add --force \"" + path + "\"");
break;
case REMOVE:
sys.CheckSystem("svn delete \"" + path + "\"");
break;
case CONFLICT:
sys.CheckSystem("svn resolved \"" + path + "\"");
break;
case REPLACE: {
RepoSvnDel(path);
String tp = AppendFileName(GetFileFolder(path), Format(Uuid::Create()));
FileMove(path, tp);
sys.CheckSystem(SvnCmd(sys, "update", path) << " \"" << path << "\"");
RepoMoveSvn(path, tp);
sRepoDeleteFolderDeep(path);
FileMove(tp, path);
Vector<String> ln = Split(RepoSys("svn status \"" + path + "\""), CharFilterCrLf);
for(int l = 0; l < ln.GetCount(); l++) {
String h = ln[l];
if(h.GetCount() > 7) {
String file = h.Mid(7);
if(IsFullPath(file)) {
h.Trim(7);
if(h == "? ")
sys.CheckSystem("svn add --force \"" + file + "\"");
if(h == "! ")
sys.CheckSystem("svn delete \"" + file + "\"");
}
}
}
}
break;
case DELETEC:
FileDelete(path);
break;
}
return findarg(action, SVN_IGNORE, DELETEC) < 0;
}
void RepoSync::Serialize(Stream& s)
{
int version = 0;
s / version;
s % msgmap;
}
void RepoSync::SetMsgs(const String& s)
{
LoadFromString(*this, Garble(s));
}
String RepoSync::GetMsgs()
{
return Garble(StoreAsString(*this));
}