1from __future__ import print_function 2from __future__ import absolute_import 3# Written by Ross Cohen 4# Maintained by Chris Hutchinson 5# see LICENSE.txt for license information 6 7from builtins import next 8from builtins import str 9from builtins import range 10from future.utils import raise_ 11from .bencode import bdecode, bencode 12from .db import db 13from .history import roothandle, dmerge, rename_conflict_check 14from .history import handle_name_at_point, __handle_name_at_point 15from .history import _handle_name_at_point 16from .history import handle_contents_at_point 17from .history import handle_last_modified 18from .history import _name_use_count, _children_count 19from .history import write_diff, write_index, db_get, db_put, HistoryError 20from .history import clean_merge_point, simplify_precursors, repo_head 21from .history import _is_ancestor 22from .merge import find_resolution 23import os 24from os import path 25from .path import breakup 26import sha 27import stat 28from time import time 29import zlib 30 31class CommitError(Exception): 32 pass 33 34def new_handle(co, txn): 35 """Create a temporary handle for new files in the working copy""" 36 num = bdecode(co.linforepo.get('lasthandle', txn=txn)) + 1 37 handle = sha.new(str(num)).digest() 38 co.linforepo.put('lasthandle', bencode(num), txn=txn) 39 return handle 40 41def create_handle(precursors, hinfo): 42 """Create a permanent identifier for use in a changeset description""" 43 binfo = bencode({'precursors': precursors, 'handle': hinfo}) 44 return sha.new(binfo).digest() 45 46def handle_name(co, handle, txn): 47 """Returns a dict specifying name, parent and other info""" 48 if handle in co.handle_name_cache: 49 return co.handle_name_cache[handle] 50 51 # XXX: reading this repeatedly is excessive 52 heads = bdecode(co.linforepo.get('heads', txn=txn)) 53 einfo = {} 54 if co.editsdb.has_key(handle, txn): 55 einfo = bdecode(co.editsdb.get(handle, txn=txn)) 56 if 'name' in einfo: 57 einfo['points'] = ['1'] 58 for head in heads: 59 pinfo = __handle_name_at_point(co, handle, head, txn) 60 if pinfo is None: 61 continue 62 einfo['points'] = dmerge(einfo['points'], pinfo['points']) 63 einfo['rename point'] = ['1'] 64 co.handle_name_cache[handle] = einfo 65 return einfo 66 state = None 67 for point in heads: 68 pinfo = __handle_name_at_point(co, handle, point, txn) 69 if pinfo is None: 70 continue 71 if 'delete' in pinfo: 72 co.handle_name_cache[handle] = pinfo 73 return pinfo 74 if state is None: 75 state = pinfo 76 continue 77 conflict, rename_points = rename_conflict_check(state, pinfo) 78 if conflict == 'remote': 79 state['name'] = pinfo['name'] 80 state['parent'] = pinfo['parent'] 81 state['rename point'] = rename_points 82 state['points'] = dmerge(state['points'], pinfo['points']) 83 if 'delete' in einfo: 84 state['delete'] = einfo['delete'] 85 co.handle_name_cache[handle] = state 86 return state 87 88def _filename_to_handle(co, parent, name, txn, deletes=0): 89 """Given a parent handle and a name, return all associated handles""" 90 handles = [] 91 cursor = co.allnamesdb.cursor(txn=txn) 92 lookup = parent + name 93 try: 94 key, value = cursor.set(lookup) 95 except (db.DBNotFoundError, TypeError): 96 key = None 97 while key == lookup: 98 hinfo = handle_name(co, value, txn) 99 if hinfo is not None and hinfo['name'] == name and hinfo['parent'] == parent: 100 if 'delete' not in hinfo: 101 handles.append(value) 102 103 elif deletes == 1 and value in co.editsdb: 104 if 'delete' in bdecode(co.editsdb.get(value, txn=txn)): 105 handles.append(value) 106 107 # next_dup() is broken 108 foo = next(cursor) 109 if foo is None: 110 break 111 key, value = foo 112 113 cursor.close() 114 return handles 115 116def filename_to_handle(co, file, txn=None, deletes=0): 117 """Given a full file path, return the associated handle""" 118 if file == '': 119 return roothandle 120 lpath = breakup(file) 121 handle = roothandle 122 for name in lpath: 123 handles = _filename_to_handle(co, handle, name, txn, deletes=deletes) 124 if handles == []: 125 return None 126 handle = handles[0] 127 return handle 128 129def _handle_to_filename(co, handle, names, txn): 130 """Same as handle_to_filename, but takes an override mapping for handles""" 131 lpath, hseen = [], {} 132 while handle != roothandle: 133 if handle in names: 134 info = names[handle] 135 else: 136 info = handle_name(co, handle, txn) 137 if info is None: 138 lpath.insert(0, '???') 139 break 140 if handle in hseen: 141 lpath.insert(0, info['name'] + '(loop)') 142 break 143 lpath.insert(0, info['name']) 144 hseen[handle] = 1 145 handle = info['parent'] 146 try: 147 return path.join(*lpath) 148 except TypeError: 149 pass 150 return '' 151 152def handle_to_filename(co, handle, txn=None): 153 """Convert the given handle to a full path name.""" 154 return _handle_to_filename(co, handle, {}, txn) 155 156def set_edit(co, handle, info, txn): 157 """Specify that a new attribute on a file is being edited.""" 158 if handle in co.editsdb: 159 oinfo = bdecode(co.editsdb.get(handle, txn=txn)) 160 oinfo.update(info) 161 info = oinfo 162 co.editsdb.put(handle, bencode(info), txn=txn) 163 164def unset_edit(co, handle, info, txn): 165 """Remove an edit attribute on a file.""" 166 oinfo = bdecode(co.editsdb.get(handle, txn=txn)) 167 for attr in info: 168 del oinfo[attr] 169 if oinfo == {}: 170 co.editsdb.delete(handle, txn=txn) 171 else: 172 co.editsdb.put(handle, bencode(oinfo), txn=txn) 173 174def _add_file(co, rep, parent, required, ltxn): 175 mode = os.lstat(path.join(co.local, rep)).st_mode 176 handle = filename_to_handle(co, rep) 177 if handle: 178 #info = bdecode(co.staticdb.get(handle)) 179 info = db_get(co, co.staticdb, handle, None) 180 if info['type'] == 'dir' and not stat.S_ISDIR(mode): 181 print('error - %s already added as a %s' % (rep, info['type'])) 182 return None 183 if info['type'] == 'file' and not stat.S_ISREG(mode): 184 print('error - %s already added as a %s' % (rep, info['type'])) 185 return None 186 if required: 187 print('warning - %s already added' % (rep,)) 188 else: 189 print('adding: ' + rep) 190 if stat.S_ISDIR(mode): 191 type = 'dir' 192 elif stat.S_ISREG(mode): 193 type = 'file' 194 else: 195 print('error - unrecognized file type for %s' % (rep,)) 196 return None 197 if rep == '': 198 handle = roothandle 199 else: 200 handle = new_handle(co, ltxn) 201 #co.staticdb.put(handle, bencode({'type': type}), txn=ltxn) 202 db_put(co, co.staticdb, handle, {'type': type}, ltxn) 203 info = {'name': path.split(rep)[1], 'parent': parent, 'add': {}} 204 set_edit(co, handle, info, ltxn) 205 co.allnamesdb.put(parent + info['name'], handle, flags=db.DB_NODUPDATA, txn=ltxn) 206 if type == 'file': 207 co.modtimesdb.put(handle, bencode(0), txn=ltxn) 208 co.filenamesdb.put(rep, handle, txn=ltxn) 209 return handle 210 211def _set_name(co, handle, parent, name, txn): 212 linfo = {'parent': parent, 'name': name} 213 co.handle_name_cache[handle] = linfo 214 215 set_edit(co, handle, linfo, txn) 216 try: 217 co.allnamesdb.put(parent + name, handle, flags=db.DB_NODUPDATA, txn=txn) 218 except db.DBKeyExistError: 219 pass 220 221def name_use_count(co, handle, txn): 222 """Returns a list of handles with the same name as the given handle.""" 223 def _name_func(co, handle, foo, txn): 224 return handle_name(co, handle, txn) 225 return _name_use_count(co, handle_name(co, handle, txn), None, _name_func, txn) 226 227def rename_race(co, handle, names, txn): 228 #def _name_func(co, handle, names, txn): 229 # if names.has_key(handle): 230 # return names[handle] 231 # return None 232 #named = _name_use_count(co, info, names, _name_func, txn) 233 info = handle_name(co, handle, txn) 234 cursor = co.allnamesdb.cursor(txn=txn) 235 lookup = info['parent'] + info['name'] 236 try: 237 key, value = cursor.set(lookup) 238 except (db.DBNotFoundError, TypeError): 239 return None 240 while key == lookup: 241 if value in names: 242 vinfo = names[value] 243 if vinfo['parent'] == info['parent'] and vinfo['name'] == info['name']: 244 cursor.close() 245 return value 246 foo = next(cursor) 247 if foo is None: 248 break 249 key, value = foo 250 cursor.close() 251 return None 252 253def children_count(co, handle, txn, deletes=0): 254 """Returns a list of children of the specified handle.""" 255 def _name_func(co, handle, foo, txn): 256 hinfo = handle_name(co, handle, txn) 257 if hinfo is not None and 'delete' in hinfo and deletes == 1: 258 # if the editsdb has it, then it was just deleted 259 if co.editsdb.has_key(handle, txn): 260 del hinfo['delete'] 261 return hinfo 262 return _children_count(co, handle, None, _name_func, txn) 263 264def _parent_loop_check(co, handle, names, txn): 265 """Same as parent_loop_check, but takes a dict of handle mapping overrides.""" 266 hseen = {} 267 while handle != roothandle: 268 if handle in hseen: 269 return handle 270 hseen[handle] = 1 271 if handle in names: 272 pinfo = names[handle] 273 else: 274 pinfo = handle_name(co, handle, txn) 275 if 'delete' in pinfo: 276 return handle 277 handle = pinfo['parent'] 278 return None 279 280def parent_loop_check(co, handle, txn): 281 """Check whether this handle exists under itself in the directory tree.""" 282 return _parent_loop_check(co, handle, {}, txn) 283 284def _rename_safe_check(co, handle, names, txn): 285 temp = names[handle] 286 del names[handle] 287 result = _parent_loop_check(co, handle, names, txn) 288 names[handle] = temp 289 if result is None: 290 return 1 291 return None 292 293def unique_name(co, parent, file, txn): 294 """Generate a name not already in use by the system.""" 295 if _filename_to_handle(co, parent, file, txn) == []: 296 return file 297 post = 2 298 while _filename_to_handle(co, parent, file + str(post), txn) != []: 299 post = post + 1 300 return file + str(post) 301 302def conflicts_in_file(co, file): 303 h = open(path.join(co.local, file), 'rb') 304 lines = h.read().split('\n') 305 h.close() 306 for l in lines: 307 if l == '<<<<<<< local' or \ 308 l == '=======' or \ 309 l == '>>>>>>> remote': 310 return 1 311 return 0 312 313def find_update_files(co, rhead, named, txn): 314 rnamed = [] 315 for handle in named: 316 linfo = handle_name(co, handle, txn) 317 rnamed.append((handle, linfo, __handle_name_at_point(co, handle, rhead, txn))) 318 return rnamed 319 320def mark_modified_files(co, txn): 321 if 'edit-mode' in co.varsdb: 322 if co.varsdb.get('edit-mode') == '1': 323 return 324 modtimesdb, editsdb, local = co.modtimesdb, co.editsdb, co.local 325 for lfile, handle in co.filenamesdb.items(txn): 326 if editsdb.has_key(handle, txn): 327 info = bdecode(editsdb.get(handle, txn=txn)) 328 if 'hash' in info or 'add' in info: 329 continue 330 #info = bdecode(co.staticdb.get(handle, txn=txn)) 331 info = db_get(co, co.staticdb, handle, txn) 332 if info['type'] != 'file': 333 continue 334 lfile = path.join(local, lfile) 335 try: 336 mtime = path.getmtime(lfile) 337 except OSError: 338 continue 339 minfo = bdecode(modtimesdb.get(handle, txn=txn)) 340 if mtime != minfo: 341 modtimesdb.put(handle, bencode(mtime), txn=txn) 342 set_edit(co, handle, {'hash': 1}, txn) 343 344def gen_diff(co, handle, precursors, lines, txn): 345 pres, plen = simplify_precursors(co, handle, co.contents, precursors, txn) 346 347 file_points = [] 348 for pre, index in pres: 349 info = handle_contents_at_point(co, handle, pre, txn) 350 file_points.append((info['lines'], info['line points'], info['points'])) 351 352 result, ms, newlines = find_resolution(file_points, lines) 353 354 # explanation of conditions: 355 # 1: check for a merge 356 # 2: check if new lines were added by the user 357 # 3: safety for the 4th condition 358 # 4: check if the first match in the first (only) file covers everything 359 if len(pres) > 1 or \ 360 len(newlines) != 0 or \ 361 not len(ms[0]) or \ 362 ms[0][0][2] != len(file_points[0][0]): 363 364 # create a set of correct matches, minus ones which are optimized out 365 matches = [[] for i in range(plen)] 366 i = 0 367 for pre, index in pres: 368 matches[index] = ms[i] 369 i += 1 370 371 return {'matches': matches, 'newlines': newlines} 372 373 return None 374 375def gen_changeset(co, files, comment, repohead, txn, tstamp=None): 376 def per_file_hash(co, handle, hinfo, precursors, lfile, txn): 377 try: 378 h = open(lfile, 'rb') 379 except IOError: 380 raise_(HistoryError, 'Could not open file ' + lfile) 381 lines = h.read().split('\n') 382 h.close() 383 384 dinfo = gen_diff(co, handle, precursors, lines, txn) 385 if 'add' in hinfo: 386 dinfo['add'] = 1 387 if 'delete' in hinfo: 388 dinfo['delete'] = 1 389 try: 390 diff = bencode(dinfo) 391 except ValueError: 392 return None 393 co.modtimesdb.put(handle, bencode(path.getmtime(lfile)), txn=txn) 394 hinfo['hash'] = sha.new(diff).digest() 395 return zlib.compress(diff, 6) 396 397 precursors = bdecode(co.linforepo.get('heads')) 398 399 # include the last known repository head in the list of changes. this is 400 # extra useful info and also forces a merge change which a comment can 401 # then be attached to. 402 # XXX: can do the wrong thing in odd merge-and-not-update cases 403 if repohead is not None and repohead not in precursors: 404 while not _is_ancestor(co, repohead, precursors, txn): 405 info = bdecode(co.lcrepo.get(repohead, txn=txn)) 406 try: 407 repohead = info['precursors'][0] 408 except IndexError: 409 repohead = rootnode 410 411 if repohead not in precursors: 412 precursors.insert(0, repohead) 413 414 changeset = {'precursors': precursors} 415 416 changeset['handles'] = handles = {} 417 adds, nedits, edits, types, names = {}, [], [], {}, {} 418 for handle, linfo in files: 419 if 'add' in linfo: 420 adds[handle] = 1 421 #types[handle] = bdecode(co.staticdb.get(handle))['type'] 422 types[handle] = db_get(co, co.staticdb, handle, None)['type'] 423 424 handles[handle] = cinfo = {} 425 if 'delete' in linfo: 426 assert 'add' not in linfo 427 assert 'hash' not in linfo 428 cinfo['delete'] = 1 429 elif 'name' in linfo or \ 430 'nmerge' in linfo: 431 nedits.append((handle, linfo)) 432 433 if 'add' in linfo: 434 assert 'hash' not in linfo 435 cinfo['add'] = {'type': types[handle]} 436 elif 'hash' in linfo or \ 437 'cmerge' in linfo: 438 assert types[handle] == 'file' 439 edits.append(handle) 440 co.editsdb.delete(handle, txn=txn) 441 442 # generate the name diffs 443 for handle, linfo in nedits: 444 # check if this is really a merge or not 445 # XXX: theoretically we can trust the 'nmerge' flag as set (and 446 # cleared) by _update_helper() 447 merge = False 448 change = prev_change = None 449 for head in precursors: 450 change = handle_last_modified(co, co.names, handle, head, txn) 451 if change is None: 452 continue 453 454 if prev_change is None: 455 prev_change = change 456 continue 457 458 left_anc = _is_ancestor(co, prev_change, [change], txn) 459 right_anc = _is_ancestor(co, change, [prev_change], txn) 460 461 if left_anc: 462 prev_change = change 463 elif not right_anc: 464 merge = True 465 break 466 467 # XXX: sanity check for now, we have to do most of the work anyway 468 assert not (('nmerge' in linfo) ^ merge) 469 470 # no merge, but maybe the user made an explicit change 471 if 'nmerge' not in linfo and change is not None: 472 old_info = handle_name_at_point(co, handle, change, txn, lookup=False) 473 if old_info['name'] == linfo['name'] and \ 474 old_info['parent'] == linfo['parent']: 475 continue 476 477 # looks like we need to include an explicit name change 478 cinfo = handles[handle] 479 hinfo = handle_name(co, handle, txn) 480 cinfo['parent'] = hinfo['parent'] 481 cinfo['name'] = hinfo['name'] 482 names[handle] = cinfo 483 484 # generate the diffs 485 indices = {} 486 for handle in edits: 487 lfile = path.join(co.local, _handle_to_filename(co, handle, names, txn)) 488 diff = per_file_hash(co, handle, handles[handle], precursors, lfile, txn) 489 if diff is None: 490 continue 491 indices[handle] = write_diff(co, handle, diff, txn) 492 493 # clear out things which didn't actually have changes 494 for handle, linfo in files: 495 if handles[handle] == {}: 496 del handles[handle] 497 498 # change all the temporary IDs to permanent, verifiable ones 499 ladds, nmap = list(adds.keys()), {} 500 while len(ladds): 501 handle = ladds.pop() 502 # check if this handle was already dealt with 503 if handle not in adds: 504 continue 505 parent = handles[handle]['parent'] 506 # if the parent was also added, it needs to be renumbered first 507 if parent in adds: 508 ladds.extend((handle, parent)) 509 continue 510 hinfo = handles[handle] 511 # if the parent has been renumbered, pick up the change 512 if parent in nmap: 513 hinfo['parent'] = nmap[parent] 514 # generate the permanent ID 515 if types[handle] == 'file': 516 # generate diffs 517 fname = _handle_to_filename(co, handle, names, txn) 518 lfile = path.join(co.local, fname) 519 diff = per_file_hash(co, handle, handles[handle], [], lfile, txn) 520 newhandle = create_handle(precursors, hinfo) 521 indices[newhandle] = write_diff(co, newhandle, diff, txn) 522 # update the db accordingly 523 co.modtimesdb.delete(handle, txn=txn) 524 co.modtimesdb.put(newhandle, bencode(path.getmtime(lfile)), txn=txn) 525 co.filenamesdb.put(fname, newhandle, txn=txn) 526 else: 527 newhandle = create_handle(precursors, hinfo) 528 handles[newhandle] = handles[handle] 529 names[newhandle] = names[handle] 530 types[newhandle] = types[handle] 531 del handles[handle] 532 del adds[handle] 533 534 # more db updating 535 co.staticdb.delete(handle, txn=txn) 536 #co.staticdb.put(newhandle, bencode({'type': types[handle]}), txn=txn) 537 db_put(co, co.staticdb, newhandle, {'type': types[handle]}, txn) 538 nmap[handle] = newhandle 539 # XXX: clean up allnamesdb 540 541 # do reparenting of all the non-added files 542 for handle in list(names.keys()): 543 if handle in nmap: 544 continue 545 hinfo = handles[handle] 546 if 'delete' in hinfo: 547 continue 548 if hinfo['parent'] in nmap: 549 hinfo['parent'] = nmap[hinfo['parent']] 550 551 if changeset['handles'] == {} and len(changeset['precursors']) == 1: 552 return None 553 554 # fill in a few other pieces of information 555 if comment is not None: 556 changeset['comment'] = comment 557 changeset['user'] = co.user 558 if tstamp is None: 559 tstamp = time() 560 changeset['time'] = int(tstamp) 561 562 # put together the changeset and calculate the point 563 bchangeset = bencode(changeset) 564 point = sha.new(bchangeset).digest() 565 566 # write the file locations of the diffs to the db 567 for handle, index in list(indices.items()): 568 write_index(co, point, handle, index, txn) 569 570 # write the new change to the db and make it the new head 571 co.lcrepo.put(point, bchangeset, txn=txn) 572 co.linforepo.put('heads', bencode([point]), txn=txn) 573 #co.editsdb.truncate(txn=txn) 574 return point 575 576def find_commit_files(co, handles): 577 r = [] 578 for handle, expanded in handles: 579 lfile = handle_to_filename(co, handle) 580 if handle not in co.editsdb: 581 if not expanded: 582 print('warning - %s is not opened for edit' % (lfile,)) 583 continue 584 585 linfo = bdecode(co.editsdb.get(handle)) 586 if 'hash' in linfo: 587 if conflicts_in_file(co, lfile): 588 raise_(CommitError, 'conflicts in %s, must resolve first' % (lfile,)) 589 r.append((handle, linfo)) 590 return r 591