This class automates connection to Microsoft's delightful* Visual Source Safe source control system. * -> :D
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 | #!/usr/bin/env python
"""
Visual Source Safe Integration Module.
Notes:
Early stages of this module - GLV, check in and check out only. Some resource management stuff, but not
iron-tight, so would recommend calling dispose() explicitly on all objects!
Known issues:
* Doesn't seem to like UNC paths on my two boxes (+ lapdog); would recommend mapping a network drive and pointing to that.
"""
__author__ = "Tim Watson"
__version__ = "$Revision: 1.1 $"
__license__ = "Python"
import win32com.client as com;
import pywintypes as win32;
#internal constants
VSS_ROOT_FOLDER = "$/";
#vs constants
VSSFILE_CHECKEDOUT =0x1 # from enum VSSFileStatus
VSSFILE_CHECKEDOUT_ME =0x2 # from enum VSSFileStatus
VSSFILE_NOTCHECKEDOUT =0x0 # from enum VSSFileStatus
VSSITEM_FILE =0x1 # from enum VSSItemType
VSSITEM_PROJECT =0x0 # from enum VSSItemType
VSSFLAG_RECURSNO =0x1000 # from enum VSSFlags
VSSFLAG_RECURSYES =0x2000 # from enum VSSFlags
class ObjectDisposedException(Exception): pass
class ComException(Exception, win32.com_error): pass
class Disposable(object):
def __init__(self):
super(Disposable, self).__init__();
def __del__(self):
self.dispose();
def dispose(self): pass
def guard(self):
if self.disposed == True: raise ObjectDisposedException("Object disposed!");
class Searchable:
"""Only works in conjunction with classes that implement the Node interface."""
def __init__(self):
super(Searchable, self).__init__();
def __contains__(self, item):
return self.contains(item);
def contains(self, item):
foundItem = None;
try:
foundItem = self.__getitem__(item);
if foundItem:
return True;
else:
return False;
finally:
foundItem.dispose();
def __getitem__(self, key):
item = None;
if hasattr(self.vssItem, "Items"):
try:
#do a quick search of the current branch
item = self.vssItem.Items.Item(key);
except win32.com_error:
#try a recursive search for the item
searches = [];
try:
for child in self.descendants():
if child.name == key:
return Node(child);
searches.append(child);
finally:
for n in searches: n.dispose();
return Node(item);
class Node(Disposable, Searchable):
def __init__(self, comobject):
self.vssItem = comobject;
def __call__(self):
return self.__repr__();
#are these really needed on subclasses?
def __del__(self):
self.dispose();
def __iter__(self):
return self.iterkeys();
def __nonzero__(self):
return self.vssItem is not None;
def __repr__(self):
return "<Node.%s>" % str(self.name);
def iterkeys(self):
"""Gets the immediate descendants of the current node."""
if self.vssItem is not None:
if not hasattr(self.vssItem, "Items"): return;
for item in self.vssItem.Items:
yield Node(item);
def descendants(self):
"""Iterates over all the descendants of the current node,
in order, to the leaf level."""
if self.vssItem is not None:
for item in self.__gen(self.vssItem):
yield Node(item);
def __gen(self, root):
yield root;
if hasattr(root, "Items"):
for node in root.Items:
for item in self.__gen(node):
yield item;
#todo: sort out this hideous duplication!
def checkout(self, local=None, flags=None):
if self.vssItem is not None:
if local is not None:
if flags is not None:
self.vssItem.Checkout(Local=local, iFlags=flags);
else:
self.vssItem.Checkout(Local=local);
else:
if flags is not None:
self.vssItem.Checkout(iFlags=flags);
else:
self.vssItem.Checkout();
#todo: sort out this hideous duplication!
def checkin(self, local=None, flags=None):
if self.vssItem is not None:
if local is not None:
if flags is not None:
self.vssItem.Checkin(Local=local, iFlags=flags);
else:
self.vssItem.Checkin(Local=local);
else:
if flags is not None:
self.vssItem.Checkin(iFlags=flags);
else:
self.vssItem.Checkin();
def get(self, local, flags=None):
if local is None: return;
if self.vssItem is not None:
if flags is not None:
self.vssItem.Get(Local=local, iFlags=flags);
else:
self.vssItem.Get(Local=local);
def __process(self, function, local=None, flags=None):
if function is None: return;
if local is not None:
if flags is not None:
function(Local=local, iFlags=flags);
else:
function(iFlags=flags);
else:
if flags is not None:
function(iFlags=flags);
else:
function();
def __checkedOut(self):
if not self.__isFile(): return False;
return self.vssItem.IsCheckedOut == VSSFILE_CHECKEDOUT or self.vssItem.IsCheckedOut == VSSFILE_CHECKEDOUT_ME;
def __checkedOutToMe(self):
if self.__checkedOut():
return self.vssItem.IsCheckedOut == VSSFILE_CHECKEDOUT_ME;
def dispose(self):
self.vssItem = None;
def __getName(self):
if self.vssItem is None:
return "None";
else:
return self.vssItem.Name;
def hasItems(self):
return not hasattr(self.vssItem, "Items");
def __isProject(self):
return self.vssItem is not None and self.vssItem.Type == VSSITEM_PROJECT;
def __isFile(self):
return not self.__isProject();
def __deleted(self):
if self.vssItem is not None:
return self.vssItem.Deleted;
def __parent(self):
if self.vssItem is not None:
try:
return Node(self.vssItem.Parent);
except: pass
return Node(None);
def __path(self):
if self.vssItem is not None: return self.vssItem.Spec;
return "None";
#todo: figure out how to access self from a lambda!
vsspath = property (fget=__path);
deleted = property (
fget=__deleted,
doc="""Indicates whether or not the current instance has been deleted."""
)
name = property (
fget=__getName,
doc="""Returns the name of the underlying node object"""
);
parent = property (
fget=__parent,
doc="""Gets the parent for the current item."""
);
isCheckedOut = property (
fget=__checkedOut,
doc="""
Indicates whether or not a file is checkout out.
Returns false is the current item is a folder.
"""
);
isMine = property (
fget=__checkedOutToMe,
doc="""
Indicates whether or not a file is checked out to the currently logged in user.
"""
);
isLeaf = property (
fget=hasItems,
doc="""Indicates whether or not this instance is at the leaf level."""
);
isFile = property (
fget=__isFile,
doc="""Indicates whether or not this instance represents a file."""
);
isProject = property (
fget=__isProject,
doc="""Indicates whether or not this instance represents a project."""
);
class Database(Node):
""" A vss database instance. """
def __init__(self):
"Database init";
self.__vss = com.Dispatch("SourceSafe");
super(Database, self).__init__(None);
self.disposed = lambda: self.__vss is None;
self.isOpen = lambda: self.vssItem is not None;
def __del__(self):
self.dispose();
def __iter__(self):
return self.iterkeys();
def iterkeys(self):
super(Database, self).guard();
for item in self.__getRoot().Items:
yield Node(item);
def open(self, iniFile, username, password=None):
"""
Opens a new instance of vss database.
"""
super(Database, self).guard();
self.__vss.Open(iniFile, username);
self.vssItem = self.__vss.VSSItem(VSS_ROOT_FOLDER, False);
@staticmethod
def openProject(iniFile, projectName, username, password=None):
"""
Opens and returns the project at the specified location in vss.
If you pass an absolute path (e.g. a path including the vss separator character '/',
will attempt to load the project from the exact path specified (adding the root namespace
declaration '$/' if missing). If this attempt fails (or if you simply pass the name of the project,
such as 'my project'), will create a new database at the root position and recursively search for the
specified project.
Returns a new project node, or 'None', if the search fails. The recursive search is quite
slow, so pass the full path if you can.
"""
prName = projectName;
if projectName.find("/") != -1:
if not projectName.startswith(VSS_ROOT_FOLDER):
prName = VSS_ROOT_FOLDER + projectName;
try:
vss = com.Dispatch("SourceSafe");
vss.Open(iniFile, username);
try:
p = vss.VSSItem(prName);
return Node(p);
except: pass
finally:
vss = None;
#out of luck - try stripping out the project name and doing a 'normal' search
if projectName.endswith("/"):
prName = projectName.rstrip("/");
lastSepChar = prName.rfind("/");
if lastSepChar == -1:
raise Exception("I Can't be bothered parsing this mess :- put in a proper project name you blithering idiot!");
prName = prName[lastSepChar, len(prName) - lastSepChar];
if prName.startswith("/"):
prName = prName.lstrip("/");
#todo: all of the above with a nice, clean regex instead.
##regular search method:
toplevels = [];
db = None;
try:
try:
db = Database();
db.open(iniFile, username, password);
#minimum depth search first:
for topLevelProj in db:
if topLevelProj.name == projectName:
return Node(topLevelProj);
toplevels.append(topLevelProj);
#now try increasing the search depth exponentially:
for proj in toplevels:
for child in proj.descendants():
if child.name == projectName: return Node(child);
else: child.dispose();
#todo: do this (above) with a list comprehension instead!
finally:
db.dispose();
finally:
for p in toplevels:
p.dispose();
def dispose(self):
"""Disposes of the current instance."""
self.__vss = None;
super(Database, self).dispose();
def hasItems(self):
return True;
def __getRoot(self):
super(Database, self).guard();
return self.vssItem;
class SourceSafe:
"""Simple vss wrapper, to make lifetime management easier."""
def __init__(self, iniFile, username):
self.iniFile = iniFile;
self.username = username;
self.checked_out = False;
self.project = None;
self.releaseFolder = None;
self.verbose = False;
def checkout(self, projectRoot, checkoutFolder, iflags=None):
if self.verbose: print "Attempting to check out files...";
if self.project is None:
self.project = Database.openProject(str(self.iniFile), projectRoot, str(self.username));
assert self.project is not None;
if projectRoot == checkoutFolder:
self.releaseFolder = self.project;
else:
self.releaseFolder = self.project[checkoutFolder];
assert self.releaseFolder, "Failed to locate vss folder %s!" % checkoutFolder;
try:
self.releaseFolder.checkout(flags=iflags);
self.checked_out = True;
if self.verbose: print "Check out successful.";
except:
print "Check out failed...";
def checkin(self, iflags=None):
if self.checked_out:
if self.verbose: print "Attempting to check in files...";
try:
self.releaseFolder.checkin(flags=iflags);
if self.verbose: print "Check in successful.";
except:
print "Check in failed..";
def get(self, project, local=None, iflags=None):
try:
if self.project is None:
if project is None: return;
self.project = Database.openProject(str(self.iniFile), project, str(self.username));
if self.verbose: print "Attempting [Get Latest Version]...";
self.releaseFolder = self.project;
self.releaseFolder.get(local, iflags);
if self.verbose: print "[Get Latest Version] complete...";
except:
print "[Get Latest Version] failed...";
def dispose(self):
if self.project is not None: self.project.dispose();
self.project = None;
if self.releaseFolder is not None: self.releaseFolder.dispose();
self.releaseFolder = None;
###########################################################################
#!/usr/bin/env python
"""
Unit test module for vss module.
"""
__author__ = "Tim Watson"
__version__ = "$Revision: 1.1 $"
__license__ = "Python"
import unittest;
from vss import *;
DATABASE = "C:\\BUILD_TEST\\srcsafe.ini";
USER = "scripting.client"
PROJECT = "ICBFData";
PROJECT_PARTIAL = "BI_Build/" + PROJECT;
PROJECT_FULL = "$/" + PROJECT_PARTIAL;
class ServerFixture(unittest.TestCase):
def setUp(self):
self.db = Database();
self.db.open(DATABASE, USER);
def tearDown(self):
self.db.dispose();
def testConnect(self):
"Tests the ability to create a new server instance."
self.db.dispose();
self.assert_(self.db.disposed, "not disposed!");
def testGetRootProjectAndIter(self):
for project in self.db:
self.failIf(project.name is None, "Project name cannot be None");
def testRecursiveIterProjects(self):
MAX_COUNT = 100;
count = 0;
for proj in self.db:
for node in proj.descendants():
if count > MAX_COUNT: return;
count += 1;
self.failIf(node.name is None);
print node.name;
if node.isLeaf:
self.failIf(hasattr(node, "Items"), "Node %s was supposed to be a leaf!" % node.name);
def testContainmentCheckSucceeds(self):
BAD_ITEM_NAME = "Doesn't Exist!";
GOOD_ITEM_NAME = "IABuild";
self.assertFalse(BAD_ITEM_NAME in self.db);
self.failIf(not GOOD_ITEM_NAME in self.db);
def testGetBadProjectYieldsNone(self):
BAD_PROJECT = "Not Real";
self.assertEqual(Database.openProject(DATABASE, BAD_PROJECT, USER), None);
def testPartialProjectOk(self):
self.doProjectLoad(PROJECT_PARTIAL);
def testFullProjectOk(self):
self.doProjectLoad(PROJECT_FULL);
def doProjectLoad(self, project):
proj = Database.openProject(DATABASE, project, USER);
self.assertFalse(proj is None, "Failed to locate project");
self.assertEqual(proj.name, PROJECT);
if __name__ == "__main__":
unittest.main();
|
I'm quite new to Python (which is the most fab language around if you ask me!) so please do feel free to comment and point things out. There's a couple of places I couldn't seem to avoid duplicating code (where I was trying to get lambdas to work and they wouldn't), so I'm hoping someone can point out how to do it.
You need the win32com module (part of ActivePython) installed, and you need to create a wrapper for the Microsoft Visual Source Safe 5.1 Type Library using pythonwin.
You'll also need source safe, obviously!
There's a very limited test suite, and as you'll see, the operations are limited to get latest version, check in and out and locate projects and/or item nodes within the VSS database.
I use this module as part of a Python scripted build process.