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.

version.py 8.5 KiB

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