/***************************************************************************** * * filelog.cpp * * View a filelog. * *****************************************************************************/ #include "sdview.h" /***************************************************************************** * * LogEntry * * A single item in a filelog treelist. * *****************************************************************************/ class LogEntry : public TreeItem { public: LogEntry(LPCTSTR pszRev, LPCTSTR pszChange, LPCTSTR pszOp, LPCTSTR pszDate, LPCTSTR pszDev); LogEntry() { } int GetRev() const { return _iRev; } void SetChildPath(LPCTSTR pszChildPath); const StringCache& GetChildPath() const { return _scChildPath; } void SetIntegrateType(LPCTSTR pszType); void SetIsDonor() { _fDonor = TRUE; } void SetComment(LPCTSTR pszComment) { _scComment = pszComment; } void SetDev(LPCTSTR pszDev) { _scDev = pszDev; } void SetFullDescription(LPCTSTR pszFullDescription) { _scFullDescription = pszFullDescription; } void SetChurn(int cAdded, int cDeleted) { _cAdded = cAdded; _cDeleted = cDeleted; } BOOL IsChurnSet() const { return _cAdded >= 0; } LRESULT GetDispInfo(NMTREELIST *pdi, int iColumn); LRESULT GetInfoTip(NMTREELIST *pdi); LPCTSTR GetChange() const { return _scChange; } LPCTSTR GetComment() const { return _scComment; } private: void GetRevDispInfo(NMTREELIST *ptl); void GetChurnDispInfo(NMTREELIST *ptl); void GetImage(NMTREELIST *ptl); private: int _iRev; // File revision number int _cDeleted; // Number of lines deleted int _cAdded; // Number of lines added int _iOp; // Checkin operation BOOL _fDonor; // Is integration donor StringCache _scChange; // Change number StringCache _scOp; // Checkin operation (edit, delete, tc.) StringCache _scDate; // Checkin date StringCache _scDev; // Checkin dev StringCache _scComment; // Checkin comment StringCache _scFullDescription; // Full checkin description StringCache _scChildPath; // Depot path of child items }; void LogEntry::SetChildPath(LPCTSTR pszChildPath) { String str(pszChildPath); LPTSTR pszSharp = StrChr(str, TEXT('#')); if (pszSharp) { LPTSTR pszComma = StrChr(pszSharp, TEXT(',')); if (!pszComma) { String strT(pszSharp); str << TEXT(",") << strT; } } _scChildPath = str; SetExpandable(); } LogEntryImageMap c_rgleim[] = { { TEXT("?") , -1 }, // OP_UNKNOWN { TEXT("edit") , 0 }, // OP_EDIT { TEXT("delete") , 1 }, // OP_DELETE { TEXT("add") , 2 }, // OP_ADD { TEXT("integrate") , 3 }, // OP_INTEGRATE { TEXT("merge") , 3 }, // OP_MERGE { TEXT("branch") , 4 }, // OP_BRANCH { TEXT("copy") , 5 }, // OP_COPY { TEXT("ignored") , 6 }, // OP_IGNORED }; int ParseOp(LPCTSTR psz) { int i; for (i = ARRAYSIZE(c_rgleim) - 1; i > 0; i--) { if (StrCmp(c_rgleim[i]._pszOp, psz) == 0) { break; } } return i; } void LogEntry::SetIntegrateType(LPCTSTR pszType) { if (_iOp == OP_INTEGRATE) { _iOp = ParseOp(pszType); } } LogEntry::LogEntry( LPCTSTR pszRev, LPCTSTR pszChange, LPCTSTR pszOp, LPCTSTR pszDate, LPCTSTR pszDev) : _iRev(StrToInt(pszRev)) , _scChange(pszChange) , _iOp(ParseOp(pszOp)) , _scDate(pszDate) , _scDev(pszDev) , _cAdded(-1) { } void LogEntry::GetImage(NMTREELIST *ptl) { ptl->iSubItem = c_rgleim[_iOp]._iImage; ptl->cchTextMax = INDEXTOOVERLAYMASK(_fDonor); } // // Combine the pszParent and the pszRev to form the real pszRev // Since the rev is the more important thing, I will display it // in the form // // 19 Lab06_DEV/foo.cpp // // void LogEntry::GetRevDispInfo(NMTREELIST *ptl) { OutputStringBuffer str(ptl->pszText, ptl->cchTextMax); str << _iRev; LogEntry *ple = SAFECAST(LogEntry*, Parent()); if (ple->GetChildPath()) { str << TEXT(" ") << BranchOf(ple->GetChildPath()) << TEXT("/") << FilenameOf(ple->GetChildPath()); } } void LogEntry::GetChurnDispInfo(NMTREELIST *ptl) { if (_cAdded >= 0) { OutputStringBuffer str(ptl->pszText, ptl->cchTextMax); str << _cDeleted << TEXT('/') << _cAdded; } } LRESULT LogEntry::GetDispInfo(NMTREELIST *ptl, int iColumn) { switch (iColumn) { case -1: GetImage(ptl); break; case 0: GetRevDispInfo(ptl); break; case 1: ptl->pszText = _scChange; break; case 2: ptl->pszText = CCAST(LPTSTR, c_rgleim[_iOp]._pszOp); break; case 3: ptl->pszText = _scDate; break; case 4: ptl->pszText = _scDev; break; case 5: GetChurnDispInfo(ptl); break; case 6: ptl->pszText = _scComment; break; } return 0; } LRESULT LogEntry::GetInfoTip(NMTREELIST *ptl) { ptl->pszText = _scFullDescription; return 0; } /***************************************************************************** * * class CFileLog * *****************************************************************************/ class CFileLog : public TLFrame { friend DWORD CALLBACK CFileLog_ThreadProc(LPVOID lpParameter); protected: LRESULT HandleMessage(UINT uiMsg, WPARAM wParam, LPARAM lParam); private: enum { FL_INITIALIZE = WM_APP }; typedef TLFrame super; LRESULT ON_WM_CREATE(UINT uiMsg, WPARAM wParam, LPARAM lParam); LRESULT ON_WM_COMMAND(UINT uiMsg, WPARAM wParam, LPARAM lParam); LRESULT ON_WM_INITMENU(UINT uiMsg, WPARAM wParam, LPARAM lParam); LRESULT ON_WM_NOTIFY(UINT uiMsg, WPARAM wParam, LPARAM lParam); LRESULT ON_FL_INITIALIZE(UINT uiMsg, WPARAM wParam, LPARAM lParam); private: /* Helpers */ CFileLog() : TLFrame(new LogEntry) { SetAcceleratorTable(MAKEINTRESOURCE(IDA_FILELOG)); } LogEntry *LEGetCurSel() { return SAFECAST(LogEntry*, TLGetCurSel()); } // -ds support was added in version 1.50 BOOL IsChurnEnabled() { return GlobalSettings.IsChurnEnabled() && GlobalSettings.IsVersion(1, 50); } BOOL _ChooseColumns(); BOOL _ParseQuery(); LRESULT _FillChildren(LogEntry *pleRoot, LPCTSTR pszRootPath); LRESULT _OnItemActivate(LogEntry *ple); LRESULT _OnGetContextMenu(LogEntry *ple); BOOL _IsViewFileLogEnabled(LogEntry *ple); LRESULT _ViewFileLog(LogEntry *ple); LRESULT _ViewChangelist(LogEntry *ple); void _AdjustMenu(HMENU hmenu, LogEntry *ple, BOOL fContextMenu); int _GetChangeNumber(LogEntry *ple); int _GetBugNumber(LogEntry *ple); struct FLCOLUMN { const LVFCOLUMN* _rgcol; const int * _rgColMap; int _ccol; }; private: const FLCOLUMN* _pflc; BOOL _fIsRestrictedRoot; // Used during initialization int _iHighlightRev; LogEntry * _pleHighlight; StringCache _scSwitches; StringCache _scPath; }; BOOL CFileLog::_ChooseColumns() { static const LVFCOLUMN c_rgcolChurn[] = { { 30 ,IDS_COL_REV ,LVCFMT_LEFT }, { 7 ,IDS_COL_CHANGE ,LVCFMT_RIGHT }, { 7 ,IDS_COL_OP ,LVCFMT_LEFT }, { 15 ,IDS_COL_DATE ,LVCFMT_LEFT }, { 10 ,IDS_COL_DEV ,LVCFMT_LEFT }, { 7 ,IDS_COL_CHURN ,LVCFMT_LEFT }, { 30 ,IDS_COL_COMMENT ,LVCFMT_LEFT }, { 0 ,0 ,0 }, }; static const int c_rgiChurn[] = { 0, 1, 2, 3, 4, 5, 6 }; static const FLCOLUMN c_flcChurn = { c_rgcolChurn, c_rgiChurn, ARRAYSIZE(c_rgiChurn), }; static const LVFCOLUMN c_rgcolNoChurn[] = { { 30 ,IDS_COL_REV ,LVCFMT_LEFT }, { 7 ,IDS_COL_CHANGE ,LVCFMT_RIGHT }, { 7 ,IDS_COL_OP ,LVCFMT_LEFT }, { 15 ,IDS_COL_DATE ,LVCFMT_LEFT }, { 10 ,IDS_COL_DEV ,LVCFMT_LEFT }, { 30 ,IDS_COL_COMMENT ,LVCFMT_LEFT }, { 0 ,0 ,0 }, }; static const int c_rgiNoChurn[] = { 0, 1, 2, 3, 4, 6 }; static const FLCOLUMN c_flcNoChurn = { c_rgcolNoChurn, c_rgiNoChurn, ARRAYSIZE(c_rgiNoChurn), }; if (IsChurnEnabled()) { _pflc = &c_flcChurn; } else { _pflc = &c_flcNoChurn; } return AddColumns(_pflc->_rgcol); } LRESULT CFileLog::ON_WM_CREATE(UINT uiMsg, WPARAM wParam, LPARAM lParam) { LRESULT lres; if (_ParseQuery()) { lres = super::HandleMessage(uiMsg, wParam, lParam); if (lres == 0 && _tree.GetRoot() && SetWindowMenu(MAKEINTRESOURCE(IDM_FILELOG)) && CreateChild(LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER, LVS_EX_LABELTIP | LVS_EX_HEADERDRAGDROP | LVS_EX_INFOTIP | LVS_EX_FULLROWSELECT) && _ChooseColumns()) { PostMessage(_hwnd, FL_INITIALIZE, 0, 0); } else { lres = -1; } } else { Help(_hwnd, TEXT("#filel")); lres = -1; } return lres; } LRESULT CFileLog::ON_FL_INITIALIZE(UINT uiMsg, WPARAM wParam, LPARAM lParam) { _FillChildren(SAFECAST(LogEntry*, _tree.GetRoot()), _scPath); _tree.Expand(_tree.GetRoot()); // Clean out the stuff that's used only for the initial root expand _scSwitches = NULL; _iHighlightRev = 0; if (_pleHighlight) { _tree.SetCurSel(_pleHighlight); } else { ListView_SetCurSel(_hwndChild, 0); } return 0; } int CFileLog::_GetBugNumber(LogEntry *ple) { if (ple) { return ParseBugNumber(ple->GetComment()); } else { return 0; } } int CFileLog::_GetChangeNumber(LogEntry *ple) { if (ple) { return StrToInt(ple->GetChange()); } else { return 0; } } // // View Filelog is enabled if it would show you something different // from what you're looking at right now. // BOOL CFileLog::_IsViewFileLogEnabled(LogEntry *ple) { if (!ple) { return FALSE; // not even an item! } if (_fIsRestrictedRoot) { return TRUE; // View Filelog shows unrestricted } // // Short-circuit the common case where you are already at top-level. // if (ple->Parent() == _tree.GetRoot()) { return FALSE; // You're looking at it already } // // Watch out for the loopback scenario where you chase integrations // out and then back in... // LPCTSTR pszRoot = SAFECAST(LogEntry *, _tree.GetRoot())->GetChildPath(); LPCTSTR pszThis = SAFECAST(LogEntry *, ple->Parent())->GetChildPath(); int cchRoot = lstrlen(pszRoot); if (StrCmpNI(pszRoot, pszThis, cchRoot) == 0 && (pszThis[cchRoot] == TEXT('#') || pszThis[cchRoot] == TEXT('\0'))) { return FALSE; } return TRUE; } LRESULT CFileLog::_ViewFileLog(LogEntry *ple) { if (!_IsViewFileLogEnabled(ple)) { return 0; } Substring ss; if (Parse(TEXT("$P"), SAFECAST(LogEntry *, ple->Parent())->GetChildPath(), &ss)) { String str; str << TEXT("-#") << ple->GetRev() << TEXT(" ") << ss; LaunchThreadTask(CFileLog_ThreadProc, str); } return 0; } LRESULT CFileLog::_ViewChangelist(LogEntry *ple) { if (ple) { LaunchThreadTask(CDescribe_ThreadProc, ple->GetChange()); } return 0; } LRESULT CFileLog::ON_WM_COMMAND(UINT uiMsg, WPARAM wParam, LPARAM lParam) { int iChange, iBug; switch (GET_WM_COMMAND_ID(wParam, lParam)) { case IDM_VIEWDESC: return _ViewChangelist(LEGetCurSel()); case IDM_VIEWFILEDIFF: return _OnItemActivate(LEGetCurSel()); case IDM_VIEWWINDIFF: WindiffChangelist(_GetChangeNumber(LEGetCurSel())); return 0; case IDM_VIEWBUG: iBug = _GetBugNumber(LEGetCurSel()); if (iBug) { OpenBugWindow(_hwnd, iBug); } break; case IDM_VIEWFILELOG: _ViewFileLog(LEGetCurSel()); break; } return super::HandleMessage(uiMsg, wParam, lParam); } void CFileLog::_AdjustMenu(HMENU hmenu, LogEntry *ple, BOOL fContextMenu) { AdjustBugMenu(hmenu, _GetBugNumber(ple), fContextMenu); // Disable IDM_VIEWFILELOG if it would just show you the same window // you're looking at right now. BOOL fEnable = _IsViewFileLogEnabled(ple); EnableDisableOrRemoveMenuItem(hmenu, IDM_VIEWFILELOG, fEnable, fContextMenu); } LRESULT CFileLog::ON_WM_INITMENU(UINT uiMsg, WPARAM wParam, LPARAM lParam) { _AdjustMenu(RECAST(HMENU, wParam), LEGetCurSel(), FALSE); return 0; } LRESULT CFileLog::_OnGetContextMenu(LogEntry *ple) { HMENU hmenu = LoadPopupMenu(MAKEINTRESOURCE(IDM_FILELOG_POPUP)); if (hmenu) { _AdjustMenu(hmenu, ple, TRUE); } return RECAST(LRESULT, hmenu); } LRESULT CFileLog::_OnItemActivate(LogEntry *ple) { if (ple) { LogEntry *pleParent = SAFECAST(LogEntry*, ple->Parent()); // Trim the parent path to remove the sharp. String strPath(pleParent->GetChildPath()); LPTSTR pszSharp = StrChr(strPath, TEXT('#')); if (pszSharp) { strPath.SetLength((int)(pszSharp - strPath)); } // Append the version we care about strPath << TEXT('#') << ple->GetRev(); // And ask windiff to view it WindiffOneChange(strPath); } return 0; } LRESULT CFileLog::ON_WM_NOTIFY(UINT uiMsg, WPARAM wParam, LPARAM lParam) { NMTREELIST *ptl = RECAST(NMTREELIST*, lParam); LogEntry *ple; switch (ptl->hdr.code) { case TLN_GETDISPINFO: ple = SAFECAST(LogEntry*, ptl->pti); if (ptl->iSubItem < 0) { return ple->GetDispInfo(ptl, ptl->iSubItem); } else if (ptl->iSubItem < _pflc->_ccol) { return ple->GetDispInfo(ptl, _pflc->_rgColMap[ptl->iSubItem]); } else { ASSERT(0); // invalid column return 0; } case TLN_FILLCHILDREN: ple = SAFECAST(LogEntry*, ptl->pti); return _FillChildren(ple, ple->GetChildPath()); case TLN_ITEMACTIVATE: ple = SAFECAST(LogEntry*, ptl->pti); return _OnItemActivate(ple); case TLN_GETINFOTIP: ple = SAFECAST(LogEntry*, ptl->pti); return ple->GetInfoTip(ptl); case TLN_DELETEITEM: ple = SAFECAST(LogEntry*, ptl->pti); delete ple; return 0; case TLN_GETCONTEXTMENU: ple = SAFECAST(LogEntry*, ptl->pti); return _OnGetContextMenu(ple); } return super::HandleMessage(uiMsg, wParam, lParam); } LRESULT CFileLog::HandleMessage(UINT uiMsg, WPARAM wParam, LPARAM lParam) { switch (uiMsg) { FW_MSG(WM_CREATE); FW_MSG(WM_COMMAND); FW_MSG(WM_INITMENU); FW_MSG(WM_NOTIFY); FW_MSG(FL_INITIALIZE); } return super::HandleMessage(uiMsg, wParam, lParam); } // // A private helper class that captures the parsing state machine. // class FileLogParseState : public CommentParser { public: FileLogParseState() : _pleCurrent(NULL), _pleInsertAfter(NULL) { } LogEntry *GetCurrentLogEntry() const { return _pleCurrent; } void Flush() { if (_pleCurrent) { // // Trim the trailing CRLF off the last line of the full // description. // _strFullDescription.Chomp(); _pleCurrent->SetFullDescription(_strFullDescription); _pleCurrent = NULL; } _cAdded = _cDeleted = 0; CommentParser::Reset(); _strFullDescription.Reset(); } void AddEntry(Tree &tree, LogEntry *pleRoot, String& str, Substring *rgss) { LPCTSTR pszChildPath = pleRoot->GetChildPath(); _strFullDescription.Append(pszChildPath, StrCSpn(pszChildPath, TEXT("#"))); _strFullDescription << TEXT("\r\n") << str; LogEntry *ple = new LogEntry(rgss[0].Finalize(), // Rev rgss[1].Finalize(), // Change rgss[2].Finalize(), // Op rgss[3].Finalize(), // Date rgss[4].Finalize()); // Dev if (ple) { if (tree.Insert(ple, pleRoot, _pleInsertAfter)) { _pleInsertAfter = _pleCurrent = ple; } else { delete ple; } } } void AddLine(const String& str) { _strFullDescription << str; } void SetDev(LPCTSTR psz) { if (_pleCurrent) { _pleCurrent->SetDev(psz); } } void SetComment(LPCTSTR psz) { if (_pleCurrent) { _pleCurrent->SetComment(psz); } } void SetIntegrateType(LPCTSTR pszType, LPCTSTR pszDepotPath) { if (_pleCurrent) { _pleCurrent->SetIntegrateType(pszType); _pleCurrent->SetChildPath(pszDepotPath); } } void SetIsDonor() { if (_pleCurrent) { _pleCurrent->SetIsDonor(); } } void AddedLines(LPCTSTR psz) { _cAdded += StrToInt(psz); } void DeletedLines(LPCTSTR psz) { _cDeleted += StrToInt(psz); } void SetChurn() { if (_pleCurrent) { _pleCurrent->SetChurn(_cAdded, _cDeleted); } } void ParseDiffResult(String& str) { Substring rgss[3]; if (Parse(TEXT("add $d chunks $d"), str, rgss)) { AddedLines(rgss[1].Finalize()); } else if (Parse(TEXT("deleted $d chunks $d"), str, rgss)) { DeletedLines(rgss[1].Finalize()); } else if (Parse(TEXT("changed $d chunks $d / $d"), str, rgss)) { DeletedLines(rgss[1].Finalize()); AddedLines(rgss[2].Finalize()); SetChurn(); } } BOOL GetFinishDiffCommand(LPCTSTR pszRootPath, String& str) { if (_pleCurrent && !_pleCurrent->IsChurnSet() && _pleCurrent->GetRev() > 1) { str = TEXT("diff2 -ds \""); int cchRootPath = StrCSpn(pszRootPath, TEXT("#")); str.Append(pszRootPath, cchRootPath); str << TEXT("#") << (_pleCurrent->GetRev() - 1) << TEXT("\" \""); str.Append(pszRootPath, cchRootPath); str << TEXT("#") << _pleCurrent->GetRev() << TEXT("\""); return TRUE; } else { return FALSE; } } private: int _cAdded; int _cDeleted; LogEntry *_pleCurrent; LogEntry *_pleInsertAfter; String _strFullDescription; }; BOOL CFileLog::_ParseQuery() { String str; /* * Parse the switches as best we can. * */ str.Reset(); GetOpt opt(TEXT("m#"), _pszQuery); for (;;) { switch (opt.NextSwitch()) { case TEXT('m'): _fIsRestrictedRoot = TRUE; str << TEXT("-m ") << opt.GetValue() << TEXT(" "); break; case TEXT('#'): _iHighlightRev = StrToInt(opt.GetValue()); break; case TEXT('\0'): goto L_switch; // two-level break default: // Caller will display help for us return FALSE; } } L_switch:; _scSwitches = str; str.Reset(); str << TEXT("sdv filelog ") << _scSwitches << opt.GetTokenizer().Unparsed(); SetWindowText(_hwnd, str); /* * There must be exactly one token remaining and it can't be a * wildcard. */ if (opt.Token() && opt.Finished() && !ContainsWildcards(opt.GetValue())) { _scPath = opt.GetValue(); if (StrChr(_scPath, TEXT('#')) || StrChr(_scPath, TEXT('@'))) { _fIsRestrictedRoot = TRUE; } } else { return FALSE; } return TRUE; } LRESULT CFileLog::_FillChildren(LogEntry *pleRoot, LPCTSTR pszRootPath) { LRESULT lres = 0; if (!pszRootPath[0]) { return -1; } WaitCursor wait; String str("filelog -l "); str << _scSwitches; if (IsChurnEnabled()) { str << TEXT("-ds "); } str << ResolveBranchAndQuoteSpaces(pszRootPath); SDChildProcess proc(str); FileLogParseState state; IOBuffer buf(proc.Handle()); while (buf.NextLine(str)) { Substring rgss[5]; // Rev, Change, Op, Date, Dev LPTSTR pszRest; if (Parse(TEXT("... #$d change $d $w on $D by $u"), str, rgss)) { state.Flush(); state.AddEntry(_tree, pleRoot, str, rgss); if (state.GetCurrentLogEntry() && state.GetCurrentLogEntry()->GetRev() == _iHighlightRev) { _pleHighlight = state.GetCurrentLogEntry(); } } else if (Parse(TEXT("$P"), str, rgss)) { if (pleRoot->GetChildPath().IsEmpty()) { str.Chomp(); pleRoot->SetChildPath(rgss[0].Finalize()); } } else { state.AddLine(str); str.Chomp(); if (str[0] == TEXT('\t')) { state.AddComment(str+1); } else if ((pszRest = Parse(TEXT("... ... $w from "), str, rgss)) != NULL) { state.SetIntegrateType(rgss[0].Finalize(), pszRest); } else if (Parse(TEXT("... ... $w into "), str, rgss) || Parse(TEXT("... ... ignored by "), str, rgss)) { state.SetIsDonor(); // SUBTLE! We check for "ignored" after "ignored by". } else if ((pszRest = Parse(TEXT("... ... ignored "), str, rgss)) != NULL) { state.SetIntegrateType("ignored", pszRest); } else { state.ParseDiffResult(str); } } } // "sd filelog -d" doesn't spit out a diff for the last guy, // so kick off a special one-shot "sd diff2" to get that diff. if (IsChurnEnabled() && state.GetFinishDiffCommand(pszRootPath, str)) { SDChildProcess proc2(str); if (proc2.IsRunning()) { buf.Init(proc2.Handle()); while (buf.NextLine(str)) { state.AddLine(str); state.ParseDiffResult(str); } } } state.Flush(); return lres; } DWORD CALLBACK CFileLog_ThreadProc(LPVOID lpParameter) { return FrameWindow::RunThread(new CFileLog, lpParameter); }