VSS 工具 – 大量取消选中
VSS 批量取消签出(undo-checkout)的解决方法。
引言
在 Code Project 上有很多关于自定义 VSS 工具的文章。
很久以前,我创建了自己的 VSS 管理员工具版本。我的工具没有特别出色的地方,只是满足了我日常的需求。
我在其他关于 VSS 工具的文章中没有看到的一个功能是“批量取消签出”功能。一个典型的场景是:当开发人员离开公司后,VSS 管理员发现许多文件被离职员工签出了。
Microsoft Visual SourceSafe Explorer 提供了一个取消签出功能,甚至可以递归地应用于任何选定的项目(子)树。但 SourceSafe Explorer 会对每个签出文件的每个细节提出太多问题。而且,最令人恼火的是,对于每个有多个签出的文件,SourceSafe Explorer 都会弹出一个对话框,列出签出项,并要求你选择要撤销的那个。
对于文中描述的情况,当员工已经离开时,一个好用的功能是能够指向项目(子)树并为特定用户撤销所有签出。
挑战
VSS COM 模型没有提供任何方法来取消除自己之外的任何用户的签出。(尽管 MS VSS Explorer 知道如何做到这一点,但并未公开。)
让我们回顾一下 VSS 类型库。
// typelib filename: ssapi.dll
[
uuid(783CD4E0-9D54-11CF-B8EE-00608CC9A71F),
version(5.2),
helpstring("Microsoft SourceSafe 8.0 Type Library")
]
library SourceSafeTypeLib
...
interface IVSSItemOld : IDispatch {
...
[id(0x0000000d)]
HRESULT UndoCheckout(
[in, optional, defaultvalue("")] BSTR Local,
[in, optional, defaultvalue(0)] long iFlags);
这就是我们拥有的全部。
其中一个选项是作为离职用户登录。但没有该离职用户的密码,我们就无法这样做。不用说,没有公开的方法可以检索密码。
解决方法
VSS 类型库显示,IVSSUser
接口有一个只写密码属性
interface IVSSUser : IDispatch {
...
[id(0x00000003), propput]
HRESULT Password([in] BSTR rhs);
...
所以我们有以下解决方法
- (以管理员身份登录)重置离职用户的密码;例如,将其设置为“exuser”。
- 以离职用户身份登录。
- 为离职用户撤销所有选定的签出。
- 再次以管理员身份登录。
通过这个解决方法,我为我的 VSS 工具实现了对项目节点以及文件列表的批量取消签出功能。
取消签出文件列表
取消签出项目节点
工作原理
在 Code Project 上有很多关于 VSS API 的优秀文章。例如
- VssReporter 2.1 - A Visual SourceSafe reporting tool for build administrators by .dan.g.
- SSBrowser: A Sample Visual SourceSafe Automation by Ferdie
我不想让网站充斥着类似的重复代码。所以,我将只发布与“撤销签出”功能相关的代码。
主对话框中实现了四个主要方法。还有一些辅助函数,用于日志记录和检索集合项,但它们都很简单。
OnUnCheck()
- “黑客”用户 – 以选定的离职用户身份登录 VSS;
- 取消签出列表或项目节点中的文件;
- 再次以管理员身份登录 VSS。
HackUser()
当用户从项列表视图控件或项目树视图控件中选择 **Uncheck** 菜单项时,将调用 OnUnCheck()
方法。
OnUnCheck()
从对话框控件中获取选定的用户,并检测它是从树视图(项目)还是从列表视图(文件)调用的。
然后它执行以下操作:
// Hack the user: change the user's password and log in as the user
bool CMainDlg::HackUser(CString sUser, CString pw);
将离职用户的 ID 和新用户密码作为参数传递给 HackUser()
。
HackUser()
执行以下步骤:
- 使用字符串 ID 查找离职用户对象:
IVSSUserPtr pUser;
- 重置用户密码:
HRESULT hr = pUser->put_Password(bsPW);
并以离职用户身份打开 VSS 数据库:hr = m_pVssDB->raw_Open(bsDB, bsUser, bsPW);
。
UndoCheckouts()
int CMainDlg::UndoCheckouts(CString strVssPath, LPCTSTR theUser);
int CMainDlg::UndoCheckouts(IVSSItemPtr vssi, CString strVssPath, LPCTSTR theUser);
UndoCheckouts()
取消签出列表视图中的文件或树视图中选定的项目节点。
根据传递的字符串参数的项的 VSS 路径,UndoCheckouts()
获取 IVSSItemPtr
。
_bstr_t bsVssPath = strVssPath;
IVSSItemPtr vssi = m_treeVss.m_pVssDB->GetVSSItem(bsVssPath, 0);
return UndoCheckouts(vssi, strVssPath, theUser);
如果 IVSSItemPtr vssi
指向项目
if (vssi->GetType() == VSSITEM_PROJECT)
该函数会循环遍历项目的子项,递归调用自身。
UndoCheckouts(vssi2, strVssPath, theUser);
当 UndoCheckouts()
最终“触及底部”——该项是 VSSITEM_FILE
类型时,它会调用该方法
UndoCheckout(vssi, strVssPath, theUser)
该方法执行项的实际取消签出操作。
UndoCheckout()
// undo checkout for the single file.
bool CMainDlg::UndoCheckout(IVSSItemPtr vssi, LPCTSTR strVssPath,
LPCTSTR theUser);
作为参数传递的文件项 IVSSItemPtr vssi
可能有多个链接——该项可能在 VSS 数据库的不同位置之间共享。
UndoCheckout()
必须遍历该项的所有链接。
IVSSItemPtr link;
IVSSItemsPtr links = vssi->Links;
int nLinks = vssi->Links->Count;
ATLTRACE(_T("Item %s has %d links\n"), (LPCTSTR)vssi->Spec, nLinks);
for(int i = 0; i < nLinks; i++)
{
IVSSItemPtr link = GetOneOfItems(links, i);
...
}
UndoCheckout()
会跳过已删除的链接。然后,对于每个 VSS 项链接,该函数会获取签出集合——一个链接可能存在多个签出。
IVSSCheckoutsPtr lnkCheckouts = link->Checkouts;
long nCheckouts = lnkCheckouts->Count;
IVSSCheckoutPtr lnkCheckout;
for(long idx = 0; idx < nCheckouts; idx++) {...}
UndoCheckout()
最终会找到签出给离职用户的链接,并尝试取消签出(使用默认参数)。
try
{
_bstr_t bsDefault;
bsDefault = "";
long nDefault = VSSFLAG_DELNO|VSSFLAG_KEEPYES;
hr = link->raw_UndoCheckout(bsDefault, nDefault);
}
catch (_com_error & e) {...}
代码
////////////////////////////////////////////
// Headers:
//
#import "C:\Program Files\Microsoft Visual SourceSafe\SSAPI.DLL" no_namespace
// UndoCheckout menu-item selected
LRESULT CMainDlg::OnUnCheck(WORD /*wNotifyCode*/, WORD wID, HWND hwnd, BOOL& bHandled);
// Hack the user: change the user's password and log in as the user
bool CMainDlg::HackUser(CString sUser, CString pw);
// undo checkout recursively for the project
// verify strVssPath is part of the item->Spec!
// (there might be multiple links on the item)
int CMainDlg::UndoCheckouts(IVSSItemPtr vssi, CString strVssPath, LPCTSTR theUser);
// undo checkout for the single file; verify spec.
bool CMainDlg::UndoCheckout(IVSSItemPtr vssi, LPCTSTR strVssPath, LPCTSTR theUser);
////////////////////////////////////////////
// Implementation:
//
// UndoCheckout for specific user for:
// all subitems of the item selected on the tree / all items
// in the report / all selected items in the report
// returns the number of unchecked items
LRESULT CMainDlg::OnUnCheck(WORD /*wNotifyCode*/, WORD wID, HWND hwnd, BOOL& bHandled)
{
int unCount = 0;
// find out was it called from the tree or from the report
bool isTree = false;
if(m_hwndActive == m_wndListView.m_hWnd)
isTree = false;
else if(m_hwndActive == m_treeVss.m_hWnd)
isTree = true;
else
return 0;
// Retrieve the user from dialog control
CString theUser;
// ...
// Retrieve selected project
CString prj = m_treeVss.GetSelectedProject();
// ...
// if called from list view, check if there are selected files
// (one file, if just one selected; all files, if none selected)
if(!isTree)
{
int count = m_wndListView.GetItemCount();
int selCount = m_wndListView.GetSelectedCount();
// ...
// start unchecking
m_bReporting = true;
EnableControls(FALSE);
CWaitCursor cursor;
// ImpersonateUser(sUser); reset PW; open DB as the user
if(theUser.Compare(_T("Admin")))
bOk = HackUser(theUser, "exuser");
while((idx = m_wndListView.GetNextItem(idx, flag)) > -1)
{
dbg = m_wndListView.GetItemText(idx, 2, buf,
sizeof(buf)/sizeof(TCHAR));
if(!_tcslen(buf))
continue;
if(UndoCheckout(buf, theUser))
unCount++;
}
// reopen DB as admin
if(theUser.Compare(_T("Admin")))
OpenDatabase(m_sDbPath);
m_bReporting = false;
EnableControls();
}
else
{
CString message;
message.Format(_T("Undo '%s' checkouts recursively in %s?"), theUser, prj);
if(IDOK == MessageBox(message, _T("Uncheck"), MB_OKCANCEL|MB_ICONQUESTION))
{
// start unchecking
m_bReporting = true;
EnableControls(FALSE);
CWaitCursor cursor;
// ImpersonateUser(sUser); reset PW; open DB as the user
if(theUser.Compare(_T("Admin")))
bool bOk = HackUser(theUser, "exuser");
unCount = UndoCheckouts(prj, theUser);
// reopen DB as admin
if(theUser.Compare(_T("Admin")))
OpenDatabase(m_sDbPath);
}
}
m_bReporting = false;
EnableControls();
// restore selections; refresh controls
// ...
return unCount;
}
// Hack the user: change the user's password and log in as the user
bool CMainDlg::HackUser(CString sUser, CString pw)
{
bool bOk = false;
IVSSUsersPtr pUsers = NULL;
try
{
pUsers = m_treeVss.m_pVssDB->GetUsers();
CComVariant var(sUser);
IVSSUserPtr pUser = pUsers->GetItem(var);
_bstr_t bsPW = pw;
HRESULT hr = pUser->put_Password(bsPW);
if(FAILED(hr))
{
ATLTRACE(_T("HackUser::put_Password() error: %X\n"), hr);
return false;
}
_bstr_t bsDB = m_sDbPath;
_bstr_t bsUser = sUser;
hr = m_treeVss.m_pVssDB->Close();
hr = m_treeVss.m_pVssDB->raw_Open(bsDB, bsUser, bsPW);
if(FAILED(hr))
{
ATLTRACE(_T("HackUser:: raw_Open() error: %X\n"), hr);
return false;
}
bOk = true;
}
catch (_com_error & e)
{
ATLTRACE(_T("HackUser() error: %s\n"), e.ErrorMessage());
}
return bOk;
}
// undo checkout recursively for the project
// parameters are retrieved from the dialog controls:
// IVSSItemPtr vssi - VSS item;
// CString strVssPath - VSS item path;
// LPCTSTR theUser - the user to undo checkouts for;
int CMainDlg::UndoCheckouts(IVSSItemPtr vssi, CString strVssPath, LPCTSTR theUser)
{
int count = 0;
int idx = 0;
_variant_t var;
IVSSItemPtr vssi2;
IVSSItemsPtr vssis; // VSS collection
CWaitCursor cursor;
// if user clicked Cancel button – cancel checkouts
GetDlgItem(IDCANCEL).EnableWindow();
if (!Continue(m_bReporting))
{
GetDlgItem(IDCANCEL).EnableWindow(FALSE);
return idx;
}
if (vssi->GetType() == VSSITEM_PROJECT)
{
vssis = vssi->GetItems(FALSE);
for(idx = 0; idx < vssis->GetCount(); idx++)
{
var = (long)(idx + 1);
vssi2 = vssis->GetItem(var);
UndoCheckouts(vssi2, strVssPath, theUser);
}
}
else
{
// we hit the bottom: it's an item (not the project/folder)
ATLTRACE(_T("Uncheck item %s, version %d\n"),
(LPCTSTR)vssi->Spec, vssi->VersionNumber);
if(UndoCheckout(vssi, strVssPath, theUser))
count++;
}
return count;
}
// undo checkout for the specified VSS item
// IVSSItemPtr vssi - VSS item;
// CString strVssPath - VSS item path;
// LPCTSTR theUser - the user to undo checkouts for;
bool CMainDlg::UndoCheckout(IVSSItemPtr vssi, LPCTSTR strVssPath, LPCTSTR theUser)
{
bool bOk = false;
long nCheckouts = 0;
long nVersion = (long)(-1);
CString csUser;
csUser = theUser;
// find in the links the one with the right spec
// NB: there might be multiple links for the VSS items
//– the item may be shared between the projects
// and for each link there maight be several checkouts by different users
IVSSItemPtr link;
IVSSItemsPtr links = vssi->Links;
int nLinks = vssi->Links->Count;
ATLTRACE(_T("Item %s has %d links\n"), (LPCTSTR)vssi->Spec, nLinks);
for(int i = 0; i < nLinks; i++)
{
IVSSItemPtr link = GetOneOfItems(links, i);
// simple helper to get links[i]
_variant_t varDeleted = link->Deleted;
// skip deleted links
if((BOOL)varDeleted)
continue;
CString csLink;
_bstr_t lnkSpec = link->Spec;
csLink = (LPCTSTR)lnkSpec;
long ChkStatus = link->IsCheckedOut;
switch(ChkStatus)
{
case VSSFILE_NOTCHECKEDOUT:
ATLTRACE(_T"Link %s is NOT checked out\n"), csLink);
break;
case VSSFILE_CHECKEDOUT:
ATLTRACE(_T"Link %s is checked out by OTHER user\n"), csLink);
break;
case VSSFILE_CHECKEDOUT_ME:
ATLTRACE(_T"Link %s is checked out by ME\n"), csLink);
break;
}
// verify there is a checkout for the user
bool bFound = false;
IVSSCheckoutsPtr lnkCheckouts = link->Checkouts;
long nCheckouts = lnkCheckouts->Count;
IVSSCheckoutPtr lnkCheckout;
for(long idx = 0; idx < nCheckouts; idx++)
{
CComVariant var(idx + 1);
lnkCheckout = lnkCheckouts->GetItem(var);
_bstr_t user = lnkCheckout->Username;
// filter by the user (to simplify comparison, make it CString)
if(0 == csUser.CompareNoCase((LPCTSTR)user))
{
bFound = true; // checkout for the user was found
nVersion = lnkCheckout->VersionNumber;
CString sLocal;
sLocal = (LPCTSTR)lnkCheckout->LocalSpec;
ATLTRACE(_T "Found checkout ver: %d to: %s\n"), nVersion, sLocal);
break;
}
}
if(!bFound)
return false;
HRESULT hr = S_OK;
try
{
_bstr_t bsDefault;
bsDefault = "";
long nDefault = VSSFLAG_DELNO|VSSFLAG_KEEPYES;
hr = link->raw_UndoCheckout(bsDefault, nDefault);
}
catch (_com_error & e)
{
ATLTRACE(_T("%s\n"), e.ErrorMessage());
}
if(SUCCEEDED(hr))
{
bOk = true;
break;
}
else
LogCOMError(hr, _T("UndoCheckout()")); // simple helper function
}
return bOk;
}
////////////////////////////////////////////
关注点
有没有其他人为 VSS 实现过“批量取消签出”功能?有人知道如何做得更好吗?
历史
目前还没有历史记录。