about summary refs log tree commit diff stats
path: root/src/config/preferences.c
Commit message (Expand)AuthorAgeFilesLines
* Have intype on by defaultMichael Vetter2021-05-081-0/+2
* Have separate settings for intypeMichael Vetter2021-05-081-0/+3
* OMEMO - trust mode (#1506)Stefan2021-04-171-0/+5
* Editor: Using preferences compose.editorStefan2021-04-161-0/+5
* MUC: Show offline members in sidebarThorben Günther2021-03-081-0/+3
* Update copyrightMichael Vetter2021-01-081-1/+1
* Add `/executable (urlsave|urlopen)` migrationsWilliam Wennerström2020-12-181-3/+29
* Fix various typosMichael Vetter2020-12-101-1/+1
* Purge omemo sendfile from configMichael Vetter2020-12-091-0/+5
* Remove scheme and filetype matching for url (save|open)William Wennerström2020-12-081-2/+2
* Move unique_filename_from_url functions to commonWilliam Wennerström2020-12-041-1/+0
* Refactor for threaded external executable for built-in download methodsWilliam Wennerström2020-12-031-45/+4
* Use fallback method when /executable urlsave is unsetWilliam Wennerström2020-11-161-2/+0
* Run make format on rebaseWilliam Wennerström2020-11-161-389/+387
* Remove /omemo sendfileWilliam Wennerström2020-11-161-390/+389
* Declare counter var inside loopMichael Vetter2020-11-091-24/+15
* Apply coding styleMichael Vetter2020-07-071-713/+709
* Revert "Apply coding style"Michael Vetter2020-07-071-712/+716
* Apply coding styleMichael Vetter2020-07-071-716/+712
* Remove prefs_free_string()Michael Vetter2020-07-021-11/+5
* Merge pull request #1374 from profanity-im/revampUrlopenMichael Vetter2020-07-021-7/+140
|\
| * Transform url.open.cmd to new default schemeMichael Vetter2020-07-021-1/+1
| * executable: actually take the user set default if a certain scheme isMichael Vetter2020-07-021-6/+11
| * cmd_url_*(): use gchar instead of charMichael Vetter2020-07-021-1/+1
| * Use correct format when transforming old urlopen.cmdMichael Vetter2020-07-021-2/+9
| * use '*' to set a default executablePierre Mazière2020-07-021-11/+24
| * Move url/avatar commands from logging to exectuables sectionMichael Vetter2020-07-011-0/+14
| * prefs_free_string() doesnt need to check if pref is NULLMichael Vetter2020-07-011-4/+1
| * Replace /urlopen with /url and adapt /executablePierre Mazière2020-06-251-3/+16
| * Add string and string list preferences with optionPierre Mazière2020-06-251-1/+85
* | Feature request - XEP-0373: OpenPGP for XMPP (OX)DebXWoody2020-06-291-0/+13
|/
* Fix reading/writing linked filesMichael Vetter2020-06-131-1/+1
* Define POSIX macro to have strdupMichael Vetter2020-06-121-3/+3
* Make option to allow hiding windows with no messages in statusbarMichael Vetter2020-05-211-0/+4
* Make urlopen command configurableMichael Vetter2020-05-201-0/+4
* Remove unanimous MAM displayMichael Vetter2020-04-251-5/+0
* Add hidden MAM setting and trigger MAM retrievel when opening new windowMichael Vetter2020-04-121-0/+3
* Add setting to not colorize own nick according to xep-0392Michael Vetter2020-03-251-0/+4
* titlebar: allow displaying MUC name and MUC jidMichael Vetter2020-03-241-5/+17
* Add slashguard featureMichael Vetter2020-03-181-0/+3
* Enable popular features by defaultMichael Vetter2020-03-171-0/+5
* xep-0084/avatar: add option to open avatar directlyMichael Vetter2020-03-101-0/+5
* Fix few memory leaksDmitry Podgorny2020-02-271-4/+17
* Improve setting encryption char error handlingMichael Vetter2020-02-211-7/+9
* pref: Use helper functions for setting/getting the encryption charMichael Vetter2020-02-211-38/+24
* Allow utf8 symbols as omemo/pgp/otr indicator charMichael Vetter2020-02-201-38/+34
* Add option to color MUC history like regular messagesMichael Vetter2020-02-201-0/+5
* Make /sendfile in PGP session configurableMichael Vetter2020-02-171-0/+3
* Make /sendfile in OTR session configurableMichael Vetter2020-02-171-0/+3
* Make /sendfile in OMEMO session configurableMichael Vetter2020-02-171-0/+3
*/ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# This file is part of ranger, the console file manager.
# License: GNU GPL version 3, see the file "AUTHORS" for details.

"""VCS module"""

from __future__ import (absolute_import, division, print_function)

import os
import subprocess
import threading
import time

from ranger.ext import spawn

# Python 2 compatibility
try:
    import queue
except ImportError:
    import Queue as queue  # pylint: disable=import-error


class VcsError(Exception):
    """VCS exception"""
    pass


class Vcs(object):  # pylint: disable=too-many-instance-attributes
    """
    This class represents a version controlled path, abstracting the usual
    operations from the different supported backends.

    The backends are declared in REPOTYPES, and are derived
    classes from Vcs with the following restrictions:

     * Override ALL interface methods
     * Only override interface methods
     * Do NOT modify internal state. All internal state is handled by Vcs

    """

    # These are abstracted revisions, representing the current index (staged files),
    # the current head and nothing. Every backend should redefine them if the
    # version control has a similar concept, or implement _sanitize_rev method to
    # clean the rev before using them
    INDEX = 'INDEX'
    HEAD = 'HEAD'
    NONE = 'NONE'

    # Backends
    REPOTYPES = {
        'bzr': {'class': 'Bzr', 'setting': 'vcs_backend_bzr'},
        'git': {'class': 'Git', 'setting': 'vcs_backend_git'},
        'hg': {'class': 'Hg', 'setting': 'vcs_backend_hg'},
        'svn': {'class': 'SVN', 'setting': 'vcs_backend_svn'},
    }

    # Possible directory statuses in order of importance
    # statuses that should not be inherited from subpaths are disabled
    DIRSTATUSES = (
        'conflict',
        'untracked',
        'deleted',
        'changed',
        'staged',
        # 'ignored',
        'sync',
        # 'none',
        'unknown',
    )

    def __init__(self, dirobj):
        self.obj = dirobj
        self.path = dirobj.path
        self.repotypes_settings = set(
            repotype for repotype, values in self.REPOTYPES.items()
            if getattr(dirobj.settings, values['setting']) in ('enabled', 'local')
        )

        self.root, self.repodir, self.repotype, self.links = self._find_root(self.path)
        self.is_root = True if self.obj.path == self.root else False
        self.is_root_link = True if self.obj.is_link and self.obj.realpath == self.root else False
        self.is_root_pointer = self.is_root or self.is_root_link
        self.in_repodir = False
        self.rootvcs = None
        self.track = False

        if self.root:
            if self.is_root:
                self.rootvcs = self
                self.__class__ = globals()[self.REPOTYPES[self.repotype]['class'] + 'Root']

                if not os.access(self.repodir, os.R_OK):
                    self.obj.vcsremotestatus = 'unknown'
                    self.obj.vcsstatus = 'unknown'
                    return

                self.track = True
            else:
                self.rootvcs = dirobj.fm.get_directory(self.root).vcs
                if self.rootvcs is None or self.rootvcs.root is None:
                    return
                self.rootvcs.links |= self.links
                self.__class__ = globals()[self.REPOTYPES[self.repotype]['class']]
                self.track = self.rootvcs.track

                if self.path == self.repodir or self.path.startswith(self.repodir + '/'):
                    self.in_repodir = True
                    self.track = False

    # Generic

    def _run(self, args, path=None,  # pylint: disable=too-many-arguments
             catchout=True, retbytes=False, rstrip_newline=True):
        """Run a command"""
        cmd = [self.repotype] + args
        if path is None:
            path = self.path

        try:
            if catchout:
                output = spawn.check_output(cmd, cwd=path, decode=not retbytes)
                if not retbytes and rstrip_newline and output.endswith('\n'):
                    return output[:-1]
                return output
            else:
                with open(os.devnull, mode='w') as fd_devnull:
                    subprocess.check_call(cmd, cwd=path, stdout=fd_devnull, stderr=fd_devnull)
        except (subprocess.CalledProcessError, OSError):
            raise VcsError('{0:s}: {1:s}'.format(str(cmd), path))

    def _get_repotype(self, path):
        """Get type for path"""
        for repotype in self.repotypes_settings:
            repodir = os.path.join(path, '.' + repotype)
            if os.path.exists(repodir):
                return (repodir, repotype)
        return (None, None)

    def _find_root(self, path):
        """Finds root path"""
        links = set()
        while True:
            if os.path.islink(path):
                links.add(path)
                relpath = os.path.relpath(self.path, path)
                path = os.path.realpath(path)
                self.path = os.path.normpath(os.path.join(path, relpath))

            repodir, repotype = self._get_repotype(path)
            if repodir:
                return (path, repodir, repotype, links)

            path_old = path
            path = os.path.dirname(path)
            if path == path_old:
                break

        return (None, None, None, None)

    def reinit(self):
        """Reinit"""
        if not self.in_repodir:
            if not self.track \
                    or (not self.is_root_pointer and self._get_repotype(self.obj.realpath)[0]) \
                    or not os.path.exists(self.repodir):
                self.__init__(self.obj)

    # Action interface

    def action_add(self, filelist):
        """Adds files to the index"""
        raise NotImplementedError

    def action_reset(self, filelist):
        """Removes files from the index"""
        raise NotImplementedError

    # Data interface

    def data_status_root(self):
        """Returns status of self.root cheaply"""
        raise NotImplementedError

    def data_status_subpaths(self):
        """
        Returns a dict indexed by subpaths not in sync with their status as values.
        Paths are given relative to self.root
        """
        raise NotImplementedError

    def data_status_remote(self):
        """
        Returns remote status of repository
        One of ('sync', 'ahead', 'behind', 'diverged', 'none')
        """
        raise NotImplementedError

    def data_branch(self):
        """Returns the current named branch, if this makes sense for the backend. None otherwise"""
        raise NotImplementedError

    def data_info(self, rev=None):
        """Returns info string about revision rev. None in special cases"""
        raise NotImplementedError


class VcsRoot(Vcs):  # pylint: disable=abstract-method
    """Vcs root"""
    rootinit = False
    head = None
    branch = None
    updatetime = None
    status_subpaths = None

    def _status_root(self):
        """Returns root status"""
        if self.status_subpaths is None:
            return 'none'

        statuses = set(status for path, status in self.status_subpaths.items())
        for status in self.DIRSTATUSES:
            if status in statuses:
                return status
        return 'sync'

    def init_root(self):
        """Initialize root cheaply"""
        try:
            self.head = self.data_info(self.HEAD)
            self.branch = self.data_branch()
            self.obj.vcsremotestatus = self.data_status_remote()
            self.obj.vcsstatus = self.data_status_root()
        except VcsError as ex:
            self.obj.fm.notify('VCS Exception: View log for more info', bad=True, exception=ex)
            return False
        self.rootinit = True
        return True

    def update_root(self):
        """Update root state"""
        try:
            self.head = self.data_info(self.HEAD)
            self.branch = self.data_branch()
            self.status_subpaths = self.data_status_subpaths()
            self.obj.vcsremotestatus = self.data_status_remote()
            self.obj.vcsstatus = self._status_root()
        except VcsError as ex:
            self.obj.fm.notify('VCS Exception: View log for more info', bad=True, exception=ex)
            return False
        self.rootinit = True
        self.updatetime = time.time()
        return True

    def _update_walk(self, path, purge):  # pylint: disable=too-many-branches
        """Update walk"""
        for wroot, wdirs, _ in os.walk(path):
            # Only update loaded directories
            try:
                wrootobj = self.obj.fm.directories[wroot]
            except KeyError:
                wdirs[:] = []
                continue
            if not wrootobj.vcs.track:
                wdirs[:] = []
                continue

            if wrootobj.content_loaded:
                has_vcschild = False
                for fsobj in wrootobj.files_all:
                    if purge:
                        if fsobj.is_directory:
                            fsobj.vcsstatus = None
                            fsobj.vcs.__init__(fsobj)
                        else:
                            fsobj.vcsstatus = None
                        continue

                    if fsobj.is_directory:
                        fsobj.vcs.reinit()
                        if not fsobj.vcs.track:
                            continue
                        if fsobj.vcs.is_root_pointer:
                            has_vcschild = True
                        else:
                            fsobj.vcsstatus = self.status_subpath(
                                os.path.join(wrootobj.realpath, fsobj.basename),
                                is_directory=True,
                            )
                    else:
                        fsobj.vcsstatus = self.status_subpath(
                            os.path.join(wrootobj.realpath, fsobj.basename))
                wrootobj.has_vcschild = has_vcschild

            # Remove dead directories
            for wdir in list(wdirs):
                try:
                    wdirobj = self.obj.fm.directories[os.path.join(wroot, wdir)]
                except KeyError:
                    wdirs.remove(wdir)
                    continue
                if not wdirobj.vcs.track or wdirobj.vcs.is_root_pointer:
                    wdirs.remove(wdir)

    def update_tree(self, purge=False):
        """Update tree state"""
        self._update_walk(self.path, purge)
        for path in list(self.links):
            self._update_walk(path, purge)
            try:
                dirobj = self.obj.fm.directories[path]
            except KeyError:
                self.links.remove(path)
                continue
            if purge:
                dirobj.vcsstatus = None
                dirobj.vcs.__init__(dirobj)
            elif dirobj.vcs.path == self.path:
                dirobj.vcsremotestatus = self.obj.vcsremotestatus
                dirobj.vcsstatus = self.obj.vcsstatus
        if purge:
            self.__init__(self.obj)

    def check_outdated(self):
        """Check if root is outdated"""
        if self.updatetime is None:
            return True

        for wroot, wdirs, _ in os.walk(self.path):
            wrootobj = self.obj.fm.get_directory(wroot)
            wrootobj.load_if_outdated()
            if wroot != self.path and wrootobj.vcs.is_root_pointer:
                wdirs[:] = []
                continue

            if wrootobj.stat and self.updatetime < wrootobj.stat.st_mtime:
                return True
            if wrootobj.files_all:
                for wfile in wrootobj.files_all:
                    if wfile.stat and self.updatetime < wfile.stat.st_mtime:
                        return True
        return False

    def status_subpath(self, path, is_directory=False):
        """
        Returns the status of path

        path needs to be self.obj.path or subpath thereof
        """
        if self.status_subpaths is None:
            return 'none'

        relpath = os.path.relpath(path, self.path)

        # check if relpath or its parents has a status
        tmppath = relpath
        while tmppath:
            if tmppath in self.status_subpaths:
                return self.status_subpaths[tmppath]
            tmppath = os.path.dirname(tmppath)

        # check if path contains some file in status
        if is_directory:
            statuses = set(status for subpath, status in self.status_subpaths.items()
                           if subpath.startswith(relpath + '/'))
            for status in self.DIRSTATUSES:
                if status in statuses:
                    return status
        return 'sync'


class VcsThread(threading.Thread):  # pylint: disable=too-many-instance-attributes
    """VCS thread"""

    def __init__(self, ui):
        super(VcsThread, self).__init__()
        self.daemon = True
        self.ui = ui
        self.queue = queue.Queue()
        self._stop = threading.Event()
        self.stopped = threading.Event()
        self.advance = threading.Event()
        self.advance.set()
        self.paused = threading.Event()
        self.awoken = threading.Event()
        self.redraw = False
        self.roots = set()

    def _is_targeted(self, dirobj):
        """Check if dirobj is targeted"""
        if self.ui.browser.main_column and self.ui.browser.main_column.target == dirobj:
            return True
        return False

    def _update_subroots(self, fsobjs):
        """Update subroots"""
        if not fsobjs:
            return False

        has_vcschild = False
        for fsobj in fsobjs:
            if not fsobj.is_directory or not fsobj.vcs or not fsobj.vcs.track:
                continue

            rootvcs = fsobj.vcs.rootvcs
            if fsobj.vcs.is_root_pointer:
                has_vcschild = True
                if not rootvcs.rootinit and not self._is_targeted(rootvcs.obj):
                    self.roots.add(rootvcs.path)
                    if not rootvcs.init_root():
                        rootvcs.update_tree(purge=True)
                    self.redraw = True
                if fsobj.is_link:
                    fsobj.vcsstatus = rootvcs.obj.vcsstatus
                    fsobj.vcsremotestatus = rootvcs.obj.vcsremotestatus
                    self.redraw = True

        return has_vcschild

    def _queue_process(self):  # pylint: disable=too-many-branches
        """Process queue"""
        dirobjs = []
        paths = set()
        self.roots.clear()

        while True:
            try:
                dirobjs.append(self.queue.get(block=False))
            except queue.Empty:
                break

        for dirobj in dirobjs:
            if dirobj.path in paths:
                continue
            paths.add(dirobj.path)

            dirobj.vcs.reinit()
            if dirobj.vcs.track:
                rootvcs = dirobj.vcs.rootvcs
                if rootvcs.path not in self.roots and rootvcs.check_outdated():
                    self.roots.add(rootvcs.path)
                    if rootvcs.update_root():
                        rootvcs.update_tree()
                    else:
                        rootvcs.update_tree(purge=True)
                    self.redraw = True

            has_vcschild = self._update_subroots(dirobj.files_all)

            if dirobj.has_vcschild != has_vcschild:
                dirobj.has_vcschild = has_vcschild
                self.redraw = True

    def run(self):
        while True:
            self.paused.set()
            self.advance.wait()
            self.awoken.wait()
            if self._stop.isSet():
                self.stopped.set()
                return
            if not self.advance.isSet():
                continue
            self.awoken.clear()
            self.paused.clear()

            try:
                self._queue_process()

                if self.redraw:
                    self.redraw = False
                    for column in self.ui.browser.columns:
                        if column.target and column.target.is_directory:
                            column.need_redraw = True
                    self.ui.status.need_redraw = True
                    self.ui.redraw()
            except Exception as ex:  # pylint: disable=broad-except
                self.ui.fm.notify('VCS Exception: View log for more info', bad=True, exception=ex)

    def stop(self):
        """Stop thread synchronously"""
        self._stop.set()
        self.paused.wait()
        self.advance.set()
        self.awoken.set()
        self.stopped.wait()

    def pause(self):
        """Pause thread"""
        self.advance.clear()

    def unpause(self):
        """Unpause thread"""
        self.advance.set()

    def process(self, dirobj):
        """Process dirobj"""
        self.queue.put(dirobj)
        self.awoken.set()


# Backend imports
from .bzr import Bzr  # NOQA pylint: disable=wrong-import-position
from .git import Git  # NOQA pylint: disable=wrong-import-position
from .hg import Hg  # NOQA pylint: disable=wrong-import-position
from .svn import SVN  # NOQA pylint: disable=wrong-import-position


class BzrRoot(VcsRoot, Bzr):
    """Bzr root"""
    pass


class GitRoot(VcsRoot, Git):
    """Git root"""
    pass


class HgRoot(VcsRoot, Hg):
    """Hg root"""
    pass


class SVNRoot(VcsRoot, SVN):
    """SVN root"""
    pass