# Written by Ross Cohen
# see LICENSE.txt for license information
from bencode import bdecode, bencode
from db import db
from history import roothandle, dmerge, rename_conflict_check
from history import handle_name_at_point, __handle_name_at_point
from history import _handle_name_at_point
from history import handle_contents_at_point
from history import handle_last_modified
from history import _name_use_count, _children_count
from history import write_diff, write_index, db_get, db_put, HistoryError
from history import clean_merge_point, simplify_precursors, repo_head
from history import _is_ancestor
from merge import find_resolution
import os
from os import path
from path import breakup
import sha
import stat
from time import time
import zlib
class CommitError(Exception):
pass
def new_handle(co, txn):
"""Create a temporary handle for new files in the working copy"""
num = bdecode(co.linforepo.get('lasthandle', txn=txn)) + 1
handle = sha.new(str(num)).digest()
co.linforepo.put('lasthandle', bencode(num), txn=txn)
return handle
def create_handle(precursors, hinfo):
"""Create a permanent identifier for use in a changeset description"""
binfo = bencode({'precursors': precursors, 'handle': hinfo})
return sha.new(binfo).digest()
def handle_name(co, handle, txn):
"""Returns a dict specifying name, parent and other info"""
if co.handle_name_cache.has_key(handle):
return co.handle_name_cache[handle]
# XXX: reading this repeatedly is excessive
heads = bdecode(co.linforepo.get('heads', txn=txn))
einfo = {}
if co.editsdb.has_key(handle, txn):
einfo = bdecode(co.editsdb.get(handle, txn=txn))
if einfo.has_key('name'):
einfo['points'] = ['1']
for head in heads:
pinfo = __handle_name_at_point(co, handle, head, txn)
if pinfo is None:
continue
einfo['points'] = dmerge(einfo['points'], pinfo['points'])
einfo['rename point'] = ['1']
co.handle_name_cache[handle] = einfo
return einfo
state = None
for point in heads:
pinfo = __handle_name_at_point(co, handle, point, txn)
if pinfo is None:
continue
if pinfo.has_key('delete'):
co.handle_name_cache[handle] = pinfo
return pinfo
if state is None:
state = pinfo
continue
conflict, rename_points = rename_conflict_check(state, pinfo)
if conflict == 'remote':
state['name'] = pinfo['name']
state['parent'] = pinfo['parent']
state['rename point'] = rename_points
state['points'] = dmerge(state['points'], pinfo['points'])
if einfo.has_key('delete'):
state['delete'] = einfo['delete']
co.handle_name_cache[handle] = state
return state
def _filename_to_handle(co, parent, name, txn, deletes=0):
"""Given a parent handle and a name, return all associated handles"""
handles = []
cursor = co.allnamesdb.cursor(txn=txn)
lookup = parent + name
try:
key, value = cursor.set(lookup)
except (db.DBNotFoundError, TypeError):
key = None
while key == lookup:
hinfo = handle_name(co, value, txn)
if hinfo is not None and hinfo['name'] == name and hinfo['parent'] == parent:
if not hinfo.has_key('delete'):
handles.append(value)
elif deletes == 1 and co.editsdb.has_key(value):
if bdecode(co.editsdb.get(value, txn=txn)).has_key('delete'):
handles.append(value)
# next_dup() is broken
foo = cursor.next()
if foo is None:
break
key, value = foo
cursor.close()
return handles
def filename_to_handle(co, file, txn=None, deletes=0):
"""Given a full file path, return the associated handle"""
if file == '':
return roothandle
lpath = breakup(file)
handle = roothandle
for name in lpath:
handles = _filename_to_handle(co, handle, name, txn, deletes=deletes)
if handles == []:
return None
handle = handles[0]
return handle
def _handle_to_filename(co, handle, names, txn):
"""Same as handle_to_filename, but takes an override mapping for handles"""
lpath, hseen = [], {}
while handle != roothandle:
if names.has_key(handle):
info = names[handle]
else:
info = handle_name(co, handle, txn)
if info is None:
lpath.insert(0, '???')
break
if hseen.has_key(handle):
lpath.insert(0, info['name'] + '(loop)')
break
lpath.insert(0, info['name'])
hseen[handle] = 1
handle = info['parent']
try:
return path.join(*lpath)
except TypeError:
pass
return ''
def handle_to_filename(co, handle, txn=None):
"""Convert the given handle to a full path name."""
return _handle_to_filename(co, handle, {}, txn)
def set_edit(co, handle, info, txn):
"""Specify that a new attribute on a file is being edited."""
if co.editsdb.has_key(handle):
oinfo = bdecode(co.editsdb.get(handle, txn=txn))
oinfo.update(info)
info = oinfo
co.editsdb.put(handle, bencode(info), txn=txn)
def unset_edit(co, handle, info, txn):
"""Remove an edit attribute on a file."""
oinfo = bdecode(co.editsdb.get(handle, txn=txn))
for attr in info:
del oinfo[attr]
if oinfo == {}:
co.editsdb.delete(handle, txn=txn)
else:
co.editsdb.put(handle, bencode(oinfo), txn=txn)
def _add_file(co, rep, parent, required, ltxn):
mode = os.lstat(path.join(co.local, rep)).st_mode
handle = filename_to_handle(co, rep)
if handle:
#info = bdecode(co.staticdb.get(handle))
info = db_get(co, co.staticdb, handle, None)
if info['type'] == 'dir' and not stat.S_ISDIR(mode):
print 'error - %s already added as a %s' % (rep, info['type'])
return None
if info['type'] == 'file' and not stat.S_ISREG(mode):
print 'error - %s already added as a %s' % (rep, info['type'])
return None
if required:
print 'warning - %s already added' % (rep,)
else:
print 'adding: ' + rep
if stat.S_ISDIR(mode):
type = 'dir'
elif stat.S_ISREG(mode):
type = 'file'
else:
print 'error - unrecognized file type for %s' % (rep,)
return None
if rep == '':
handle = roothandle
else:
handle = new_handle(co, ltxn)
#co.staticdb.put(handle, bencode({'type': type}), txn=ltxn)
db_put(co, co.staticdb, handle, {'type': type}, ltxn)
info = {'name': path.split(rep)[1], 'parent': parent, 'add': {}}
set_edit(co, handle, info, ltxn)
co.allnamesdb.put(parent + info['name'], handle, flags=db.DB_NODUPDATA, txn=ltxn)
if type == 'file':
co.modtimesdb.put(handle, bencode(0), txn=ltxn)
co.filenamesdb.put(rep, handle, txn=ltxn)
return handle
def _set_name(co, handle, parent, name, txn):
linfo = {'parent': parent, 'name': name}
co.handle_name_cache[handle] = linfo
set_edit(co, handle, linfo, txn)
try:
co.allnamesdb.put(parent + name, handle, flags=db.DB_NODUPDATA, txn=txn)
except db.DBKeyExistError:
pass
def name_use_count(co, handle, txn):
"""Returns a list of handles with the same name as the given handle."""
def _name_func(co, handle, foo, txn):
return handle_name(co, handle, txn)
return _name_use_count(co, handle_name(co, handle, txn), None, _name_func, txn)
def rename_race(co, handle, names, txn):
#def _name_func(co, handle, names, txn):
# if names.has_key(handle):
# return names[handle]
# return None
#named = _name_use_count(co, info, names, _name_func, txn)
info = handle_name(co, handle, txn)
cursor = co.allnamesdb.cursor(txn=txn)
lookup = info['parent'] + info['name']
try:
key, value = cursor.set(lookup)
except (db.DBNotFoundError, TypeError):
return None
while key == lookup:
if names.has_key(value):
vinfo = names[value]
if vinfo['parent'] == info['parent'] and vinfo['name'] == info['name']:
cursor.close()
return value
foo = cursor.next()
if foo is None:
break
key, value = foo
cursor.close()
return None
def children_count(co, handle, txn, deletes=0):
"""Returns a list of children of the specified handle."""
def _name_func(co, handle, foo, txn):
hinfo = handle_name(co, handle, txn)
if hinfo is not None and hinfo.has_key('delete') and deletes == 1:
# if the editsdb has it, then it was just deleted
if co.editsdb.has_key(handle, txn):
del hinfo['delete']
return hinfo
return _children_count(co, handle, None, _name_func, txn)
def _parent_loop_check(co, handle, names, txn):
"""Same as parent_loop_check, but takes a dict of handle mapping overrides."""
hseen = {}
while handle != roothandle:
if hseen.has_key(handle):
return handle
hseen[handle] = 1
if names.has_key(handle):
pinfo = names[handle]
else:
pinfo = handle_name(co, handle, txn)
if pinfo.has_key('delete'):
return handle
handle = pinfo['parent']
return None
def parent_loop_check(co, handle, txn):
"""Check whether this handle exists under itself in the directory tree."""
return _parent_loop_check(co, handle, {}, txn)
def _rename_safe_check(co, handle, names, txn):
temp = names[handle]
del names[handle]
result = _parent_loop_check(co, handle, names, txn)
names[handle] = temp
if result is None:
return 1
return None
def unique_name(co, parent, file, txn):
"""Generate a name not already in use by the system."""
if _filename_to_handle(co, parent, file, txn) == []:
return file
post = 2
while _filename_to_handle(co, parent, file + str(post), txn) != []:
post = post + 1
return file + str(post)
def conflicts_in_file(co, file):
h = open(path.join(co.local, file), 'rb')
lines = h.read().split('\n')
h.close()
for l in lines:
if l == '<<<<<<< local' or \
l == '=======' or \
l == '>>>>>>> remote':
return 1
return 0
def find_update_files(co, rhead, named, txn):
rnamed = []
for handle in named:
linfo = handle_name(co, handle, txn)
rnamed.append((handle, linfo, __handle_name_at_point(co, handle, rhead, txn)))
return rnamed
def mark_modified_files(co, txn):
if co.varsdb.has_key('edit-mode'):
if co.varsdb.get('edit-mode') == '1':
return
modtimesdb, editsdb, local = co.modtimesdb, co.editsdb, co.local
for lfile, handle in co.filenamesdb.items(txn):
if editsdb.has_key(handle, txn):
info = bdecode(editsdb.get(handle, txn=txn))
if info.has_key('hash') or info.has_key('add'):
continue
#info = bdecode(co.staticdb.get(handle, txn=txn))
info = db_get(co, co.staticdb, handle, txn)
if info['type'] != 'file':
continue
lfile = path.join(local, lfile)
try:
mtime = path.getmtime(lfile)
except OSError:
continue
minfo = bdecode(modtimesdb.get(handle, txn=txn))
if mtime != minfo:
modtimesdb.put(handle, bencode(mtime), txn=txn)
set_edit(co, handle, {'hash': 1}, txn)
def gen_diff(co, handle, precursors, lines, txn):
pres, plen = simplify_precursors(co, handle, co.contents, precursors, txn)
file_points = []
for pre, index in pres:
info = handle_contents_at_point(co, handle, pre, txn)
file_points.append((info['lines'], info['line points'], info['points']))
result, ms, newlines = find_resolution(file_points, lines)
# explanation of conditions:
# 1: check for a merge
# 2: check if new lines were added by the user
# 3: safety for the 4th condition
# 4: check if the first match in the first (only) file covers everything
if len(pres) > 1 or \
len(newlines) != 0 or \
not len(ms[0]) or \
ms[0][0][2] != len(file_points[0][0]):
# create a set of correct matches, minus ones which are optimized out
matches = [[] for i in xrange(plen)]
i = 0
for pre, index in pres:
matches[index] = ms[i]
i += 1
return {'matches': matches, 'newlines': newlines}
return None
def gen_changeset(co, files, comment, repohead, txn, tstamp=None):
def per_file_hash(co, handle, hinfo, precursors, lfile, txn):
try:
h = open(lfile, 'rb')
except IOError:
raise HistoryError, 'Could not open file ' + lfile
lines = h.read().split('\n')
h.close()
dinfo = gen_diff(co, handle, precursors, lines, txn)
if hinfo.has_key('add'):
dinfo['add'] = 1
if hinfo.has_key('delete'):
dinfo['delete'] = 1
try:
diff = bencode(dinfo)
except ValueError:
return None
co.modtimesdb.put(handle, bencode(path.getmtime(lfile)), txn=txn)
hinfo['hash'] = sha.new(diff).digest()
return zlib.compress(diff, 6)
precursors = bdecode(co.linforepo.get('heads'))
# include the last known repository head in the list of changes. this is
# extra useful info and also forces a merge change which a comment can
# then be attached to.
# XXX: can do the wrong thing in odd merge-and-not-update cases
if repohead is not None and repohead not in precursors:
while not _is_ancestor(co, repohead, precursors, txn):
info = bdecode(co.lcrepo.get(repohead, txn=txn))
try:
repohead = info['precursors'][0]
except IndexError:
repohead = rootnode
if repohead not in precursors:
precursors.insert(0, repohead)
changeset = {'precursors': precursors}
changeset['handles'] = handles = {}
adds, nedits, edits, types, names = {}, [], [], {}, {}
for handle, linfo in files:
if linfo.has_key('add'):
adds[handle] = 1
#types[handle] = bdecode(co.staticdb.get(handle))['type']
types[handle] = db_get(co, co.staticdb, handle, None)['type']
handles[handle] = cinfo = {}
if linfo.has_key('delete'):
assert not linfo.has_key('add')
assert not linfo.has_key('hash')
cinfo['delete'] = 1
elif linfo.has_key('name') or \
linfo.has_key('nmerge'):
nedits.append((handle, linfo))
if linfo.has_key('add'):
assert not linfo.has_key('hash')
cinfo['add'] = {'type': types[handle]}
elif linfo.has_key('hash') or \
linfo.has_key('cmerge'):
assert types[handle] == 'file'
edits.append(handle)
co.editsdb.delete(handle, txn=txn)
# generate the name diffs
for handle, linfo in nedits:
# check if this is really a merge or not
# XXX: theoretically we can trust the 'nmerge' flag as set (and
# cleared) by _update_helper()
merge = False
change = prev_change = None
for head in precursors:
change = handle_last_modified(co, co.names, handle, head, txn)
if change is None:
continue
if prev_change is None:
prev_change = change
continue
left_anc = _is_ancestor(co, prev_change, [change], txn)
right_anc = _is_ancestor(co, change, [prev_change], txn)
if left_anc:
prev_change = change
elif not right_anc:
merge = True
break
# XXX: sanity check for now, we have to do most of the work anyway
assert not (linfo.has_key('nmerge') ^ merge)
# no merge, but maybe the user made an explicit change
if not linfo.has_key('nmerge') and change is not None:
old_info = handle_name_at_point(co, handle, change, txn, lookup=False)
if old_info['name'] == linfo['name'] and \
old_info['parent'] == linfo['parent']:
continue
# looks like we need to include an explicit name change
cinfo = handles[handle]
hinfo = handle_name(co, handle, txn)
cinfo['parent'] = hinfo['parent']
cinfo['name'] = hinfo['name']
names[handle] = cinfo
# generate the diffs
indices = {}
for handle in edits:
lfile = path.join(co.local, _handle_to_filename(co, handle, names, txn))
diff = per_file_hash(co, handle, handles[handle], precursors, lfile, txn)
if diff is None:
continue
indices[handle] = write_diff(co, handle, diff, txn)
# clear out things which didn't actually have changes
for handle, linfo in files:
if handles[handle] == {}:
del handles[handle]
# change all the temporary IDs to permanent, verifiable ones
ladds, nmap = adds.keys(), {}
while len(ladds):
handle = ladds.pop()
# check if this handle was already dealt with
if not adds.has_key(handle):
continue
parent = handles[handle]['parent']
# if the parent was also added, it needs to be renumbered first
if adds.has_key(parent):
ladds.extend((handle, parent))
continue
hinfo = handles[handle]
# if the parent has been renumbered, pick up the change
if nmap.has_key(parent):
hinfo['parent'] = nmap[parent]
# generate the permanent ID
if types[handle] == 'file':
# generate diffs
fname = _handle_to_filename(co, handle, names, txn)
lfile = path.join(co.local, fname)
diff = per_file_hash(co, handle, handles[handle], [], lfile, txn)
newhandle = create_handle(precursors, hinfo)
indices[newhandle] = write_diff(co, newhandle, diff, txn)
# update the db accordingly
co.modtimesdb.delete(handle, txn=txn)
co.modtimesdb.put(newhandle, bencode(path.getmtime(lfile)), txn=txn)
co.filenamesdb.put(fname, newhandle, txn=txn)
else:
newhandle = create_handle(precursors, hinfo)
handles[newhandle] = handles[handle]
names[newhandle] = names[handle]
types[newhandle] = types[handle]
del handles[handle]
del adds[handle]
# more db updating
co.staticdb.delete(handle, txn=txn)
#co.staticdb.put(newhandle, bencode({'type': types[handle]}), txn=txn)
db_put(co, co.staticdb, newhandle, {'type': types[handle]}, txn)
nmap[handle] = newhandle
# XXX: clean up allnamesdb
# do reparenting of all the non-added files
for handle in names.keys():
if nmap.has_key(handle):
continue
hinfo = handles[handle]
if hinfo.has_key('delete'):
continue
if nmap.has_key(hinfo['parent']):
hinfo['parent'] = nmap[hinfo['parent']]
if changeset['handles'] == {} and len(changeset['precursors']) == 1:
return None
# fill in a few other pieces of information
if comment is not None:
changeset['comment'] = comment
changeset['user'] = co.user
if tstamp is None:
tstamp = time()
changeset['time'] = int(tstamp)
# put together the changeset and calculate the point
bchangeset = bencode(changeset)
point = sha.new(bchangeset).digest()
# write the file locations of the diffs to the db
for handle, index in indices.items():
write_index(co, point, handle, index, txn)
# write the new change to the db and make it the new head
co.lcrepo.put(point, bchangeset, txn=txn)
co.linforepo.put('heads', bencode([point]), txn=txn)
#co.editsdb.truncate(txn=txn)
return point
def find_commit_files(co, handles):
r = []
for handle, expanded in handles:
lfile = handle_to_filename(co, handle)
if not co.editsdb.has_key(handle):
if not expanded:
print 'warning - %s is not opened for edit' % (lfile,)
continue
linfo = bdecode(co.editsdb.get(handle))
if linfo.has_key('hash'):
if conflicts_in_file(co, lfile):
raise CommitError, 'conflicts in %s, must resolve first' % (lfile,)
r.append((handle, linfo))
return r
syntax highlighted by Code2HTML, v. 0.9.1