您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

458 行
11 KiB

  1. """
  2. Version Control:
  3. Schema:
  4. properties (key, value)
  5. uncommitted (fname, ftype, content, timestamp)
  6. files (fname, ftype, content, timestamp, version)
  7. log (fname, ftype, version)
  8. bundle_files (fname primary key)
  9. Discussion:
  10. There are 2 databases, versions.db and versions-local.db
  11. All changes are commited to versions-local.db, when the patches are complete, the developer
  12. must pull the latest .wnf db and merge
  13. versions-local.db is never commited in the global repository
  14. """
  15. import unittest
  16. import os
  17. test_file = {'fname':'test.js', 'ftype':'js', 'content':'test_code', 'timestamp':'1100'}
  18. root_path = os.curdir
  19. def edit_file():
  20. # edit a file
  21. p = os.path.join(root_path, 'lib/js/core.js')
  22. # read
  23. f1 = open(p, 'r')
  24. content = f1.read()
  25. f1.close()
  26. # write
  27. f = open(p, 'w')
  28. f.write(content)
  29. f.close()
  30. return os.path.relpath(p, root_path)
  31. verbose = False
  32. class TestVC(unittest.TestCase):
  33. def setUp(self):
  34. self.vc = VersionControl(root_path, True)
  35. self.vc.repo.setup()
  36. def test_add(self):
  37. self.vc.add(**test_file)
  38. ret = self.vc.repo.sql('select * from uncommitted', as_dict=1)[0]
  39. self.assertTrue(ret['content']==test_file['content'])
  40. def test_commit(self):
  41. last_number = self.vc.repo.get_value('last_version_number')
  42. self.vc.add(**test_file)
  43. self.vc.commit()
  44. # test version
  45. number = self.vc.repo.get_value('last_version_number')
  46. version = self.vc.repo.sql("select version from versions where number=?", (number,))[0][0]
  47. self.assertTrue(number != last_number)
  48. # test file
  49. self.assertTrue(self.vc.repo.get_file('test.js')['content'] == test_file['content'])
  50. # test uncommitted
  51. self.assertFalse(self.vc.repo.sql("select * from uncommitted"))
  52. # test log
  53. self.assertTrue(self.vc.repo.sql("select * from log where version=?", (version,)))
  54. def test_diff(self):
  55. self.vc.add(**test_file)
  56. self.vc.commit()
  57. self.assertTrue(self.vc.repo.diff(None), ['test.js'])
  58. def test_walk(self):
  59. # add
  60. self.vc.add_all()
  61. # check if added
  62. ret = self.vc.repo.sql("select * from uncommitted", as_dict=1)
  63. self.assertTrue(len(ret)>0)
  64. self.vc.commit()
  65. p = edit_file()
  66. # add
  67. self.vc.add_all()
  68. # check if added
  69. ret = self.vc.repo.sql("select * from uncommitted", as_dict=1)
  70. self.assertTrue(p in [r['fname'] for r in ret])
  71. def test_merge(self):
  72. self.vc.add_all()
  73. self.vc.commit()
  74. # write the file
  75. self.vc.repo.conn.commit()
  76. # make master (copy)
  77. self.vc.setup_master()
  78. p = edit_file()
  79. self.vc.add_all()
  80. self.vc.commit()
  81. self.vc.merge(self.vc.repo, self.vc.master)
  82. log = self.vc.master.diff(int(self.vc.master.get_value('last_version_number'))-1)
  83. self.assertTrue(p in log)
  84. def tearDown(self):
  85. self.vc.close()
  86. if os.path.exists(self.vc.local_db_name()):
  87. os.remove(self.vc.local_db_name())
  88. if os.path.exists(self.vc.master_db_name()):
  89. os.remove(self.vc.master_db_name())
  90. class VersionControl:
  91. def __init__(self, root=None, testing=False):
  92. self.testing = testing
  93. self.set_root(root)
  94. self.repo = Repository(self, self.local_db_name())
  95. self.ignore_folders = ['.git', '.', '..']
  96. self.ignore_files = ['py', 'pyc', 'DS_Store', 'txt', 'db-journal', 'db']
  97. def local_db_name(self):
  98. """return local db name"""
  99. return os.path.join(self.root_path, 'versions-local.db' + (self.testing and '.test' or ''))
  100. def master_db_name(self):
  101. """return master db name"""
  102. return os.path.join(self.root_path, 'versions-master.db' + (self.testing and '.test' or ''))
  103. def setup_master(self):
  104. """
  105. setup master db from local (if not present)
  106. """
  107. import os
  108. if not os.path.exists(self.master_db_name()):
  109. os.system('cp %s %s' % (self.local_db_name(), self.master_db_name()))
  110. self.master = Repository(self, self.master_db_name())
  111. def set_root(self, path=None):
  112. """
  113. set / reset root and connect
  114. (the root path is the path of the folder)
  115. """
  116. import os
  117. if not path:
  118. path = os.path.abspath(os.path.curdir)
  119. self.root_path = path
  120. def relpath(self, fname):
  121. """
  122. get relative path from root path
  123. """
  124. import os
  125. return os.path.relpath(fname, self.root_path)
  126. def timestamp(self, path):
  127. """
  128. returns timestamp
  129. """
  130. import os
  131. if os.path.exists(path):
  132. return int(os.stat(path).st_mtime)
  133. else:
  134. return 0
  135. def add_all(self):
  136. """
  137. walk the root folder Add all dirty files to the vcs
  138. """
  139. import os
  140. for wt in os.walk(self.root_path, followlinks = True):
  141. # ignore folders
  142. for folder in self.ignore_folders:
  143. if folder in wt[1]:
  144. wt[1].remove(folder)
  145. for fname in wt[2]:
  146. fpath = os.path.join(wt[0], fname)
  147. if fname.endswith('build.json'):
  148. self.repo.add_bundle(fpath)
  149. continue
  150. if fname.split('.')[-1] in self.ignore_files:
  151. # nothing to do
  152. continue
  153. # file does not exist
  154. if not self.exists(fpath):
  155. if verbose:
  156. print "%s added" % fpath
  157. self.repo.add(fpath)
  158. # file changed
  159. else:
  160. if self.timestamp(fpath) != self.repo.timestamp(fpath):
  161. if verbose:
  162. print "%s changed" % fpath
  163. self.repo.add(fpath)
  164. def version_diff(self, source, target):
  165. """
  166. get missing versions in target
  167. """
  168. # find versions in source not in target
  169. d = []
  170. versions = source.sql("select version from versions")
  171. for v in versions:
  172. if not target.sql("select version from versions where version=?", v):
  173. d.append(v)
  174. return d
  175. def merge(self, source, target):
  176. """
  177. merges with two repositories
  178. """
  179. diff = self.version_diff(source, target)
  180. if not len(diff):
  181. print 'nothing to merge'
  182. return
  183. for d in diff:
  184. for f in source.sql("select * from files where version=?", d, as_dict=1):
  185. print 'merging %s' % f['fname']
  186. target.add(**f)
  187. target.commit(d[0])
  188. """
  189. short hand
  190. """
  191. def commit(self, version=None):
  192. """commit to local"""
  193. self.repo.commit(version)
  194. def add(self, **args):
  195. """add to local"""
  196. self.repo.add(**args)
  197. def remove(self, fname):
  198. """remove from local"""
  199. self.repo.add(fname=fname, action='remove')
  200. def exists(self, fname):
  201. """exists in local"""
  202. return len(self.repo.sql("select fname from files where fname=?", (self.relpath(fname),)))
  203. def get_file(self, fname):
  204. """return file"""
  205. return self.repo.sql("select * from files where fname=?", (self.relpath(fname),), as_dict=1)[0]
  206. def close(self):
  207. self.repo.conn.commit()
  208. self.repo.conn.close()
  209. if hasattr(self, 'master'):
  210. self.master.conn.commit()
  211. self.master.conn.close()
  212. class Repository:
  213. def __init__(self, vc, fname):
  214. self.vc = vc
  215. import sqlite3
  216. self.db_path = os.path.join(self.vc.root_path, fname)
  217. self.conn = sqlite3.connect(self.db_path)
  218. self.cur = self.conn.cursor()
  219. def setup(self):
  220. """
  221. setup the schema
  222. """
  223. print "setting up %s..." % self.db_path
  224. self.cur.executescript("""
  225. create table properties(pkey primary key, value);
  226. create table uncommitted(fname primary key, ftype, content, timestamp, action);
  227. create table files (fname primary key, ftype, content, timestamp, version);
  228. create table log (fname, ftype, version);
  229. create table versions (number integer primary key, version);
  230. create table bundles(fname primary key);
  231. """)
  232. def sql(self, query, values=(), as_dict=None):
  233. """
  234. like webnotes.db.sql
  235. """
  236. self.cur.execute(query, values)
  237. res = self.cur.fetchall()
  238. if as_dict:
  239. out = []
  240. for row in res:
  241. d = {}
  242. for idx, col in enumerate(self.cur.description):
  243. d[col[0]] = row[idx]
  244. out.append(d)
  245. return out
  246. return res
  247. def get_value(self, key):
  248. """
  249. returns value of a property
  250. """
  251. ret = self.sql("select `value` from properties where `pkey`=?", (key,))
  252. return ret and ret[0][0] or None
  253. def set_value(self, key, value):
  254. """
  255. returns value of a property
  256. """
  257. self.sql("insert or replace into properties(pkey, value) values (?, ?)", (key,value))
  258. def add(self, fname, ftype=None, timestamp=None, content=None, version=None, action=None):
  259. """
  260. add to uncommitted
  261. """
  262. import os
  263. if not timestamp:
  264. timestamp = self.vc.timestamp(fname)
  265. # commit relative path
  266. fname = self.vc.relpath(fname)
  267. if not action:
  268. action = 'add'
  269. if not ftype:
  270. ftype = fname.split('.')[-1]
  271. self.sql("insert or replace into uncommitted(fname, ftype, timestamp, content, action) values (?, ?, ?, ?, ?)" \
  272. , (fname, ftype, timestamp, content, action))
  273. def new_version(self):
  274. """
  275. return a random version id
  276. """
  277. import random
  278. # genarate id (global)
  279. return '%016x' % random.getrandbits(64)
  280. def update_number(self, version):
  281. """
  282. update version.number
  283. """
  284. # set number (local)
  285. self.sql("insert into versions (number, version) values (null, ?)", (version,))
  286. number = self.sql("select last_insert_rowid()")[0][0]
  287. self.set_value('last_version_number', number)
  288. def commit(self, version=None):
  289. """
  290. copy uncommitted files to repository, update the log and add the change
  291. """
  292. # get a new version number
  293. if not version: version = self.new_version()
  294. self.update_number(version)
  295. # find added files to commit
  296. self.add_from_uncommitted(version)
  297. # clear uncommitted
  298. self.sql("delete from uncommitted")
  299. def add_from_uncommitted(self, version):
  300. """
  301. move files from uncommitted table to files table
  302. """
  303. added = self.sql("select * from uncommitted", as_dict=1)
  304. for f in added:
  305. if f['action']=='add':
  306. # move them to "files"
  307. self.sql("""
  308. insert or replace into files
  309. (fname, ftype, timestamp, content, version)
  310. values (?,?,?,?,?)
  311. """, (f['fname'], f['ftype'], f['timestamp'], f['content'], version))
  312. elif f['action']=='remove':
  313. self.sql("""delete from files where fname=?""", (f['fname'],))
  314. else:
  315. raise Exception, 'bad action %s' % action
  316. # update log
  317. self.add_log(f['fname'], f['ftype'], version)
  318. def timestamp(self, fname):
  319. """
  320. get timestamp
  321. """
  322. fname = self.vc.relpath(fname)
  323. return int(self.sql("select timestamp from files where fname=?", (fname,))[0][0] or 0)
  324. def diff(self, number):
  325. """
  326. get changed files since number
  327. """
  328. if number is None: number = 0
  329. ret = self.sql("""
  330. select log.fname from log, versions
  331. where versions.number > ?
  332. and versions.version = log.version""", (number,))
  333. return list(set([f[0] for f in ret]))
  334. def uncommitted(self):
  335. """
  336. return list of uncommitted files
  337. """
  338. return [f[0] for f in self.sql("select fname from uncommitted")]
  339. def add_log(self, fname, ftype, version):
  340. """
  341. add file to log
  342. """
  343. self.sql("insert into log(fname, ftype, version) values (?,?,?)", (fname, ftype, version))
  344. def add_bundle(self, fname):
  345. """
  346. add to bundles
  347. """
  348. self.sql("insert or replace into bundles(fname) values (?)", (fname,))
  349. if __name__=='__main__':
  350. import os, sys
  351. sys.path.append('py')
  352. sys.path.append('lib/py')
  353. unittest.main()