From 6078e18aebc335d7032a78b3790f210ecd29b40a Mon Sep 17 00:00:00 2001 From: Bob Hepple Date: Thu, 24 Apr 2025 11:09:30 +1000 Subject: [PATCH 1/5] Add support for SRC vcs --- data/usr/share/man/man1/diffuse.1 | 2 +- src/diffuse/preferences.py | 3 +- src/diffuse/vcs/src.py | 261 ++++++++++++++++++++++++++++++ src/diffuse/vcs/vcs_registry.py | 8 +- 4 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 src/diffuse/vcs/src.py diff --git a/data/usr/share/man/man1/diffuse.1 b/data/usr/share/man/man1/diffuse.1 index 02eaceb7..4249c9af 100644 --- a/data/usr/share/man/man1/diffuse.1 +++ b/data/usr/share/man/man1/diffuse.1 @@ -18,7 +18,7 @@ is a graphical tool for merging and comparing text files\&. Diffuse is able to compare an arbitrary number of files side\-by\-side and gives users the ability to manually adjust line matching and directly edit files\&. Diffuse -can also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS and Subversion repositories for comparison and merging\&. +can also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, SRC and Subversion repositories for comparison and merging\&. .SH "OPTIONS" .SS "Help Options" .PP diff --git a/src/diffuse/preferences.py b/src/diffuse/preferences.py index 750fce58..8c74db02 100644 --- a/src/diffuse/preferences.py +++ b/src/diffuse/preferences.py @@ -149,13 +149,14 @@ def __init__(self, path: str) -> None: ('hg', 'Mercurial', 'hg'), ('mtn', 'Monotone', 'mtn'), ('rcs', 'RCS', None), + ('src', 'SRC', 'src'), ('svn', 'Subversion', 'svn')] vcs_template = [ 'List', [ 'String', 'vcs_search_order', - 'bzr cvs darcs git hg mtn rcs svn', + 'bzr cvs darcs git hg mtn rcs svn src', _('Version control system search order') ] ] diff --git a/src/diffuse/vcs/src.py b/src/diffuse/vcs/src.py new file mode 100644 index 00000000..19de22be --- /dev/null +++ b/src/diffuse/vcs/src.py @@ -0,0 +1,261 @@ +# 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 gettext import gettext as _ +from typing import Optional, Tuple + +from diffuse import utils +from diffuse.preferences import Preferences +from diffuse.vcs.folder_set import FolderSet +from diffuse.vcs.vcs_interface import VcsInterface + + +# SRC support +class Src(VcsInterface): + def __init__(self, root: str): + super().__init__(root) + self.url: Optional[str] = None + + @staticmethod + def _getVcs() -> str: + return 'src' + + @staticmethod + def _getURLPrefix() -> str: + return 'URL: ' + + @staticmethod + def _parseStatusLine(s: str) -> Tuple[str, str]: + # src status prints eg "Mfilename" - as opposed to svn which expands the TAB! + if len(s) < 3 or s[0] not in 'ACDMR': + return '', '' + return s[0], s[2:] + + @staticmethod + def _getPreviousRevision(rev: Optional[str]) -> str: + if rev is None: + return 'BASE' + m = int(rev) + return str(max(m > 1, 0)) + + def _getURL(self, prefs: Preferences) -> Optional[str]: + 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: Preferences, name: str) -> VcsInterface.PathRevisionList: + # 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): + # there's no concept like a 'commit' for src - each file + # has an independent history. So you can either: + + # diffuse -c 14 todo.org + # or + # diffuse -m [files] + + # Nothing else really makes much sense to me so let's assume that if we have a rev + # then we're doing a -m otherwise it's a -c + + # FIXME: removed, added, conflicting files - only modified files work!! + + result = [] + + # build command + vcs = self._getVcs() + vcs_bin, vcs_bash = prefs.getString(vcs + '_bin'), vcs + '_bash' + args = [ vcs_bin, 'status' ] + if rev is None: + if names == [ '.' ]: + names = glob.glob("*") + prev = 'BASE' + else: + prev = None + args.append(names[0]) + + # args = [ vcs_bin, 'diff', '-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 + 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 v == 'D': + # deleted file or directory + # the contents of deleted folders are not reported + # by "src 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, '%s/%s' % (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, '%s/%s' % (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, '%s/%s' % (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: Preferences, name: str, rev: str) -> bytes: + if rev == "BASE": + # get the equivalent of svn's 'BASE': + # 'src list ' fails! Need to assume we're in + # the directory and just use basename()!! + ss = utils.popenReadLines(self.root, [ prefs.getString('src_bin'), 'list', '-1', '-f', '{1}', os.path.basename(name) ], prefs, 'src_bash') + if len(ss) != 1: + raise IOError('Unknown working revision') + rev = ss[0] + + return utils.popenRead(self.root, [ prefs.getString('src_bin'), "cat", rev, os.path.basename(name) ], prefs, 'src_bash') diff --git a/src/diffuse/vcs/vcs_registry.py b/src/diffuse/vcs/vcs_registry.py index 074447d9..36035b41 100644 --- a/src/diffuse/vcs/vcs_registry.py +++ b/src/diffuse/vcs/vcs_registry.py @@ -32,6 +32,7 @@ from diffuse.vcs.mtn import Mtn from diffuse.vcs.rcs import Rcs from diffuse.vcs.svn import Svn +from diffuse.vcs.src import Src class VcsRegistry: @@ -45,7 +46,8 @@ def __init__(self) -> None: 'hg': _get_hg_repo, 'mtn': _get_mtn_repo, 'rcs': _get_rcs_repo, - 'svn': _get_svn_repo + 'svn': _get_svn_repo, + 'src': _get_src_repo } # determines which VCS to use for files in the named folder @@ -165,3 +167,7 @@ def _get_rcs_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: def _get_svn_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: p = _find_parent_dir_with(path, '.svn') return Svn(p) if p else None + +def _get_src_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: + p = _find_parent_dir_with(path, '.src') + return Src(p) if p else None From bdb9757bbe569fbc13c9901ef06eaaffdfb9d797 Mon Sep 17 00:00:00 2001 From: Bob Hepple Date: Thu, 24 Apr 2025 16:46:22 +1000 Subject: [PATCH 2/5] Fix _getPrevRevision; remove some cruft; add some doco --- src/diffuse/vcs/src.py | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/diffuse/vcs/src.py b/src/diffuse/vcs/src.py index 19de22be..4ff87495 100644 --- a/src/diffuse/vcs/src.py +++ b/src/diffuse/vcs/src.py @@ -17,6 +17,11 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# The only use-cases that make sense for src are: +# diffuse -r +# diffuse -c NB a single file +# diffuse -m [ ] + import os import glob @@ -45,17 +50,20 @@ def _getURLPrefix() -> str: @staticmethod def _parseStatusLine(s: str) -> Tuple[str, str]: - # src status prints eg "Mfilename" - as opposed to svn which expands the TAB! - if len(s) < 3 or s[0] not in 'ACDMR': + # src status prints 'M filename' + if len(s) < 3 or s[0] not in 'M': return '', '' - return s[0], s[2:] + k = 3 + if k < len(s) and s[k] == ' ': + k += 1 + return s[0], s[k:] @staticmethod def _getPreviousRevision(rev: Optional[str]) -> str: if rev is None: return 'BASE' m = int(rev) - return str(max(m > 1, 0)) + return str(max(m - 1, 0)) def _getURL(self, prefs: Preferences) -> Optional[str]: if self.url is None: @@ -70,7 +78,6 @@ def _getURL(self, prefs: Preferences) -> Optional[str]: def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRevisionList: # 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*') @@ -86,17 +93,17 @@ def getFileTemplate(self, prefs: Preferences, name: str) -> VcsInterface.PathRev return [ (name, self._getPreviousRevision(None)), (name, None) ] def _getCommitTemplate(self, prefs, rev, names): - # there's no concept like a 'commit' for src - each file - # has an independent history. So you can either: + # there's no concept like a 'commit' for src - each file has + # an independent history with its own version numbers. So, if + # we get here we have either: # diffuse -c 14 todo.org # or # diffuse -m [files] - # Nothing else really makes much sense to me so let's assume that if we have a rev - # then we're doing a -m otherwise it's a -c - - # FIXME: removed, added, conflicting files - only modified files work!! + # Nothing else really makes much sense in src as far as + # diffuse is concerned so let's assume that if we have a rev + # then we're doing a -c otherwise it's a -m result = [] @@ -122,20 +129,13 @@ def _getCommitTemplate(self, prefs, rev, names): # run command 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 v == 'D': - # deleted file or directory - # the contents of deleted folders are not reported - # by "src diff --summarize -c " - removed.add(rel) - elif v == 'A': + if v == 'A': # new file or directory added.add(rel) elif v == 'M': @@ -144,9 +144,6 @@ def _getCommitTemplate(self, prefs, rev, names): 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) From 6eaf9b759aa3c48a1b4d950e8b9bc5476a66996e Mon Sep 17 00:00:00 2001 From: Bob Hepple Date: Thu, 24 Apr 2025 19:17:58 +1000 Subject: [PATCH 3/5] Allow unchanged files to participate in --commit --- src/diffuse/vcs/src.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diffuse/vcs/src.py b/src/diffuse/vcs/src.py index 4ff87495..4639b9aa 100644 --- a/src/diffuse/vcs/src.py +++ b/src/diffuse/vcs/src.py @@ -51,7 +51,7 @@ def _getURLPrefix() -> str: @staticmethod def _parseStatusLine(s: str) -> Tuple[str, str]: # src status prints 'M filename' - if len(s) < 3 or s[0] not in 'M': + if len(s) < 3 or s[0] not in 'M=': return '', '' k = 3 if k < len(s) and s[k] == ' ': @@ -138,7 +138,7 @@ def _getCommitTemplate(self, prefs, rev, names): if v == 'A': # new file or directory added.add(rel) - elif v == 'M': + elif v == 'M' or v == '=': # modified file or merge conflict k = os.path.join(self.root, k) if not isabs: From 279c9eab70ff5989f032c3198f98ef6c157d5909 Mon Sep 17 00:00:00 2001 From: Bob Hepple Date: Fri, 25 Apr 2025 09:57:08 +1000 Subject: [PATCH 4/5] cater for unmodified file under -c --- src/diffuse/vcs/src.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffuse/vcs/src.py b/src/diffuse/vcs/src.py index 4639b9aa..b4f85aa8 100644 --- a/src/diffuse/vcs/src.py +++ b/src/diffuse/vcs/src.py @@ -138,7 +138,7 @@ def _getCommitTemplate(self, prefs, rev, names): if v == 'A': # new file or directory added.add(rel) - elif v == 'M' or v == '=': + elif v == 'M' or (len(names) == 1 and v == '='): # modified file or merge conflict k = os.path.join(self.root, k) if not isabs: From eef5af404e4f71e3e8488f126c88f190d6b26691 Mon Sep 17 00:00:00 2001 From: Bob Hepple Date: Sat, 26 Apr 2025 09:44:07 +1000 Subject: [PATCH 5/5] mention src --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ebc1c38..281f55b8 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Some key features of Diffuse: * Ability to directly edit files * Syntax highlighting * Supports several VCS: [Bazaar][bzr], [CVS][cvs], [Darcs][darcs], [Git][git], - [Mercurial][hg], [Monotone][mtn], [RCS][rcs] and [Subversion][svn] + [Mercurial][hg], [Monotone][mtn], [RCS][rcs], [Subversion][svn] and [Src][src] * Unicode support * Unlimited undo * Easy keyboard navigation @@ -49,6 +49,7 @@ Some key features of Diffuse: [mtn]: https://www.monotone.ca [rcs]: https://www.gnu.org/software/rcs/ [svn]: https://subversion.apache.org +[src]: http://www.catb.org/~esr/src/ ## Documentation