# Written by Bram Cohen and Ross Cohen
# see LICENSE.txt for license information
from bencode import bdecode, bencode
import binascii
from cdv_glob import Glob
from client_helpers import new_handle, create_handle, unique_name, _set_name
from client_helpers import filename_to_handle
from client_helpers import handle_to_filename, _handle_to_filename, handle_name
from client_helpers import set_edit, unset_edit, clean_merge_point, gen_changeset
from client_helpers import _add_file, conflicts_in_file, CommitError
from client_helpers import name_use_count, rename_race, children_count, parent_loop_check, _rename_safe_check
from client_helpers import mark_modified_files, find_update_files, find_commit_files
from client_net import ClientHandler, ClientError, ServerError
from client_net import network_prep, authenticate
from db import db, ChangeDBs, write_format_version, write_rebuild_version
from DFS import DFS
from diff import unified_diff
from getpass import getpass
from history import HistoryError, roothandle, rootnode
from history import dmerge, damerge, rename_conflict_check, db_get
from history import sync_history, is_ancestor, _is_ancestor
from history import handle_contents_at_point, handles_in_branch
from history import handle_name_at_point, fullpath_at_point
from history import handle_last_modified
from history import short_id, long_id, write_changeset, rebuild_from_points
from history import server_to_tuple, tuple_to_server, repo_head
from history import dump_changeinfo
from history import pretty_print_dag, pretty_print_big_dag
from history import simplify_precursors
import locale
from merge import find_conflict, find_conflict_multiple_safe, find_annotation
import merge
from network import NetworkError
import os
from os import path
from path import mdir, subpath, breakup, preserving_rename
from random import randrange
import re
from sets import Set
import sha
import shlex
import stat
from sys import maxint, stdin, stdout, version_info, platform, stderr
import tempfile
from time import ctime, strftime, localtime
assert version_info >= (2,3), "Python 2.3 or higher is required"
term_encoding = stdin.encoding
text_encoding = locale.getpreferredencoding()
class CheckoutError(Exception):
pass
class Checkout:
def __init__(self, local, init=False, metadata_dir='.cdv', rw=True):
self.local = local
self.conf_path = path.join(local, metadata_dir)
if init == True:
try:
os.mkdir(self.conf_path)
except OSError, err:
raise CheckoutError, 'Could not create metadata directory: %s' % (err[1],)
self.dbenv = None
self.txn = None
txn = None
if rw == True:
flags = db.DB_CREATE|db.DB_INIT_MPOOL|db.DB_INIT_TXN|db.DB_PRIVATE
flags |= db.DB_RECOVER
self.dbenv = db.DBEnv()
self.dbenv.set_cachesize(0, 4 * 1024 * 1024)
self.dbenv.set_lg_bsize(1024 * 1024)
self.dbenv.set_get_returns_none(2)
self.dbenv.open(self.conf_path, flags)
txn = self.txn_begin()
self._openDBs(txn, init, rw)
self.name_cache = {}
self.handle_name_cache = {}
self.db_cache = {}
self.temppath = path.join(self.conf_path, 'temp')
self.cpath = path.join(self.conf_path, 'contents')
mdir(self.temppath)
mdir(self.cpath)
self.nopass = 0
self.user = None
if rw == True:
if init:
write_format_version(self.conf_path)
write_rebuild_version(self.conf_path)
populate_local_repos(self, txn)
self.txn_commit(txn)
return
def _openDBs(self, txn, init, rw):
flags = 0
if not rw:
flags |= db.DB_RDONLY
if init:
flags |= db.DB_CREATE
self.lcrepo = db.DB(dbEnv=self.dbenv)
self.lcrepo.open('changesets.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
self.linforepo = db.DB(dbEnv=self.dbenv)
self.linforepo.open('info.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
self.changesdb = db.DB(dbEnv=self.dbenv)
self.changesdb.open('changenums.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
self.branchmapdb = db.DB(dbEnv=self.dbenv)
self.branchmapdb.open('branchmap.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
self.branchdb = db.DB(dbEnv=self.dbenv)
self.branchdb.open('branch.db', dbtype=db.DB_RECNO, flags=flags, txn=txn)
self.staticdb = db.DB(dbEnv=self.dbenv)
self.staticdb.open('static.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
# open the mini-dags and their indices
self.contents = ChangeDBs(self.dbenv, 'content', flags, txn)
self.names = ChangeDBs(self.dbenv, 'name', flags, txn)
self.allnamesdb = db.DB(dbEnv=self.dbenv)
self.allnamesdb.set_flags(db.DB_DUPSORT)
self.allnamesdb.open('allnames.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
# checkout-specific dbs
self.modtimesdb = db.DB(dbEnv=self.dbenv)
self.modtimesdb.open('modtimes.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
self.editsdb = db.DB(dbEnv=self.dbenv)
self.editsdb.open('edits.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
self.varsdb = db.DB(dbEnv=self.dbenv)
self.varsdb.open('vars.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
try:
self.filenamesdb = db.DB(dbEnv=self.dbenv)
self.filenamesdb.open('filenames.db', dbtype=db.DB_BTREE, flags=flags, txn=txn)
except db.DBNoSuchFileError:
self.filenamesdb = None
return
def close(self):
if self.txn is not None:
self.txn_abort(self.txn)
self.lcrepo.close()
self.linforepo.close()
self.changesdb.close()
self.branchmapdb.close()
self.branchdb.close()
self.staticdb.close()
self.contents.close()
self.names.close()
self.allnamesdb.close()
self.modtimesdb.close()
self.editsdb.close()
self.varsdb.close()
if self.filenamesdb is not None:
self.filenamesdb.close()
if self.dbenv is not None:
self.dbenv.txn_checkpoint()
for lfile in self.dbenv.log_archive():
os.remove(path.join(self.dbenv.db_home, lfile))
self.dbenv.close()
return
def txn_begin(self):
self.txn = self.dbenv.txn_begin()
return self.txn
def txn_abort(self, txn):
assert self.txn == txn
self.txn = None
return txn.abort()
def txn_commit(self, txn):
assert self.txn == txn
self.txn = None
return txn.commit()
def cli_init(args):
local = args[0]
try:
co = Checkout(local, init=True)
co.close()
except CheckoutError, msg:
print 'error - %s' % (msg,)
return 1
return 0
def add(co, files):
ltxn = co.txn_begin()
cpats = ignore_patterns(co)
glob = Glob(co, files)
for file, expanded in glob.fs_walk():
fpath = breakup(file)
if '.cdv' in fpath:
print 'warning - .cdv is a reserved name'
continue
# XXX: check for other frobridden names, i.e. '.' and '..'
# ignore user specified patterns, but only for expansions
if expanded:
ignore = 0
for cpat in cpats:
if cpat.search(file) is not None:
ignore = 1
break
if ignore:
continue
# add all the directories leading up to the file, then the file
rep, parent = '', roothandle
required = 0
for d in fpath:
rep = path.join(rep, d)
if rep == file:
required = not expanded
parent = _add_file(co, rep, parent, required, ltxn)
if not parent:
co.txn_abort(ltxn)
return 1
co.txn_commit(ltxn)
return 0
def delete(co, files):
co.handle_name_cache = {}
ltxn = co.txn_begin()
fnames = []
for handle, expanded in Glob(co, files).db_walk():
file = handle_to_filename(co, handle)
fnames.append((file, handle))
# reverse the ordering so that dirs are deleted after their contents
fnames.sort()
fnames.reverse()
# update the database
for fname, handle in fnames:
linfo = handle_name(co, handle, ltxn)
if co.editsdb.has_key(handle, ltxn):
co.editsdb.delete(handle, txn=ltxn)
if linfo.has_key('add'):
#unset_edit(co, handle, {}, ltxn)
del co.handle_name_cache[handle]
else:
set_edit(co, handle, {'delete': 1}, ltxn)
co.handle_name_cache[handle]['delete'] = 1
# make sure the directories are empty
for fname, handle in fnames:
linfo = db_get(co, co.staticdb, handle, None)
if linfo['type'] == 'dir':
if len(children_count(co, handle, ltxn)):
print 'error - %s is not empty' % (fname,)
co.txn_abort(ltxn)
return 1
# finally, do the deleting
for fname, handle in fnames:
print 'deleting: ' + fname
linfo = db_get(co, co.staticdb, handle, None)
if linfo['type'] == 'dir':
try:
os.rmdir(path.join(co.local, fname))
except OSError, err:
print 'warning - %s: %s' % (err[1], fname)
elif linfo['type'] == 'file':
try:
os.remove(path.join(co.local, fname))
except OSError, err:
print 'warning - %s: %s' % (err[1], fname)
co.modtimesdb.delete(handle, txn=ltxn)
co.filenamesdb.delete(fname, txn=ltxn)
co.txn_commit(ltxn)
return 0
def rename(co, oldname, newname):
co.handle_name_cache = {}
try:
loldfile, foldname = subpath(co.local, oldname)
except ValueError:
print 'error - ' + oldname + ' is outside repository'
return 1
ohandle = filename_to_handle(co, foldname)
if ohandle is None:
print 'error - ' + foldname + ' is not in repository'
return 1
if not path.exists(oldname):
print 'error - ' + oldname + ' does not exist'
return 1
try:
lnewfile, fnewname = subpath(co.local, newname)
except ValueError:
print 'error - ' + newname + ' is outside repository'
return 1
dirmove, foo = 0, path.split(newname)[1]
if foo == '' or foo == '..' or foo == '.':
dirmove = 1
npath, nname = fnewname, ''
else:
npath, nname = path.split(fnewname)
nhandle = filename_to_handle(co, fnewname)
if nhandle is not None:
#if not dirmove or bdecode(co.staticdb.get(nhandle))['type'] != 'dir':
if not dirmove or db_get(co, co.staticdb, nhandle, None)['type'] != 'dir':
print 'error - ' + newname + ' already exists in repository'
return 1
phandle = nhandle
nname = path.split(foldname)[1]
fnewname = path.join(fnewname, nname)
newname = path.join(newname, nname)
else:
phandle = filename_to_handle(co, npath)
if phandle is None:
print 'error - cannot rename into directory not in repository'
return 1
if nname == '.cdv':
print 'error - .cdv is a reserved name'
return 1
if filename_to_handle(co, fnewname) is not None:
print 'error - ' + newname + ' already exists in repository'
return 1
if path.exists(path.join(co.local, fnewname)):
print 'error - ' + newname + ' already exists in filesystem'
return 1
print 'renaming: ' + foldname + ' -> ' + fnewname
ltxn = co.txn_begin()
_set_name(co, ohandle, phandle, nname, ltxn)
os.rename(path.join(co.local, foldname), path.join(co.local, fnewname))
_rebuild_fndb(co, ltxn)
co.txn_commit(ltxn)
return 0
def _rebuild_fndb(co, txn):
# XXX: crude, should do partial trees
co.filenamesdb.truncate(txn=txn)
for handle in co.modtimesdb.keys(txn):
lfile = handle_to_filename(co, handle)
co.filenamesdb.put(lfile, handle, txn=txn)
return
def edit(co, files, by_id=False):
txn = co.txn_begin()
generator = None
if by_id:
generator = [(binascii.unhexlify(handle), 0) for handle in files]
else:
generator = Glob(co, files).db_walk()
for handle, expanded in generator:
file = handle_to_filename(co, handle)
sinfo = db_get(co, co.staticdb, handle, None)
if sinfo['type'] != 'file':
print 'warning - %s is not a file' % (file,)
continue
print 'editting: %s' % (file,)
set_edit(co, handle, {'hash': 1}, txn)
co.txn_commit(txn)
return 0
def set_password(co):
network_prep(co)
ch = ClientHandler(co)
remote = server_to_tuple(co.repo)
try:
s = authenticate(co, ch, remote, None, srp=True)
newpassword = getpass('New password: ')
confpassword = getpass('Confirm new password: ')
if newpassword != confpassword:
print 'Confirmation failed. Password not changed.'
return 1
ch.set_password(s, newpassword)
except NetworkError, msg:
print 'password failed: ' + str(msg)
return 1
except ServerError, msg:
print 'password failed: ' + str(msg)
return 1
print 'Password changed'
return 0
def merge_analyze(co, heads, rhead, named, modified, deleted, txn):
# clear out the merges which the new head resolved for us
old_heads = heads[:]
old_heads.remove(rhead)
for handle, binfo in co.editsdb.items(txn):
info = bdecode(binfo)
if info.has_key('nmerge'):
merge = False
for head in old_heads:
change = handle_last_modified(co, co.names, handle, head, txn)
if change is not None and not is_ancestor(co, change, rhead, txn):
merge = True
if not merge:
unset_edit(co, handle, ['nmerge'], txn)
if info.has_key('cmerge'):
merge = False
for head in old_heads:
change = handle_last_modified(co, co.contents, handle, head, txn)
if change is not None and not is_ancestor(co, change, rhead, txn):
merge = True
if not merge:
unset_edit(co, handle, ['cmerge'], txn)
# keep track of what's been merged because we have to generate explicit
# merge information for them later.
lnamed, lmodified, ladded, ldeleted = \
handles_in_branch(co, [rhead], heads, txn)
sdeleted, sldeleted = Set(deleted), Set(ldeleted)
sdmerges = sldeleted ^ sdeleted
s_all_deleted = sldeleted | sdeleted
lnamed = damerge(lnamed, ladded)
lmodified = damerge(lmodified, ladded)
slnamed, snamed = Set(lnamed), Set(named)
for handle in (slnamed & snamed) - s_all_deleted:
set_edit(co, handle, {'nmerge': 1}, txn)
slmodified, smodified = Set(lmodified), Set(modified)
for handle in (slmodified & smodified) - s_all_deleted:
set_edit(co, handle, {'cmerge': 1}, txn)
for handle in (snamed | smodified) & (sldeleted - sdeleted):
set_edit(co, handle, {'delete': 1}, txn)
for handle in (slnamed | slmodified) & (sdeleted - sldeleted):
set_edit(co, handle, {'delete': 1}, txn)
#for handle in named:
# for head in heads:
# change = handle_last_modified(co, co.names, handle, head, txn)
# if change is not None and not is_ancestor(co, change, rhead, txn):
# set_edit(co, handle, {'nmerge': 1}, txn)
#for handle in modified:
# for head in heads:
# change = handle_last_modified(co, co.contents, handle, head, txn)
# if change is not None and not is_ancestor(co, change, rhead, txn):
# set_edit(co, handle, {'cmerge': 1}, txn)
return
def _update_helper(co, uinfo, named, modified, added, deleted, txn):
uinfo['newfiles'] = newfiles = {}
uinfo['deletes'] = deletes = {}
uinfo['names'] = names = {}
uinfo['infofiles'] = infofiles = []
if not co.merge:
return ([(handle, 0) for handle in modified], {})
local = co.local
rhead = uinfo['head']
heads = bdecode(co.linforepo.get('heads'))
if co.branchmapdb.has_key(rhead) and _is_ancestor(co, rhead, heads, None):
return ([], {})
named = damerge(named, added, deleted)
modified = damerge(modified, added, deleted)
co.handle_name_cache = {}
try:
named_files = find_update_files(co, rhead, named, txn)
except HistoryError, msg:
raise ClientError, 'Got garbage from repository: ' + str(msg)
handles, nconflicts = {}, {}
for handle, linfo, rinfo in named_files:
if linfo is None:
if handle == roothandle:
continue
if not rinfo.has_key('delete'):
newfiles[handle] = 1
handles[handle] = 1
else:
handles[handle] = 0
continue
if rinfo.has_key('delete'):
if not linfo.has_key('delete'):
names[handle] = linfo
deletes[handle] = 1
handles[handle] = 1
else:
deletes[handle] = 0
handles[handle] = 0
continue
elif linfo.has_key('delete'):
handles[handle] = 0
continue
conflict, rename_points = rename_conflict_check(linfo, rinfo)
if conflict == 'remote':
names[handle] = linfo
handles[handle] = 1
elif conflict == 'conflict':
#lfile = _handle_to_filename(co, handle, names, txn)
#print 'file ' + lfile + ' was renamed both locally and remotely'
names[handle] = linfo
uname = unique_name(co, linfo['parent'], linfo['name'] + '.nameconflict', txn)
_set_name(co, handle, linfo['parent'], uname, txn)
infofiles.append((handle, rinfo['parent'], rinfo['name']))
nconflicts[handle] = 1
handles[handle] = 1
# create list of all the handles to be updated, will not include ones
# which don't exist and are in a deleted state after being pulled in
for handle in modified:
if handles.has_key(handle):
continue
linfo = handle_name(co, handle, txn)
if linfo is None:
rinfo = handle_name_at_point(co, handle, rhead, txn)
if rinfo is None or rinfo.has_key('delete'):
handles[handle] = 0
continue
elif linfo.has_key('delete'):
handles[handle] = 0
continue
handles[handle] = 1
orphans = []
for handle in deletes.keys():
for chandle in children_count(co, handle, txn):
if deletes.has_key(chandle):
continue
#file = _handle_to_filename(co, chandle, {}, txn)
#print 'parent of ' + file + ' was deleted, orphaning'
cinfo = handle_name(co, chandle, txn)
names[chandle] = cinfo
infofiles.append((chandle, cinfo['parent'], ''))
nconflicts[chandle] = 1
handles[chandle] = 1
orphans.append((chandle, cinfo['name']))
# generate the new list of heads, the new one might encompass zero, some
# or all of our existing heads
pre_heads, temp_heads = heads[:], []
inserted_head = False
for head in heads:
if is_ancestor(co, head, rhead, txn):
if not inserted_head:
inserted_head = True
temp_heads.append(rhead)
else:
temp_heads.append(head)
if not inserted_head:
temp_heads.append(rhead)
heads = temp_heads
co.linforepo.put('heads', bencode(heads), txn=txn)
merge_analyze(co, heads, rhead, named, modified, deleted, txn)
# clear the name cache
co.handle_name_cache = {}
for handle, name in orphans:
uname = unique_name(co, roothandle, name + '.orphaned', txn)
_set_name(co, handle, roothandle, uname, txn)
for handle, linfo, rinfo in named_files:
if deletes.has_key(handle):
continue
if not newfiles.has_key(handle) and linfo is None:
continue
linfo = handle_name(co, handle, txn)
pinfo = handle_name(co, linfo['parent'], txn)
if pinfo.has_key('delete'):
#file = _handle_to_filename(co, handle, {}, txn)
#print 'parent of ' + file + ' was deleted, orphaning'
if not newfiles.has_key(handle):
names[handle] = linfo
uname = unique_name(co, roothandle, linfo['name'] + '.orphaned', txn)
_set_name(co, handle, roothandle, uname, txn)
infofiles.append((handle, linfo['parent'], ''))
nconflicts[handle] = 1
breaks = 1
while breaks:
breaks = 0
for handle, info in names.items():
if deletes.has_key(handle):
continue
#if bdecode(co.staticdb.get(handle, txn=txn))['type'] != 'dir':
if db_get(co, co.staticdb, handle, txn)['type'] != 'dir':
continue
lhandle = parent_loop_check(co, handle, txn)
if lhandle == handle:
breaks += 1
linfo = handle_name(co, handle, txn)
#file = _handle_to_filename(co, handle, names, txn)
#rfile = handle_to_filename(co, linfo['parent'], txn)
#rfile = path.join(rfile, linfo['name'])
#print 'parent loop for ' + file + ' -> ' + rfile
uname = unique_name(co, linfo['parent'], linfo['name'] + '.parentloop', txn)
_set_name(co, handle, info['parent'], uname, txn)
infofiles.append((handle, info['parent'], ''))
nconflicts[handle] = 1
newnames = names.keys()
newnames.extend(newfiles.keys())
for handle in newnames:
if deletes.has_key(handle):
continue
lhandles = name_use_count(co, handle, txn)
if len(lhandles) == 1:
continue
lhandles.remove(handle)
lhandle = lhandles[0]
#print 'name conflict for ' + _handle_to_filename(co, handle, {}, txn)
linfo = handle_name(co, lhandle, txn)
names[lhandle] = linfo
uname = unique_name(co, linfo['parent'], linfo['name'] + '.nameconflict.local', txn)
_set_name(co, lhandle, linfo['parent'], uname, txn)
nconflicts[lhandle] = 1
handles[lhandle] = 1
linfo = handle_name(co, handle, txn)
uname = unique_name(co, linfo['parent'], linfo['name'] + '.nameconflict.remote', txn)
_set_name(co, handle, linfo['parent'], uname, txn)
nconflicts[handle] = 1
for handle in newnames:
if deletes.has_key(handle):
continue
linfo = handle_name(co, handle, txn)
if newfiles.has_key(linfo['parent']):
continue
lfile = _handle_to_filename(co, linfo['parent'], names, txn)
if not path.exists(path.join(local, lfile)):
raise ClientError, 'directory ' + lfile + ' does not exist, you must revert it before updating'
mode = os.lstat(path.join(local, lfile)).st_mode
if not stat.S_ISDIR(mode):
raise ClientError, lfile + ' is not a directory, you must revert it before updating'
chandle = rename_race(co, handle, names, txn)
lfile = path.join(lfile, linfo['name'])
if not chandle and path.exists(path.join(local, lfile)):
raise ClientError, 'file ' + lfile + ' was added or renamed remotely but already exists, you must move or delete'
for handle in names.keys():
lfile = _handle_to_filename(co, handle, names, txn)
if not path.exists(path.join(local, lfile)):
raise ClientError, 'file ' + lfile + ' does not exist, you must revert it before updating'
#linfo = bdecode(co.staticdb.get(handle, txn=txn))
linfo = db_get(co, co.staticdb, handle, txn)
mode = os.lstat(path.join(local, lfile)).st_mode
if linfo['type'] == 'file' and not stat.S_ISREG(mode):
raise ClientError, 'file ' + lfile + ' is expected to be of type ' + linfo['type']
if linfo['type'] == 'dir' and not stat.S_ISDIR(mode):
raise ClientError, 'file ' + lfile + ' is expected to be of type ' + linfo['type']
return handles.items(), nconflicts
def update(co, remote, merge=True):
try:
network_prep(co)
except NetworkError, msg:
print msg
return 1
editsdb, local = co.editsdb, co.local
txn = co.txn_begin()
mark_modified_files(co, txn)
co.txn_commit(txn)
co.merge = merge
ch = ClientHandler(co)
txn = co.txn_begin()
try:
s = authenticate(co, ch, remote, txn)
co.txn_commit(txn)
except NetworkError, msg:
co.txn_abort(txn)
print 'update failed: ' + str(msg)
return 1
except ServerError, msg:
co.txn_abort(txn)
print 'update failed: ' + str(msg)
return 1
txn = co.txn_begin()
try:
updateinfo = ch.update(s, remote[2], _update_helper, txn)
except NetworkError, msg:
print 'update failed: ' + str(msg)
co.txn_abort(txn)
return 1
except (ClientError, ServerError), msg:
print 'update failed: ' + str(msg)
co.txn_abort(txn)
ch.close(s)
return 1
ch.close(s)
ch = None
# record the repo head so we can do things relative to it later
co.linforepo.put(tuple_to_server(remote), updateinfo['head'], txn=txn)
if co.txn_commit(txn):
print 'Updating local database failed, aborting...'
return 1
if not co.merge:
print 'Repository head is ' + short_id(co, updateinfo['head'])
print 'Update succeeded'
return 0
modified_files = updateinfo['modified']
if not updateinfo.has_key('newfiles'):
print 'Update succeeded'
return 0
newfiles = updateinfo['newfiles']
deletes = updateinfo['deletes']
names = updateinfo['names']
infofiles = updateinfo['infofiles']
# XXX: need to do something about making updates atomic
txn = co.txn_begin()
for handle in newfiles.keys():
lfile = _handle_to_filename(co, handle, names, None)
#staticinfo = bdecode(co.staticdb.get(handle, txn=txn))
staticinfo = db_get(co, co.staticdb, handle, txn)
if staticinfo['type'] == 'dir':
mdir(path.join(local, lfile))
else:
co.filenamesdb.put(lfile, handle, txn=txn)
do_fndb_rebuild = False
if names != {}:
do_fndb_rebuild = True
renames = names.keys()
while renames:
handle = renames.pop()
if deletes.has_key(handle) or newfiles.has_key(handle):
continue
if not _rename_safe_check(co, handle, names, None):
renames.insert(0, handle)
continue
spath = _handle_to_filename(co, handle, names, None)
safename = '.safename.' + str(randrange(0, maxint, 1))
names[handle]['name'] = names[handle]['name'] + safename
info = handle_name(co, handle, None)
names[handle]['parent'] = info['parent']
dpath = _handle_to_filename(co, handle, names, None)
os.rename(path.join(local, spath), path.join(local, dpath))
delete_files, delete_dirs = [], []
for handle, present in deletes.items():
if not present:
continue
#info = bdecode(co.staticdb.get(handle))
info = db_get(co, co.staticdb, handle, None)
lfile = _handle_to_filename(co, handle, names, None)
if info['type'] == 'dir':
delete_dirs.append(lfile)
elif info['type'] == 'file':
delete_files.append((lfile, handle))
if co.editsdb.has_key(handle, txn):
co.editsdb.delete(handle, txn=txn)
del names[handle]
for lfile, handle in delete_files:
os.remove(path.join(local, lfile))
co.modtimesdb.delete(handle, txn=txn)
co.filenamesdb.delete(lfile, txn=txn)
if do_fndb_rebuild:
_rebuild_fndb(co, txn)
delete_dirs.sort()
delete_dirs.reverse()
for lfile in delete_dirs:
try:
os.rmdir(path.join(local, lfile))
except OSError:
print 'warning - %s could not be deleted because it is not empty' % \
(lfile,)
for handle in names.keys():
spath = _handle_to_filename(co, handle, names, None)
del names[handle]
dpath = _handle_to_filename(co, handle, names, None)
os.rename(path.join(local, spath), path.join(local, dpath))
for handle in modified_files:
if deletes.has_key(handle):
continue
temppath = path.join(co.temppath, binascii.hexlify(handle))
filename = path.join(co.local, _handle_to_filename(co, handle, names, txn))
preserving_rename(temppath, filename)
for handle, rparent, rname in infofiles:
assert not deletes.has_key(handle)
info = handle_name(co, handle, txn)
file = unique_name(co, info['parent'], _handle_to_filename(co, handle, names, None) + '.info', None)
rfile = path.join(handle_to_filename(co, rparent), rname)
h = open(path.join(local, file), 'wb')
h.write(rfile + '\n')
h.close()
if co.txn_commit(txn):
print 'Updating local database failed, aborting...'
return 1
print 'Update succeeded'
return 0
def cli_construct(co, spoint):
point = long_id(co, spoint)
# now create everything at the specified point
adds, deletes = handles_in_branch(co, [rootnode], [point], None)[2:4]
deletes_dict = {}.fromkeys(deletes)
newfiles = []
for handle in adds:
if deletes_dict.has_key(handle):
continue
hfile = fullpath_at_point(co, handle, point, None)
htype = bdecode(co.staticdb.get(handle))['type']
newfiles.append((hfile, handle, htype))
newfiles.sort()
for hfile, handle, htype in newfiles:
print 'preparing: %s' % (hfile,)
if htype == 'file':
cinfo = handle_contents_at_point(co, handle, point, None)
temppath = path.join(co.temppath, binascii.hexlify(handle))
fd = open(temppath, 'w')
fd.write('\n'.join(cinfo['lines']))
fd.close()
# put together a list of all the files we are managing
handles = Glob(co, [path.join(co.local, '...')]).db_walk(deletes=0)
sheep = []
for handle, expanded in handles:
hfile = handle_to_filename(co, handle, None)
sheep.append((hfile, handle))
# delete all of them
sheep.sort()
sheep.reverse()
for hfile, handle in sheep:
print 'removing: %s' % (hfile,)
destpath = path.join(co.local, hfile)
htype = bdecode(co.staticdb.get(handle))['type']
try:
if htype == 'dir':
os.rmdir(destpath)
else:
os.unlink(destpath)
except OSError, msg:
print 'warning - %s' % (str(msg),)
txn = co.txn_begin()
# clear out whatever we were editing
co.editsdb.truncate(txn)
# rename everything to the right place
co.modtimesdb.truncate(txn)
co.filenamesdb.truncate(txn)
for hfile, handle, htype in newfiles:
print 'creating: %s' % (hfile,)
destpath = path.join(co.local, hfile)
if htype == 'dir':
try:
os.mkdir(destpath)
except OSError:
if not os.path.isdir(destpath):
raise
continue
elif htype == 'file':
temppath = path.join(co.temppath, binascii.hexlify(handle))
preserving_rename(temppath, destpath)
co.modtimesdb.put(handle, bencode(path.getmtime(destpath)), txn=txn)
co.filenamesdb.put(hfile, handle, txn=txn)
co.linforepo.put('heads', bencode([point]), txn=txn)
co.txn_commit(txn)
return 0
def rebuild(co, uheads):
txn = co.txn_begin()
if uheads == []:
heads = bdecode(co.linforepo.get('heads'))
else:
heads = [long_id(co, head) for head in uheads]
try:
rebuild_from_points(co, heads, txn)
except HistoryError, msg:
print 'error - ' + str(msg)
co.txn_abort(txn)
return 1
co.filenamesdb.truncate(txn)
for handle in co.modtimesdb.keys():
hinfo = handle_name(co, handle, None)
if hinfo is None or hinfo.has_key('delete'):
co.modtimesdb.delete(handle, txn)
if co.editsdb.has_key(handle):
co.editsdb.delete(handle, txn)
lfile = handle_to_filename(co, handle, txn)
co.filenamesdb.put(lfile, handle, txn=txn)
for handle, value in co.editsdb.items(txn):
if bdecode(value).has_key('delete'):
co.editsdb.delete(handle, txn)
set_edit(co, handle, {'delete': 1}, txn)
for handle, value in co.editsdb.items(txn):
linfo = bdecode(value)
merges = []
if linfo.has_key('nmerge'):
merges.append('nmerge')
if linfo.has_key('cmerge'):
merges.append('cmerge')
unset_edit(co, handle, merges, txn)
for i in range(1, len(heads)):
named, modified, added, deleted = \
handles_in_branch(co, heads[:i], [heads[i]], txn)
named = damerge(named, added, deleted)
modified = damerge(modified, added, deleted)
merge_analyze(co, heads[:i+1], heads[i], named, modified, deleted, txn)
print 'Rebuild done.'
co.txn_commit(txn)
write_rebuild_version(co.conf_path)
return 0
def cli_is_ancestor(co, point1, point2):
a, b = long_id(co, point1), long_id(co, point2)
if is_ancestor(co, a, b, None):
print point1 + ' is an ancestor of ' + point2
return 0
print point1 + ' is not an ancestor of ' + point2
return 1
def cli_print_dag(co, uheads):
if uheads == []:
heads = bdecode(co.linforepo.get('heads'))
else:
heads = [long_id(co, head) for head in uheads]
pretty_print_big_dag(co, heads)
return 0
def cli_print_mini_dag(co, file, uheads, by_id):
if by_id:
handle = binascii.unhexlify(file)
else:
fname = subpath(co.local, file)[1]
handle = filename_to_handle(co, fname)
if uheads == []:
heads = bdecode(co.linforepo.get('heads'))
else:
heads = [long_id(co, head) for head in uheads]
pretty_print_dag(co, handle, heads)
return 0
def populate_local_repos(co, ltxn):
if co.linforepo.has_key('heads', ltxn):
return
root = bencode({'precursors': [], 'handles': {roothandle: {'add': {'type': 'dir'}, 'name': ''}}})
head = sha.new(root).digest()
assert head == rootnode
co.lcrepo.put(head, root, txn=ltxn)
co.linforepo.put('heads', bencode([head]), txn=ltxn)
co.linforepo.put('branchmax', bencode(0), txn=ltxn)
co.linforepo.put('lasthandle', bencode(0), txn=ltxn)
sync_history(co, head, ltxn)
return
def _list_merge_files(co):
if co.repo is None:
return []
repohead = repo_head(co, co.repo)
if repohead == None:
repohead = rootnode
heads = bdecode(co.linforepo.get('heads'))
named, modified, added, deleted = \
handles_in_branch(co, [repohead], heads, None)
handles = damerge(named, modified, added, deleted)
named, modified, added, deleted = \
Set(named), Set(modified), Set(added), Set(deleted)
files = []
for handle in handles:
mletter, nletter = ' ', ' '
if handle in added:
if handle in deleted:
continue
mletter, nletter = 'A', 'A'
elif handle in deleted:
mletter, nletter = 'D', 'D'
else:
if handle in named:
nletter = 'N'
if handle in modified:
mletter = 'M'
assert not (mletter == ' ' and nletter == ' ')
files.append((handle_to_filename(co, handle), mletter + nletter))
files.sort()
return files
class GarbledCommitError(StandardError): pass
def _commit_helper(co, commit_files):
output = []
output.extend(['', '### Enter comment above', '### Files'])
files, name_map = [], {}
for handle, info in commit_files:
name = handle_to_filename(co, handle)
files.append((name, ''.join(_letters(info))))
name_map[name] = (handle, info)
files.sort()
for name, letters in files:
output.append("%s\t%s" % (letters, name))
files = _list_merge_files(co)
if len(files):
output.append('### Merge files')
for name, letters in files:
output.append('%s\t%s' % (letters, name))
output.append(os.linesep)
fd, fname = tempfile.mkstemp()
fhandle = os.fdopen(fd, 'w+')
out_str = os.linesep.join(output)
out_ustr = out_str.decode('utf8')
out_str = out_ustr.encode(text_encoding)
fhandle.write(out_str)
fhandle.close()
if platform == 'win32':
spawn = os.spawnv
editor = os.environ['WINDIR'] + '\\notepad.exe'
else:
spawn = os.spawnvp
editor = 'vi'
if os.environ.has_key('CDVEDITOR'):
editor = os.environ['CDVEDITOR']
elif os.environ.has_key('EDITOR'):
editor = os.environ['EDITOR']
args = editor.split() + [fname]
errored = True
while errored:
errored = False
if spawn(os.P_WAIT, args[0], args):
raise CommitError, 'Could not run editor "%s"' % (editor,)
fhandle = open(fname, 'rU')
text = fhandle.read()
fhandle.close()
try:
try:
utext = text.decode(text_encoding)
except UnicodeDecodeError:
raise GarbledCommitError, \
"Invalid %s characters in comment" % (text_encoding,)
lines = utext.encode('utf8').splitlines()
cur_line = 0
try:
while lines[cur_line].startswith('### Error: '):
lines.pop(cur_line)
except IndexError:
pass
while cur_line < len(lines):
if lines[cur_line] == '### Enter comment above':
break
cur_line += 1
if cur_line == len(lines):
raise GarbledCommitError, "Could not find end of comment"
comment = '\n'.join(lines[:cur_line])
cur_line += 1
if lines[cur_line] != '### Files':
raise GarbledCommitError, "Expected '### Files' line after end of comment"
fcommit_files = []
cur_line += 1
while cur_line < len(lines):
line = lines[cur_line]
cur_line += 1
if line.strip() == '':
continue
if line.startswith('### '):
break
try:
tab = line.index('\t')
except ValueError:
raise GarbledCommitError, "Bad commit file line:\n%s" % \
(line[:].rstrip(),)
name = line[tab+1:].rstrip()
fcommit_files.append(name_map[name])
except GarbledCommitError, msg:
error = msg.args[0]
errored = True
if errored:
answer = raw_input("Error: %s\nReturn to editor? [Y/n]: " % (error,))
if answer == 'n':
raise CommitError, msg
output = ['### Error: %s' % (line,) for line in error.split('\n')]
out_str = os.linesep.join(output)
out_ustr = out_str.decode('utf8')
out_str = out_ustr.encode(text_encoding)
fhandle = open(fname, 'w')
fhandle.write(out_str)
fhandle.write(text)
fhandle.close()
os.remove(fname)
return fcommit_files, comment
def commit(co, remote, comment, tstamp=None, backup=False, files=list()):
try:
if remote is None:
if co.user is None:
co.user = co.varsdb.get('user')
if co.user is None:
raise NetworkError, 'You must set the "user" variable'
repohead = None
else:
network_prep(co)
repohead = repo_head(co, co.repo)
if repohead is None:
repohead = rootnode
except NetworkError, msg:
print msg
return 1
co.handle_name_cache = {}
ltxn = co.txn_begin()
mark_modified_files(co, ltxn)
co.txn_commit(ltxn)
try:
if files == []:
handles = [(handle, 0) for handle in co.editsdb.keys()]
else:
handles = Glob(co, files).db_walk(deletes=1)
commit_files = find_commit_files(co, handles)
# get the comment from the user if it was supplied on the command line
if not backup:
if comment is None:
commit_files, comment = _commit_helper(co, commit_files)
# clean up the comment a bit
comment = comment.rstrip()
if comment == '' and not co.nopass:
print 'No comment given, aborting.'
return 1
comment = comment + '\n'
except CommitError, msg:
print 'commit failed: ' + str(msg)
return 1
# create and verify the changeset
ltxn = co.txn_begin()
point = None
try:
if not backup:
point = gen_changeset(co, commit_files, comment, repohead, ltxn, tstamp=tstamp)
except HistoryError, msg:
co.txn_abort(ltxn)
print 'error - ' + str(msg)
return 1
if point is not None:
try:
sync_history(co, point, ltxn)
except HistoryError, msg:
print 'commit failed: ' + str(msg)
print 'THIS IS REALLY BAD!!!'
co.txn_abort(ltxn)
return 1
else:
precursors = bdecode(co.linforepo.get('heads', txn=ltxn))
point = precursors[0]
if len(precursors) > 1 and backup is True:
print 'error - cannot use backup flag when merging'
co.txn_abort(ltxn)
return 1
if remote is not None:
ch = ClientHandler(co)
try:
s = authenticate(co, ch, remote, ltxn)
except NetworkError, msg:
co.txn_abort(ltxn)
print 'commit failed: ' + str(msg)
return 1
except ServerError, msg:
co.txn_abort(ltxn)
print 'commit failed: ' + str(msg)
return 1
try:
ch.commit(s, remote[2], point, ltxn)
except NetworkError, msg:
co.txn_abort(ltxn)
print 'commit failed: ' + str(msg)
return 1
except ServerError, msg:
ch.close(s)
co.txn_abort(ltxn)
print 'commit failed: ' + str(msg)
return 1
ch.close(s)
ch = None
co.linforepo.put(tuple_to_server(remote), point, txn=ltxn)
co.txn_commit(ltxn)
print 'commit succeeded'
return 0
def create_repo(co, remote):
try:
network_prep(co)
except NetworkError, msg:
print msg
return 1
ch = ClientHandler(co)
txn = co.txn_begin()
try:
s = authenticate(co, ch, remote, txn)
co.txn_commit(txn)
except NetworkError, msg:
co.txn_abort(txn)
print 'creation failed: ' + str(msg)
return 1
except ServerError, msg:
co.txn_abort(txn)
print 'creation failed: ' + str(msg)
return 1
try:
ch.create_repo(s, remote[2])
except NetworkError, msg:
print 'creation failed: ' + str(msg)
return 1
except ServerError, msg:
print 'creation failed: ' + str(msg)
retval = 1
else:
print 'creation succeeded'
retval = 0
ch.close(s)
return retval
def remove_repo(co, remote):
try:
network_prep(co)
except NetworkError, msg:
print msg
return 1
ch = ClientHandler(co)
txn = co.txn_begin()
try:
s = authenticate(co, ch, remote, txn)
co.txn_commit(txn)
except NetworkError, msg:
co.txn_abort(txn)
print 'destroy failed: ' + str(msg)
return 1
except ServerError, msg:
co.txn_abort(txn)
print 'destroy failed: ' + str(msg)
return 1
try:
ch.remove_repo(s, remote[2])
except NetworkError, msg:
print 'destroy failed: ' + str(msg)
return 1
except ServerError, msg:
print 'destroy failed: ' + str(msg)
retval = 1
else:
print 'destroy succeeded'
retval = 0
ch.close(s)
return retval
def list_repos(co):
try:
network_prep(co)
except NetworkError, msg:
print msg
return 1
ch = ClientHandler(co)
txn = co.txn_begin()
try:
s = authenticate(co, ch, server_to_tuple(co.repo), txn)
co.txn_commit(txn)
except NetworkError, msg:
co.txn_abort(txn)
print 'list failed: ' + str(msg)
return 1
except ServerError, msg:
co.txn_abort(txn)
print 'list failed: ' + str(msg)
return 1
try:
rlist = ch.list_repos(s)
except NetworkError, msg:
print 'list failed: ' + str(msg)
return 1
except ServerError, msg:
print 'list failed: ' + str(msg)
ch.close(s)
return 1
ch.close(s)
print 'Server has the following repositories:\n\t',
print '\n\t'.join(rlist)
return 0
def _letters(value):
if value.has_key('add'):
return ['A', 'A']
if value.has_key('delete'):
return ['D', 'D']
letters = [' ', ' ']
if value.has_key('hash'):
letters[0] = 'M'
if value.has_key('name'):
letters[1] = 'N'
return letters
def describe(co, point, short, xml, dodiff, files):
point = long_id(co, point)
if xml:
try:
print dump_changeinfo(co, point)
except ValueError:
print 'error - XML can only be written for clean merges.'
return 1
return 0
cset = bdecode(co.lcrepo.get(point))
_print_change(co, point, cset, not short)
if dodiff and point != rootnode:
return diff(co, [short_id(co, cset['precursors'][0]), short_id(co, point)], files, True)
return 0
def status(co, files, verbose):
ltxn = co.txn_begin()
mark_modified_files(co, ltxn)
co.txn_commit(ltxn)
# go do the stuff
if verbose:
try:
plist = _status_verbose(co, files)
except re.error, msg:
print 'error - bad ignore list: %s' % (str(msg),)
return 1
else:
plist = _status(co, files)
# print the list of modified files
if len(plist):
print '### Files'
plist.sort()
olist = []
for value in plist:
olist.append(value[1][0] + value[1][1] + '\t' + value[0])
print os.linesep.join(olist)
# print the list of merged files
plist = _list_merge_files(co)
if len(plist):
print '### Merge files'
for name, letters in plist:
print letters + '\t' + name
return 0
def _status(co, files):
# collect info on editted files
ed_set = Set(co.editsdb.keys())
# no args means show all editted files
if files == []:
db_set = ed_set
else:
db_set = Set([handle for handle, expanded in Glob(co, files).db_walk(deletes=1)])
# print the ones we care about
plist = []
for handle in (db_set & ed_set):
file = handle_to_filename(co, handle)
info = bdecode(co.editsdb.get(handle))
plist.append((file, _letters(info)))
return plist
def _status_verbose(co, files):
# Read ignore patterns
cpats = ignore_patterns(co)
# no args means search the whole client
if files == []:
files.append(path.join(co.local, '...'))
glob = Glob(co, files)
# do the filename expansion
fs_set = Set([file for file, expanded in glob.fs_walk()])
# get the list of files we manage
db_set = Set([handle_to_filename(co, handle) for handle, expanded in glob.db_walk(deletes=1)])
# collect info on editted files
ed_set, de_set = Set(), Set()
for handle, value in co.editsdb.items():
file = handle_to_filename(co, handle)
if bdecode(value).has_key('delete'):
de_set.add(file)
ed_set.add(file)
plist = []
# record unmanaged files
for file in (fs_set - db_set):
ignore = 0
for cpat in cpats:
if cpat.search(file) is not None:
ignore = 1
break
if ignore:
continue
plist.append((file, ['?', '?']))
# record files inconsistent in the filesystem
for file in (db_set - fs_set - de_set):
plist.append((file, ['!', '!']))
# record all the modified files
for file in ((ed_set & db_set) - (db_set - fs_set - de_set)):
handle = filename_to_handle(co, file, deletes=1)
hinfo = bdecode(co.editsdb.get(handle))
plist.append((file, _letters(hinfo)))
return plist
def cli_heads(co):
heads = bdecode(co.linforepo.get('heads'))
pheads = [short_id(co, head) for head in heads]
print ', '.join(pheads)
return 0
def cli_last_modified(co, lname, uhead, by_id):
if by_id:
ohandle = binascii.unhexlify(lname)
else:
try:
lfile, fname = subpath(co.local, lname)
except ValueError:
print 'error - ' + lname + ' is outside repository'
return 1
ohandle = filename_to_handle(co, fname)
if ohandle is None:
print 'error - ' + fname + ' is not in repository'
return 1
repohead = None
if uhead is None:
repohead = repo_head(co, co.repo)
if repohead is None:
heads = bdecode(co.linforepo.get('heads'))
repohead = heads[0]
else:
repohead = long_id(co, uhead)
point = handle_last_modified(co, co.contents, ohandle, repohead, None)
print short_id(co, point)
return 0
def ignore_patterns(co):
patterns = []
try:
fd = open(path.join(co.conf_path, 'ignore'), 'rU')
patterns = fd.readlines()
fd.close()
except IOError:
pass
# compile all the patterns and ensure they match full paths
return [re.compile('^%s$' % pat.strip()) for pat in patterns]
def diff(co, revs, files, print_new):
OK = 0
NEW_FILE = 1
DELETED = 2
MISSING = 3
def file_lines(handle, rev, lfile):
if rev == 'local':
linfo = handle_name(co, handle, None)
if linfo is None:
return (NEW_FILE, [''])
if linfo.has_key('delete'):
return (DELETED, [''])
try:
h = open(path.join(co.local, lfile), 'rb')
lines = h.read().split('\n')
h.close()
except IOError:
return (MISSING, [''])
else:
linfo = handle_name_at_point(co, handle, rev, None)
if linfo is None:
return (NEW_FILE, [''])
if linfo.has_key('delete'):
return (DELETED, [''])
pinfo = handle_contents_at_point(co, handle, rev, None)
lines = pinfo['lines']
return (OK, lines)
def print_format(error, lfile):
if error == OK:
return (1, lfile, None)
elif error == NEW_FILE:
return (0, '(new file)', 'File "%s" added.')
elif error == DELETED:
return (0, '(deleted)', 'File "%s" deleted.')
elif error == MISSING:
print 'WARNING - File not found: ' + lfile
return (0, '(File not found!)', None)
assert 0
return
co.handle_name_cache = {}
ltxn = co.txn_begin()
mark_modified_files(co, ltxn)
co.txn_commit(ltxn)
editsdb = co.editsdb
pathfunc, patharg = [], []
heads = bdecode(co.linforepo.get('heads'))
for i in xrange(len(revs)):
if revs[i] == 'repo':
revs[i] = repo_head(co, co.repo)
pathfunc.append(fullpath_at_point)
patharg.append(revs[i])
elif revs[i] == 'local':
pathfunc.append(handle_to_filename)
patharg.append(None)
else:
if revs[i] is None:
revs[i] = heads[0]
else:
revs[i] = long_id(co, revs[i])
pathfunc.append(fullpath_at_point)
patharg.append(revs[i])
branch = []
for i in xrange(len(revs)):
if revs[i] == 'local':
branch.append(heads)
else:
branch.append([revs[i]])
if files == []:
named, modified, added, deleted = \
handles_in_branch(co, branch[0], branch[1], None)
names2, modified2, added2, deleted2 = \
handles_in_branch(co, branch[1], branch[0], None)
handles = dmerge(modified, modified2)
if print_new:
handles = damerge(handles, added, deleted, added2, deleted2)
if revs[0] == 'local' or revs[1] == 'local':
handles = dmerge(handles, editsdb.keys())
else:
handles = []
for handle, expanded in Glob(co, files).db_walk():
if expanded and not editsdb.has_key(handle):
continue
handles.append(handle)
diffprog, diffpath = None, None
if os.environ.has_key('CDVDIFF'):
cdvdiff = shlex.split(os.environ['CDVDIFF'])
if cdvdiff != []:
diffargs = cdvdiff
diffprog = diffargs[0]
if platform == 'win32':
# windows inteprets the argument, excitement abounds
diffargs = ['"%s"' % (arg,) for arg in diffargs]
diffpath = tempfile.mkdtemp('', 'cdv-')
fd = open(path.join(diffpath, 'holder'), 'a')
fd.close()
hlist = []
for handle in handles:
hlist.append((pathfunc[0](co, handle, patharg[0]),
pathfunc[1](co, handle, patharg[1]),
handle))
hlist.sort()
if platform == 'win32':
spawn = os.spawnv
else:
spawn = os.spawnvp
retval = 0
for pre_lfile, lfile, handle in hlist:
#linfo = bdecode(co.staticdb.get(handle))
linfo = db_get(co, co.staticdb, handle, None)
if linfo['type'] != 'file':
continue
error, pre_lines = file_lines(handle, revs[0], pre_lfile)
printable, pre_lfile, msg0 = print_format(error, pre_lfile)
error, lines = file_lines(handle, revs[1], lfile)
printable2, lfile, msg1 = print_format(error, lfile)
printable += printable2
if printable == 0:
continue
if printable == 1 and not print_new:
if msg0 is not None:
print msg0 % lfile
if msg1 is not None:
print msg1 % pre_lfile
continue
if diffprog:
file1 = path.join(diffpath, 'old', pre_lfile)
file2 = path.join(diffpath, 'new', lfile)
os.makedirs(path.split(file1)[0])
foo = open(file1, 'w+')
foo.write('\n'.join(pre_lines))
foo.close()
os.makedirs(path.split(file2)[0])
foo = open(file2, 'w+')
foo.write('\n'.join(lines))
foo.close()
fileargs = [file1, file2]
if platform == 'win32':
fileargs = ['"%s"' % (arg) for arg in fileargs]
args = diffargs + fileargs
try:
ret = spawn(os.P_WAIT, diffprog, args)
except OSError:
ret = 127
os.remove(file1)
os.removedirs(path.split(file1)[0])
os.remove(file2)
os.removedirs(path.split(file2)[0])
if ret == 127:
print "error - Could not run diff program specified by CDVDIFF"
retval = 1
break
else:
print '--- ' + pre_lfile
print '+++ ' + lfile
# the diff code assumes \n after each line, not between lines
if pre_lines[-1] == '':
pre_lines.pop()
if lines[-1] == '':
lines.pop()
stdout.write(unified_diff(pre_lines, lines))
if diffpath is not None:
os.unlink(path.join(diffpath, 'holder'))
os.rmdir(diffpath)
return retval
def _comment_compress(comment):
try:
offset = comment.index('\n')
comment = comment[:offset]
except ValueError:
pass
if len(comment) > 76:
comment = comment[:73] + '...'
return comment
def _print_change(co, point, pinfo, v, owner=None, time=None):
if not v:
print 'Change %s' % (short_id(co, point),),
if owner is not None:
print '(%s)' % (owner,),
print 'by ' + pinfo['user'],
if time is not None:
print 'on ' + ctime(time)
elif pinfo.has_key('time'):
print 'on ' + ctime(pinfo['time'])
if pinfo.has_key('comment'):
print '"' + _comment_compress(pinfo['comment']) + '"'
else:
print '### Change: ' + binascii.hexlify(point)
print '### Short change: ' + short_id(co, point)
if owner is not None:
print '### Commit change: ' + owner
if pinfo['precursors'] != []:
print '### Precursors:',
ps = []
for p in pinfo['precursors']:
ps.append(short_id(co, p))
print ', '.join(ps)
if pinfo.has_key('user'):
print '### User: ' + pinfo['user']
if time is not None:
print '### Date: ' + ctime(time)
elif pinfo.has_key('time'):
print '### Date: ' + ctime(pinfo['time'])
if pinfo.has_key('comment'):
print '### Comment'
print pinfo['comment'].rstrip()
plist = []
for handle, value in pinfo['handles'].items():
if handle == roothandle:
continue
plist.append((fullpath_at_point(co, handle, point), _letters(value)))
if len(plist):
print '### Files'
plist.sort()
olist = []
for value in plist:
olist.append(value[1][0] + value[1][1] + '\t' + value[0])
print os.linesep.join(olist)
return
def _history_increment(co, handles, precursors, changes):
for handle in handles:
for pre in precursors:
change = handle_last_modified(co, co.contents, handle, pre, None)
if change is not None:
changes.setdefault(change, {})[handle] = 1
return
def _history_deps(node, args):
co, cutoffs = args[0], args[1]
if len(cutoffs) and _is_ancestor(co, node, cutoffs, None):
return []
cset = bdecode(co.lcrepo.get(node))
return cset['precursors']
def _owner_deps(node, args):
co, limit = args[0], args[1]
# heuristic shortcut
limit -= 1
if limit == 0:
return []
args[1] = limit
cset = bdecode(co.lcrepo.get(node))
try:
return [cset['precursors'][0]]
except IndexError:
pass
return []
def history(co, head, limit, skip, v, by_id, files):
repohead = None
heads = None
if head is not None:
heads = [head]
else:
heads = bdecode(co.linforepo.get('heads'))
repohead = repo_head(co, co.repo)
if repohead is None:
if co.repo is not None:
repohead = rootnode
heads.insert(0, repohead)
else:
heads.insert(0, repohead)
head = heads[0]
# the repository head may have more than we have merged locally
if repohead is not None:
while not _is_ancestor(co, head, heads, None):
head = bdecode(co.lcrepo.get(head))['precursors'][0]
# make an initial list of points to print based on user-specified files
owner_cutoff = limit + skip
changes = None
if files != []:
changes = {}
if by_id:
handles = [binascii.unhexlify(handle) for handle in files]
else:
handles = [handle for handle, expanded in Glob(co, files).db_walk()]
_history_increment(co, handles, heads, changes)
# the heuristic breaks if we're not printing everything
owner_cutoff = -1
# get a list of the clean merge heads for this repository
dfs = DFS(_owner_deps, [co, owner_cutoff + 1])
dfs.search(head)
owners = dfs.result()
# sort all the history points in reverse print order
cutoffs = []
if owners[0] != rootnode:
cutoffs.append(owners[0])
dfs = DFS(_history_deps, [co, cutoffs])
dfs.search(rootnode)
for head in heads:
dfs.search(head)
ordering = dfs.result()
ordering.reverse()
# pop off the root node
assert ordering[-1] == rootnode
ordering.pop()
owner = 'local'
time = None
for point in ordering:
hinfo = bdecode(co.lcrepo.get(point))
clean = False
if clean_merge_point(hinfo):
clean = True
if point == owners[-1]:
# committed change, but we don't know the server merge change
owners.pop()
time = hinfo['time']
owner = '----'
if clean:
# this is the server merge change
owner = short_id(co, point)
assert changes is None or not changes.has_key(point)
continue
# only display if it's not a clean merge and it was asked for
if clean:
continue
if changes is not None and not changes.has_key(point):
continue
# figure out the next set of changes to print
if changes is not None and changes.has_key(point):
_history_increment(co, changes[point].keys(), hinfo['precursors'],
changes)
del changes[point]
if skip > 0:
skip -= 1
continue
if limit == 0:
break
limit -=1
_print_change(co, point, hinfo, v, owner=owner, time=time)
if v:
print '-' * 78,
print
return 0
def revert(co, files, unmod_flag):
co.handle_name_cache = {}
txn = co.txn_begin()
mark_modified_files(co, txn)
heads = bdecode(co.linforepo.get('heads'))
editsdb = co.editsdb
#modified, names, deletes, newhandles = [], {}, {}, {}
modified = []
for handle, expanded in Glob(co, files).db_walk(deletes=1):
filename = handle_to_filename(co, handle, txn)
filepath = path.join(co.local, filename)
editted = False
exists = path.exists(filepath)
if editsdb.has_key(handle, txn):
info = bdecode(editsdb.get(handle, txn=txn))
if info.has_key('add') or \
info.has_key('name') or \
info.has_key('delete'):
print 'warning - cannot revert name operation on %s' % (filename,)
# XXX: hack until reverts on name ops work
if info.has_key('add'):
exists = True
elif info.has_key('hash'):
editted = True
elif exists:
if not expanded:
file = handle_to_filename(co, handle, txn)
print 'warning - %s is not opened for edit' % (filename,)
continue
sinfo = bdecode(co.staticdb.get(handle, txn=txn))
if sinfo['type'] == 'file' and (editted or not exists):
file_points = []
for point in heads:
linfo = handle_contents_at_point(co, handle, point, None)
if linfo is None:
continue
file_points.append((linfo['lines'], linfo['line points'], linfo['points']))
# XXX: hack until merge-through-conflict code is done
if len(file_points) == 2:
local = file_points[0]
remote = file_points[1]
lines = find_conflict(local[0], local[1], local[2],
remote[0], remote[1], remote[2])
else:
lines = find_conflict_multiple_safe(file_points)[0]
if lines is None:
print 'error - cannot revert %s' % (filename,)
co.txn_abort(txn)
return 1
#modified.append((handle, linfo))
ls, conflict = [], 0
for l in lines:
if type(l) is str:
ls.append(l)
else:
conflict = 1
ls.append('<<<<<<< local')
ls.extend(l[0])
ls.append('=======')
ls.extend(l[1])
ls.append('>>>>>>> remote')
if unmod_flag:
if not exists:
continue
h = open(filepath, 'rb')
contents = h.read()
h.close()
if '\n'.join(ls) == contents:
unset_edit(co, handle, ['hash'], txn)
co.modtimesdb.put(handle, bencode(path.getmtime(filepath)), txn=txn)
print 'reverting: %s' % (filename,)
continue
if not conflict and editted:
unset_edit(co, handle, ['hash'], txn)
hfile = path.join(co.temppath, binascii.hexlify(handle))
h = open(hfile, 'wb')
h.write('\n'.join(ls))
h.close()
modified.append((handle, filepath))
print 'reverting: %s' % (filename,)
# XXX: use update code
for handle, filename in modified:
temppath = path.join(co.temppath, binascii.hexlify(handle))
destpath = path.join(co.local, filename)
preserving_rename(temppath, destpath)
co.modtimesdb.put(handle, bencode(path.getmtime(destpath)), txn=txn)
co.txn_commit(txn)
print 'revert succeeded'
return 0
def annotate(co, rev, files):
"""Print each line of files with information on the last modification"""
if rev == 'repo':
rev = repo_head(co, co.repo)
elif rev != 'local':
rev = long_id(co, rev)
if len(files) == 0:
files = ['...']
if rev == 'local':
precursors = bdecode(co.linforepo.get('heads'))
repohead = repo_head(co, co.repo)
if repohead is None:
repohead = rootnode
if repohead not in precursors:
while not _is_ancestor(co, repohead, precursors, None):
info = bdecode(co.lcrepo.get(repohead))
try:
repohead = info['precursors'][0]
except IndexError:
repohead = rootnode
if repohead not in precursors:
precursors.insert(0, repohead)
cache = {}
for handle, expanded in Glob(co, files).db_walk(deletes=1):
sinfo = bdecode(co.staticdb.get(handle))
if sinfo['type'] == 'file':
if rev == 'local':
filename = handle_to_filename(co, handle)
file_points = []
pres = simplify_precursors(co, handle, co.contents, precursors, None)[0]
for pre, index in pres:
info = handle_contents_at_point(co, handle, pre, None, replayfunc=merge.annotate)
file_points.append((info['lines'], info['line points'], info['points']))
lfile = path.join(co.local, filename)
try:
h = open(lfile, 'rb')
except IOError:
print 'error - cannot open %s' % (filename,)
return 1
lines = h.read().split('\n')
h.close()
lpoints = find_annotation(file_points, lines)
else:
filename = fullpath_at_point (co, handle, rev)
cinfo = handle_contents_at_point(co, handle, rev, None, replayfunc=merge.annotate)
if cinfo is None or cinfo.has_key('delete'):
if expanded:
continue
print 'error - cannot find %s' % (filename,)
return 1
lines = cinfo['lines']
lpoints = cinfo['line points']
print >> stderr, 'Annotations for %s' % (filename,)
print >> stderr, '***************'
for i in xrange(len(lines)):
point = lpoints[i]
if point is None:
lrev = 'local'
user = '-----'
time = '-----'
elif cache.has_key(point):
lrev, user, time = cache[point]
else:
lrev = short_id(co, point)
pinfo = bdecode(co.lcrepo.get(point))
user = pinfo['user']
if pinfo.has_key('time'):
time = strftime('%Y-%m-%d', localtime(pinfo['time']))
else:
time = '-----'
cache[point] = (lrev, user, time)
print '%s (%s %s):' % (lrev, user, time,), lines[i]
return 0
class PathError(Exception):
pass
def find_co(local, metadata_dir='.cdv'):
if not path.exists(local):
raise Exception, 'path ' + local + ' does not exist'
while not path.exists(path.join(local, metadata_dir)):
parent = path.split(local)[0]
if local == parent:
raise PathError, 'cannot find checkout, use "cdv init" to create one'
local = parent
return local
#try:
# import psyco
# psyco.bind(diff, 0)
#except ImportError:
# pass
# Everything below here is for testing
def file_contents(file):
h = open(file, 'rb')
contents = h.read()
h.close()
return contents
def set_file_contents(file, contents):
h = open(file, 'wb')
h.write(contents)
h.close()
def append_file_contents(file, contents):
h = open(file, 'ab')
h.write(contents)
h.close()
def rm_rf(files):
for local in files:
mode = os.lstat(local).st_mode
if stat.S_ISDIR(mode):
for lamb in os.listdir(local):
rm_rf([path.join(local, lamb)])
os.rmdir(local)
elif stat.S_ISREG(mode):
os.unlink(local)
sh = None
sht = None
ag = None
agt = None
def init_test(local, remote, remote2):
global sh, sht
global ag, agt
if path.exists(local):
rm_rf([local])
os.makedirs(path.join(local, 'repo'))
set_file_contents(path.join(local, 'repo', 'codeville_repository'), '')
from passwd import Passwd
pw = Passwd(path.join(local, 'repo', 'passwd'), create=1)
pw.add('unittest', '')
pw.add('unittest2', '')
from ConfigParser import ConfigParser
sconfig = ConfigParser()
sconfig.add_section('control')
sconfig.set('control', 'backup', 'False')
sconfig.set('control', 'datadir', path.join(local, 'repo'))
sconfig.add_section('post-commit')
sh = ServerHandler(sconfig)
sh.bind(remote[1])
sh.db_init(init=True)
sht = Thread(target = sh.listen_forever, args = [])
sht.start()
from agent import Agent
ag = Agent()
auth_path = tempfile.mkdtemp('', 'cdv-')
auth_file = path.join(auth_path, 'agent.test')
ag.listen_sock(auth_path, auth_file)
agt = Thread(target = ag.listen, args = [])
agt.start()
os.makedirs(path.join(local, 'co'))
co = Checkout(path.join(local, 'co'), init=True)
txn = co.txn_begin()
co.varsdb.put('user', 'unittest', txn=txn)
co.txn_commit(txn)
os.makedirs(path.join(local, 'co2'))
co2 = Checkout(path.join(local, 'co2'), init=True)
txn = co2.txn_begin()
co2.varsdb.put('user', 'unittest2', txn=txn)
co2.txn_commit(txn)
co.nopass = co2.nopass = 1
co.repo = co2.repo = tuple_to_server(remote)
create_repo(co, remote)
create_repo(co, remote2)
return co, co2
def shutdown_test(local, cos):
if sh is not None:
sh.rs.doneflag.set()
sh.rs.start_connection(('localhost', 6602))
sh.shutdown.wait()
sh.close()
from errno import ECONNRESET
import socket
if ag is not None:
ag.shutdown_flag = 1
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(ag.auth_file)
try:
sock.recv(1)
except socket.error, reason:
assert reason[0] == ECONNRESET
else:
sock.close()
for co in cos:
co.close()
def reset_co(co):
txn = co.txn_begin()
co.linforepo.put('heads', bencode([rootnode]), txn=txn)
co.editsdb.truncate(txn=txn)
co.modtimesdb.truncate(txn=txn)
co.filenamesdb.truncate(txn=txn)
co.txn_commit(txn)
co.handle_name_cache = {}
for lamb in os.listdir(co.local):
if lamb == '.cdv':
continue
rm_rf([path.join(co.local, lamb)])
def reset_test(co, co2, remote, remote2=None):
remove_repo(co, remote)
create_repo(co, remote)
if remote2:
remove_repo(co, remote2)
create_repo(co, remote2)
reset_co(co)
reset_co(co2)
def test_client():
global ServerHandler, Thread
from server import ServerHandler
from threading import Thread
local = path.abspath('test')
cop = path.join(local, 'co')
co2p = path.join(local, 'co2')
repo = server_to_tuple('cdv://localhost:6602/unittest')
repo2 = server_to_tuple('cdv://localhost:6602/unittest2')
co, co2 = init_test(local, repo, repo2)
try:
_test_client(co, cop, co2, co2p, repo, repo2)
except (AssertionError, Exception):
shutdown_test(local, [co, co2])
raise
shutdown_test(local, [co, co2])
if path.exists(local):
rm_rf([local])
return
def _test_client(co, cop, co2, co2p, repo, repo2):
print 'TESTING merge conflict'
set_file_contents(path.join(cop, 'a'), "aaa\nbbb\nccc\nddd\neee\nfff\n")
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
set_file_contents(path.join(cop, 'a'), "aaa\nbbb\nccc\nfoo\nddd\neee\nfff\n")
os.utime(path.join(cop, 'a'), (0, 0))
assert commit(co, repo, '') == 0
set_file_contents(path.join(co2p, 'a'), "aaa\nbbb\nccc\nbar\nddd\neee\nfff\n")
os.utime(path.join(co2p, 'a'), (0, 0))
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert file_contents(path.join(co2p, 'a')) == "aaa\nbbb\nccc\n<<<<<<< local\nbar\n=======\nfoo\n>>>>>>> remote\nddd\neee\nfff\n"
print 'TESTING add conflict'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), 'foo')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
set_file_contents(path.join(co2p, 'a'), 'bar')
add(co2, [path.join(co2p, 'a')])
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'a.nameconflict.local'))
assert path.exists(path.join(co2p, 'a.nameconflict.remote'))
# use the agent for the rest of the tests
os.environ['CDV_AUTH_SOCK'] = ag.auth_file
co.nopass = co2.nopass = 2
print 'TESTING rename and add file of same name'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), '')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co2, path.join(co2p, 'a'), path.join(co2p, 'b'))
set_file_contents(path.join(co2p, 'a'), '')
add(co2, [path.join(co2p, 'a')])
assert commit(co2, repo, '') == 0
assert update(co, repo) == 0
assert path.exists(path.join(cop, 'a'))
assert path.exists(path.join(cop, 'b'))
print 'TESTING add file conflicting with remote rename'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), '')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
set_file_contents(path.join(co2p, 'b'), '')
add(co2, [path.join(co2p, 'b')])
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'b.nameconflict.local'))
assert path.exists(path.join(co2p, 'b.nameconflict.remote'))
print 'TESTING add file conflicting with remote rename and merge conflict'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), "foo\n")
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
append_file_contents(path.join(cop, 'a'), "bar\n")
edit(co, [path.join(cop, 'a')])
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
set_file_contents(path.join(co2p, 'b'), '')
add(co2, [path.join(co2p, 'b')])
append_file_contents(path.join(co2p, 'a'), "baz\n")
edit(co2, [path.join(co2p, 'a')])
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'b.nameconflict.local'))
assert path.exists(path.join(co2p, 'b.nameconflict.remote'))
assert file_contents(path.join(co2p, 'b.nameconflict.remote')) == '<<<<<<< local\nfoo\nbaz\n\n=======\nfoo\nbar\n\n>>>>>>> remote'
print 'TESTING conflicting local and remote rename'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), '')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'a'), path.join(co2p, 'c'))
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'c.nameconflict'))
assert path.exists(path.join(co2p, 'c.nameconflict.info'))
print 'TESTING multiple conflicting local and remote rename'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
set_file_contents(path.join(cop, 'a', 'x'), '')
os.makedirs(path.join(cop, 'b'))
set_file_contents(path.join(cop, 'b', 'y'), '')
os.makedirs(path.join(cop, 'c'))
set_file_contents(path.join(cop, 'c', 'z'), '')
add(co, [path.join(cop, 'a', 'x'), path.join(cop, 'b', 'y'), path.join(cop, 'c', 'z')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'b', 'y'), path.join(cop, 'a', 'y'))
rename(co, path.join(cop, 'c', 'z'), path.join(cop, 'a', 'z'))
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'b', 'y'), path.join(co2p, 'b', 'x'))
rename(co2, path.join(co2p, 'c', 'z'), path.join(co2p, 'c', 'x'))
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'b', 'x.nameconflict'))
assert path.exists(path.join(co2p, 'b', 'x.nameconflict.info'))
assert path.exists(path.join(co2p, 'c', 'x.nameconflict'))
assert path.exists(path.join(co2p, 'c', 'x.nameconflict.info'))
print 'TESTING rename and back again'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), 'a')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
rename(co, path.join(cop, 'b'), path.join(cop, 'a'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'a'))
print 'TESTING rename swap'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), 'a')
set_file_contents(path.join(cop, 'b'), 'b')
add(co, [path.join(cop, 'a'), path.join(cop, 'b')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'c'))
rename(co, path.join(cop, 'b'), path.join(cop, 'a'))
rename(co, path.join(cop, 'c'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert file_contents(path.join(co2p, 'a')) == 'b'
assert file_contents(path.join(co2p, 'b')) == 'a'
print 'TESTING rename circular'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), 'a')
set_file_contents(path.join(cop, 'b'), 'b')
set_file_contents(path.join(cop, 'c'), 'c')
add(co, [path.join(cop, 'a'), path.join(cop, 'b'), path.join(cop, 'c')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'd'))
rename(co, path.join(cop, 'b'), path.join(cop, 'a'))
rename(co, path.join(cop, 'c'), path.join(cop, 'b'))
rename(co, path.join(cop, 'd'), path.join(cop, 'c'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert file_contents(path.join(co2p, 'a')) == 'b'
assert file_contents(path.join(co2p, 'b')) == 'c'
assert file_contents(path.join(co2p, 'c')) == 'a'
print 'TESTING clean reparent with loops in intermediate rename stages'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a', 'b', 'c', 'd'))
add(co, [path.join(cop, 'a', 'b', 'c', 'd')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a', 'b', 'c', 'd'), path.join(cop, 'd'))
rename(co, path.join(cop, 'a', 'b', 'c'), path.join(cop, 'd', 'c'))
rename(co, path.join(cop, 'a', 'b'), path.join(cop, 'd', 'c', 'b'))
rename(co, path.join(cop, 'a'), path.join(cop, 'd', 'c', 'b', 'a'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'd', 'c', 'b', 'a'))
print 'TESTING reparent twisted conflict'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
os.makedirs(path.join(cop, 'b'))
add(co, [path.join(cop, 'a'), path.join(cop, 'b')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b', 'a'))
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'b'), path.join(co2p, 'a', 'b'))
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'a.parentloop', 'b'))
assert path.exists(path.join(co2p, 'a.parentloop.info'))
print 'TESTING reparent twisted conflict'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'c', 'a'))
os.makedirs(path.join(cop, 'b'))
add(co, [path.join(cop, 'c', 'a'), path.join(cop, 'b')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'c', 'a'), path.join(cop, 'b', 'a'))
rename(co, path.join(cop, 'c'), path.join(cop, 'b', 'a', 'c'))
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'b'), path.join(co2p, 'c', 'a', 'b'))
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'c.parentloop', 'a.parentloop', 'b'))
assert path.exists(path.join(co2p, 'c.parentloop.info'))
assert path.exists(path.join(co2p, 'c.parentloop', 'a.parentloop.info'))
print 'TESTING rename incidental'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'a'), path.join(co2p, 'b'))
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert commit(co2, repo, '') == 0
rename(co, path.join(cop, 'b'), path.join(cop, 'c'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'c'))
print 'TESTING rename dependent'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a', 'a', 'a'))
set_file_contents(path.join(cop, 'a', 'a', 'a', 'a'), '')
add(co, [path.join(cop, 'a', 'a', 'a', 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a', 'a', 'a', 'a'), path.join(cop, 'a', 'a', 'a', 'b'))
rename(co, path.join(cop, 'a', 'a', 'a'), path.join(cop, 'a', 'a', 'b'))
rename(co, path.join(cop, 'a', 'a'), path.join(cop, 'a', 'b'))
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'b', 'b', 'b', 'b'))
print 'TESTING update overrides coincidental name merge'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), '')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'a'), path.join(co2p, 'b'))
assert commit(co2, repo2, '') == 0
assert update(co, repo2) == 0
assert update(co2, repo) == 0
assert commit(co2, repo, '') == 0
assert update(co, repo) == 0
rename(co, path.join(cop, 'b'), path.join(cop, 'c'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'c'))
print 'TESTING delete orphan'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
set_file_contents(path.join(cop, 'a', 'a'), '')
add(co, [path.join(cop, 'a', 'a')])
assert commit(co, repo, '') == 0
delete(co2, [path.join(co2p, 'a')])
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'a.orphaned'))
assert path.exists(path.join(co2p, 'a.orphaned.info'))
print 'TESTING remote orphan'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
delete(co2, [path.join(co2p, 'a')])
assert commit(co2, repo, '') == 0
set_file_contents(path.join(cop, 'a', 'a'), '')
add(co, [path.join(cop, 'a', 'a')])
assert commit(co, repo, '') == 1
assert update(co, repo) == 0
assert path.exists(path.join(cop, 'a.orphaned'))
assert path.exists(path.join(cop, 'a.orphaned.info'))
print 'TESTING delete and reuse name'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a', 'a'))
os.makedirs(path.join(cop, 'b'))
add(co, [path.join(cop, 'a', 'a'), path.join(cop, 'b')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
delete(co, [path.join(cop, 'b')])
rename(co, path.join(cop, 'a'), path.join(cop, 'b'))
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'b', 'a'))
assert not path.exists(path.join(co2p, 'a'))
print 'TESTING delete dependent'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a', 'a', 'a', 'a'))
add(co, [path.join(cop, 'a', 'a', 'a', 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
delete(co, [path.join(cop, 'a', 'a', 'a', 'a')])
delete(co, [path.join(cop, 'a', 'a', 'a')])
delete(co, [path.join(cop, 'a', 'a')])
delete(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert not path.exists(path.join(co2p, 'a'))
print 'TESTING delete within parent loop'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
os.makedirs(path.join(cop, 'b'))
add(co, [path.join(cop, 'a'), path.join(cop, 'b')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b', 'a'))
assert commit(co, None, '') == 0
assert update(co, repo) == 0
delete(co, [path.join(cop, 'b', 'a')])
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'b'), path.join(co2p, 'a', 'b'))
assert commit(co2, repo, '') == 1
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'b.orphaned'))
assert path.exists(path.join(co2p, 'b.orphaned.info'))
print 'TESTING delete entire parent loop'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
os.makedirs(path.join(cop, 'b'))
add(co, [path.join(cop, 'a'), path.join(cop, 'b')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b', 'a'))
assert commit(co, None, '') == 0
delete(co, [path.join(cop, 'b', 'a')])
assert commit(co, repo, '') == 0
rename(co2, path.join(co2p, 'b'), path.join(co2p, 'a', 'b'))
assert commit(co2, None, '') == 0
delete(co2, [path.join(co2p, 'a', 'b')])
assert commit(co2, repo, '') == 0
assert update(co, repo) == 0
assert update(co2, repo) == 0
assert not path.exists(path.join(cop, 'a'))
assert not path.exists(path.join(cop, 'b'))
assert not path.exists(path.join(co2p, 'a'))
assert not path.exists(path.join(co2p, 'b'))
print 'TESTING remote add and delete'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), '')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
delete(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
assert not path.exists(path.join(co2p, 'a'))
print 'TESTING unique name mangling'
reset_test(co, co2, repo)
set_file_contents(path.join(cop, 'a'), '')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'a1'))
assert commit(co, repo, '') == 0
set_file_contents(path.join(co2p, 'a2.nameconflict'), '')
add(co2, [path.join(co2p, 'a2.nameconflict')])
rename(co2, path.join(co2p, 'a'), path.join(co2p, 'a2'))
assert update(co2, repo) == 0
assert path.exists(path.join(co2p, 'a2.nameconflict'))
assert path.exists(path.join(co2p, 'a2.nameconflict2'))
assert path.exists(path.join(co2p, 'a2.nameconflict2.info'))
# XXX: had to relax this restriction for now, fix next history rewrite
#print 'TESTING deleted file modified remotely'
#reset_test(co, co2, repo)
#set_file_contents(path.join(cop, 'a'), '')
#add(co, [path.join(cop, 'a')])
#assert commit(co, repo, '') == 0
#assert update(co2, repo) == 0
#delete(co, [path.join(cop, 'a')])
#assert commit(co, repo, '') == 0
#set_file_contents(path.join(co2p, 'a'), 'foo')
#edit(co2, [path.join(co2p, 'a')])
#assert commit(co2, repo, '') == 1
#assert update(co2, repo) == 0
#assert commit(co2, repo, '') == 0
#assert update(co, repo) == 0
#assert not path.exists(path.join(cop, 'a'))
print 'TESTING independent local and remote deletes'
set_file_contents(path.join(cop, 'a'), '')
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
delete(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
delete(co2, [path.join(co2p, 'a')])
assert commit(co2, repo, '') == 0
assert update(co2, repo) == 0
assert not path.exists(path.join(cop, 'a'))
assert not path.exists(path.join(co2p, 'a'))
print 'TESTING local filesystem conflicts with repository'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
add(co, [path.join(cop, 'a')])
assert commit(co, repo, '') == 0
set_file_contents(path.join(co2p, 'a'), '')
assert update(co2, repo) == 1
assert path.isfile(path.join(co2p, 'a'))
print 'TESTING non-existent parents needed for update'
reset_test(co, co2, repo)
os.makedirs(path.join(cop, 'a'))
os.makedirs(path.join(cop, 'b'))
add(co, [path.join(cop, 'a',), path.join(cop, 'b',)])
assert commit(co, repo, '') == 0
assert update(co2, repo) == 0
rename(co, path.join(cop, 'a'), path.join(cop, 'b', 'a'))
assert commit(co, repo, '') == 0
os.rmdir(path.join(co2p, 'a'))
assert update(co2, repo) == 1
os.rmdir(path.join(co2p, 'b'))
assert update(co2, repo) == 1
set_file_contents(path.join(co2p, 'b'), '')
assert update(co2, repo) == 1
assert not path.exists(path.join(co2p, 'a'))
assert path.isfile(path.join(co2p, 'b'))
syntax highlighted by Code2HTML, v. 0.9.1