diff --git a/include/lgi/common/DateTime.h b/include/lgi/common/DateTime.h --- a/include/lgi/common/DateTime.h +++ b/include/lgi/common/DateTime.h @@ -1,433 +1,450 @@ /// \file /// \author Matthew Allen /** * \defgroup Time Time and date handling * \ingroup Lgi */ #ifndef __DATE_TIME_H #define __DATE_TIME_H #include #include "lgi/common/StringClass.h" #define GDTF_DEFAULT 0 /// Format the date as DD/MM/YYYY /// \ingroup Time #define GDTF_DAY_MONTH_YEAR 0x001 /// Format the date as MM/DD/YYYY /// \ingroup Time #define GDTF_MONTH_DAY_YEAR 0x002 /// Format the date as YYYY/MM/DD /// \ingroup Time #define GDTF_YEAR_MONTH_DAY 0x004 /// The bit mask for the date /// \ingroup Time #define GDTF_DATE_MASK 0x00f /// Format the time as HH:MM and an am/pm marker /// \ingroup Time #define GDTF_12HOUR 0x010 /// Format the time as 24 hour time /// \ingroup Time #define GDTF_24HOUR 0x020 /// The bit mask for the time /// \ingroup Time #define GDTF_TIME_MASK 0x0f0 /// Format the date with a leading zero /// \ingroup Time #define GDTF_DAY_LEADINGZ 0x100 /// Format the month with a leading zero /// \ingroup Time #define GDTF_MONTH_LEADINGZ 0x200 +class LgiClass LTimeStamp +{ + uint64_t ts = 0; + +public: + constexpr static uint64_t WINDOWS_TICK = 10000000; + constexpr static uint64_t SEC_TO_UNIX_EPOCH = 11644473600LL; + + LTimeStamp(time_t unixTime = 0) + { + if (unixTime) + *this = unixTime; + } + + uint64_t &Get() + { + return ts; + } + + // Convert from unit time to LGI time stamp + LTimeStamp &operator =(const time_t unixTime); + + // Convert from LGI time stamp to unix time + operator time_t() const; +}; + + /// A date/time class /// /// This class interacts with system times represented as 64bit ints. The various OS support different -/// formats for that 64bit int values. On windows the system times are in 100-nanosecond intervals since -/// January 1, 1601 (UTC), as per the FILETIME structure, on Posix systems (Linux/Mac) the 64bit values -/// are in milliseconds since January 1, 1970 UTC. This is just unix time * 1000. If you are serializing -/// these 64bit values you should take that into account, they are NOT cross platform. The LDirectory class -/// uses the same 64bit values as accepted here for the file's last modified timestamp etc. To convert the -/// 64bit values to seconds, divide by LDateTime::Second64Bit, useful for calculating the time in seconds -/// between 2 LDateTime objects. +/// formats for that 64bit int values. +/// - On windows the system times are in 100-nanosecond intervals since +/// January 1, 1601 (UTC), as per the FILETIME structure. +/// - On Posix systems (Linux/Mac) the 64bit values are in milliseconds since January 1, 1800 UTC. +/// This is just (unixTime + Offset1800) * 1000. +/// If you are serializing these 64bit values you should take that into account, they are NOT cross platform. +/// The LDirectory class uses the same 64bit values as accepted here for the file's last modified timestamp +/// etc. To convert the 64bit values to seconds, divide by LDateTime::Second64Bit, useful for calculating +/// the time in seconds between 2 LDateTime objects. /// /// \ingroup Time class LgiClass LDateTime // This class can't have a virtual table, because it's used in // LArray's which initialize with all zero bytes. { /// 1 - DaysInMonth int16 _Day; /// #### int16 _Year; /// Milliseconds: 0-999 int16 _Thousands; /// 1-12 int16 _Month; /// 0-59 int16 _Seconds; /// 0-59 int16 _Minutes; /// 0-23 (24hr) int16 _Hours; /// The current timezone of this object, defaults to the system timezone int16 _Tz; // in minutes (+10 == 600 etc) /// Combination of (#GDTF_DAY_MONTH_YEAR or #GDTF_MONTH_DAY_YEAR or #GDTF_YEAR_MONTH_DAY) and (#GDTF_12HOUR or #GDTF_24HOUR) uint16 _Format; /// The default formatting of datetimes static uint16 DefaultFormat; /// The default date separator character static char DefaultSeparator; public: LDateTime(const char *Init = NULL); - LDateTime(uint64 Ts); + LDateTime(time_t unixTime); + LDateTime(const LTimeStamp &Ts); LDateTime(const LDateTime &dt) { *this = dt; } ~LDateTime(); /// Resolution of a second when using 64 bit int's /// \sa LDateTime::Get(int64), LDateTime::Set(int64) #ifdef WIN32 static constexpr int64_t Second64Bit = 10000000; #else static constexpr int64_t Second64Bit = 1000; #endif static constexpr int64_t MinuteLength = 60; // seconds static constexpr int64_t HourLength = MinuteLength * 60; // seconds static constexpr int64_t DayLength = HourLength * 24; // seconds static constexpr const char *WeekdaysShort[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static constexpr const char *WeekdaysLong[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; static constexpr const char *MonthsShort[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; static constexpr const char *MonthsLong[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; // On Posix systems this allows representation of times before 1/1/1970. // The Lgi epoch is considered to be 1/1/1800 instead and this offset converts // from one to the other. static constexpr int64_t Offset1800 = 5364662400; // Seconds from 1/1/1800 to 1/1/1970 /// Returns true if all the components are in a valid range bool IsValid() const; /// Sets the date to an NULL state void Empty(); /// Returns the day int Day() const { return _Day; } /// Sets the day void Day(int d) { _Day = d; } /// Returns the month int Month() const { return _Month; } /// Sets the month void Month(int m) { _Month = m; } /// Sets the month by it's name void Month(char *m); /// Returns the year int Year() const { return _Year; } /// Sets the year void Year(int y) { _Year = y; } /// Returns the millisecond part of the time int Thousands() const { return _Thousands; } /// Sets the millisecond part of the time void Thousands(int t) { _Thousands = t; } /// Returns the seconds part of the time int Seconds() const { return _Seconds; } /// Sets the seconds part of the time void Seconds(int s) { _Seconds = s; } /// Returns the minutes part of the time int Minutes() const { return _Minutes; } /// Sets the minutes part of the time void Minutes(int m) { _Minutes = m; } /// Returns the hours part of the time int Hours() const { return _Hours; } /// Sets the hours part of the time void Hours(int h) { _Hours = h; } /// Returns the timezone of this current date time object in minutes (+10 = 600) int GetTimeZone() const { return _Tz; } /// Returns the timezone in hours double GetTimeZoneHours() const { return (double)_Tz / 60.0; } /// Sets the timezone of this current object.in minutes (+10 = 600) void SetTimeZone ( /// The new timezone (in minutes, 600 = +10:00) int Tz, /// True if you want to convert the date and time to the new zone, /// False if you want to leave the date/time as it is. bool ConvertTime ); /// Set this object to UTC timezone, changing the other members as /// needed LDateTime &ToUtc(bool AssumeLocal = false) { if (AssumeLocal) _Tz = SystemTimeZone(); SetTimeZone(0, true); return *this; } /// \returns the UTC version of this object. LDateTime Utc() const { LDateTime dt = *this; return dt.ToUtc(); } /// Changes the timezone to the local zone, changing other members /// as needed. LDateTime &ToLocal(bool AssumeUtc = false) { if (AssumeUtc) _Tz = 0; SetTimeZone(SystemTimeZone(), true); return *this; } /// \returns the local time version of this object. LDateTime Local() const { LDateTime dt = *this; return dt.ToLocal(); } /// Gets the current formatting of the date, the format only effects /// the representation of the date when converted to/from a string. /// \returns a bit mask of (#GDTF_DAY_MONTH_YEAR or #GDTF_MONTH_DAY_YEAR or #GDTF_YEAR_MONTH_DAY) and (#GDTF_12HOUR or #GDTF_24HOUR) uint16 GetFormat() const { return _Format; } /// Sets the current formatting of the date, the format only effects /// the representation of the date when converted to/from a string void SetFormat ( /// a bit mask of (#GDTF_DAY_MONTH_YEAR or #GDTF_MONTH_DAY_YEAR or #GDTF_YEAR_MONTH_DAY) and (#GDTF_12HOUR or #GDTF_24HOUR) uint16 f ) { _Format = f; } /// \returns zero based index of weekday, or -1 if not found. static int IsWeekDay(const char *s); /// \returns zero based index of month, or -1 if not found. static int IsMonth(const char *s); /// The default format for the date when formatted as a string static uint16 GetDefaultFormat(); /// Sets the default format for the date when formatted as a string static void SetDefaultFormat(uint16 f) { DefaultFormat = f; } /// Gets the data and time as a LString LString Get() const; /// Gets the date and time as a string /// \sa LDateTime::GetFormat() void Get(char *Str, size_t SLen) const; /// Gets the data and time as a 64 bit int (os specific) - bool Get(uint64 &s) const; + bool Get(LTimeStamp &s) const; /// Gets just the date as a string /// \sa LDateTime::GetFormat() /// \returns The number of characters written to 'Str' int GetDate(char *Str, size_t SLen) const; /// Gets just the date as a LString /// \sa LDateTime::GetFormat() LString GetDate() const; /// Gets just the time as a string /// \sa LDateTime::GetFormat() /// \returns The number of characters written to 'Str' int GetTime(char *Str, size_t SLen) const; /// Gets just the time as a LString /// \sa LDateTime::GetFormat() LString GetTime() const; // Unix epoch support uint64_t GetUnix(); bool SetUnix(uint64_t s); - /// Returns the 64bit timestamp. + /// Returns the 64bit LTimeStamp. uint64 Ts() const; /// Get the timestamp in a format compatible with the current operating system APIs. /// \returns the timestamp or zero on error. uint64_t OsTime() const; bool OsTime(uint64_t ts); /// Get the current time... static LDateTime Now(); /// Sets the date and time to the system clock LDateTime &SetNow(); /// Parses a date time from a string /// \sa LDateTime::GetFormat() bool Set(const char *Str); /// Sets the date and time from a 64 bit int (os specific) - bool Set(uint64 s); - /// Sets the time from a time_t + bool Set(const LTimeStamp &s); + /// Sets the time from a unit time_t bool Set(time_t tt); /// Parses the date from a string /// \sa LDateTime::GetFormat() bool SetDate(const char *Str); /// Parses the time from a string /// \sa LDateTime::GetFormat() bool SetTime(const char *Str); /// Parses the date time from a free form string bool Parse(LString s); /// Set to a tm struct bool Set(struct tm *t, bool inferTimezone); /// Describes the perios between this and 'to' in the form: /// ##d ##h ##m ##s /// Order of the dates isn't important. LString DescribePeriod(LDateTime to); static LString DescribePeriod(double seconds); /// \returns true if 'd' is on the same day as this object bool IsSameDay(LDateTime &d) const; /// \returns true if 'd' is on the same month as this object bool IsSameMonth(LDateTime &d) const; /// \returns true if 'd' is on the same year as this object bool IsSameYear(LDateTime &d) const; /// \returns the start of day, same date but time: 00:00:00 LDateTime StartOfDay() const; /// \returns the end of day, same date but time: 23:59:59 LDateTime EndOfDay() const; /// \returns whether a year is a leap year or not bool IsLeapYear ( /// Pass a specific year here, or ignore to return if the current Date/Time is in a leap year. int Year = -1 ) const; /// Returns the day of the week as an index, 0=sun, 1=mon, 2=teus etc int DayOfWeek() const; /// \returns the number of days in the current month int DaysInMonth() const; /// Adds a number of seconds to the current date/time void AddSeconds(int64 Seconds); /// Adds a number of minutes to the current date/time void AddMinutes(int64 Minutes); /// Adds a number of hours to the current date/time void AddHours(int64 Hours); /// Adds a number of days to the current date/time bool AddDays(int64 Days); /// Adds a number of months to the current date/time void AddMonths(int64 Months); /// Adds years void AddYears(int yr) { _Year += yr; } /// The system timezone including daylight savings offset in minutes, +10 would return 600 static int SystemTimeZone(bool ForceUpdate = false); /// Any daylight savings offset applied to TimeZone(), in minutes. e.g. to retreive the /// timezone uneffected by DST use TimeZone() - TimeZoneOffset(). static int SystemTimeZoneOffset(); /// Daylight savings info record struct LgiClass LDstInfo { /// Timestamp where the DST timezone changes to 'Offset' - uint64 UtcTimeStamp; + LTimeStamp Utc; /// The new offset in minutes (e.g. 600 = +10 hours) int Offset; LDateTime GetLocal(); }; /// Retreives daylight savings start and end events for a given period. One event will be emitted /// for the current DST/TZ setting at the datetime specified by 'Start', followed by any changes that occur /// between that and the 'End' datetime. To retreive just the DST info for start, use NULL for end. static bool GetDaylightSavingsInfo ( /// [Out] The array to receive DST info. At minimum one record will be returned /// matching the TZ in place for the start datetime. LArray &Out, /// [In] The start date that you want DST info for. LDateTime &Start, /// [Optional In] The end of the period you want DST info for. LDateTime *End = 0 ); /// Using the DST info this will convert 'dt' from UTC to local static bool DstToLocal(LArray &Dst, LDateTime &dt); /// Decodes an email date into the current instance bool Decode(const char *In); /// Returns a month index from a month name static int MonthFromName(const char *Name); // File int Sizeof(); bool Serialize(class LFile &f, bool Write); bool Serialize(class LDom *Props, char *Name, bool Write); // operators bool operator <(const LDateTime &dt) const; bool operator <=(const LDateTime &dt) const; bool operator >(const LDateTime &dt) const; bool operator >=(const LDateTime &dt) const; bool operator ==(const LDateTime &dt) const; bool operator !=(const LDateTime &dt) const; int Compare(const LDateTime *d) const; LDateTime operator -(const LDateTime &dt); LDateTime operator +(const LDateTime &dt); int DiffMonths(const LDateTime &dt); - operator uint64() - { - uint64 ts = 0; - Get(ts); - return ts; - } - - LDateTime &operator =(uint64 ts) - { - Set(ts); - return *this; - } - LDateTime &operator =(struct tm *t); LDateTime &operator =(const LDateTime &t); LDateTime &operator =(LDateTime const *t) { if (t) *this = *t; return *this; } /// LDom interface. /// /// Even though we don't inherit from a LDom class this class supports the same /// interface for ease of use. Currently there are cases where LDateTime is used /// in LArray's which don't implement calling a constructor (they init with all /// zeros). bool GetVariant(const char *Name, class LVariant &Value, char *Array = NULL); bool SetVariant(const char *Name, class LVariant &Value, char *Array = NULL); bool CallMethod(const char *Name, class LVariant *ReturnValue, LArray &Args); }; /// Time zone information struct LTimeZone { public: /// The offset from UTC float Offset; /// The name of the zone const char *Text; }; /// A list of all known timezones. extern LTimeZone LTimeZones[]; #ifdef _DEBUG LgiFunc bool LDateTime_Test(); #endif #endif diff --git a/include/lgi/common/Stat.h b/include/lgi/common/Stat.h --- a/include/lgi/common/Stat.h +++ b/include/lgi/common/Stat.h @@ -1,76 +1,66 @@ #pragma once #include #include #if !defined(WINDOWS) #include #endif struct LStat : #if defined(WINDOWS) public _stat64 #else public stat #endif { - constexpr static uint64_t WINDOWS_TICK = 10000000; - constexpr static uint64_t SEC_TO_UNIX_EPOCH = 11644473600LL; - uint64_t UnixToNative(uint64_t ts) const - { - #if defined(WINDOWS) - return (ts + SEC_TO_UNIX_EPOCH) * WINDOWS_TICK; - #else - return ts; - #endif - } int Result = 0; LStat(const char *path) { #if defined(WINDOWS) LAutoWString w(Utf8ToWide(path)); Result = _wstat64(w, this); #else Result = ::stat(path, this); #endif } operator bool() const { return Result == 0; } bool IsFile() const { #if defined(WINDOWS) return (st_mode & _S_IFREG) != 0; #else return S_ISREG(st_mode); #endif } bool IsDir() const { #if defined(WINDOWS) return (st_mode & _S_IFDIR) != 0; #else return S_ISDIR(st_mode); #endif } bool IsPipe() const { #if defined(WINDOWS) return (st_mode & _S_IFIFO) != 0; #else return S_ISFIFO(st_mode); #endif } int64_t Size() const { return st_size; } uint64_t GetCreationTime() const { return st_ctime; } - LDateTime GetCreation() const { return LDateTime(UnixToNative(st_ctime)); } + LDateTime GetCreation() const { return LDateTime(LTimeStamp(st_ctime)); } uint64_t GetLastAccessTime() const { return st_atime; } - LDateTime GetLastAccess() const { return LDateTime(UnixToNative(st_atime)); } + LDateTime GetLastAccess() const { return LDateTime(LTimeStamp(st_atime)); } uint64_t GetLastWriteTime() const { return st_mtime; } - LDateTime GetLastWrite() const { return LDateTime(UnixToNative(st_mtime)); } + LDateTime GetLastWrite() const { return LDateTime(LTimeStamp(st_mtime)); } }; 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,1987 @@ #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 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 LTimeStamp: + 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"); } LBox *ToolsBox = new LBox(IDC_TOOLS_BOX, true, "ToolsBox"); FoldersBox = new LBox(IDC_FOLDERS_BOX, false, "FoldersBox"); LBox *CommitsBox = new LBox(IDC_COMMITS_BOX, true, "CommitsBox"); ToolBar *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->ShowColumnHeader(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); LBox *MsgBox = new LBox(IDC_MSG_BOX, true); MsgBox->Attach(FilesBox); CommitCtrls *Commit = new CommitCtrls; Commit->Attach(MsgBox); Commit->GetCss(true)->Height("25%"); if (Commit->GetViewById(IDC_MSG, Msg)) { LTextView3 *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); const char *Style = "Padding: 0px 8px 8px 0px"; Tabs->GetCss(true)->Parse(Style); LTabPage *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(); LXmlTag *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(); } 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_PUSH: { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) f->Push(); break; } case IDC_PULL: { VcFolder *f = dynamic_cast(Tree->Selection()); if (f) 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/VcCommit.cpp b/lvc/src/VcCommit.cpp --- a/lvc/src/VcCommit.cpp +++ b/lvc/src/VcCommit.cpp @@ -1,575 +1,575 @@ #include "Lvc.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Path.h" #include "lgi/common/HashTable.h" #include "resdefs.h" const char *sPalette[] = { "9696ff", "ff8787", "ff934b", "a8d200", "00f0c0", "87e7ff", "a5a5ff", "f3c3ff", "b40090", "00b400" }; LColour GetPaletteColour(int i) { LColour c; const char *s = sPalette[i % CountOf(sPalette)]; #define Comp(comp, off) { const char h[3] = {s[off], s[off+1], 0}; c.comp(htoi(h)); } Comp(r, 0); Comp(g, 2); Comp(b, 4); return c; } VcEdge::~VcEdge() { if (Parent) Parent->Edges.Delete(this); if (Child) Child->Edges.Delete(this); } void VcEdge::Detach(VcCommit *c) { if (Parent == c) Parent = NULL; if (Child == c) Child = NULL; if (Parent == NULL && Child == NULL) delete this; } void VcEdge::Set(VcCommit *p, VcCommit *c) { if ((Parent = p)) Parent->Edges.Add(this); if ((Child = c)) Child->Edges.Add(this); } size_t VcCommit::Instances = 0; VcCommit::VcCommit(AppPriv *priv, VcFolder *folder) : Pos(32, -1) { VcCommit::Instances++; d = priv; Index = -1; Folder = folder; Vcs = Folder->GetType(); Current = false; NodeIdx = -1; NodeColour = GetPaletteColour(0); Parents.SetFixedLength(false); } VcCommit::VcCommit(const VcCommit &c) { VcCommit::Instances++; d = c.d; Folder = c.Folder; Vcs = c.Vcs; Rev = c.Rev; Index = c.Index; Parents = c.Parents; Branch = c.Branch; Author = c.Author; Ts = c.Ts; Msg = c.Msg; Files = c.Files; } VcCommit::~VcCommit() { for (auto e: Edges) e->Detach(this); VcCommit::Instances--; } char *VcCommit::GetRev() { return Rev; } char *VcCommit::GetAuthor() { return Author; } char *VcCommit::GetMsg() { return Msg; } char *VcCommit::GetBranch() { return Branch; } void VcCommit::SetCurrent(bool b) { Current = b; } void VcCommit::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { if (i == 0) { double Px = 12; int Ht = Ctx.Y(); double Half = 5.5; #define MAP(col) ((col) * Px + Half) LMemDC Mem(Ctx.X(), Ctx.Y(), System32BitColourSpace); #ifdef LINUX { LItem::ItemPaintCtx TmpCtx = Ctx; TmpCtx.Offset(-TmpCtx.x1, -TmpCtx.y1); TmpCtx.pDC = &Mem; LListItem::OnPaintColumn(TmpCtx, i, c); } #else LListItem::OnPaintColumn(Ctx, i, c); #endif double r = Half - 1; double x = MAP(NodeIdx); Mem.Colour(LColour::Black); VcCommit *Prev = NULL, *Next = NULL; Prev = Folder->Log.IdxCheck(Idx - 1) ? Folder->Log[Idx - 1] : NULL; Next = Folder->Log.IdxCheck(Idx + 1) ? Folder->Log[Idx + 1] : NULL; for (auto it: Pos) { VcEdge *e = it.key; int CurIdx = it.value; if (CurIdx < 0) { continue; } double CurX = MAP(CurIdx); #define I(v) ((int)(v)) if (e->Child != this) { // Line to previous commit int PrevIdx = Prev ? Prev->Pos.Find(e) : -1; if (PrevIdx >= 0) { double PrevX = MAP(PrevIdx); Mem.Line(I(PrevX), I(-(Ht/2)), I(CurX), I(Ht/2)); } else { Mem.Colour(LColour::Red); Mem.Line(I(CurX), I(Ht/2), I(CurX), I(Ht/2-5)); Mem.Colour(LColour::Black); } } if (e->Parent != this) { int NextIdx = Next ? Next->Pos.Find(e) : -1; if (NextIdx >= 0) { double NextX = MAP(NextIdx); Mem.Line(I(NextX), I(Ht+(Ht/2)), I(CurX), I(Ht/2)); } else { Mem.Colour(LColour::Red); Mem.Line(I(CurX), I(Ht/2), I(CurX), I(Ht/2+5)); Mem.Colour(LColour::Black); } } } if (NodeIdx >= 0) { double Cx = x; double Cy = Ht / 2; { LPath p; p.Circle(Cx, Cy, r + (Current ? 1 : 0)); LSolidBrush sb(LColour::Black); p.Fill(&Mem, sb); } { LPath p; p.Circle(Cx, Cy, r-1); LAssert(NodeColour.IsValid()); // LgiTrace("%s = %s\n", Branch.Get(), NodeColour.GetStr()); LSolidBrush sb(NodeColour); p.Fill(&Mem, sb); } } // Mem.ConvertPreMulAlpha(false); Ctx.pDC->Op(GDC_ALPHA); Ctx.pDC->Blt(Ctx.x1, Ctx.y1, &Mem); } else { LListItem::OnPaintColumn(Ctx, i, c); } } const char *VcCommit::GetFieldText(CommitField Fld) { switch (Fld) { case LRevision: return Rev; case LIndex: if (!sIndex) sIndex.Printf(LPrintfInt64, Index); return sIndex; case LBranch: if (Vcs == VcGit) { if (!sNames) sNames = Folder->GetGitNames(Rev); return sNames; } else { if (Branch) return Branch; return "default"; } break; case LAuthor: return Author; - case LTimeStamp: + case LTime: if (!sTimeStamp) sTimeStamp = Ts.Get(); return sTimeStamp; case LMessageTxt: if (!Msg) return NULL; if (!sMsg) sMsg = Msg.Split("\n", 1)[0]; return sMsg; default: break; } return NULL; } const char *VcCommit::GetText(int Col) { if (!Folder) { LAssert(0); return "#nofolder"; } if (!Folder->Fields.IdxCheck(Col)) { LAssert(0); return "#nofield"; } return (char*)GetFieldText(Folder->Fields[Col]); } bool VcCommit::GitParse(LString s, bool RevList) { LString::Array lines = s.Replace("\r","").Split("\n"); if (lines.Length() < 3) return false; if (RevList) { auto a = lines[0].SplitDelimit(); if (a.Length() != 2) return false; Ts.SetUnix((uint64) a[0].Int()); Rev = a[1]; for (auto Ln: lines) { if (IsWhite(Ln(0))) { if (Msg) Msg += "\n"; Msg += Ln.Strip(); } else { a = Ln.SplitDelimit(" \t\r", 1); if (a.Length() <= 1) continue; if (a[0].Equals("parent")) Parents.Add(a[1]); else if (a[0].Equals("author")) Author = a[1].RStrip("0123456789+ "); } } } else { for (unsigned ln = 0; ln < lines.Length(); ln++) { LString &l = lines[ln]; if (ln == 0) { auto parts = l.SplitDelimit("()"); auto commit = parts[0].SplitDelimit(); Rev = commit.Last(); } else if (l.Find("Author:") >= 0) { Author = l.Split(":", 1)[1].Strip(); } else if (l.Find("Date:") >= 0) { Ts.Parse(l.Split(":", 1)[1].Strip()); } else if (l.Strip().Length() > 0) { if (Msg) Msg += "\n"; Msg += l.Strip(); } } } OnParse(); return Author && Rev; } bool VcCommit::CvsParse(LDateTime &Dt, LString Auth, LString Message) { Ts = Dt; Ts.ToLocal(); - uint64 i; + LTimeStamp i; if (Ts.Get(i)) - Rev.Printf(LPrintfInt64, i); + Rev.Printf(LPrintfInt64, i.Get()); Author = Auth; Msg = Message; OnParse(); return true; } void VcCommit::OnParse() { NodeColour = Folder->BranchColour(Branch); } bool VcCommit::HgParse(LString s) { LString::Array Lines = s.SplitDelimit("\n"); if (Lines.Length() < 1) return false; for (auto Ln: Lines) { LString::Array f = Ln.Split(":", 1); if (f.Length() == 2) { if (f[0].Equals("changeset")) { auto p = f[1].Strip().Split(":"); Index = p[0].Int(); Rev = p[1]; } else if (f[0].Equals("user")) Author = f[1].Strip(); else if (f[0].Equals("date")) Ts.Parse(f[1].Strip()); else if (f[0].Equals("summary")) Msg = f[1].Strip(); else if (f[0].Equals("parent")) { auto p = f[1].Strip().Split(":"); Parents.Add(p.Last()); } else if (f[0].Equals("branch")) Branch = f[1].Strip(); } } OnParse(); return Rev.Get() != NULL; } bool VcCommit::SvnParse(LString s) { LString::Array lines = s.Split("\n"); if (lines.Length() < 1) return false; for (unsigned ln = 0; ln < lines.Length(); ln++) { LString &l = lines[ln]; if (ln == 0) { LString::Array a = l.Split("|"); if (a.Length() > 3) { Rev = a[0].Strip(" \tr"); Index = Rev.Int(); Author = a[1].Strip(); Ts.Parse(a[2]); } } else { if (Msg) Msg += "\n"; Msg += l.Strip(); } } Msg = Msg.Strip(); OnParse(); // LgiTrace("SvnParse %s %i %s %s %s\n", Rev.Get(), (int)Index, Author.Get(), Ts.GetDate().Get(), Msg.Get()); return Author && Rev && Ts.IsValid(); } VcFolder *VcCommit::GetFolder() { for (LTreeItem *i = d->Tree->Selection(); i; i = i->GetParent()) { auto f = dynamic_cast(i); if (f) return f; } return NULL; } void VcCommit::Select(bool b) { LListItem::Select(b); if (Rev && b) { VcFolder *f = GetFolder(); // LgiTrace("%s:%i select %s %i f=%p\n", _FL, Rev.Get(), b, f); if (f) { auto Rng = d->GetCommitRange(); // LgiTrace("%s:%i select rng=%i\n", _FL, (int)Rng.Length()); if (Rng.Length() > 1) { // Show the diffs over a range of commits. f->DiffRange(Rng[0], Rng[1]); } else { // Show a single commit's files: f->ListCommit(this); } } if (d->Msg) { d->Msg->Name(Msg); auto *w = d->Msg->GetWindow(); if (w) { w->SetCtrlEnabled(IDC_COMMIT, false); w->SetCtrlEnabled(IDC_COMMIT_AND_PUSH, false); } } else LAssert(0); } } void VcCommit::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.IsContextMenu()) { LArray Sel; GetList()->GetSelection(Sel); LSubMenu s; s.AppendItem("Update", IDM_UPDATE, !Current); s.AppendSeparator(); s.AppendItem("Merge With Local", IDM_MERGE, !Current); if (Rev) s.AppendItem("Copy Revision", IDM_COPY_REV); if (Index >= 0) s.AppendItem("Copy Index", IDM_COPY_INDEX); s.AppendItem("Rename Branch", IDM_RENAME_BRANCH); int Cmd = s.Float(GetList(), m); switch (Cmd) { case IDM_MERGE: { VcFolder *f = GetFolder(); if (!f) { LAssert(!"No folder?"); break; } f->MergeToLocal(Rev); break; } case IDM_UPDATE: { VcFolder *f = GetFolder(); if (!f) { LAssert(!"No folder?"); break; } f->Checkout(Rev, false); break; } case IDM_COPY_REV: { LClipBoard c(GetList()); LString::Array a; a.SetFixedLength(false); for (auto i: Sel) a.New() = i->GetRev(); c.Text(LString(",").Join(a)); break; } case IDM_COPY_INDEX: { LClipBoard c(GetList()); LString b; b.Printf(LPrintfInt64, Index); c.Text(b); break; } case IDM_RENAME_BRANCH: { LArray Revs; if (GetList()->GetSelection(Revs)) { auto Fld = Folder; auto Inp = new LInput(GetList(), "", "New branch name:", AppName); Inp->DoModal([this, Fld, Inp, Revs](auto dlg, auto ctrlId) { if (ctrlId) { auto Revisions = Revs; Fld->RenameBranch(Inp->GetStr(), Revisions); } delete dlg; }); } break; } } } } diff --git a/lvc/src/VcCommit.h b/lvc/src/VcCommit.h --- a/lvc/src/VcCommit.h +++ b/lvc/src/VcCommit.h @@ -1,95 +1,95 @@ #ifndef _VcCommit_h_ #define _VcCommit_h_ class VcFolder; class VcCommit; enum CommitField { LNone, LGraph, LRevision, LIndex, LBranch, LAuthor, - LTimeStamp, + LTime, LMessageTxt, LParents, }; struct VcEdge { VcCommit *Parent, *Child; int Idx; VcEdge(VcCommit *p, VcCommit *c) { Set(p, c); Idx = -1; } ~VcEdge(); void Set(VcCommit *p, VcCommit *c); void Detach(VcCommit *c); }; class VcCommit : public LListItem { LString sIndex, sNames, sTimeStamp, sMsg; bool Current; protected: AppPriv *d = NULL; VcFolder *Folder = NULL; VersionCtrl Vcs = VcNone; // Commit Meta LString Rev; int64_t Index = -1; // Revision index (svn/hg) LString::Array Parents; LString Branch; LString Author; LDateTime Ts; LString Msg; void OnParse(); public: static size_t Instances; LString::Array Files; LArray Edges; int NodeIdx = -1; // Used by LinkParents int Idx = -1; // Used by LinkParents LColour NodeColour; LHashTbl, int> Pos; VcCommit(AppPriv *priv, VcFolder *folder); VcCommit(const VcCommit &c); ~VcCommit(); char *GetRev(); int64_t GetIndex() { return Index; } bool IsRev(const char *r) { return !Strcmp(GetRev(), r); } char *GetAuthor(); char *GetMsg(); char *GetBranch(); LString::Array *GetParents() { return &Parents; } LDateTime &GetTs() { return Ts; } void SetCurrent(bool b); void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c); const char *GetText(int Col); const char *GetFieldText(CommitField Fld); bool GitParse(LString s, bool RevList); bool SvnParse(LString s); bool HgParse(LString s); bool CvsParse(LDateTime &Dt, LString Auth, LString Msg); VcFolder *GetFolder(); // Events void OnMouseClick(LMouse &m); void Select(bool b); }; #endif \ No newline at end of file diff --git a/lvc/src/VcFolder.cpp b/lvc/src/VcFolder.cpp --- a/lvc/src/VcFolder.cpp +++ b/lvc/src/VcFolder.cpp @@ -1,4986 +1,4986 @@ #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(LTimeStamp); + Fields.Add(LTime); Fields.Add(LMessageTxt); break; } case VcGit: { Fields.Add(LGraph); Fields.Add(LRevision); Fields.Add(LBranch); Fields.Add(LAuthor); - Fields.Add(LTimeStamp); + Fields.Add(LTime); Fields.Add(LMessageTxt); break; } default: { Fields.Add(LGraph); Fields.Add(LRevision); Fields.Add(LAuthor); - Fields.Add(LTimeStamp); + Fields.Add(LTime); Fields.Add(LMessageTxt); break; } } } } 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 LTimeStamp: lst->AddColumn("Date", 130); 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::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(); 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) { - uint64 ats, bts; + LTimeStamp ats, bts; (*a)->GetTs().Get(ats); (*b)->GetTs().Get(bts); - int64 diff = (int64)bts - ats; + 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)) { - uint64 Ts; + LTimeStamp Ts; if (Dt.Get(Ts)) { - VcCommit *Cc = Map.Find(Ts); + VcCommit *Cc = Map.Find(Ts.Get()); if (!Cc) { Map.Add(Ts, 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/src/common/General/DateTime.cpp b/src/common/General/DateTime.cpp --- a/src/common/General/DateTime.cpp +++ b/src/common/General/DateTime.cpp @@ -1,2315 +1,2337 @@ /* ** FILE: LDateTime.cpp ** AUTHOR: Matthew Allen ** DATE: 11/11/98 ** DESCRIPTION: Scribe Date Time Object ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #define _DEFAULT_SOURCE #include #include #include #include #include #if defined(MAC) #include #endif #ifdef WINDOWS #include #endif #include "lgi/common/Lgi.h" #include "lgi/common/DateTime.h" #include "lgi/common/DocView.h" constexpr const char *LDateTime::WeekdaysShort[7]; constexpr const char *LDateTime::WeekdaysLong[7]; constexpr const char *LDateTime::MonthsShort[12]; constexpr const char *LDateTime::MonthsLong[12]; #if !defined(WINDOWS) #define MIN_YEAR 1800 #endif #if defined(LINUX) #define USE_ZDUMP 1 #elif defined(HAIKU) #include "lgi/common/TimeZoneInfo.h" #endif #define DEBUG_DST_INFO 0 ////////////////////////////////////////////////////////////////////////////// uint16 LDateTime::DefaultFormat = GDTF_DEFAULT; char LDateTime::DefaultSeparator = '/'; uint16 LDateTime::GetDefaultFormat() { if (DefaultFormat == GDTF_DEFAULT) { #ifdef WIN32 TCHAR s[80] = _T("1"); GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_IDATE, s, CountOf(s)); switch (_tstoi(s)) { case 0: DefaultFormat = GDTF_MONTH_DAY_YEAR; break; default: case 1: DefaultFormat = GDTF_DAY_MONTH_YEAR; break; case 2: DefaultFormat = GDTF_YEAR_MONTH_DAY; break; } GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_ITIME, s, sizeof(s)); if (_tstoi(s) == 1) { DefaultFormat |= GDTF_24HOUR; } else { DefaultFormat |= GDTF_12HOUR; } if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDATE, s, sizeof(s))) DefaultSeparator = (char)s[0]; if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SSHORTDATE, s, sizeof(s))) { char Sep[] = { DefaultSeparator, '/', '\\', '-', '.', 0 }; LString Str = s; auto t = Str.SplitDelimit(Sep); for (int i=0; i= low && (v) <= high) bool LDateTime::IsValid() const { return InRange(_Day, 1, 31) && InRange(_Year, 1600, 2100) && InRange(_Thousands, 0, 999) && InRange(_Month, 1, 12) && InRange(_Seconds, 0, 59) && InRange(_Minutes, 0, 59) && InRange(_Hours, 0, 23) && InRange(_Tz, -780, 780); } void LDateTime::SetTimeZone(int NewTz, bool ConvertTime) { if (ConvertTime && NewTz != _Tz) { // printf("SetTimeZone: %i\n", NewTz - _Tz); AddMinutes(NewTz - _Tz); } _Tz = NewTz; } int LDateTime::SystemTimeZone(bool ForceUpdate) { if (ForceUpdate || CurTz == NO_ZONE) { CurTz = 0; CurTzOff = 0; #ifdef MAC #ifdef LGI_COCOA NSTimeZone *timeZone = [NSTimeZone localTimeZone]; if (timeZone) { NSDate *Now = [NSDate date]; CurTz = (int) [timeZone secondsFromGMTForDate:Now] / 60; CurTzOff = [timeZone daylightSavingTimeOffsetForDate:Now] / 60; CurTz -= CurTzOff; } #elif defined LGI_CARBON CFTimeZoneRef tz = CFTimeZoneCopySystem(); CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); Boolean dst = CFTimeZoneIsDaylightSavingTime(tz, now); if (dst) { CFAbsoluteTime next = CFTimeZoneGetNextDaylightSavingTimeTransition(tz, now); CurTz = CFTimeZoneGetSecondsFromGMT(tz, next + 100) / 60; } else { CurTz = CFTimeZoneGetSecondsFromGMT(tz, now) / 60; } CurTzOff = CFTimeZoneGetDaylightSavingTimeOffset(tz, now) / 60; CFRelease(tz); #endif #elif defined(WIN32) timeb tbTime; ftime(&tbTime); CurTz = -tbTime.timezone; TIME_ZONE_INFORMATION Tzi; if (GetTimeZoneInformation(&Tzi) == TIME_ZONE_ID_DAYLIGHT) CurTzOff = -Tzi.DaylightBias; #elif defined(LINUX) || defined(HAIKU) int six_months = (365 * 24 * 60 * 60) / 2; time_t now = 0, then = 0; time (&now); then = now - six_months; tm now_tz, then_tz; tm *t = localtime_r(&now, &now_tz); if (t) { localtime_r(&then, &then_tz); CurTz = now_tz.tm_gmtoff / 60; if (now_tz.tm_isdst) { CurTzOff = (now_tz.tm_gmtoff - then_tz.tm_gmtoff) / 60; CurTz = then_tz.tm_gmtoff / 60; } else // This is not DST so there is no offset right? CurTzOff = 0; // (then_tz.tm_gmtoff - now_tz.tm_gmtoff) / 60; } else return NO_ZONE; #else #error "Impl me." #endif } return CurTz + CurTzOff; } int LDateTime::SystemTimeZoneOffset() { if (CurTz == NO_ZONE) SystemTimeZone(); return CurTzOff; } #if defined WIN32 LDateTime ConvertSysTime(SYSTEMTIME &st, int year) { LDateTime n; if (st.wYear) { n.Year(st.wYear); n.Month(st.wMonth); n.Day(st.wDay); } else { n.Year(year); n.Month(st.wMonth); // Find the 'nth' matching weekday, starting from the first day in the month n.Day(1); LDateTime c = n; for (int i=0; iCompare(b); } #elif USE_ZDUMP static bool ParseValue(char *s, LString &var, LString &val) { if (!s) return false; char *e = strchr(s, '='); if (!e) return false; *e++ = 0; var = s; val = e; *e = '='; return var != 0 && val != 0; } #endif /* Testing code... LDateTime Start, End; LArray Info; Start.Set("1/1/2010"); End.Set("31/12/2014"); LDateTime::GetDaylightSavingsInfo(Info, Start, &End); LStringPipe p; for (int i=0; i,int> { MonthHash() { for (int i=0; i &Info, LDateTime &Start, LDateTime *End) { bool Status = false; #if defined(WIN32) TIME_ZONE_INFORMATION Tzi; auto r = GetTimeZoneInformation(&Tzi); if (r > TIME_ZONE_ID_UNKNOWN) { Info.Length(0); // Find the dates for the previous year from Start. This allows // us to cover the start of the current year. LDateTime s = ConvertSysTime(Tzi.StandardDate, Start.Year() - 1); LDateTime d = ConvertSysTime(Tzi.DaylightDate, Start.Year() - 1); // Create initial Info entry, as the last change in the previous year auto *i = &Info.New(); if (s < d) { // Year is: Daylight->Standard->Daylight LDateTime tmp = d; i->Offset = -(Tzi.Bias + Tzi.DaylightBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); } else { // Year is: Standard->Daylight->Standard LDateTime tmp = s; i->Offset = -(Tzi.Bias + Tzi.StandardBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts();; } for (auto y=Start.Year(); y<=(End?End->Year():Start.Year()); y++) { if (s < d) { // Cur year, first event: end of DST i = &Info.New(); auto tmp = ConvertSysTime(Tzi.StandardDate, y); i->Offset = -(Tzi.Bias + Tzi.StandardBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); // Cur year, second event: start of DST i = &Info.New(); tmp = ConvertSysTime(Tzi.DaylightDate, y); i->Offset = -(Tzi.Bias + Tzi.DaylightBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); } else { // Cur year, first event: start of DST i = &Info.New(); auto tmp = ConvertSysTime(Tzi.DaylightDate, Start.Year()); i->Offset = -(Tzi.Bias + Tzi.DaylightBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); // Cur year, second event: end of DST i = &Info.New(); tmp = ConvertSysTime(Tzi.StandardDate, Start.Year()); i->Offset = -(Tzi.Bias + Tzi.StandardBias); tmp.AddMinutes(-i->Offset); i->UtcTimeStamp = tmp.Ts(); } } Status = true; } #elif defined(MAC) LDateTime From = Start; From.AddMonths(-6); LDateTime To = End ? *End : Start; To.AddMonths(6); auto ToUnix = To.GetUnix(); auto tz = [NSTimeZone systemTimeZone]; auto startDate = [[NSDate alloc] initWithTimeIntervalSince1970:(From.Ts() / Second64Bit) - Offset1800]; while (startDate) { auto next = [tz nextDaylightSavingTimeTransitionAfterDate:startDate]; auto &i = Info.New(); auto nextTs = [next timeIntervalSince1970]; i.UtcTimeStamp = (nextTs + Offset1800) * Second64Bit; i.Offset = (int)([tz secondsFromGMTForDate:[next dateByAddingTimeInterval:60]]/60); #if DEBUG_DST_INFO { LDateTime dt; dt.Set(i.UtcTimeStamp); LgiTrace("%s:%i - Ts=%s Off=%i\n", _FL, dt.Get().Get(), i.Offset); } #endif if (nextTs >= ToUnix) break; [startDate release]; startDate = next; } if (startDate) [startDate release]; #elif USE_ZDUMP if (!Zdump.Length()) { static bool First = true; auto linkLoc = "/etc/localtime"; #if defined(LINUX) auto zoneLoc = "/usr/share/zoneinfo"; #elif defined(HAIKU) auto zoneLoc = "/boot/system/data/zoneinfo"; #else #error "Impl me" #endif if (!LFileExists(linkLoc)) { if (First) { LgiTrace("%s:%i - LDateTime::GetDaylightSavingsInfo error: '%s' doesn't exist.\n" " It should link to something in the '%s' tree.\n", _FL, linkLoc, zoneLoc); #ifdef HAIKU LgiTrace(" To fix that: pkgman install timezone_data and then create the '%s' link.\n", linkLoc); #endif } return First = false; } auto f = popen(LString::Fmt("zdump -v %s", linkLoc), "r"); if (f) { char s[1024]; size_t r; LStringPipe p(1024); while ((r = fread(s, 1, sizeof(s), f)) > 0) p.Write(s, (int)r); fclose(f); Zdump = p.NewLStr().Split("\n"); } else { if (First) { LgiTrace("%s:%i - LDateTime::GetDaylightSavingsInfo error: zdump didn't run.\n", _FL); #ifdef HAIKU LgiTrace("To fix that: pkgman install timezone_data\n"); #endif } return First = false; } } MonthHash Lut; LDateTime Prev; int PrevOff = 0; for (auto Line: Zdump) { auto l = Line.SplitDelimit(" \t"); if (l.Length() >= 16 && l[0].Equals("/etc/localtime")) { // /etc/localtime Sat Oct 3 15:59:59 2037 UTC = Sun Oct 4 01:59:59 2037 EST isdst=0 gmtoff=36000 // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 LDateTime Utc; Utc.Year(l[5].Int()); #if DEBUG_DST_INFO if (Utc.Year() < 2020) continue; // printf("DST: %s\n", Line.Get()); #endif auto Tm = l[4].SplitDelimit(":"); if (Tm.Length() != 3) { #if DEBUG_DST_INFO printf("%s:%i - Tm '%s' has wrong parts: %s\n", _FL, l[4].Get(), Line.Get()); #endif continue; } Utc.Hours(Tm[0].Int()); Utc.Minutes(Tm[1].Int()); Utc.Seconds(Tm[2].Int()); if (Utc.Minutes() < 0) { #if DEBUG_DST_INFO printf("%s:%i - Mins is zero: %s\n", _FL, l[4].Get()); #endif continue; } int m = Lut.Find(l[2]); if (!m) { #if DEBUG_DST_INFO printf("%s:%i - Unknown month '%s'\n", _FL, l[2].Get()); #endif continue; } Utc.Day(l[3].Int()); Utc.Month(m); LString Var, Val; if (!ParseValue(l[14], Var, Val) || Var != "isdst") { #if DEBUG_DST_INFO printf("%s:%i - Unknown value for isdst\n", _FL); #endif continue; } if (!ParseValue(l[15], Var, Val) || Var != "gmtoff") { #if DEBUG_DST_INFO printf("%s:%i - Unknown value for isdst\n", _FL); #endif continue; } int Off = atoi(Val) / 60; if (Utc.Ts() == 0) continue; if (Prev.Year() && Prev < Start && Start < Utc) { // Emit initial entry for 'start' auto &inf = Info.New(); - inf.UtcTimeStamp = Prev; + Prev.Get(inf.Utc); inf.Offset = PrevOff; #if DEBUG_DST_INFO printf("Info: Start=%s %i\n", Prev.Get().Get(), inf.Offset); #endif } if (Utc > Start) { // Emit furthur entries for DST events between start and end. auto &inf = Info.New(); - inf.UtcTimeStamp = Utc; + Utc.Get(inf.Utc); inf.Offset = Off; #if DEBUG_DST_INFO printf("Info: Next=%s %i\n", Utc.Get().Get(), inf.Offset); #endif if (End && Utc > *End) { // printf("Utc after end: %s > %s\n", Utc.Get().Get(), End->Get().Get()); break; } } Prev = Utc; PrevOff = Off; } } Status = Info.Length() > 1; #elif defined(HAIKU) LTimeZoneInfo tzinfo; if (!tzinfo.Read()) { #if DEBUG_DST_INFO LgiTrace("%s:%i - info read failed.\n", _FL); #endif return false; } Status = tzinfo.GetDaylightSavingsInfo(Info, Start, End); #if DEBUG_DST_INFO if (!Status) printf("%s:%i - GetDaylightSavingsInfo failed.\n", _FL); #endif #else LAssert(!"Not implemented."); #endif return Status; } bool LDateTime::DstToLocal(LArray &Dst, LDateTime &dt) { if (dt.GetTimeZone()) { LAssert(!"Should be a UTC date."); return true; } #if DEBUG_DST_INFO LgiTrace("DstToLocal: %s\n", dt.Get().Get()); #endif LAssert(Dst.Length() > 1); // Needs to have at least 2 entries...? for (size_t i=0; i= start && dt < end; if (InRange) { dt.SetTimeZone(a.Offset, true); #if DEBUG_DST_INFO LgiTrace("\tRng[%i]: %s -> %s, SetTimeZone(%g), dt=%s\n", (int)i, start.Get().Get(), end.Get().Get(), (double)a.Offset/60.0, dt.Get().Get()); #endif return true; } } auto Last = Dst.Last(); LDateTime d; - d.Set(Last.UtcTimeStamp); + d.Set(Last.Utc); if (dt >= d && dt.Year() == d.Year()) { // If it's after the last DST change but in the same year... it's ok... // Just use the last offset. dt.SetTimeZone(Last.Offset, true); return true; } #if DEBUG_DST_INFO for (auto d: Dst) LgiTrace("Dst: %s = %i\n", d.GetLocal().Get().Get(), d.Offset); #endif LgiTrace("%s:%i - No valid DST range for: %s\n", _FL, dt.Get().Get()); LAssert(!"No valid DST range for this date."); return false; } int LDateTime::DayOfWeek() const { int Index = 0; int Day = IsLeapYear() ? 29 : 28; switch (_Year / 100) { case 19: { Index = 3; break; } case 20: { Index = 2; break; } } // get year right int y = _Year % 100; int r = y % 12; Index = (Index + (y / 12) + r + (r / 4)) % 7; // get month right if (_Month % 2 == 0) { // even month if (_Month > 2) Day = _Month; } else { // odd month switch (_Month) { case 1: { Day = 31; if (IsLeapYear()) { Index = Index > 0 ? Index - 1 : Index + 6; } break; } case 11: case 3: { Day = 7; break; } case 5: { Day = 9; break; } case 7: { Day = 11; break; } case 9: { Day = 5; break; } } } // get day right int Diff = Index - (Day - _Day); while (Diff < 0) Diff += 7; return Diff % 7; } LDateTime LDateTime::Now() { LDateTime dt; dt.SetNow(); return dt; } LDateTime &LDateTime::SetNow() { #ifdef WIN32 SYSTEMTIME stNow; FILETIME ftNow; GetSystemTime(&stNow); SystemTimeToFileTime(&stNow, &ftNow); uint64 i64 = ((uint64)ftNow.dwHighDateTime << 32) | ftNow.dwLowDateTime; Set(i64); #else time_t now; time(&now); struct tm *time = localtime(&now); if (time) *this = time; #ifndef LGI_STATIC else { LgiTrace("%s:%i - Error: localtime failed, now=%u\n", _FL, now); } #endif #endif return *this; } #define Convert24HrTo12Hr(h) ( (h) == 0 ? 12 : (h) > 12 ? (h) % 12 : (h) ) #define Convert24HrToAmPm(h) ( (h) >= 12 ? "p" : "a" ) LString LDateTime::GetDate() const { char s[32]; int Ch = GetDate(s, sizeof(s)); return LString(s, Ch); } int LDateTime::GetDate(char *Str, size_t SLen) const { int Ch = 0; if (Str && SLen > 0) { switch (_Format & GDTF_DATE_MASK) { case GDTF_MONTH_DAY_YEAR: Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%2.2i" :"%i" , _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%c%2.2i":"%c%i", DefaultSeparator, _Day); Ch += sprintf_s(Str+Ch, SLen-Ch, "%c%i", DefaultSeparator, _Year); break; default: case GDTF_DAY_MONTH_YEAR: Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%2.2i" :"%i" , _Day); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%c%2.2i":"%c%i", DefaultSeparator, _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, "%c%i", DefaultSeparator, _Year); break; case GDTF_YEAR_MONTH_DAY: Ch += sprintf_s(Str+Ch, SLen-Ch, "%i", _Year); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_MONTH_LEADINGZ?"%c%2.2i":"%c%i", DefaultSeparator, _Month); Ch += sprintf_s(Str+Ch, SLen-Ch, _Format&GDTF_DAY_LEADINGZ ?"%c%2.2i":"%c%i", DefaultSeparator, _Day); break; } } return Ch; } LString LDateTime::GetTime() const { char s[32]; int Ch = GetTime(s, sizeof(s)); return LString(s, Ch); } int LDateTime::GetTime(char *Str, size_t SLen) const { int Ch = 0; if (Str && SLen > 0) { switch (_Format & GDTF_TIME_MASK) { case GDTF_12HOUR: default: { Ch += sprintf_s(Str, SLen, "%i:%2.2i:%2.2i%s", Convert24HrTo12Hr(_Hours), _Minutes, _Seconds, Convert24HrToAmPm(_Hours)); break; } case GDTF_24HOUR: { Ch += sprintf_s(Str, SLen, "%i:%2.2i:%2.2i", _Hours, _Minutes, _Seconds); break; } } } return Ch; } uint64 LDateTime::Ts() const { - uint64 ts = 0; + LTimeStamp ts; Get(ts); - return ts; + return ts.Get(); } uint64_t LDateTime::GetUnix() { - uint64_t s; + LTimeStamp s; Get(s); #if defined(WINDOWS) - return s / LDateTime::Second64Bit / 116445168000000000LL; + return s.Get() / LDateTime::Second64Bit / 116445168000000000LL; #else - return s / LDateTime::Second64Bit - Offset1800; + return s.Get() / LDateTime::Second64Bit - Offset1800; #endif } bool LDateTime::SetUnix(uint64 s) { #if defined(WINDOWS) return Set(s * LDateTime::Second64Bit + 116445168000000000LL); #else return Set((s + Offset1800) * LDateTime::Second64Bit); #endif } -bool LDateTime::Set(uint64 s) +bool LDateTime::Set(const LTimeStamp &s) { #if defined WIN32 FILETIME Utc; SYSTEMTIME System; // Adjust to the desired timezone - uint64 u = s + ((int64)_Tz * 60 * Second64Bit); + uint64 u = s.Get() + ((int64)_Tz * 60 * Second64Bit); Utc.dwHighDateTime = u >> 32; Utc.dwLowDateTime = u & 0xffffffff; if (!FileTimeToSystemTime(&Utc, &System)) return false; _Year = System.wYear; _Month = System.wMonth; _Day = System.wDay; _Hours = System.wHour; _Minutes = System.wMinute; _Seconds = System.wSecond; _Thousands = System.wMilliseconds; return true; #else - time_t t = (time_t) (((int64)(s / Second64Bit)) - Offset1800); + time_t t = s; Set(t); _Thousands = s % Second64Bit; return true; #endif } bool LDateTime::Set(struct tm *t, bool inferTimezone) { if (!t) return false; _Year = t->tm_year + 1900; _Month = t->tm_mon + 1; _Day = t->tm_mday; _Hours = t->tm_hour; _Minutes = t->tm_min; _Seconds = t->tm_sec; _Thousands = 0; if (inferTimezone) { #ifdef WINDOWS #define timegm _mkgmtime #endif auto diff = timegm(t) - mktime(t); _Tz = (int16)(diff / 60); } return true; } bool LDateTime::Set(time_t tt) { struct tm *t; if (_Tz) tt += _Tz * 60; #if !defined(_MSC_VER) || _MSC_VER < _MSC_VER_VS2005 t = gmtime(&tt); if (t) #else struct tm tmp; if (_gmtime64_s(t = &tmp, &tt) == 0) #endif { return Set(t, false); } return false; } uint64_t LDateTime::OsTime() const { #ifdef WINDOWS FILETIME Utc; SYSTEMTIME System; System.wYear = _Year; System.wMonth = limit(_Month, 1, 12); System.wDay = limit(_Day, 1, 31); System.wHour = limit(_Hours, 0, 23); System.wMinute = limit(_Minutes, 0, 59); System.wSecond = limit(_Seconds, 0, 59); System.wMilliseconds = limit(_Thousands, 0, 999); System.wDayOfWeek = DayOfWeek(); if (SystemTimeToFileTime(&System, &Utc)) { uint64_t s = ((uint64_t)Utc.dwHighDateTime << 32) | Utc.dwLowDateTime; if (_Tz) // Adjust for timezone s -= (int64)_Tz * 60 * Second64Bit; return s; } else { DWORD Err = GetLastError(); LAssert(!"SystemTimeToFileTime failed."); } #else if (_Year < MIN_YEAR) return 0; struct tm t; ZeroObj(t); t.tm_year = _Year - 1900; t.tm_mon = _Month - 1; t.tm_mday = _Day; t.tm_hour = _Hours; t.tm_min = _Minutes; t.tm_sec = _Seconds; t.tm_isdst = -1; time_t sec = timegm(&t); if (sec == -1) return 0; if (_Tz) { // Adjust the output to UTC from the current timezone. sec -= _Tz * 60; } return sec; #endif return 0; } bool LDateTime::OsTime(uint64_t ts) { return Set((time_t)ts); } -bool LDateTime::Get(uint64 &s) const +bool LDateTime::Get(LTimeStamp &s) const { #ifdef WINDOWS if (!IsValid()) { LAssert(!"Needs a valid date."); return false; } s = OsTime(); if (!s) return false; return true; #else if (_Year < MIN_YEAR) return false; auto sec = OsTime(); s = (uint64)(sec + Offset1800) * Second64Bit + _Thousands; return true; #endif } LString LDateTime::Get() const { char buf[32]; int Ch = GetDate(buf, sizeof(buf)); buf[Ch++] = ' '; Ch += GetTime(buf+Ch, sizeof(buf)-Ch); return LString(buf, Ch); } void LDateTime::Get(char *Str, size_t SLen) const { if (Str) { GetDate(Str, SLen); size_t len = strlen(Str); if (len < SLen - 1) { Str[len++] = ' '; GetTime(Str+len, SLen-len); } } } bool LDateTime::Set(const char *Str) { if (!Str) return false; if (Strlen(Str) > 100) return false; char Local[256]; strcpy_s(Local, sizeof(Local), Str); char *Sep = strchr(Local, ' '); if (Sep) { *Sep++ = 0; if (!SetTime(Sep)) return false; } if (!SetDate(Local)) return false; return true; } void LDateTime::Month(char *m) { int i = IsMonth(m); if (i >= 0) _Month = i + 1; } int DateComponent(const char *s) { int64 i = Atoi(s); return i ? (int)i : LDateTime::IsMonth(s); } bool LDateTime::SetDate(const char *Str) { bool Status = false; if (Str) { auto T = LString(Str).SplitDelimit("/-.,_\\"); if (T.Length() == 3) { int i[3] = { DateComponent(T[0]), DateComponent(T[1]), DateComponent(T[2]) }; int fmt = _Format & GDTF_DATE_MASK; // Do some guessing / overrides. // Don't let _Format define the format completely. if (i[0] > 1000) { fmt = GDTF_YEAR_MONTH_DAY; } else if (i[2] > 1000) { if (i[0] > 12) fmt = GDTF_DAY_MONTH_YEAR; else if (i[1] > 12) fmt = GDTF_MONTH_DAY_YEAR; } switch (fmt) { case GDTF_MONTH_DAY_YEAR: { _Month = i[0]; _Day = i[1]; _Year = i[2]; break; } case GDTF_DAY_MONTH_YEAR: { _Day = i[0]; _Month = i[1]; _Year = i[2]; break; } case GDTF_YEAR_MONTH_DAY: { _Year = i[0]; _Month = i[1]; _Day = i[2]; break; } default: { _Year = i[2]; if ((DefaultFormat & GDTF_DATE_MASK) == GDTF_MONTH_DAY_YEAR) { // Assume m/d/yyyy _Day = i[1]; _Month = i[0]; } else { // Who knows??? // Assume d/m/yyyy _Day = i[0]; _Month = i[1]; } break; } } if (_Year < 100) { LAssert(_Day < 1000 && _Month < 1000); if (_Year >= 80) _Year += 1900; else _Year += 2000; } Status = true; } else { // Fall back to fuzzy matching auto T = LString(Str).SplitDelimit(" ,"); MonthHash Lut; int FMonth = 0; int FDay = 0; int FYear = 0; for (unsigned i=0; i 0) { if (i >= 1000) { FYear = i; } else if (i < 32) { FDay = i; } } } else { int i = Lut.Find(p); if (i) FMonth = i; } } if (FMonth && FDay) { Day(FDay); Month(FMonth); } if (FYear) { Year(FYear); } else { LDateTime Now; Now.SetNow(); Year(Now.Year()); } } } return Status; } bool LDateTime::SetTime(const char *Str) { if (!Str) return false; auto T = LString(Str).SplitDelimit(":."); if (T.Length() < 2 || T.Length() > 4) return false; #define SetClamp(out, in, minVal, maxVal) \ out = (int)Atoi(in.Get(), 10, 0); \ if (out > maxVal) out = maxVal; \ else if (out < minVal) out = minVal SetClamp(_Hours, T[0], 0, 23); SetClamp(_Minutes, T[1], 0, 59); SetClamp(_Seconds, T[2], 0, 59); _Thousands = 0; const char *s = T.Last(); if (s) { if (strchr(s, 'p') || strchr(s, 'P')) { if (_Hours != 12) _Hours += 12; } else if (strchr(s, 'a') || strchr(s, 'A')) { if (_Hours == 12) _Hours -= 12; } } if (T.Length() > 3) { LString t = "0."; t += s; _Thousands = (int) (t.Float() * 1000); } return true; } int LDateTime::IsWeekDay(const char *s) { for (unsigned n=0; n= 4) { Year((int)t[0].Int()); Month((int)t[1].Int()); Day((int)t[2].Int()); } else if (t[2].Length() >= 4) { Day((int)t[0].Int()); Month((int)t[1].Int()); Year((int)t[2].Int()); } else { LAssert(!"Unknown date format?"); return false; } } } else if (a[i].Length() == 4) Year((int)a[i].Int()); else if (!Day()) Day((int)a[i].Int()); } else if (IsAlpha(*c)) { int WkDay = IsWeekDay(c); if (WkDay >= 0) continue; int Mnth = IsMonth(c); if (Mnth >= 0) Month(Mnth + 1); } else if (*c == '-' || *c == '+') { c++; if (strlen(c) == 4) { // Timezone.. int64 Tz = a[i].Int(); int Hrs = (int) (Tz / 100); int Min = (int) (Tz % 100); SetTimeZone(Hrs * 60 + Min, false); } } } return IsValid(); } int LDateTime::Sizeof() { return sizeof(int) * 7; } bool LDateTime::Serialize(LFile &f, bool Write) { int32 i; if (Write) { #define wf(fld) i = fld; f << i; wf(_Day); wf(_Month); wf(_Year); wf(_Thousands); wf(_Seconds); wf(_Minutes); wf(_Hours); } else { #define rf(fld) f >> i; fld = i; rf(_Day); rf(_Month); rf(_Year); rf(_Thousands); rf(_Seconds); rf(_Minutes); rf(_Hours); } return true; } /* bool LDateTime::Serialize(ObjProperties *Props, char *Name, bool Write) { #ifndef LGI_STATIC if (Props && Name) { struct _Date { uint8_t Day; uint8_t Month; int16_t Year; uint8_t Hour; uint8_t Minute; uint16_t ThouSec; }; LAssert(sizeof(_Date) == 8); if (Write) { _Date d; d.Day = _Day; d.Month = _Month; d.Year = _Year; d.Hour = _Hours; d.Minute = _Minutes; d.ThouSec = (_Seconds * 1000) + _Thousands; return Props->Set(Name, &d, sizeof(d)); } else // Read { void *Ptr; int Len; if (Props->Get(Name, Ptr, Len) && sizeof(_Date) == Len) { _Date *d = (_Date*) Ptr; _Day = d->Day; _Month = d->Month; _Year = d->Year; _Hours = d->Hour; _Minutes = d->Minute; _Seconds = d->ThouSec / 1000; _Thousands = d->ThouSec % 1000; return true; } } } #endif return false; } */ int LDateTime::Compare(const LDateTime *Date) const { // this - *Date auto ThisTs = IsValid() ? Ts() : 0; auto DateTs = Date->IsValid() ? Date->Ts() : 0; // If these ever fire, the cast to int64_t will overflow LAssert((ThisTs & 0x800000000000000) == 0); LAssert((DateTs & 0x800000000000000) == 0); int64_t Diff = (int64_t)ThisTs - DateTs; if (Diff < 0) return -1; return Diff > 0 ? 1 : 0; } #define DATETIME_OP(op) \ bool LDateTime::operator op(const LDateTime &dt) const \ { \ auto a = Ts(); \ auto b = dt.Ts(); \ return a op b; \ } DATETIME_OP(<) DATETIME_OP(<=) DATETIME_OP(>) DATETIME_OP(>=) bool LDateTime::operator ==(const LDateTime &dt) const { return _Year == dt._Year && _Month == dt._Month && _Day == dt._Day && _Hours == dt._Hours && _Minutes == dt._Minutes && _Seconds == dt._Seconds && _Thousands == dt._Thousands; } bool LDateTime::operator !=(const LDateTime &dt) const { return _Year != dt._Year || _Month != dt._Month || _Day != dt._Day || _Hours != dt._Hours || _Minutes != dt._Minutes || _Seconds != dt._Seconds || _Thousands != dt._Thousands; } int LDateTime::DiffMonths(const LDateTime &dt) { int a = (Year() * 12) + Month(); int b = (dt.Year() * 12) + dt.Month(); return b - a; } LDateTime LDateTime::operator -(const LDateTime &dt) { - uint64 a, b; + LTimeStamp a, b; Get(a); dt.Get(b); /// Resolution of a second when using 64 bit timestamps int64 Sec = Second64Bit; int64 Min = 60 * Sec; int64 Hr = 60 * Min; int64 Day = 24 * Hr; - int64 d = (int64)a - (int64)b; + int64 d = (int64)a.Get() - (int64)b.Get(); LDateTime r; r._Day = (int16) (d / Day); d -= r._Day * Day; r._Hours = (int16) (d / Hr); d -= r._Hours * Hr; r._Minutes = (int16) (d / Min); d -= r._Minutes * Min; r._Seconds = (int16) (d / Sec); #ifdef WIN32 d -= r._Seconds * Sec; r._Thousands = (int16) (d / 10000); #else r._Thousands = 0; #endif return r; } LDateTime LDateTime::operator +(const LDateTime &dt) { LDateTime s = *this; s.AddMonths(dt.Month()); s.AddDays(dt.Day()); s.AddHours(dt.Hours()); s.AddMinutes(dt.Minutes()); // s.AddSeconds(dt.Seconds()); return s; } LDateTime &LDateTime::operator =(const LDateTime &t) { _Day = t._Day; _Year = t._Year; _Thousands = t._Thousands; _Month = t._Month; _Seconds = t._Seconds; _Minutes = t._Minutes; _Hours = t._Hours; _Tz = t._Tz; _Format = t._Format; return *this; } LDateTime &LDateTime::operator =(struct tm *time) { if (time) { _Seconds = time->tm_sec; _Minutes = time->tm_min; _Hours = time->tm_hour; _Day = time->tm_mday; _Month = time->tm_mon + 1; _Year = time->tm_year + 1900; } else Empty(); return *this; } bool LDateTime::IsSameDay(LDateTime &d) const { return Day() == d.Day() && Month() == d.Month() && Year() == d.Year(); } bool LDateTime::IsSameMonth(LDateTime &d) const { return Day() == d.Day() && Month() == d.Month(); } bool LDateTime::IsSameYear(LDateTime &d) const { return Year() == d.Year(); } LDateTime LDateTime::StartOfDay() const { LDateTime dt = *this; dt.Hours(0); dt.Minutes(0); dt.Seconds(0); dt.Thousands(0); return dt; } LDateTime LDateTime::EndOfDay() const { LDateTime dt = *this; dt.Hours(23); dt.Minutes(59); dt.Seconds(59); dt.Thousands(999); return dt; } bool LDateTime::IsLeapYear(int Year) const { if (Year < 0) Year = _Year; if (Year % 4 != 0) return false; if (Year % 400 == 0) return true; if (Year % 100 == 0) return false; return true; } int LDateTime::DaysInMonth() const { if (_Month == 2 && IsLeapYear()) { return 29; } short DaysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; return _Month >= 1 && _Month <= 12 ? DaysInMonth[_Month-1] : 0; } void LDateTime::AddSeconds(int64 Seconds) { - uint64 i; + LTimeStamp i; if (Get(i)) { - i += Seconds * Second64Bit; + i.Get() += Seconds * Second64Bit; Set(i); } } void LDateTime::AddMinutes(int64 Minutes) { - uint64 i; + LTimeStamp i; if (Get(i)) { int64 delta = Minutes * 60 * Second64Bit; - uint64 n = i + delta; - // printf("AddMin " LPrintfInt64 " + " LPrintfInt64 " = " LPrintfInt64 "\n", i, delta, n); - Set(n); - - #if 0 - uint64 i2; - Get(i2); - int64 diff = (int64)i2-(int64)i; - #endif + i.Get() += delta; + Set(i); } } void LDateTime::AddHours(int64 Hours) { - uint64 i; + LTimeStamp i; if (Get(i)) { - i += Hours * LDateTime::HourLength * Second64Bit; + i.Get() += Hours * HourLength * Second64Bit; Set(i); } } bool LDateTime::AddDays(int64 Days) { if (!Days) return true; - uint64 Ts; + LTimeStamp Ts; if (!Get(Ts)) return false; - Ts += Days * LDateTime::DayLength * Second64Bit; + Ts.Get() += Days * LDateTime::DayLength * Second64Bit; bool b = Set(Ts); return b; } void LDateTime::AddMonths(int64 Months) { int64 m = _Month + Months; do { if (m < 1) { _Year--; m += 12; } else if (m > 12) { _Year++; m -= 12; } else { break; } } while (1); _Month = (int16) m; if (_Day > DaysInMonth()) _Day = DaysInMonth(); } LString LDateTime::DescribePeriod(double seconds) { int mins = (int) (seconds / 60); seconds -= mins * 60; int hrs = mins / 60; mins -= hrs * 60; int days = hrs / 24; hrs -= days * 24; LString s; if (days > 0) s.Printf("%id %ih %im %is", days, hrs, mins, (int)seconds); else if (hrs > 0) s.Printf("%ih %im %is", hrs, mins, (int)seconds); else if (mins > 0) s.Printf("%im %is", mins, (int)seconds); else s.Printf("%is", (int)seconds); return s; } LString LDateTime::DescribePeriod(LDateTime to) { auto ThisTs = Ts(); auto ToTs = to.Ts(); auto diff = ThisTs < ToTs ? ToTs - ThisTs : ThisTs - ToTs; auto seconds = (double)diff / LDateTime::Second64Bit; return DescribePeriod(seconds); } int LDateTime::MonthFromName(const char *Name) { if (Name) { for (int m=0; m<12; m++) { if (strnicmp(Name, MonthsShort[m], strlen(MonthsShort[m])) == 0) { return m + 1; break; } } } return -1; } bool LDateTime::Decode(const char *In) { // Test data: // // Tue, 6 Dec 2005 1:25:32 -0800 Empty(); if (!In) { LAssert(0); return false; } bool Status = false; // Tokenize delimited by whitespace LString::Array T = LString(In).SplitDelimit(", \t\r\n"); if (T.Length() < 2) { if (T[0].IsNumeric()) { // Some sort of timestamp? uint64_t Ts = Atoi(T[0].Get()); if (Ts > 0) { return SetUnix(Ts); } else return false; } else { // What now? return false; } } else { bool GotDate = false; for (unsigned i=0; i 31) { // Y/M/D? Year((int)Date[0].Int()); Day((int)Date[2].Int()); } else if (Date[2].Int() > 31) { // D/M/Y? Day((int)Date[0].Int()); Year((int)Date[2].Int()); } else { // Ambiguous year... bool YrFirst = true; if (Date[0].Length() == 1) YrFirst = false; // else we really can't tell.. just go with year first if (YrFirst) { Year((int)Date[0].Int()); Day((int)Date[2].Int()); } else { Day((int)Date[0].Int()); Year((int)Date[2].Int()); } LDateTime Now; Now.SetNow(); if (Year() + 2000 <= Now.Year()) Year(2000 + Year()); else Year(1900 + Year()); } if (Date[1].IsNumeric()) Month((int)Date[1].Int()); else { int m = MonthFromName(Date[1]); if (m > 0) Month(m); } GotDate = true; Status = true; } else if (s.Find(":") >= 0) { // whole time // Do some validation bool Valid = true; for (char *c = s; *c && Valid; c++) { if (!(IsDigit(*c) || *c == ':')) Valid = false; } if (Valid) { LString::Array Time = s.Split(":"); if (Time.Length() == 2 || Time.Length() == 3) { // Hour int i = (int) Time[0].Int(); if (i >= 0) Hours(i); if (s.Lower().Find("p") >= 0) { if (Hours() < 12) Hours(Hours() + 12); } // Minute i = (int) Time[1].Int(); if (i >= 0) Minutes(i); if (Time.Length() == 3) { // Second i = (int) Time[2].Int(); if (i >= 0) Seconds(i); } Status = true; } } } else if (IsAlpha(s(0))) { // text int m = MonthFromName(s); if (m > 0) Month(m); } else if (strchr("+-", *s)) { // timezone DoTimeZone: LDateTime Now; double OurTmz = (double)Now.SystemTimeZone() / 60; if (s && strchr("-+", *s) && strlen(s) == 5) { #if 1 int i = atoi(s); int hr = i / 100; int min = i % 100; SetTimeZone(hr * 60 + min, false); #else // adjust for timezone char Buf[32]; memcpy(Buf, s, 3); Buf[3] = 0; double TheirTmz = atof(Buf); memcpy(Buf+1, s + 3, 2); TheirTmz += (atof(Buf) / 60); if (Tz) { *Tz = TheirTmz; } double AdjustHours = OurTmz - TheirTmz; AddMinutes((int) (AdjustHours * 60)); #endif } else { // assume GMT AddMinutes((int) (OurTmz * 60)); } } else if (s.IsNumeric()) { int Count = 0; for (char *c = s; *c; c++) { if (!IsDigit(*c)) break; Count++; } if (Count <= 2) { if (Day()) { // We already have a day... so this might be // a 2 digit year... LDateTime Now; Now.SetNow(); int Yr = atoi(s); if (2000 + Yr <= Now.Year()) Year(2000 + Yr); else Year(1900 + Yr); } else { // A day number (hopefully)? Day((int)s.Int()); } } else if (Count == 4) { if (!Year()) { // A year! Year((int)s.Int()); Status = true; } else { goto DoTimeZone; } // My one and only Y2K fix // d.Year((Yr < 100) ? (Yr > 50) ? 1900+Yr : 2000+Yr : Yr); } } } } return Status; } bool LDateTime::GetVariant(const char *Name, LVariant &Dst, char *Array) { LDomProperty p = LStringToDomProp(Name); switch (p) { case DateYear: // Type: Int32 Dst = Year(); break; case DateMonth: // Type: Int32 Dst = Month(); break; case DateDay: // Type: Int32 Dst = Day(); break; case DateHour: // Type: Int32 Dst = Hours(); break; case DateMinute: // Type: Int32 Dst = Minutes(); break; case DateSecond: // Type: Int32 Dst = Seconds(); break; case DateDate: // Type: String { char s[32]; GetDate(s, sizeof(s)); Dst = s; break; } case DateTime: // Type: String { char s[32]; GetTime(s, sizeof(s)); Dst = s; break; } case TypeString: // Type: String case DateDateAndTime: // Type: String { char s[32]; Get(s, sizeof(s)); Dst = s; break; } case TypeInt: // Type: Int64 case DateTimestamp: // Type: Int64 { - uint64 i = 0; - Get(i); - Dst = (int64)i; + LTimeStamp i; + if (Get(i)) + Dst = (int64)i.Get(); break; } case DateSecond64Bit: { Dst = Second64Bit; break; } default: { return false; } } return true; } bool LDateTime::SetVariant(const char *Name, LVariant &Value, char *Array) { LDomProperty p = LStringToDomProp(Name); switch (p) { case DateYear: Year(Value.CastInt32()); break; case DateMonth: Month(Value.CastInt32()); break; case DateDay: Day(Value.CastInt32()); break; case DateHour: Hours(Value.CastInt32()); break; case DateMinute: Minutes(Value.CastInt32()); break; case DateSecond: Seconds(Value.CastInt32()); break; case DateDate: SetDate(Value.Str()); break; case DateTime: SetTime(Value.Str()); break; case DateDateAndTime: Set(Value.Str()); break; case DateTimestamp: Set((uint64)Value.CastInt64()); break; default: return false; } return true; } bool LDateTime::CallMethod(const char *Name, LVariant *ReturnValue, LArray &Args) { switch (LStringToDomProp(Name)) { case DateSetNow: SetNow(); if (ReturnValue) *ReturnValue = true; break; case DateSetStr: if (Args.Length() < 1) return false; bool Status; if (Args[0]->Type == GV_INT64) Status = Set((uint64) Args[0]->Value.Int64); else Status = Set(Args[0]->Str()); if (ReturnValue) *ReturnValue = Status; break; case DateGetStr: { char s[256] = ""; Get(s, sizeof(s)); if (ReturnValue) *ReturnValue = s; break; } default: return false; } return true; } #ifdef _DEBUG #define DATE_ASSERT(i) \ if (!(i)) \ { \ LAssert(!"LDateTime unit test failed."); \ return false; \ } bool LDateTime_Test() { // Check 64bit get/set LDateTime t("1/1/2017 0:0:0"); - uint64 i; + LTimeStamp i; DATE_ASSERT(t.Get(i)); LgiTrace("Get='%s'\n", t.Get().Get()); uint64 i2 = i + (24ULL * 60 * 60 * LDateTime::Second64Bit); LDateTime t2; t2.SetFormat(GDTF_DAY_MONTH_YEAR); t2.Set(i2); LString s = t2.Get(); LgiTrace("Set='%s'\n", s.Get()); DATE_ASSERT(!stricmp(s, "2/1/2017 12:00:00a") || !stricmp(s, "2/01/2017 12:00:00a")); t.SetNow(); LgiTrace("Now.Local=%s Tz=%.2f\n", t.Get().Get(), t.GetTimeZoneHours()); t2 = t; t2.ToUtc(); LgiTrace("Now.Utc=%s Tz=%.2f\n", t2.Get().Get(), t2.GetTimeZoneHours()); t2.ToLocal(); LgiTrace("Now.Local=%s Tz=%.2f\n", t2.Get().Get(), t2.GetTimeZoneHours()); DATE_ASSERT(t == t2); return true; } #endif + + +//////////////////////////////////////////////////////////////////////////////////////////////////// +LTimeStamp <imeStamp::operator =(const time_t unixTime) +{ + #if defined(WINDOWS) + ts = (unixTime + SEC_TO_UNIX_EPOCH) * WINDOWS_TICK; + #else + ts = (unixTime + LDateTime::Offset1800) * LDateTime::Second64Bit; + #endif + + return *this; +} + +LTimeStamp::operator time_t() const +{ + #if defined(WINDOWS) + #else + #endif + + return 0; +}