Convert Svn VCS

This commit is contained in:
Romain Failliot 2021-11-18 14:12:37 -05:00
parent f314f46309
commit 5d6ece84e1
3 changed files with 265 additions and 239 deletions

View File

@ -61,6 +61,7 @@ from diffuse.vcs.git import Git
from diffuse.vcs.hg import Hg from diffuse.vcs.hg import Hg
from diffuse.vcs.mtn import Mtn from diffuse.vcs.mtn import Mtn
from diffuse.vcs.rcs import Rcs from diffuse.vcs.rcs import Rcs
from diffuse.vcs.svn import Svn
if not hasattr(__builtins__, 'WindowsError'): if not hasattr(__builtins__, 'WindowsError'):
# define 'WindowsError' so 'except' statements will work on all platforms # 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 # this is sorted based upon frequency to speed up code for stripping whitespace
whitespace = ' \t\n\r\x0b\x0c' 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 # colour resources
class Colour: class Colour:
def __init__(self, r, g, b, a=1.0): def __init__(self, r, g, b, a=1.0):
@ -495,7 +491,7 @@ class Resources:
if args[0] == 'import' and len(args) == 2: if args[0] == 'import' and len(args) == 2:
path = os.path.expanduser(args[1]) path = os.path.expanduser(args[1])
# relative paths are relative to the parsed file # 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) paths = glob.glob(path)
if len(paths) == 0: if len(paths) == 0:
paths = [ path ] paths = [ path ]
@ -1290,242 +1286,10 @@ def _get_rcs_repo(path, prefs):
# the user specified an invalid folder name # the user specified an invalid folder name
pass 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 <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, '{}/{}'.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): def _get_svn_repo(path, prefs):
p = _find_parent_dir_with(path, '.svn') p = _find_parent_dir_with(path, '.svn')
if p: if p:
return _Svn(p) return Svn(p)
class _Svk(_Svn): class _Svk(_Svn):
def __init__(self, root): def __init__(self, root):

View File

@ -166,6 +166,11 @@ def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref):
s, a = 0, [] s, a = 0, []
return ss 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 # use the program's location as a starting place to search for supporting files
# such as icon and help documentation # such as icon and help documentation
if hasattr(sys, 'frozen'): if hasattr(sys, 'frozen'):

257
src/vcs/svn.py Normal file
View File

@ -0,0 +1,257 @@
# 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 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 <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, '{}/{}'.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')