Introduction
There are many articles on custom VSS tools on The Code Project.
A while ago, I created my own version of a VSS administrator tool. There is nothing outstanding in my tool, it just suits my everyday needs.
One of the features that I did not see in the other articles on VSS tools is the "mass-uncheck" feature. A typical scenario is: after the developer leaves the company, the VSS administrator finds that many files were left checked out by the ex-employee.
Microsoft Visual SourceSafe Explorer provides an uncheck feature, which may be even applied recursively to any selected projects (sub)tree. But the SourceSafe Explorer asks too many questions on every detail of each checked out file. And, what is most annoying, for every file that has multiple checkouts, VSS Explorer would pop-up the dialog with the list of checkouts, and requests to select the one you want to undo.
For the case described, when the employee has already left, a nice feature would be to point to the projects (sub)tree and undo all for the specific user.
Challenge
The VSS COM model does not publish any method to undo checkout for any user but yourself. (MS VSS Explorer knows how to do it, though, but does not tell.)
Let's review the VSS type-library.
[
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);
That is all we have.
One of the options would be to log in as the ex-User. But we can not do that without the ex-User's password. Needless to say, there are no exposed methods to retrieve the password.
Workaround
The VSS type-library shows that the IVSSUser
interface has the password write-only property:
interface IVSSUser : IDispatch {
...
[id(0x00000003), propput]
HRESULT Password([in] BSTR rhs);
...
So we have the following workarounds available:
- (Logged-in as Admin) Reset the password for the ex-User; set it to “exuser”, for example.
- Log-in as the ex-User.
- Undo all selected checkouts for the ex-User.
- Log in as Admin again.
Using this workaround, I have implemented a mass-uncheck feature with my VSS tool for the projects' node as well as for the list of files.
Uncheck the list of files:
Uncheck the projects' node:
How it works
There are many good articles on the VSS APIs on The Code Project. For example:
I would not like to clutter the site with similar code. So I will just post the code relevant to the "undo checkout" functionality.
There are four main methods implemented in the main dialog. There are a couple of helpers also, for logging and retrieving the collection item, but they are pretty trivial.
OnUnCheck()
When the user selects the Uncheck menu item, either from the items list-view control or from the projects tree-view control, the OnUnCheck()
method is invoked.
OnUnCheck()
gets the selected user from the dialog control and detects if it was called from the tree-view (projects) or from the list-view (files).
Then it does the following:
- "Hacks" the user – logs in to VSS as the selected ex-user;
- Un-checks files in the list or in the project node;
- Logs in to VSS again as Admin.
HackUser()
bool CMainDlg::HackUser(CString sUser, CString pw);
The ex-user ID and the new user password are passed to HackUser()
as parameters.
HackUser()
does the following steps:
- Finds the ex-user object using the string ID:
IVSSUserPtr pUser;
- Re-sets the user password:
HRESULT hr = pUser->put_Password(bsPW);
And opens the VSS database as the ex-user: 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()
un-checks files in the list-view or in the project node selected in the tree-view.
From the VSS path to the item passed as the string parameter, UndoCheckouts()
gets IVSSItemPtr
:
_bstr_t bsVssPath = strVssPath;
IVSSItemPtr vssi = m_treeVss.m_pVssDB->GetVSSItem(bsVssPath, 0);
return UndoCheckouts(vssi, strVssPath, theUser);
If IVSSItemPtr vssi
points to the project:
if (vssi->GetType() == VSSITEM_PROJECT)
the function loops through the sub-items of the project, calling itself recursively:
UndoCheckouts(vssi2, strVssPath, theUser);
When eventually UndoCheckouts()
"hits the bottom" – the item is of VSSITEM_FILE
type, it calls the method:
UndoCheckout(vssi, strVssPath, theUser)
which performs the actual un-checking for the item.
UndoCheckout()
bool CMainDlg::UndoCheckout(IVSSItemPtr vssi, LPCTSTR strVssPath,
LPCTSTR theUser);
The file item IVSSItemPtr vssi
passed as the parameter may possibly have multiple links – the item may be shared between several locations in the VSS database.
UndoCheckout()
has to loop through all links of the item:
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()
skips the deleted links. Then, the function for each VSS item link gets the collection on checkouts - there might be multiple checkouts for one link:
IVSSCheckoutsPtr lnkCheckouts = link->Checkouts;
long nCheckouts = lnkCheckouts->Count;
IVSSCheckoutPtr lnkCheckout;
for(long idx = 0; idx < nCheckouts; idx++) {...}
UndoCheckout()
eventually finds the link checked out to the ex-user and tries to un-check it (using the default parameters):
try
{
_bstr_t bsDefault;
bsDefault = "";
long nDefault = VSSFLAG_DELNO|VSSFLAG_KEEPYES;
hr = link->raw_UndoCheckout(bsDefault, nDefault);
}
catch (_com_error & e) {...}
The code
#import "C:\Program Files\Microsoft Visual SourceSafe\SSAPI.DLL" no_namespace
LRESULT CMainDlg::OnUnCheck(WORD , WORD wID, HWND hwnd, BOOL& bHandled);
bool CMainDlg::HackUser(CString sUser, CString pw);
int CMainDlg::UndoCheckouts(IVSSItemPtr vssi, CString strVssPath, LPCTSTR theUser);
bool CMainDlg::UndoCheckout(IVSSItemPtr vssi, LPCTSTR strVssPath, LPCTSTR theUser);
LRESULT CMainDlg::OnUnCheck(WORD , WORD wID, HWND hwnd, BOOL& bHandled)
{
int unCount = 0;
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;
CString theUser;
CString prj = m_treeVss.GetSelectedProject();
if(!isTree)
{
int count = m_wndListView.GetItemCount();
int selCount = m_wndListView.GetSelectedCount();
m_bReporting = true;
EnableControls(FALSE);
CWaitCursor cursor;
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++;
}
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))
{
m_bReporting = true;
EnableControls(FALSE);
CWaitCursor cursor;
if(theUser.Compare(_T("Admin")))
bool bOk = HackUser(theUser, "exuser");
unCount = UndoCheckouts(prj, theUser);
if(theUser.Compare(_T("Admin")))
OpenDatabase(m_sDbPath);
}
}
m_bReporting = false;
EnableControls();
return unCount;
}
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;
}
int CMainDlg::UndoCheckouts(IVSSItemPtr vssi, CString strVssPath, LPCTSTR theUser)
{
int count = 0;
int idx = 0;
_variant_t var;
IVSSItemPtr vssi2;
IVSSItemsPtr vssis; CWaitCursor cursor;
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
{
ATLTRACE(_T("Uncheck item %s, version %d\n"),
(LPCTSTR)vssi->Spec, vssi->VersionNumber);
if(UndoCheckout(vssi, strVssPath, theUser))
count++;
}
return count;
}
bool CMainDlg::UndoCheckout(IVSSItemPtr vssi, LPCTSTR strVssPath, LPCTSTR theUser)
{
bool bOk = false;
long nCheckouts = 0;
long nVersion = (long)(-1);
CString csUser;
csUser = theUser;
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);
_variant_t varDeleted = link->Deleted;
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;
}
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;
if(0 == csUser.CompareNoCase((LPCTSTR)user))
{
bFound = true; 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()")); }
return bOk;
}
Points of interest
Has anybody else implemented the "mass uncheck" functionality for VSS? Anybody knows how to do it better?
History
No history yet.