You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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