diff --git a/src/main.py b/src/main.py index 3040cf4..201fe55 100644 --- a/src/main.py +++ b/src/main.py @@ -61,6 +61,7 @@ from diffuse.vcs.git import Git from diffuse.vcs.hg import Hg from diffuse.vcs.mtn import Mtn from diffuse.vcs.rcs import Rcs +from diffuse.vcs.svn import Svn if not hasattr(__builtins__, 'WindowsError'): # define 'WindowsError' so 'except' statements will work on all platforms @@ -70,11 +71,6 @@ if not hasattr(__builtins__, 'WindowsError'): # this is sorted based upon frequency to speed up code for stripping whitespace whitespace = ' \t\n\r\x0b\x0c' -# escape special glob characters -def globEscape(s): - m = dict([ (c, f'[{c}]') for c in '[]?*' ]) - return ''.join([ m.get(c, c) for c in s ]) - # colour resources class Colour: def __init__(self, r, g, b, a=1.0): @@ -495,7 +491,7 @@ class Resources: if args[0] == 'import' and len(args) == 2: path = os.path.expanduser(args[1]) # relative paths are relative to the parsed file - path = os.path.join(globEscape(os.path.dirname(file_name)), path) + path = os.path.join(utils.globEscape(os.path.dirname(file_name)), path) paths = glob.glob(path) if len(paths) == 0: paths = [ path ] @@ -1290,242 +1286,10 @@ def _get_rcs_repo(path, prefs): # the user specified an invalid folder name pass -# Subversion support -# SVK support subclasses from this -class _Svn: - def __init__(self, root): - self.root = root - self.url = None - - def _getVcs(self): - return 'svn' - - def _getURLPrefix(self): - return 'URL: ' - - def _parseStatusLine(self, s): - if len(s) < 8 or s[0] not in 'ACDMR': - return - # subversion 1.6 adds a new column - k = 7 - if k < len(s) and s[k] == ' ': - k += 1 - return s[0], s[k:] - - def _getPreviousRevision(self, rev): - if rev is None: - return 'BASE' - m = int(rev) - if m > 1: - return str(m - 1) - - def _getURL(self, prefs): - if self.url is None: - vcs, prefix = self._getVcs(), self._getURLPrefix() - n = len(prefix) - args = [ prefs.getString(vcs + '_bin'), 'info' ] - for s in utils.popenReadLines(self.root, args, prefs, vcs + '_bash'): - if s.startswith(prefix): - self.url = s[n:] - break - return self.url - - def getFileTemplate(self, prefs, name): - # FIXME: verify this - # merge conflict - escaped_name = globEscape(name) - left = glob.glob(escaped_name + '.merge-left.r*') - right = glob.glob(escaped_name + '.merge-right.r*') - if len(left) > 0 and len(right) > 0: - return [ (left[-1], None), (name, None), (right[-1], None) ] - # update conflict - left = sorted(glob.glob(escaped_name + '.r*')) - right = glob.glob(escaped_name + '.mine') - right.extend(glob.glob(escaped_name + '.working')) - if len(left) > 0 and len(right) > 0: - return [ (left[-1], None), (name, None), (right[0], None) ] - # default case - return [ (name, self._getPreviousRevision(None)), (name, None) ] - - def _getCommitTemplate(self, prefs, rev, names): - result = [] - try: - prev = self._getPreviousRevision(rev) - except ValueError: - utils.logError(_('Error parsing revision %s.') % (rev, )) - return result - - # build command - vcs = self._getVcs() - vcs_bin, vcs_bash = prefs.getString(vcs + '_bin'), vcs + '_bash' - if rev is None: - args = [ vcs_bin, 'status', '-q' ] - else: - args = [ vcs_bin, 'diff', '--summarize', '-c', rev ] - # build list of interesting files - pwd, isabs = os.path.abspath(os.curdir), False - for name in names: - isabs |= os.path.isabs(name) - if rev is None: - args.append(utils.safeRelativePath(self.root, name, prefs, vcs + '_cygwin')) - # run command - fs = FolderSet(names) - modified, added, removed = {}, set(), set() - for s in utils.popenReadLines(self.root, args, prefs, vcs_bash): - status = self._parseStatusLine(s) - if status is None: - continue - v, k = status - rel = prefs.convertToNativePath(k) - k = os.path.join(self.root, rel) - if fs.contains(k): - if v == 'D': - # deleted file or directory - # the contents of deleted folders are not reported - # by "svn diff --summarize -c " - removed.add(rel) - elif v == 'A': - # new file or directory - added.add(rel) - elif v == 'M': - # modified file or merge conflict - k = os.path.join(self.root, k) - if not isabs: - k = utils.relpath(pwd, k) - modified[k] = [ (k, prev), (k, rev) ] - elif v == 'C': - # merge conflict - modified[k] = self.getFileTemplate(prefs, k) - elif v == 'R': - # replaced file - removed.add(rel) - added.add(rel) - # look for files in the added items - if rev is None: - m, added = added, {} - for k in m: - if not os.path.isdir(k): - # confirmed as added file - k = os.path.join(self.root, k) - if not isabs: - k = utils.relpath(pwd, k) - added[k] = [ (None, None), (k, None) ] - else: - m = {} - for k in added: - d, b = os.path.dirname(k), os.path.basename(k) - if d not in m: - m[d] = set() - m[d].add(b) - # remove items we can easily determine to be directories - for k in m.keys(): - d = os.path.dirname(k) - if d in m: - m[d].discard(os.path.basename(k)) - if not m[d]: - del m[d] - # determine which are directories - added = {} - for p, v in m.items(): - for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', rev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): - if s in v: - # confirmed as added file - k = os.path.join(self.root, os.path.join(p, s)) - if not isabs: - k = utils.relpath(pwd, k) - added[k] = [ (None, None), (k, rev) ] - # determine if removed items are files or directories - if prev == 'BASE': - m, removed = removed, {} - for k in m: - if not os.path.isdir(k): - # confirmed item as file - k = os.path.join(self.root, k) - if not isabs: - k = utils.relpath(pwd, k) - removed[k] = [ (k, prev), (None, None) ] - else: - m = {} - for k in removed: - d, b = os.path.dirname(k), os.path.basename(k) - if d not in m: - m[d] = set() - m[d].add(b) - removed_dir, removed = set(), {} - for p, v in m.items(): - for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): - if s.endswith('/'): - s = s[:-1] - if s in v: - # confirmed item as directory - removed_dir.add(os.path.join(p, s)) - else: - if s in v: - # confirmed item as file - k = os.path.join(self.root, os.path.join(p, s)) - if not isabs: - k = utils.relpath(pwd, k) - removed[k] = [ (k, prev), (None, None) ] - # recursively find all unreported removed files - while removed_dir: - tmp = removed_dir - removed_dir = set() - for p in tmp: - for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): - if s.endswith('/'): - # confirmed item as directory - removed_dir.add(os.path.join(p, s[:-1])) - else: - # confirmed item as file - k = os.path.join(self.root, os.path.join(p, s)) - if not isabs: - k = utils.relpath(pwd, k) - removed[k] = [ (k, prev), (None, None) ] - # sort the results - r = set() - for m in removed, added, modified: - r.update(m.keys()) - for k in sorted(r): - for m in removed, added, modified: - if k in m: - result.append(m[k]) - return result - - def getCommitTemplate(self, prefs, rev, names): - return self._getCommitTemplate(prefs, rev, names) - - def getFolderTemplate(self, prefs, names): - return self._getCommitTemplate(prefs, None, names) - - def getRevision(self, prefs, name, rev): - vcs_bin = prefs.getString('svn_bin') - if rev in [ 'BASE', 'COMMITTED', 'PREV' ]: - return utils.popenRead( - self.root, - [ - vcs_bin, - 'cat', - '{}@{}'.format(utils.safeRelativePath(self.root, name, prefs, 'svn_cygwin'), rev) - ], - prefs, - 'svn_bash') - return utils.popenRead( - self.root, - [ - vcs_bin, - 'cat', - '{}/{}@{}'.format( - self._getURL(prefs), - utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'), - rev) - ], - prefs, - 'svn_bash') - def _get_svn_repo(path, prefs): p = _find_parent_dir_with(path, '.svn') if p: - return _Svn(p) + return Svn(p) class _Svk(_Svn): def __init__(self, root): diff --git a/src/utils.py b/src/utils.py index 9ab73f2..9428c07 100644 --- a/src/utils.py +++ b/src/utils.py @@ -166,6 +166,11 @@ def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref): s, a = 0, [] return ss +# escape special glob characters +def globEscape(s): + m = dict([ (c, f'[{c}]') for c in '[]?*' ]) + return ''.join([ m.get(c, c) for c in s ]) + # use the program's location as a starting place to search for supporting files # such as icon and help documentation if hasattr(sys, 'frozen'): diff --git a/src/vcs/svn.py b/src/vcs/svn.py new file mode 100644 index 0000000..b6d3ca7 --- /dev/null +++ b/src/vcs/svn.py @@ -0,0 +1,257 @@ +# Diffuse: a graphical tool for merging and comparing text files. +# +# Copyright (C) 2019 Derrick Moser +# Copyright (C) 2021 Romain Failliot +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import glob + +from diffuse import utils +from diffuse.vcs.folder_set import FolderSet +from diffuse.vcs.vcs_interface import VcsInterface + +# Subversion support +# SVK support subclasses from this +class Svn(VcsInterface): + def __init__(self, root): + VcsInterface.__init__(self, root) + self.url = None + + def _getVcs(self): + return 'svn' + + def _getURLPrefix(self): + return 'URL: ' + + def _parseStatusLine(self, s): + if len(s) < 8 or s[0] not in 'ACDMR': + return + # subversion 1.6 adds a new column + k = 7 + if k < len(s) and s[k] == ' ': + k += 1 + return s[0], s[k:] + + def _getPreviousRevision(self, rev): + if rev is None: + return 'BASE' + m = int(rev) + if m > 1: + return str(m - 1) + + def _getURL(self, prefs): + if self.url is None: + vcs, prefix = self._getVcs(), self._getURLPrefix() + n = len(prefix) + args = [ prefs.getString(vcs + '_bin'), 'info' ] + for s in utils.popenReadLines(self.root, args, prefs, vcs + '_bash'): + if s.startswith(prefix): + self.url = s[n:] + break + return self.url + + def getFileTemplate(self, prefs, name): + # FIXME: verify this + # merge conflict + escaped_name = utils.globEscape(name) + left = glob.glob(escaped_name + '.merge-left.r*') + right = glob.glob(escaped_name + '.merge-right.r*') + if len(left) > 0 and len(right) > 0: + return [ (left[-1], None), (name, None), (right[-1], None) ] + # update conflict + left = sorted(glob.glob(escaped_name + '.r*')) + right = glob.glob(escaped_name + '.mine') + right.extend(glob.glob(escaped_name + '.working')) + if len(left) > 0 and len(right) > 0: + return [ (left[-1], None), (name, None), (right[0], None) ] + # default case + return [ (name, self._getPreviousRevision(None)), (name, None) ] + + def _getCommitTemplate(self, prefs, rev, names): + result = [] + try: + prev = self._getPreviousRevision(rev) + except ValueError: + utils.logError(_('Error parsing revision %s.') % (rev, )) + return result + + # build command + vcs = self._getVcs() + vcs_bin, vcs_bash = prefs.getString(vcs + '_bin'), vcs + '_bash' + if rev is None: + args = [ vcs_bin, 'status', '-q' ] + else: + args = [ vcs_bin, 'diff', '--summarize', '-c', rev ] + # build list of interesting files + pwd, isabs = os.path.abspath(os.curdir), False + for name in names: + isabs |= os.path.isabs(name) + if rev is None: + args.append(utils.safeRelativePath(self.root, name, prefs, vcs + '_cygwin')) + # run command + fs = FolderSet(names) + modified, added, removed = {}, set(), set() + for s in utils.popenReadLines(self.root, args, prefs, vcs_bash): + status = self._parseStatusLine(s) + if status is None: + continue + v, k = status + rel = prefs.convertToNativePath(k) + k = os.path.join(self.root, rel) + if fs.contains(k): + if v == 'D': + # deleted file or directory + # the contents of deleted folders are not reported + # by "svn diff --summarize -c " + removed.add(rel) + elif v == 'A': + # new file or directory + added.add(rel) + elif v == 'M': + # modified file or merge conflict + k = os.path.join(self.root, k) + if not isabs: + k = utils.relpath(pwd, k) + modified[k] = [ (k, prev), (k, rev) ] + elif v == 'C': + # merge conflict + modified[k] = self.getFileTemplate(prefs, k) + elif v == 'R': + # replaced file + removed.add(rel) + added.add(rel) + # look for files in the added items + if rev is None: + m, added = added, {} + for k in m: + if not os.path.isdir(k): + # confirmed as added file + k = os.path.join(self.root, k) + if not isabs: + k = utils.relpath(pwd, k) + added[k] = [ (None, None), (k, None) ] + else: + m = {} + for k in added: + d, b = os.path.dirname(k), os.path.basename(k) + if d not in m: + m[d] = set() + m[d].add(b) + # remove items we can easily determine to be directories + for k in m.keys(): + d = os.path.dirname(k) + if d in m: + m[d].discard(os.path.basename(k)) + if not m[d]: + del m[d] + # determine which are directories + added = {} + for p, v in m.items(): + for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', rev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): + if s in v: + # confirmed as added file + k = os.path.join(self.root, os.path.join(p, s)) + if not isabs: + k = utils.relpath(pwd, k) + added[k] = [ (None, None), (k, rev) ] + # determine if removed items are files or directories + if prev == 'BASE': + m, removed = removed, {} + for k in m: + if not os.path.isdir(k): + # confirmed item as file + k = os.path.join(self.root, k) + if not isabs: + k = utils.relpath(pwd, k) + removed[k] = [ (k, prev), (None, None) ] + else: + m = {} + for k in removed: + d, b = os.path.dirname(k), os.path.basename(k) + if d not in m: + m[d] = set() + m[d].add(b) + removed_dir, removed = set(), {} + for p, v in m.items(): + for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): + if s.endswith('/'): + s = s[:-1] + if s in v: + # confirmed item as directory + removed_dir.add(os.path.join(p, s)) + else: + if s in v: + # confirmed item as file + k = os.path.join(self.root, os.path.join(p, s)) + if not isabs: + k = utils.relpath(pwd, k) + removed[k] = [ (k, prev), (None, None) ] + # recursively find all unreported removed files + while removed_dir: + tmp = removed_dir + removed_dir = set() + for p in tmp: + for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): + if s.endswith('/'): + # confirmed item as directory + removed_dir.add(os.path.join(p, s[:-1])) + else: + # confirmed item as file + k = os.path.join(self.root, os.path.join(p, s)) + if not isabs: + k = utils.relpath(pwd, k) + removed[k] = [ (k, prev), (None, None) ] + # sort the results + r = set() + for m in removed, added, modified: + r.update(m.keys()) + for k in sorted(r): + for m in removed, added, modified: + if k in m: + result.append(m[k]) + return result + + def getCommitTemplate(self, prefs, rev, names): + return self._getCommitTemplate(prefs, rev, names) + + def getFolderTemplate(self, prefs, names): + return self._getCommitTemplate(prefs, None, names) + + def getRevision(self, prefs, name, rev): + vcs_bin = prefs.getString('svn_bin') + if rev in [ 'BASE', 'COMMITTED', 'PREV' ]: + return utils.popenRead( + self.root, + [ + vcs_bin, + 'cat', + '{}@{}'.format(utils.safeRelativePath(self.root, name, prefs, 'svn_cygwin'), rev) + ], + prefs, + 'svn_bash') + return utils.popenRead( + self.root, + [ + vcs_bin, + 'cat', + '{}/{}@{}'.format( + self._getURL(prefs), + utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'), + rev) + ], + prefs, + 'svn_bash')