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

448 行
10 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. 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. copy uncommitted files to repository, update the log and add the change
  283. """
  284. # get a new version number
  285. if not version: version = self.new_version()
  286. self.update_number(version)
  287. # find added files to commit
  288. self.add_from_uncommitted(version)
  289. # clear uncommitted
  290. self.sql("delete from uncommitted")
  291. def add_from_uncommitted(self, version):
  292. """
  293. move files from uncommitted table to files table
  294. """
  295. added = self.sql("select * from uncommitted", as_dict=1)
  296. for f in added:
  297. if f['action']=='add':
  298. # move them to "files"
  299. self.sql("""
  300. insert or replace into files
  301. (fname, ftype, timestamp, content, version)
  302. values (?,?,?,?,?)
  303. """, (f['fname'], f['ftype'], f['timestamp'], f['content'], version))
  304. elif f['action']=='remove':
  305. self.sql("""delete from files where fname=?""", (f['fname'],))
  306. else:
  307. raise Exception, 'bad action %s' % action
  308. # update log
  309. self.add_log(f['fname'], f['ftype'], version)
  310. def timestamp(self, fname):
  311. """
  312. get timestamp
  313. """
  314. fname = self.vc.relpath(fname)
  315. return int(self.sql("select timestamp from files where fname=?", (fname,))[0][0] or 0)
  316. def diff(self, number):
  317. """
  318. get changed files since number
  319. """
  320. if number is None: number = 0
  321. ret = self.sql("""
  322. select log.fname from log, versions
  323. where versions.number > ?
  324. and versions.version = log.version""", (number,))
  325. return list(set([f[0] for f in ret]))
  326. def uncommitted(self):
  327. """
  328. return list of uncommitted files
  329. """
  330. return [f[0] for f in self.sql("select fname from uncommitted")]
  331. def add_log(self, fname, ftype, version):
  332. """
  333. add file to log
  334. """
  335. self.sql("insert into log(fname, ftype, version) values (?,?,?)", (fname, ftype, version))
  336. def add_bundle(self, fname):
  337. """
  338. add to bundles
  339. """
  340. self.sql("insert or replace into bundles(fname) values (?)", (fname,))
  341. if __name__=='__main__':
  342. import os, sys
  343. sys.path.append('py')
  344. sys.path.append('lib/py')
  345. unittest.main()