Merge pull request #109 from MightyCreak/modularize-vcs

Modularize VCSs
This commit is contained in:
Creak 2021-11-18 14:54:17 -05:00 committed by GitHub
commit 85975f40a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1706 additions and 1317 deletions

View File

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
message message
### Changed ### Changed
- Modularized the VCSs (reducing main.py by around 1300 lines)
### Fixed ### Fixed
- Fixed 'APP_NAME' error when opening non existing file - Fixed 'APP_NAME' error when opening non existing file

View File

@ -31,5 +31,4 @@ gettext.install('diffuse', localedir)
if __name__ == '__main__': if __name__ == '__main__':
from diffuse import main from diffuse import main
sys.exit(main.main()) sys.exit(main.main())

File diff suppressed because it is too large Load Diff

View File

@ -37,3 +37,4 @@ diffuse_sources = [
] ]
install_data(diffuse_sources, install_dir: moduledir) install_data(diffuse_sources, install_dir: moduledir)
install_subdir('vcs', install_dir: moduledir, strip_directory: false)

View File

@ -79,6 +79,42 @@ def make_subdirs(p, ss):
def useFlatpak(): def useFlatpak():
return constants.use_flatpak return constants.use_flatpak
# constructs a relative path from 'a' to 'b', both should be absolute paths
def relpath(a, b):
if isWindows():
if drive_from_path(a) != drive_from_path(b):
return b
c1 = [ c for c in a.split(os.sep) if c != '' ]
c2 = [ c for c in b.split(os.sep) if c != '' ]
i, n = 0, len(c1)
while i < n and i < len(c2) and c1[i] == c2[i]:
i += 1
r = (n - i) * [ os.pardir ]
r.extend(c2[i:])
return os.sep.join(r)
# helper function prevent files from being confused with command line options
# by prepending './' to the basename
def safeRelativePath(abspath1, name, prefs, cygwin_pref):
s = os.path.join(os.curdir, utils.relpath(abspath1, os.path.abspath(name)))
if utils.isWindows():
if prefs.getBool(cygwin_pref):
s = s.replace('\\', '/')
else:
s = s.replace('/', '\\')
return s
# returns the Windows drive or share from a from an absolute path
def drive_from_path(s):
c = s.split(os.sep)
if len(c) > 3 and c[0] == '' and c[1] == '':
return os.path.join(c[:4])
return c[0]
# escape arguments for use with bash
def bashEscape(s):
return "'" + s.replace("'", "'\\''") + "'"
# use popen to read the output of a command # use popen to read the output of a command
def popenRead(dn, cmd, prefs, bash_pref, success_results=None): def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
if success_results is None: if success_results is None:
@ -141,6 +177,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'):

0
src/vcs/__init__.py Normal file
View File

194
src/vcs/bzr.py Normal file
View File

@ -0,0 +1,194 @@
# 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
from diffuse import utils
from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface
# Bazaar support
class Bzr(VcsInterface):
def getFileTemplate(self, prefs, name):
# merge conflict
left = name + '.OTHER'
right = name + '.THIS'
if os.path.isfile(left) and os.path.isfile(right):
return [ (left, None), (name, None), (right, None) ]
# default case
return [ (name, '-1'), (name, None) ]
def getCommitTemplate(self, prefs, rev, names):
# build command
args = [ prefs.getString('bzr_bin'), 'log', '-v', '-r', rev ]
# build list of interesting files
pwd, isabs = os.path.abspath(os.curdir), False
for name in names:
isabs |= os.path.isabs(name)
args.append(utils.safeRelativePath(self.root, name, prefs, 'bzr_cygwin'))
# run command
ss = utils.popenReadLines(self.root, args, prefs, 'bzr_bash')
# parse response
prev = 'before:' + rev
fs = FolderSet(names)
added, modified, removed, renamed = {}, {}, {}, {}
i, n = 0, len(ss)
while i < n:
s = ss[i]
i += 1
if s.startswith('added:'):
# added files
while i < n and ss[i].startswith(' '):
k = prefs.convertToNativePath(ss[i][2:])
i += 1
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
added[k] = [ (None, None), (k, rev) ]
elif s.startswith('modified:'):
# modified files
while i < n and ss[i].startswith(' '):
k = prefs.convertToNativePath(ss[i][2:])
i += 1
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
modified[k] = [ (k, prev), (k, rev) ]
elif s.startswith('removed:'):
# removed files
while i < n and ss[i].startswith(' '):
k = prefs.convertToNativePath(ss[i][2:])
i += 1
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
removed[k] = [ (k, prev), (None, None) ]
elif s.startswith('renamed:'):
# renamed files
while i < n and ss[i].startswith(' '):
k = ss[i][2:].split(' => ')
i += 1
if len(k) == 2:
k0 = prefs.convertToNativePath(k[0])
k1 = prefs.convertToNativePath(k[1])
if not k0.endswith(os.sep) and not k1.endswith(os.sep):
k0 = os.path.join(self.root, k0)
k1 = os.path.join(self.root, k1)
if fs.contains(k0) or fs.contains(k1):
if not isabs:
k0 = utils.relpath(pwd, k0)
k1 = utils.relpath(pwd, k1)
renamed[k1] = [ (k0, prev), (k1, rev) ]
# sort the results
result, r = [], set()
for m in removed, added, modified, renamed:
r.update(m.keys())
for k in sorted(r):
for m in removed, added, modified, renamed:
if k in m:
result.append(m[k])
return result
def getFolderTemplate(self, prefs, names):
# build command
args = [ prefs.getString('bzr_bin'), 'status', '-SV' ]
# build list of interesting files
pwd, isabs = os.path.abspath(os.curdir), False
for name in names:
isabs |= os.path.isabs(name)
args.append(utils.safeRelativePath(self.root, name, prefs, 'bzr_cygwin'))
# run command
prev = '-1'
fs = FolderSet(names)
added, modified, removed, renamed = {}, {}, {}, {}
for s in utils.popenReadLines(self.root, args, prefs, 'bzr_bash'):
# parse response
if len(s) < 5:
continue
y, k = s[1], s[4:]
if y == 'D':
# removed
k = prefs.convertToNativePath(k)
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
removed[k] = [ (k, prev), (None, None) ]
elif y == 'N':
# added
k = prefs.convertToNativePath(k)
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
added[k] = [ (None, None), (k, None) ]
elif y == 'M':
# modified or merge conflict
k = prefs.convertToNativePath(k)
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
modified[k] = self.getFileTemplate(prefs, k)
elif s[0] == 'R':
# renamed
k = k.split(' => ')
if len(k) == 2:
k0 = prefs.convertToNativePath(k[0])
k1 = prefs.convertToNativePath(k[1])
if not k0.endswith(os.sep) and not k1.endswith(os.sep):
k0 = os.path.join(self.root, k0)
k1 = os.path.join(self.root, k1)
if fs.contains(k0) or fs.contains(k1):
if not isabs:
k0 = utils.relpath(pwd, k0)
k1 = utils.relpath(pwd, k1)
renamed[k1] = [ (k0, prev), (k1, None) ]
# sort the results
result, r = [], set()
for m in removed, added, modified, renamed:
r.update(m.keys())
for k in sorted(r):
for m in removed, added, modified, renamed:
if k in m:
result.append(m[k])
return result
def getRevision(self, prefs, name, rev):
return utils.popenRead(
self.root,
[
prefs.getString('bzr_bin'),
'cat',
'--name-from-revision',
'-r',
rev,
utils.safeRelativePath(self.root, name, prefs, 'bzr_cygwin')
],
prefs,
'bzr_bash')

112
src/vcs/cvs.py Normal file
View File

@ -0,0 +1,112 @@
# 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
from diffuse import utils
from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface
# CVS support
class Cvs(VcsInterface):
def getFileTemplate(self, prefs, name):
return [ (name, 'BASE'), (name, None) ]
def getCommitTemplate(self, prefs, rev, names):
result = []
try:
r, prev = rev.split('.'), None
if len(r) > 1:
m = int(r.pop())
if m > 1:
r.append(str(m - 1))
else:
m = int(r.pop())
if len(r):
prev = '.'.join(r)
for k in sorted(names):
if prev is None:
k0 = None
else:
k0 = k
result.append([ (k0, prev), (k, rev) ])
except ValueError:
utils.logError(_('Error parsing revision %s.') % (rev, ))
return result
def getFolderTemplate(self, prefs, names):
# build command
args = [ prefs.getString('cvs_bin'), '-nq', 'update', '-R' ]
# build list of interesting files
pwd, isabs = os.path.abspath(os.curdir), False
for name in names:
isabs |= os.path.isabs(name)
args.append(utils.safeRelativePath(self.root, name, prefs, 'cvs_cygwin'))
# run command
prev = 'BASE'
fs = FolderSet(names)
modified = {}
for s in utils.popenReadLines(self.root, args, prefs, 'cvs_bash'):
# parse response
if len(s) < 3 or s[0] not in 'ACMR':
continue
k = os.path.join(self.root, prefs.convertToNativePath(s[2:]))
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
if s[0] == 'R':
# removed
modified[k] = [ (k, prev), (None, None) ]
pass
elif s[0] == 'A':
# added
modified[k] = [ (None, None), (k, None) ]
else:
# modified
modified[k] = [ (k, prev), (k, None) ]
# sort the results
return [ modified[k] for k in sorted(modified.keys()) ]
def getRevision(self, prefs, name, rev):
if rev == 'BASE' and not os.path.exists(name):
# find revision for removed files
for s in utils.popenReadLines(
self.root,
[
prefs.getString('cvs_bin'),
'status',
utils.safeRelativePath(self.root, name, prefs, 'cvs_cygwin')
],
prefs,
'cvs_bash'):
if s.startswith(' Working revision:\t-'):
rev = s.split('\t')[1][1:]
return utils.popenRead(
self.root,
[
prefs.getString('cvs_bin'),
'-Q',
'update',
'-p',
'-r',
rev,
utils.safeRelativePath(self.root, name, prefs, 'cvs_cygwin')
],
prefs,
'cvs_bash')

144
src/vcs/darcs.py Normal file
View File

@ -0,0 +1,144 @@
# 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
from diffuse import utils
from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface
# Darcs support
class Darcs(VcsInterface):
def getFileTemplate(self, prefs, name):
return [ (name, ''), (name, None) ]
def _getCommitTemplate(self, prefs, names, rev):
mods = (rev is None)
# build command
args = [ prefs.getString('darcs_bin') ]
if mods:
args.extend(['whatsnew', '-s'])
else:
args.extend(['log', '--number', '-s'])
try:
args.extend(['-n', str(int(rev))])
except ValueError:
args.extend(['-h', rev])
# build list of interesting files
pwd, isabs = os.path.abspath(os.curdir), False
for name in names:
isabs |= os.path.isabs(name)
if mods:
args.append(utils.safeRelativePath(self.root, name, prefs, 'darcs_cygwin'))
# run command
# 'darcs whatsnew' will return 1 if there are no changes
ss = utils.popenReadLines(self.root, args, prefs, 'darcs_bash', [0, 1])
# parse response
i, n = 0, len(ss)
if mods:
prev = ''
rev = None
else:
try:
rev = ss[0].split(':')[0]
prev = str(int(rev) + 1)
# skip to the beginning of the summary
while i < n and len(ss[i]):
i += 1
except (ValueError, IndexError):
i = n
fs = FolderSet(names)
added, modified, removed, renamed = {}, {}, {}, {}
while i < n:
s = ss[i]
i += 1
if not mods:
if s.startswith(' '):
s = s[4:]
else:
continue
if len(s) < 2:
continue
x = s[0]
if x == 'R':
# removed
k = prefs.convertToNativePath(s[2:])
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
removed[k] = [ (k, prev), (None, None) ]
elif x == 'A':
# added
k = prefs.convertToNativePath(s[2:])
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
added[k] = [ (None, None), (k, rev) ]
elif x == 'M':
# modified
k = prefs.convertToNativePath(s[2:].split(' ')[0])
if not k.endswith(os.sep):
k = os.path.join(self.root, k)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
if k not in renamed:
modified[k] = [ (k, prev), (k, rev) ]
elif x == ' ':
# renamed
k = s[1:].split(' -> ')
if len(k) == 2:
k0 = prefs.convertToNativePath(k[0])
k1 = prefs.convertToNativePath(k[1])
if not k0.endswith(os.sep):
k0 = os.path.join(self.root, k0)
k1 = os.path.join(self.root, k1)
if fs.contains(k0) or fs.contains(k1):
if not isabs:
k0 = utils.relpath(pwd, k0)
k1 = utils.relpath(pwd, k1)
renamed[k1] = [ (k0, prev), (k1, rev) ]
# sort the results
result, r = [], set()
for m in added, modified, removed, renamed:
r.update(m.keys())
for k in sorted(r):
for m in removed, added, modified, renamed:
if k in m:
result.append(m[k])
return result
def getCommitTemplate(self, prefs, rev, names):
return self._getCommitTemplate(prefs, names, rev)
def getFolderTemplate(self, prefs, names):
return self._getCommitTemplate(prefs, names, None)
def getRevision(self, prefs, name, rev):
args = [ prefs.getString('darcs_bin'), 'show', 'contents' ]
try:
args.extend([ '-n', str(int(rev)) ])
except ValueError:
args.extend([ '-h', rev ])
args.append(utils.safeRelativePath(self.root, name, prefs, 'darcs_cygwin'))
return utils.popenRead(self.root, args, prefs, 'darcs_bash')

45
src/vcs/folder_set.py Normal file
View File

@ -0,0 +1,45 @@
# 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
class FolderSet:
'''Utility class to help support Git and Monotone.
Represents a set of files and folders of interest for "git status" or
"mtn automate inventory."'''
def __init__(self, names):
self.folders = f = []
for name in names:
name = os.path.abspath(name)
# ensure all names end with os.sep
if not name.endswith(os.sep):
name += os.sep
f.append(name)
# returns True if the given abspath is a file that should be included in
# the interesting file subset
def contains(self, abspath):
if not abspath.endswith(os.sep):
abspath += os.sep
for f in self.folders:
if abspath.startswith(f):
return True
return False

155
src/vcs/git.py Normal file
View File

@ -0,0 +1,155 @@
# 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
from diffuse import utils
from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface
# Git support
class Git(VcsInterface):
def getFileTemplate(self, prefs, name):
return [ (name, 'HEAD'), (name, None) ]
def getCommitTemplate(self, prefs, rev, names):
# build command
args = [ prefs.getString('git_bin'), 'show', '--pretty=format:', '--name-status', rev ]
# build list of interesting files
pwd = os.path.abspath(os.curdir)
isabs = False
for name in names:
isabs |= os.path.isabs(name)
# run command
prev = rev + '^'
fs = FolderSet(names)
modified = {}
for s in utils.popenReadLines(self.root, args, prefs, 'git_bash'):
# parse response
if len(s) < 2 or s[0] not in 'ADM':
continue
k = self._extractPath(s[2:], prefs)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
if s[0] == 'D':
# removed
modified[k] = [ (k, prev), (None, None) ]
elif s[0] == 'A':
# added
modified[k] = [ (None, None), (k, rev) ]
else:
# modified
modified[k] = [ (k, prev), (k, rev) ]
# sort the results
return [ modified[k] for k in sorted(modified.keys()) ]
def _extractPath(self, s, prefs):
return os.path.join(self.root, prefs.convertToNativePath(s.strip()))
def getFolderTemplate(self, prefs, names):
# build command
args = [ prefs.getString('git_bin'), 'status', '--porcelain', '-s', '--untracked-files=no', '--ignore-submodules=all' ]
# build list of interesting files
pwd = os.path.abspath(os.curdir)
isabs = False
for name in names:
isabs |= os.path.isabs(name)
# run command
prev = 'HEAD'
fs = FolderSet(names)
modified, renamed = {}, {}
# 'git status' will return 1 when a commit would fail
for s in utils.popenReadLines(self.root, args, prefs, 'git_bash', [0, 1]):
# parse response
if len(s) < 3:
continue
x, y, k = s[0], s[1], s[2:]
if x == 'R':
# renamed
k = k.split(' -> ')
if len(k) == 2:
k0 = self._extractPath(k[0], prefs)
k1 = self._extractPath(k[1], prefs)
if fs.contains(k0) or fs.contains(k1):
if not isabs:
k0 = utils.relpath(pwd, k0)
k1 = utils.relpath(pwd, k1)
renamed[k1] = [ (k0, prev), (k1, None) ]
elif x == 'U' or y == 'U' or (x == 'D' and y == 'D'):
# merge conflict
k = self._extractPath(k, prefs)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
if x == 'D':
panes = [ (None, None) ]
else:
panes = [ (k, ':2') ]
panes.append((k, None))
if y == 'D':
panes.append((None, None))
else:
panes.append((k, ':3'))
if x != 'A' and y != 'A':
panes.append((k, ':1'))
modified[k] = panes
else:
k = self._extractPath(k, prefs)
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
if x == 'A':
# added
panes = [ (None, None) ]
else:
panes = [ (k, prev) ]
# staged changes
if x == 'D':
panes.append((None, None))
elif x != ' ':
panes.append((k, ':0'))
# working copy changes
if y == 'D':
panes.append((None, None))
elif y != ' ':
panes.append((k, None))
modified[k] = panes
# sort the results
result, r = [], set()
for m in modified, renamed:
r.update(m.keys())
for k in sorted(r):
for m in modified, renamed:
if k in m:
result.append(m[k])
return result
def getRevision(self, prefs, name, rev):
return utils.popenRead(
self.root,
[
prefs.getString('git_bin'),
'show',
'{}:{}'.format(
rev,
utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'))
],
prefs,
'git_bash')

100
src/vcs/hg.py Normal file
View File

@ -0,0 +1,100 @@
# 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
from diffuse import utils
from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface
# Mercurial support
class Hg(VcsInterface):
def __init__(self, root):
VcsInterface.__init__(self, root)
self.working_rev = None
def _getPreviousRevision(self, prefs, rev):
if rev is None:
if self.working_rev is None:
ss = utils.popenReadLines(self.root, [ prefs.getString('hg_bin'), 'id', '-i', '-t' ], prefs, 'hg_bash')
if len(ss) != 1:
raise IOError('Unknown working revision')
ss = ss[0].split(' ')
prev = ss[-1]
if len(ss) == 1 and prev.endswith('+'):
# remove local modifications indicator
prev = prev[:-1]
self.working_rev = prev
return self.working_rev
return f'p1({rev})'
def getFileTemplate(self, prefs, name):
return [ (name, self._getPreviousRevision(prefs, None)), (name, None) ]
def _getCommitTemplate(self, prefs, names, cmd, rev):
# build command
args = [ prefs.getString('hg_bin') ]
args.extend(cmd)
# build list of interesting files
pwd, isabs = os.path.abspath(os.curdir), False
for name in names:
isabs |= os.path.isabs(name)
args.append(utils.safeRelativePath(self.root, name, prefs, 'hg_cygwin'))
# run command
prev = self._getPreviousRevision(prefs, rev)
fs = FolderSet(names)
modified = {}
for s in utils.popenReadLines(self.root, args, prefs, 'hg_bash'):
# parse response
if len(s) < 3 or s[0] not in 'AMR':
continue
k = os.path.join(self.root, prefs.convertToNativePath(s[2:]))
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
if s[0] == 'R':
# removed
modified[k] = [ (k, prev), (None, None) ]
elif s[0] == 'A':
# added
modified[k] = [ (None, None), (k, rev) ]
else:
# modified or merge conflict
modified[k] = [ (k, prev), (k, rev) ]
# sort the results
return [ modified[k] for k in sorted(modified.keys()) ]
def getCommitTemplate(self, prefs, rev, names):
return self._getCommitTemplate(prefs, names, [ 'log', '--template', 'A\t{file_adds}\nM\t{file_mods}\nR\t{file_dels}\n', '-r', rev ], rev)
def getFolderTemplate(self, prefs, names):
return self._getCommitTemplate(prefs, names, [ 'status', '-q' ], None)
def getRevision(self, prefs, name, rev):
return utils.popenRead(
self.root,
[
prefs.getString('hg_bin'),
'cat',
'-r',
rev,
utils.safeRelativePath(self.root, name, prefs, 'hg_cygwin')
],
prefs,
'hg_bash')

211
src/vcs/mtn.py Normal file
View File

@ -0,0 +1,211 @@
# 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 shlex
from diffuse import utils
from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface
# Monotone support
class Mtn(VcsInterface):
def getFileTemplate(self, prefs, name):
# FIXME: merge conflicts?
return [ (name, 'h:'), (name, None) ]
def getCommitTemplate(self, prefs, rev, names):
# build command
vcs_bin = prefs.getString('mtn_bin')
ss = utils.popenReadLines(self.root, [ vcs_bin, 'automate', 'select', '-q', rev ], prefs, 'mtn_bash')
if len(ss) != 1:
raise IOError('Ambiguous revision specifier')
args = [ vcs_bin, 'automate', 'get_revision', ss[0] ]
# build list of interesting files
fs = FolderSet(names)
pwd, isabs = os.path.abspath(os.curdir), False
for name in names:
isabs |= os.path.isabs(name)
# run command
prev = None
removed, added, modified, renamed = {}, {}, {}, {}
ss = utils.popenReadLines(self.root, args, prefs, 'mtn_bash')
i = 0
while i < len(ss):
# process results
s = shlex.split(ss[i])
i += 1
if len(s) < 2:
continue
arg, arg1 = s[0], s[1]
if arg == 'old_revision' and len(arg1) > 2:
if prev is not None:
break
prev = arg1[1:-1]
continue
elif prev is None:
continue
if arg == 'delete':
# deleted file
k = os.path.join(self.root, prefs.convertToNativePath(arg1))
if fs.contains(k):
removed[arg1] = k
elif arg == 'add_file':
# new file
k = os.path.join(self.root, prefs.convertToNativePath(arg1))
if fs.contains(k):
added[arg1] = k
elif arg == 'patch':
# modified file
k = os.path.join(self.root, prefs.convertToNativePath(arg1))
if fs.contains(k):
modified[arg1] = k
elif arg == 'rename':
s = shlex.split(ss[i])
i += 1
if len(s) > 1 and s[0] == 'to':
# renamed file
k0 = os.path.join(self.root, prefs.convertToNativePath(arg1))
k1 = os.path.join(self.root, prefs.convertToNativePath(s[1]))
if fs.contains(k0) or fs.contains(k1):
renamed[s[1]] = (arg1, k0, k1)
if removed or renamed:
# remove directories
removed_dirs = set()
for s in utils.popenReadLines(self.root, [ vcs_bin, 'automate', 'get_manifest_of', prev ], prefs, 'mtn_bash'):
s = shlex.split(s)
if len(s) > 1 and s[0] == 'dir':
removed_dirs.add(s[1])
for k in removed_dirs:
for m in removed, modified:
if k in m:
del m[k]
for k, v in renamed.items():
arg1, k0, k1 = v
if arg1 in removed_dirs:
del renamed[k]
# sort results
result, r = [], set()
for m in removed, added, modified, renamed:
r.update(m)
for k in sorted(r):
if k in removed:
k = removed[k]
if not isabs:
k = utils.relpath(pwd, k)
result.append([ (k, prev), (None, None) ])
elif k in added:
k = added[k]
if not isabs:
k = utils.relpath(pwd, k)
result.append([ (None, None), (k, rev) ])
else:
if k in renamed:
arg1, k0, k1 = renamed[k]
else:
k0 = k1 = modified[k]
if not isabs:
k0 = utils.relpath(pwd, k0)
k1 = utils.relpath(pwd, k1)
result.append([ (k0, prev), (k1, rev) ])
return result
def getFolderTemplate(self, prefs, names):
fs = FolderSet(names)
result = []
pwd, isabs = os.path.abspath(os.curdir), False
args = [ prefs.getString('mtn_bin'), 'automate', 'inventory', '--no-ignored', '--no-unchanged', '--no-unknown' ]
for name in names:
isabs |= os.path.isabs(name)
# build list of interesting files
prev = 'h:'
ss = utils.popenReadLines(self.root, args, prefs, 'mtn_bash')
removed, added, modified, renamed = {}, {}, {}, {}
i = 0
while i < len(ss):
# parse properties
m = {}
while i < len(ss):
s = ss[i]
i += 1
# properties are terminated by a blank line
s = shlex.split(s)
if len(s) == 0:
break
m[s[0]] = s[1:]
# scan the list of properties for files that interest us
if len(m.get('path', [])) > 0:
p, s, processed = m['path'][0], m.get('status', []), False
if 'dropped' in s and 'file' in m.get('old_type', []):
# deleted file
k = os.path.join(self.root, prefs.convertToNativePath(p))
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
removed[k] = [ (k, prev), (None, None) ]
processed = True
if 'added' in s and 'file' in m.get('new_type', []):
# new file
k = os.path.join(self.root, prefs.convertToNativePath(p))
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
added[k] = [ (None, None), (k, None) ]
processed = True
if 'rename_target' in s and 'file' in m.get('new_type', []) and len(m.get('old_path', [])) > 0:
# renamed file
k0 = os.path.join(self.root, prefs.convertToNativePath(m['old_path'][0]))
k1 = os.path.join(self.root, prefs.convertToNativePath(p))
if fs.contains(k0) or fs.contains(k1):
if not isabs:
k0 = utils.relpath(pwd, k0)
k1 = utils.relpath(pwd, k1)
renamed[k1] = [ (k0, prev), (k1, None) ]
processed = True
if not processed and 'file' in m.get('fs_type', []):
# modified file or merge conflict
k = os.path.join(self.root, prefs.convertToNativePath(p))
if fs.contains(k):
if not isabs:
k = utils.relpath(pwd, k)
modified[k] = [ (k, prev), (k, None) ]
# sort the results
r = set()
for m in removed, added, modified, renamed:
r.update(m.keys())
for k in sorted(r):
for m in removed, added, modified, renamed:
if k in m:
result.append(m[k])
return result
def getRevision(self, prefs, name, rev):
return utils.popenRead(
self.root,
[
prefs.getString('mtn_bin'),
'automate',
'get_file_of',
'-q',
'-r',
rev,
utils.safeRelativePath(self.root, name, prefs, 'mtn_cygwin')
],
prefs,
'mtn_bash')

129
src/vcs/rcs.py Normal file
View File

@ -0,0 +1,129 @@
# 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
from diffuse import utils
from diffuse.vcs.vcs_interface import VcsInterface
# RCS support
class Rcs(VcsInterface):
def getFileTemplate(self, prefs, name):
args = [ prefs.getString('rcs_bin_rlog'), '-L', '-h', utils.safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ]
rev = ''
for line in utils.popenReadLines(self.root, args, prefs, 'rcs_bash'):
if line.startswith('head: '):
rev = line[6:]
return [ (name, rev), (name, None) ]
def getCommitTemplate(self, prefs, rev, names):
result = []
try:
r, prev = rev.split('.'), None
if len(r) > 1:
m = int(r.pop())
if m > 1:
r.append(str(m - 1))
else:
m = int(r.pop())
if len(r):
prev = '.'.join(r)
for k in sorted(names):
if prev is None:
k0 = None
else:
k0 = k
result.append([ (k0, prev), (k, rev) ])
except ValueError:
utils.logError(_('Error parsing revision %s.') % (rev, ))
return result
def getFolderTemplate(self, prefs, names):
# build command
cmd = [ prefs.getString('rcs_bin_rlog'), '-L', '-h' ]
# build list of interesting files
pwd, isabs = os.path.abspath(os.curdir), False
r = []
for k in names:
if os.path.isdir(k):
# the user specified a folder
n, ex = [ k ], True
while len(n) > 0:
s = n.pop()
recurse = os.path.isdir(os.path.join(s, 'RCS'))
if ex or recurse:
ex = False
for v in os.listdir(s):
dn = os.path.join(s, v)
if v.endswith(',v') and os.path.isfile(dn):
# map to checkout name
r.append(dn[:-2])
elif v == 'RCS' and os.path.isdir(dn):
for v in os.listdir(dn):
if os.path.isfile(os.path.join(dn, v)):
if v.endswith(',v'):
v = v[:-2]
r.append(os.path.join(s, v))
elif recurse and os.path.isdir(dn) and not os.path.islink(dn):
n.append(dn)
else:
# the user specified a file
s = k + ',v'
if os.path.isfile(s):
r.append(k)
continue
s = k.split(os.sep)
s.insert(-1, 'RCS')
# old-style RCS repository
if os.path.isfile(os.sep.join(s)):
r.append(k)
continue
# new-style RCS repository
s[-1] += ',v'
if os.path.isfile(os.sep.join(s)):
r.append(k)
for k in r:
isabs |= os.path.isabs(k)
args = [ utils.safeRelativePath(self.root, k, prefs, 'rcs_cygwin') for k in r ]
# run command
r, k = {}, ''
for line in utils.popenXArgsReadLines(self.root, cmd, args, prefs, 'rcs_bash'):
# parse response
if line.startswith('Working file: '):
k = prefs.convertToNativePath(line[14:])
k = os.path.join(self.root, os.path.normpath(k))
if not isabs:
k = utils.relpath(pwd, k)
elif line.startswith('head: '):
r[k] = line[6:]
# sort the results
return [ [ (k, r[k]), (k, None) ] for k in sorted(r.keys()) ]
def getRevision(self, prefs, name, rev):
return utils.popenRead(
self.root,
[
prefs.getString('rcs_bin_co'),
'-p',
'-q',
'-r' + rev,
utils.safeRelativePath(self.root, name, prefs, 'rcs_cygwin')
],
prefs,
'rcs_bash')

58
src/vcs/svk.py Normal file
View File

@ -0,0 +1,58 @@
# 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.svn import Svn
class Svk(Svn):
def _getVcs(self):
return 'svk'
def _getURLPrefix(self):
return 'Depot Path: '
def _parseStatusLine(self, s):
if len(s) < 4 or s[0] not in 'ACDMR':
return
return s[0], s[4:]
def _getPreviousRevision(self, rev):
if rev is None:
return 'HEAD'
if rev.endswith('@'):
return str(int(rev[:-1]) - 1) + '@'
return str(int(rev) - 1)
def getRevision(self, prefs, name, rev):
return utils.popenRead(
self.root,
[
prefs.getString('svk_bin'),
'cat',
'-r',
rev,
'{}/{}'.format(
self._getURL(prefs),
utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'))
],
prefs,
'svk_bash')

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

39
src/vcs/vcs_interface.py Normal file
View File

@ -0,0 +1,39 @@
# 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.
class VcsInterface:
def __init__(self, root):
"""The object will initialized with the repository's root folder."""
self.root = root
def getFileTemplate(self, prefs, name):
"""Indicates which revisions to display for a file when none were explicitly requested."""
pass
def getCommitTemplate(self, prefs, rev, names):
"""Indicates which file revisions to display for a commit."""
pass
def getFolderTemplate(self, prefs, names):
"""Indicates which file revisions to display for a set of folders."""
def getRevision(self, prefs, name, rev):
"""Returns the contents of the specified file revision"""
pass

216
src/vcs/vcs_registry.py Normal file
View File

@ -0,0 +1,216 @@
# 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
from diffuse import utils
from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.bzr import Bzr
from diffuse.vcs.cvs import Cvs
from diffuse.vcs.darcs import Darcs
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.svk import Svk
from diffuse.vcs.svn import Svn
class VcsRegistry:
def __init__(self):
# initialise the VCS objects
self._get_repo = {
'bzr': _get_bzr_repo,
'cvs': _get_cvs_repo,
'darcs': _get_darcs_repo,
'git': _get_git_repo,
'hg': _get_hg_repo,
'mtn': _get_mtn_repo,
'rcs': _get_rcs_repo,
'svk': _get_svk_repo,
'svn': _get_svn_repo
}
def setSearchOrder(self, ordering):
self._search_order = ordering
# determines which VCS to use for files in the named folder
def findByFolder(self, path, prefs):
path = os.path.abspath(path)
for vcs in prefs.getString('vcs_search_order').split():
if vcs in self._get_repo:
repo = self._get_repo[vcs](path, prefs)
if repo:
return repo
# determines which VCS to use for the named file
def findByFilename(self, name, prefs):
if name is not None:
return self.findByFolder(os.path.dirname(name), prefs)
# utility method to help find folders used by version control systems
def _find_parent_dir_with(path, dir_name):
while True:
name = os.path.join(path, dir_name)
if os.path.isdir(name):
return path
newpath = os.path.dirname(path)
if newpath == path:
break
path = newpath
def _get_bzr_repo(path, prefs):
p = _find_parent_dir_with(path, '.bzr')
if p:
return Bzr(p)
def _get_cvs_repo(path, prefs):
if os.path.isdir(os.path.join(path, 'CVS')):
return Cvs(path)
def _get_darcs_repo(path, prefs):
p = _find_parent_dir_with(path, '_darcs')
if p:
return Darcs(p)
def _get_git_repo(path, prefs):
if 'GIT_DIR' in os.environ:
try:
d = path
ss = utils.popenReadLines(d, [ prefs.getString('git_bin'), 'rev-parse', '--show-prefix' ], prefs, 'git_bash')
if len(ss) > 0:
# be careful to handle trailing slashes
d = d.split(os.sep)
if d[-1] != '':
d.append('')
ss = strip_eol(ss[0]).split('/')
if ss[-1] != '':
ss.append('')
n = len(ss)
if n <= len(d):
del d[-n:]
if len(d) == 0:
d = os.curdir
else:
d = os.sep.join(d)
return Git(d)
except (IOError, OSError, WindowsError):
# working tree not found
pass
# search for .git directory (project) or .git file (submodule)
while True:
name = os.path.join(path, '.git')
if os.path.isdir(name) or os.path.isfile(name):
return Git(path)
newpath = os.path.dirname(path)
if newpath == path:
break
path = newpath
def _get_hg_repo(path, prefs):
p = _find_parent_dir_with(path, '.hg')
if p:
return Hg(p)
def _get_mtn_repo(path, prefs):
p = _find_parent_dir_with(path, '_MTN')
if p:
return Mtn(p)
def _get_rcs_repo(path, prefs):
if os.path.isdir(os.path.join(path, 'RCS')):
return Rcs(path)
# [rfailliot] this code doesn't seem to work, but was in 0.4.8 too.
# I'm letting it here until further tests are done, but it is possible
# this code never actually worked.
try:
for s in os.listdir(path):
if s.endswith(',v') and os.path.isfile(os.path.join(path, s)):
return Rcs(path)
except OSError:
# the user specified an invalid folder name
pass
def _get_svn_repo(path, prefs):
p = _find_parent_dir_with(path, '.svn')
if p:
return Svn(p)
def _get_svk_repo(path, prefs):
name = path
# parse the ~/.svk/config file to discover which directories are part of
# SVK repositories
if utils.isWindows():
name = name.upper()
svkroot = os.environ.get('SVKROOT', None)
if svkroot is None:
svkroot = os.path.expanduser('~/.svk')
svkconfig = os.path.join(svkroot, 'config')
if os.path.isfile(svkconfig):
try:
# find working copies by parsing the config file
f = open(svkconfig, 'r')
ss = readlines(f)
f.close()
projs, sep = [], os.sep
# find the separator character
for s in ss:
if s.startswith(' sep: ') and len(s) > 7:
sep = s[7]
# find the project directories
i = 0
while i < len(ss):
s = ss[i]
i += 1
if s.startswith(' hash: '):
while i < len(ss) and ss[i].startswith(' '):
s = ss[i]
i += 1
if s.endswith(': ') and i < len(ss) and ss[i].startswith(' depotpath: '):
key = s[4:-2].replace(sep, os.sep)
# parse directory path
j, n, tt = 0, len(key), []
while j < n:
if key[j] == '"':
# quoted string
j += 1
while j < n:
if key[j] == '"':
j += 1
break
elif key[j] == '\\':
# escaped character
j += 1
if j < n:
tt.append(key[j])
j += 1
else:
tt.append(key[j])
j += 1
key = ''.join(tt).replace(sep, os.sep)
if utils.isWindows():
key = key.upper()
projs.append(key)
break
# check if the file belongs to one of the project directories
if FolderSet(projs).contains(name):
return Svk(path)
except IOError:
utils.logError(_('Error parsing %s.') % (svkconfig, ))