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