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 11 KiB

14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
14 vuotta sitten
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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. for d in self.version_diff(source, target):
  180. for f in source.sql("select * from files where version=?", d, as_dict=1):
  181. target.add(**f)
  182. target.commit(d[0])
  183. """
  184. short hand
  185. """
  186. def commit(self, version=None):
  187. """commit to local"""
  188. self.repo.commit(version)
  189. def add(self, **args):
  190. """add to local"""
  191. self.repo.add(**args)
  192. def remove(self, fname):
  193. """remove from local"""
  194. self.repo.add(fname=fname, action='remove')
  195. def exists(self, fname):
  196. """exists in local"""
  197. return len(self.repo.sql("select fname from files where fname=?", (self.relpath(fname),)))
  198. def get_file(self, fname):
  199. """return file"""
  200. return self.repo.sql("select * from files where fname=?", (self.relpath(fname),), as_dict=1)[0]
  201. def close(self):
  202. self.repo.conn.commit()
  203. self.repo.conn.close()
  204. class Repository:
  205. def __init__(self, vc, fname):
  206. self.vc = vc
  207. import sqlite3
  208. self.db_path = os.path.join(self.vc.root_path, fname)
  209. self.conn = sqlite3.connect(self.db_path)
  210. self.cur = self.conn.cursor()
  211. def setup(self):
  212. """
  213. setup the schema
  214. """
  215. print "setting up %s..." % self.db_path
  216. self.cur.executescript("""
  217. create table properties(pkey primary key, value);
  218. create table uncommitted(fname primary key, ftype, content, timestamp, action);
  219. create table files (fname primary key, ftype, content, timestamp, version);
  220. create table log (fname, ftype, version);
  221. create table versions (number integer primary key, version);
  222. create table bundles(fname primary key);
  223. """)
  224. def sql(self, query, values=(), as_dict=None):
  225. """
  226. like webnotes.db.sql
  227. """
  228. self.cur.execute(query, values)
  229. res = self.cur.fetchall()
  230. if as_dict:
  231. out = []
  232. for row in res:
  233. d = {}
  234. for idx, col in enumerate(self.cur.description):
  235. d[col[0]] = row[idx]
  236. out.append(d)
  237. return out
  238. return res
  239. def get_value(self, key):
  240. """
  241. returns value of a property
  242. """
  243. ret = self.sql("select `value` from properties where `pkey`=?", (key,))
  244. return ret and ret[0][0] or None
  245. def set_value(self, key, value):
  246. """
  247. returns value of a property
  248. """
  249. self.sql("insert or replace into properties(pkey, value) values (?, ?)", (key,value))
  250. def add(self, fname, ftype=None, timestamp=None, content=None, version=None, action=None):
  251. """
  252. add to uncommitted
  253. """
  254. import os
  255. if not timestamp:
  256. timestamp = self.vc.timestamp(fname)
  257. # commit relative path
  258. fname = self.vc.relpath(fname)
  259. if not action:
  260. action = 'add'
  261. if not ftype:
  262. ftype = fname.split('.')[-1]
  263. self.sql("insert or replace into uncommitted(fname, ftype, timestamp, content, action) values (?, ?, ?, ?, ?)" \
  264. , (fname, ftype, timestamp, content, action))
  265. def new_version(self):
  266. """
  267. return a random version id
  268. """
  269. import random
  270. # genarate id (global)
  271. return '%016x' % random.getrandbits(64)
  272. def update_number(self, version):
  273. """
  274. update version.number
  275. """
  276. # set number (local)
  277. self.sql("insert into versions (number, version) values (null, ?)", (version,))
  278. number = self.sql("select last_insert_rowid()")[0][0]
  279. self.set_value('last_version_number', number)
  280. def commit(self, version=None):
  281. """
  282. rebuild bundles if necessary
  283. copy uncommitted files to repository, update the log and add the change
  284. """
  285. # make bundles
  286. from bundle import Bundle
  287. Bundle().bundle(self.vc)
  288. # get a new version number
  289. if not version: version = self.new_version()
  290. self.update_number(version)
  291. # find added files to commit
  292. self.add_from_uncommitted(version)
  293. # clear uncommitted
  294. self.sql("delete from uncommitted")
  295. def add_from_uncommitted(self, version):
  296. """
  297. move files from uncommitted table to files table
  298. """
  299. added = self.sql("select * from uncommitted", as_dict=1)
  300. for f in added:
  301. if f['action']=='add':
  302. # move them to "files"
  303. self.sql("""
  304. insert or replace into files
  305. (fname, ftype, timestamp, content, version)
  306. values (?,?,?,?,?)
  307. """, (f['fname'], f['ftype'], f['timestamp'], f['content'], version))
  308. elif f['action']=='remove':
  309. self.sql("""delete from files where fname=?""", (f['fname'],))
  310. else:
  311. raise Exception, 'bad action %s' % action
  312. # update log
  313. self.add_log(f['fname'], f['ftype'], version)
  314. def timestamp(self, fname):
  315. """
  316. get timestamp
  317. """
  318. fname = self.vc.relpath(fname)
  319. return int(self.sql("select timestamp from files where fname=?", (fname,))[0][0] or 0)
  320. def diff(self, number):
  321. """
  322. get changed files since number
  323. """
  324. if number is None: number = 0
  325. ret = self.sql("""
  326. select log.fname from log, versions
  327. where versions.number > ?
  328. and versions.version = log.version""", (number,))
  329. return list(set([f[0] for f in ret]))
  330. def uncommitted(self):
  331. """
  332. return list of uncommitted files
  333. """
  334. return [f[0] for f in self.sql("select fname from uncommitted")]
  335. def add_log(self, fname, ftype, version):
  336. """
  337. add file to log
  338. """
  339. self.sql("insert into log(fname, ftype, version) values (?,?,?)", (fname, ftype, version))
  340. def add_bundle(self, fname):
  341. """
  342. add to bundles
  343. """
  344. self.sql("insert or replace into bundles(fname) values (?)", (fname,))
  345. if __name__=='__main__':
  346. import os, sys
  347. sys.path.append('py')
  348. sys.path.append('lib/py')
  349. unittest.main()