CompDir
Compares directory contents

textdiff.h
#ifndef _CompDir_textdiff_h_
#define _CompDir_textdiff_h_
class TextSection
{
public:
TextSection(int start1, int count1, int start2, int count2, bool same)
: start1(start1), count1(count1), start2(start2), count2(count2), same(same) {}
public:
int start1;
int count1;
int start2;
int count2 : 31;
unsigned same : 1;
};
Array<TextSection> CompareLineMaps(const Vector<String>& l1, const Vector<String>& l2);
Vector<String> GetLineMap(Stream& stream);
Vector<String> GetFileLineMap(const String& path);
Vector<String> GetStringLineMap(const String &s);
#endif
CompDir.h
#ifndef _CompDir_CompDir_h
#define _CompDir_CompDir_h
#include <CtrlLib/CtrlLib.h>
using namespace Upp;
#include "textdiff.h"
#endif
main.cpp
#include "CompDir.h"
#pragma hdrstop
#define LAYOUTFILE <CompDir/CompDir.lay>
#include <CtrlCore/lay.h>
#define IMAGEFILE <CompDir/CompDir.iml>
#define IMAGECLASS CompDirImg
#include <Draw/iml.h>
String NormalizePathCase(String fn)
{
#ifdef PLATFORM_WIN32 // !PATH_CASE
return ToLower(fn);
#else
return fn;
#endif
}
static String ExpandTabs(String line, int tabsize = 4)
{
String out;
int pos = 0;
for(const char *p = line; *p; p++)
if(*p == '\t') {
int left = tabsize - pos % tabsize;
out.Cat(' ', left);
pos += left;
}
else {
out.Cat(*p);
pos++;
}
return out;
}
class DlgCompareDir : public WithCompareDirLayout<TopWindow> {
public:
DlgCompareDir();
void Run();
void Serialize(Stream& stream);
private:
void CmdRefresh();
void DoTreeCursor();
int Refresh(String rel_path, int parent);
void DoBrowse(Ctrl *field);
void ToolTree(Bar& bar);
String GetTreePath() const;
private:
struct FileInfo : Moveable<FileInfo>
{
FileInfo() {}
FileInfo(String name, int64 size, Time time) : name(name), size(size), time(time) {}
String name;
int64 size;
Time time;
};
bool FetchDir(String dir, VectorMap<String, FileInfo>& files, VectorMap<String, String>& dirs);
FrameRight<Button> browse_a, browse_b;
TreeCtrl tree;
StaticRect editor;
LineEdit lineedit;
RichTextCtrl qtf;
String pa, pb, fm;
};
DlgCompareDir::DlgCompareDir()
{
CtrlLayout(*this, "Compare directories");
Sizeable().Zoomable();
refresh << [=] { CmdRefresh(); };
splitter.Vert(tree, editor);
editor << lineedit.SizePos() << qtf.SizePos();
qtf.Background(White());
qtf.SetFrame(InsetFrame());
path_a.AddFrame(browse_a);
browse_a.SetImage(CtrlImg::right_arrow());
browse_a << [=] { DoBrowse(&path_a); };
path_b.AddFrame(browse_b);
browse_b.SetImage(CtrlImg::right_arrow());
browse_b << [=] { DoBrowse(&path_b); };
file_mask <<= "*.cpp *.h *.hpp *.c *.C *.cxx *.cc *.lay *.iml *.upp *.sch *.dph";
tree.WhenCursor = [=] { DoTreeCursor(); };
lineedit.SetReadOnly();
lineedit.SetFont(Courier(14));
}
void DlgCompareDir::Run()
{
TopWindow::Run();
}
void DlgCompareDir::Serialize(Stream& stream)
{
int version = 1;
stream / version;
stream % path_a % path_b % file_mask;
SerializePlacement(stream);
stream % splitter;
}
void DlgCompareDir::CmdRefresh()
{
pa = ~path_a;
pb = ~path_b;
fm = ~file_mask;
tree.Clear();
Image icon;
switch(Refresh(Null, 0)) {
case 0: icon = CtrlImg::Dir(); break;
case 1: icon = CompDirImg::a_dir(); break;
case 2: icon = CompDirImg::b_dir(); break;
case 3: icon = CompDirImg::ab_dir(); break;
}
tree.SetRoot(icon, "Root");
}
bool DlgCompareDir::FetchDir(String dir, VectorMap<String, FileInfo>& files, VectorMap<String, String>& dirs)
{
FindFile ff;
if(!ff.Search(AppendFileName(dir, "*")))
return false;
do
if(ff.IsFile() && PatternMatchMulti(fm, ff.GetName()))
files.Add(NormalizePathCase(ff.GetName()), FileInfo(ff.GetName(), ff.GetLength(), ff.GetLastWriteTime()));
else if(ff.IsFolder())
dirs.Add(NormalizePathCase(ff.GetName()), ff.GetName());
while(ff.Next());
return true;
}
int DlgCompareDir::Refresh(String rel_path, int parent)
{
FindFile ff;
VectorMap<String, FileInfo> afile, bfile;
VectorMap<String, String> adir, bdir;
String arel = AppendFileName(pa, rel_path);
String brel = AppendFileName(pb, rel_path);
int done = 0;
if(!FetchDir(arel, afile, adir))
done |= 2;
if(!FetchDir(brel, bfile, bdir))
done |= 1;
Index<String> dir_index;
dir_index <<= adir.GetIndex();
FindAppend(dir_index, bdir.GetKeys());
Vector<String> dirs(dir_index.PickKeys());
Sort(dirs, GetLanguageInfo());
for(int i = 0; i < dirs.GetCount(); i++) {
int fa = adir.Find(dirs[i]), fb = bdir.Find(dirs[i]);
String dn = (fb >= 0 ? bdir[fb] : adir[fa]);
int dirpar = tree.Add(parent, CtrlImg::Dir(), dn);
int dirdone = Refresh(AppendFileName(rel_path, dirs[i]), dirpar);
done |= dirdone;
switch(dirdone) {
case 0: tree.Remove(dirpar); break;
case 1: tree.SetNode(dirpar, TreeCtrl::Node().SetImage(CompDirImg::a_dir()).Set(dn)); break;
case 2: tree.SetNode(dirpar, TreeCtrl::Node().SetImage(CompDirImg::b_dir()).Set(dn)); break;
case 3: tree.SetNode(dirpar, TreeCtrl::Node().SetImage(CompDirImg::ab_dir()).Set(dn)); break;
}
}
Index<String> name_index;
name_index <<= afile.GetIndex();
FindAppend(name_index, bfile.GetKeys());
Vector<String> names(name_index.PickKeys());
Sort(names, GetLanguageInfo());
for(int i = 0; i < names.GetCount(); i++) {
int fa = afile.Find(names[i]), fb = bfile.Find(names[i]);
if(fa < 0) {
tree.Add(parent, CompDirImg::b_file(), NFormat("%s: B (%`, %0n)", bfile[fb].name, bfile[fb].time, bfile[fb].size));
done |= 2;
}
else if(fb < 0) {
tree.Add(parent, CompDirImg::a_file(), NFormat("%s: A (%`, %0n)", afile[fa].name, afile[fa].time, afile[fa].size));
done |= 1;
}
else if(afile[fa].size != bfile[fb].size
|| LoadFile(AppendFileName(arel, names[i])) != LoadFile(AppendFileName(brel, names[i]))) {
tree.Add(parent, CompDirImg::ab_file(), NFormat("%s: A (%`, %0n), B (%`, %0n)",
bfile[fb].name, afile[fa].time, afile[fa].size, bfile[fb].time, bfile[fb].size));
done |= 3;
}
}
return done;
}
String DlgCompareDir::GetTreePath() const
{
int i = tree.GetCursor();
if(i < 0)
return String::GetVoid();
if(i == 0)
return Null;
String s = tree.Get(i);
int f = s.Find(':');
if(f >= 0)
s.Trim(f);
while((i = tree.GetParent(i)) != 0)
s = AppendFileName(String(tree.Get(i)), s);
return s;
}
void DlgCompareDir::DoTreeCursor()
{
String s = GetTreePath();
if(IsNull(s))
return;
String fa = AppendFileName(pa, s), fb = AppendFileName(pb, s);
String da = LoadFile(fa), db = LoadFile(fb);
if(!IsNull(da) || !IsNull(db)) {
if(IsNull(da) || IsNull(db)) {
qtf.Hide();
lineedit.Show();
lineedit <<= Nvl(db, da);
}
else {
lineedit.Hide();
qtf.Show();
String comptext = "[C2 ";
Vector<String> la = GetStringLineMap(da), lb = GetStringLineMap(db);
Array<TextSection> sections = CompareLineMaps(la, lb);
for(int s = 0; s < sections.GetCount(); s++) {
const TextSection& sec = sections[s];
if(sec.same) {
comptext << "[@(0.0.0) \1";
if(sec.count1 <= 6)
for(int i = 0; i < sec.count1; i++)
comptext << ExpandTabs(la[i + sec.start1]) << '\n';
else {
for(int i = 0; i < 3; i++)
comptext << ExpandTabs(la[i + sec.start1]) << '\n';
comptext << "...\n";
for(int i = -3; i < 0; i++)
comptext << ExpandTabs(la[i + sec.start1 + sec.count1]) << '\n';
}
comptext << "\1]";
}
else {
if(sec.count1) {
comptext << "[@(0.160.0) \1";
for(int i = 0; i < sec.count1; i++)
comptext << ExpandTabs(la[sec.start1 + i]) << '\n';
comptext << "\1]";
}
if(sec.count2) {
comptext << "[@(0.0.255) \1";
for(int i = 0; i < sec.count2; i++)
comptext << ExpandTabs(lb[sec.start2 + i]) << '\n';
comptext << "\1]";
}
}
}
qtf.SetQTF(comptext);
}
}
}
void DlgCompareDir::DoBrowse(Ctrl *field)
{
FileSel fsel;
fsel.AllFilesType();
static String recent_dir;
fsel <<= Nvl((String)~*field, recent_dir);
if(fsel.ExecuteSelectDir())
*field <<= recent_dir = ~fsel;
}
void DlgCompareDir::ToolTree(Bar& bar)
{
}
GUI_APP_MAIN
{
DlgCompareDir cmpdlg;
LoadFromFile(cmpdlg, ConfigFile());
cmpdlg.Run();
StoreToFile(cmpdlg, ConfigFile());
}
textdiff.cpp
#include "CompDir.h"
template <class I>
static int CompareGetCount(I a, I b, int max_count)
{
if(max_count <= 0 || *a != *b)
return 0;
int left;
for(left = max_count; --left > 0;)
if(*++a != *++b)
return max_count - left;
return max_count;
}
Vector<String> GetLineMap(Stream& stream)
{
Vector<String> out;
int emp = 0;
if(stream.IsOpen())
while(!stream.IsEof()) {
String s = stream.GetLine();
const char *p = s, *e = s.End(), *f = e;
while(e > p && (byte)e[-1] <= ' ')
e--;
if(e == p)
emp++;
else
{
while(emp-- > 0)
out.Add(Null);
if(e != f)
s.Trim(e - p);
out.Add(s);
emp = 0;
}
}
return out;
}
Vector<String> GetFileLineMap(const String& path)
{
FileIn fi(path);
return GetLineMap(fi);
}
Vector<String> GetStringLineMap(const String& s)
{
StringStream ss(s);
return GetLineMap(ss);
}
class TextComparator
{
public:
TextComparator(const Vector<String>& f1, const Vector<String>& f2);
Array<TextSection> GetSections() const;
private:
bool Find(int start1, int end1, int start2, int end2, int& best_match, int& best_count) const;
void Split(Array<TextSection>& dest, int start1, int end1, int start2, int end2) const;
private:
Vector<Index<dword>> hash1;
Vector<Index<dword>> hash2;
const Vector<String>& file1;
const Vector<String>& file2;
};
Array<TextSection> CompareLineMaps(const Vector<String>& s1, const Vector<String>& s2)
{
return TextComparator(s1, s2).GetSections();
}
static void CalcHash(Vector<Index<dword>>& hash, const Vector<String>& file, int limit)
{
{ // 1st row
Index<dword>& first = hash.Add();
for(int i = 0; i < file.GetCount(); i++)
first.Add(GetHashValue(file[i]));
}
static const int prime[] =
{
3, 5, 7, 11, 13, 17, 19, 21,
23, 29, 31, 37, 41, 43, 47, 51,
53, 61, 67, 71, 73, 79, 83, 87,
89, 97, 101, 103, 107, 109, 113, 117,
};
const int *pp = prime;
for(int l = 1; l < limit; l <<= 1) {
Index<dword>& nhash = hash.Add();
const Index<dword>& ohash = hash[hash.GetCount() - 2];
int pri = *pp++;
int t;
for(t = l; t < ohash.GetCount(); t++)
nhash.Add(ohash[t - l] + pri * ohash[t]);
for(t -= l; t < ohash.GetCount(); t++)
nhash.Add(ohash[t]);
}
}
TextComparator::TextComparator(const Vector<String>& f1, const Vector<String>& f2)
: file1(f1), file2(f2)
{
int limit = min(f1.GetCount(), f2.GetCount());
CalcHash(hash1, f1, limit);
CalcHash(hash2, f2, limit);
}
static bool CompareSection(const TextSection& ta, const TextSection& tb)
{
return ta.start1 < tb.start1 || ta.start1 == tb.start1 && ta.start2 < tb.start2;
}
Array<TextSection> TextComparator::GetSections() const
{
Array<TextSection> output;
Split(output, 0, file1.GetCount(), 0, file2.GetCount());
Sort(output, &CompareSection);
return output;
}
static int GetHashLevel(int min_count, int hash_count)
{
int l = 0;
hash_count--;
while(min_count > 1 && l < hash_count)
{
min_count >>= 1;
l++;
}
return l;
}
bool TextComparator::Find(int start1, int end1, int start2, int end2, int& best_match, int& best_count) const
{
ASSERT(end1 > start1 && end2 > start2);
bool done = false;
const String *f1 = file1.Begin() + start1;
int len1 = end1 - start1;
int lvl = GetHashLevel(best_count + 1, hash1.GetCount());
int chunk = 1 << lvl;
int last = max(best_count - chunk + 1, 0);
const Index<dword> *hp1 = &hash1[lvl];
const Index<dword> *hp2 = &hash2[lvl];
const dword *h1 = hp1->begin() + start1;
int i = hp2->Find(*h1);
while(i >= 0)
if(i + best_count >= end2)
return done;
else {
if(i >= start2 && h1[last] == (*hp2)[i + last]) {
int top = min(len1, end2 - i);
int hc = CompareGetCount(h1, hp2->begin() + i, top) + chunk - 1;
int cnt = CompareGetCount(f1, file2.begin() + i, min(hc, top));
if(cnt > best_count) {
best_count = cnt;
best_match = i;
done = true;
last = best_count - chunk + 1;
if(best_count + 1 >= 2 * chunk)
{
lvl = GetHashLevel(best_count + 1, hash1.GetCount());
chunk = 1 << lvl;
last = best_count - chunk + 1;
hp1 = &hash1[lvl];
hp2 = &hash2[lvl];
h1 = hp1->begin() + start1;
int oi = i;
for(i = hp2->Find(*h1); i >= 0 && i <= oi; i = hp2->FindNext(i))
;
continue;
}
}
}
i = hp2->FindNext(i);
}
return done;
}
void TextComparator::Split(Array<TextSection>& dest, int start1, int end1, int start2, int end2) const
{
ASSERT(start1 <= end1 && start2 <= end2);
while(start1 < end1 && start2 < end2) {
int new1 = -1, new2 = -1, count = 0;
for(int i = start1; i + count < end1; i++)
if(Find(i, end1, start2, end2, new2, count))
new1 = i;
if(count == 0)
break; // no match at all
ASSERT(new1 >= start1 && new1 + count <= end1);
ASSERT(new2 >= start2 && new2 + count <= end2);
dest.Add(TextSection(new1, count, new2, count, true));
if(new1 - start1 >= end1 - new1 - count) { // head is longer - recurse for tail
Split(dest, new1 + count, end1, new2 + count, end2);
end1 = new1;
end2 = new2;
}
else { // tail is longer - recurse for head
Split(dest, start1, new1, start2, new2);
start1 = new1 + count;
start2 = new2 + count;
}
ASSERT(start1 <= end1 && start2 <= end2);
}
if(start1 < end1 || start2 < end2)
dest.Add(TextSection(start1, end1 - start1, start2, end2 - start2, false));
}
CompDir.lay
LAYOUT(CompareDirLayout, 496, 492)
ITEM(Label, dv___0, SetLabel(t_("Folder &A:")).LeftPosZ(4, 52).TopPosZ(4, 19))
ITEM(EditField, path_a, HSizePosZ(56, 4).TopPosZ(4, 19))
ITEM(Label, dv___2, SetLabel(t_("Folder &B:")).LeftPosZ(4, 52).TopPosZ(26, 19))
ITEM(EditField, path_b, HSizePosZ(56, 4).TopPosZ(26, 19))
ITEM(Label, dv___4, SetLabel(t_("File &mask:")).LeftPosZ(4, 52).TopPosZ(48, 19))
ITEM(EditField, file_mask, HSizePosZ(56, 68).TopPosZ(48, 19))
ITEM(Button, refresh, SetLabel(t_("&Refresh")).RightPosZ(4, 60).TopPosZ(48, 18))
ITEM(Splitter, splitter, HSizePosZ(4, 4).VSizePosZ(70, 4))
END_LAYOUT
|