Skip to content

Add support for SRC vcs #245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion data/usr/share/man/man1/diffuse.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/diffuse/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
]
]
Expand Down
261 changes: 261 additions & 0 deletions src/diffuse/vcs/src.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# Diffuse: a graphical tool for merging and comparing text files.
#
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.com>
#
# 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 "M<TAB>filename" - 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))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to have the same error as the SVN code has. I reported it in #227.

Copy link
Author

@bhepple bhepple Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm - I had this in my 2018 version (not public, probably derived from the sourceforge code):

+            if m > 1:
+                return str(m - 1)

but I bowed to the svn version when I ported that old cruft to 0.9.0

That said, I think this is only used in conflict resolution which is kinda moot in the way I use SRC (single-user). Probably should be corrected. I wonder why #227 was not fixed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not conflict resolution, it's for getting the previous revision of the passed argument.
Does this mean that you didn't test all the options?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only use-cases that make sense for src are:

# diffuse -r <rev> <file>
# diffuse -c <rev> <file>             ie a single file
# diffuse -m [ <file> ]

... those work in my testing.

Unfortunately, diffuse has bug #246 which obviates "diffuse -r v1 -r v2 " in src as well as git.

I have updated getPrevRevision as suggested. Thanks.


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 <rev>"
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 <fully qualified filename>' 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')
8 changes: 7 additions & 1 deletion src/diffuse/vcs/vcs_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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