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.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 <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):
p = _find_parent_dir_with(path, '.svn')
if p:
return _Svn(p)
return Svn(p)
class _Svk(_Svn):
def __init__(self, root):

View File

@ -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'):

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')