Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

VSS tool – mass unchecking

0.00/5 (No votes)
30 Mar 2010 1  
VSS mass undo-checkout workaround.

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.

// 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);

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()
  • // Hack the user: change the user's password and log in as the user
    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()
  • // undo checkout for the single file.
    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

////////////////////////////////////////////
// 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;
}

////////////////////////////////////////////

Points of interest

Has anybody else implemented the "mass uncheck" functionality for VSS? Anybody knows how to do it better?

History

No history yet.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here