#!/usr/bin/env python import os import sys path_join = os.path.join path_exists = os.path.exists import empty import cmd_ut import tm_ut import yaml_ut import dbg import arg import base import snap_ut to_str = base.to_str msg_done = lambda done, targ: dbg.out( '{} {}'.format( done, targ ) ) cmd_call = lambda cmd: cmd_ut.call( cmd, b2s=True ) cmd_lst = lambda cmd: sorted( cmd_call( cmd ).strip().split('\n') ) def confirm(msg): dbg.out( msg + ' (y/n): ', '' ) s = sys.stdin.readline().strip() return s == 'y' def fsyn_new(dir_path): fsyn_path = dir_path + '.fsyn' id_fn = lambda id, name: path_join( fsyn_path, id, name ) def set_curr(id): fn = path_join( fsyn_path, 'curr' ) cmd = 'echo {} > {}'.format( id, fn ) cmd_call( cmd ) def get_curr(): fn = path_join( fsyn_path, 'curr' ) cmd = 'cat ' + fn return cmd_call( cmd ).strip() def get_snap(id): if id == 'now': return snap_ut.new_dir( dir_path ) fn = id_fn( id, 'snap' ) return snap_ut.new_fn( fn ) def get_cmp(id_old, id_new): snap_old = get_snap( id_old ) snap_new = get_snap( id_new ) return snap_ut.cmp_new( snap_old, snap_new ) def exists_id(id): f = lambda name: path_exists( id_fn( id, name ) ) return all( map( f, ( 'link', 'snap' ) ) ) def get_link(id): fn = id_fn( id, 'link' ) if path_exists( fn ): d = yaml_ut.load_fn( fn ) return empty.new( d ) return empty.new( from_='', to=[] ) # ! def put_link(id, link): fn = id_fn( id, 'link' ) d = vars( link ) yaml_ut.save( d, fn ) def create(): if path_exists( fsyn_path ): dbg.err_exit( 'exists {}'.format( fsyn_path ) ) id = 'org' cmd = 'mkdir -p ' + path_join( fsyn_path, id ) cmd_call( cmd ) snap = get_snap( 'now' ) fn = id_fn( id, 'snap' ) snap.write( fn ) fn = id_fn( id, 'update.tgz' ) cmd = '( cd {} ; ls -A ) | xargs tar czf {} -C {}'.format( dir_path, fn, dir_path ) cmd_call( cmd ) put_link( 'org', empty.new( from_='', to=[] ) ) set_curr( id ) msg_done( 'create', fsyn_path ) def get_curr_cmp(): id_old = get_curr() cmp = get_cmp( id_old, 'now' ) id_new = tm_ut.sec_to_str( cmp.snap_new.sec, tm_ut.sample ) return empty.new( id_old=id_old, id_new=id_new, cmp=cmp, same=cmp.is_same() ) def is_ancestor_lst(ancestor_id, child_id): lst = [] id = child_id while id: lst.insert( 0, id ) if id == ancestor_id: return lst link = get_link( id ) id = link.from_ return [] def is_child_lst(child_id, ancestor_id): return is_ancestor_lst(ancestor_id, child_id) def commit(): curr_cmp = get_curr_cmp() if curr_cmp.same: msg = 'not changed from curr {} ?'.format( curr_cmp.id_old ) dbg.err_exit( msg ) id = curr_cmp.id_new id_old = curr_cmp.id_old cmp = curr_cmp.cmp cmd = 'mkdir ' + path_join( fsyn_path, id ) cmd_call( cmd ) fn = id_fn( id, 'snap' ) cmp.snap_new.write( fn ) add_chg = cmp.add + cmp.chg if add_chg: fn = id_fn( id, 'update.tgz' ) cmd = 'tar czf {} -C {} {}'.format( fn, dir_path, to_str( add_chg, delim=' ' ) ) cmd_call( cmd ) put_link( id, empty.new( from_=id_old, to=[] ) ) link = get_link( id_old ) link.to.append( id ) put_link( id_old, link ) set_curr( id ) msg_done( 'commit', id ) def curr(): msg = 'curr ' + get_curr() dbg.out( msg ) def log(): lst = is_ancestor_lst( 'org', get_curr() ) msg = to_str( reversed( lst ) ) dbg.out( msg ) def ids_from(id='org'): link = get_link( id ) return sum( map( ids_from, link.to ), [] ) + [ id ] def ids(): lst = ids_from( 'org' ) msg = to_str( lst ) dbg.out( msg ) def ids_in_dir(): cmd = 'ls ' + fsyn_path lst = cmd_lst( cmd ) f = lambda s: s == 'org' or tm_ut.is_date_time_str( s, tm_ut.sample ) return list( filter( f, lst ) ) def tails(): id_lst = ids_from() msg = to_str( filter( lambda id: not get_link( id ).to, id_lst ) ) dbg.out( msg ) def ckout_org(): cmd = '( cd {} ; ls -A | xargs rm -rf )'.format( dir_path ) cmd_call( cmd ) targ_id = 'org' fn = id_fn( targ_id, 'update.tgz' ) if path_exists( fn ): cmd = 'tar xzf {} -C {}'.format( fn, dir_path ) cmd_call( cmd ) set_curr( targ_id ) def ckout_to(id): (targ_id, id) = ( id, get_curr() ) fn = id_fn( targ_id, 'update.tgz' ) if path_exists( fn ): cmd = 'tar xzf {} -C {}'.format( fn, dir_path ) cmd_call( cmd ) cmp = get_cmp( id, targ_id ) if cmp.rm: cmd = 'rm -f ' + to_str( cmp.rm, delim=' ' ) cmd_call( cmd ) set_curr( targ_id ) def ckout(id): (targ_id, id) = ( id, get_curr() ) if not path_exists( path_join( fsyn_path, targ_id ) ): msg = 'not found ' + targ_id dbg.err_exit( msg ) curr_cmp = get_curr_cmp() if curr_cmp.same: # not dirty if targ_id == id: return if targ_id == 'org': ckout_org() return link = get_link( id ) if targ_id in link.to: ckout_to( targ_id ) return lst = is_child_lst( targ_id, id ) if lst: lst.pop( 0 ) # == id while lst: id = lst.pop( 0 ) ckout( id ) return # targ_id is not child else: # dirty msg = 'dirty from curr {}. checkout {} ?'.format( curr_cmp.id_old, id ) if not confirm( msg ): return ckout_org() ckout( targ_id ) def checkout(id): ckout( id ) msg_done( 'checkout', id ) def check(): curr() curr_cmp = get_curr_cmp() dbg.out( 'clean' if curr_cmp.same else 'dirty' ) if not curr_cmp.same: cmp = curr_cmp.cmp for k in ( 'add', 'chg', 'rm' ): lst = getattr( cmp, k ) if lst: msg = k + ':\n' + to_str( lst, pre=' ' ) dbg.out( msg ) def fix_link(id): link = get_link( id ) chg = False def fix_from(): id_from = link.from_ if not id_from: return if not exists_id( id_from ): msg = '{} not exists in {}.from_'.format( id_from, id ) dbg.out( msg ) link.from_ = '' chg = True msg_done( 'remove', id_from ) return link_f = get_link( link.from_ ) if id not in link_f.to: msg = '{} not in {}.to'.format( id, link.from_ ) dbg.out( msg ) link_f.to.append( id ) put_link( link.from_, link_f ) msg_done( 'add', id ) def fix_to(id_to): if not exists_id( id_to ): msg = '{} not exits in {}.to'.format( id_to, id ) dbg.out( msg ) link.to.remove( id_to ) chg = True msg_done( 'remove', id_to ) return link_t = get_link( id_to ) if link_t.from_ != id: msg = '{}.from == {}, not {}'.format( id_to, link_t.from_, id ) dbg.out( msg ) link_t.from_ = id # ! put_link( id_to, link_t ) msg_done( 'fix to', id ) for id_to in link.to: fix_to( id_to ) fix_from() if chg: put_link( id, link ) def fixlink(): id_lst = ids_in_dir() for id in id_lst: fix_link( id ) return empty.new( locals() ) def get_name_args(): cmd_new = lambda name, args=[], comment='': empty.new( name=name, args=args, comment=comment ) cmds = [ cmd_new( 'create' ), cmd_new( 'commit' ), cmd_new( 'log' ), cmd_new( 'curr', comment='show current id' ), cmd_new( 'ids', comment='show all id' ), cmd_new( 'tails', comment='show tail ids' ), cmd_new( 'checkout', [ 'id' ] ), cmd_new( 'check', comment='show dirty' ), cmd_new( 'fixlink', comment='fix link info' ), ] dic = dict( map( lambda cmd: ( cmd.name, cmd ), cmds ) ) def help(): def get(cmd): lst = [ cmd.name ] + cmd.args return to_str( lst, delim=' ' ) max_n = max( map( lambda cmd: len( get( cmd ) ), cmds ) ) def get_(cmd): s = get( cmd ) if cmd.comment: s += ' ' * ( max_n - len(s) ) s += ' # ' + cmd.comment return s msg = 'cmd arg ...\n' msg += to_str( map( get_, cmds ), ' ' ) dbg.help_exit( msg ) a = arg.new() name = a.pop() if name not in dic: help() cmd = dic.get( name ) n = len( cmd.args ) av = a.get_av() if len( av ) < n: help() return cmd_new( cmd.name, av[ :n ] ) if __name__ == "__main__": cmd = get_name_args() dir_path = os.getcwd() fsyn = fsyn_new( dir_path ) func = getattr( fsyn, cmd.name ) func( *cmd.args ) # EOF