Fix subprocess call from within Flatpak

Fixes #103
This commit is contained in:
Romain Failliot 2021-11-16 22:42:29 -05:00
parent 64be61d6b1
commit be40411b66
10 changed files with 163 additions and 123 deletions

View File

@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
### Fixed ### Fixed
- Fixed #103: the flatpak app can now call binaries on the host, such as `git`,
`svn`, etc. (PR #105)
## [0.7.0] - 2021-11-16 ## [0.7.0] - 2021-11-16

View File

@ -2,4 +2,4 @@
# #
# Copyright (C) 2006-2009 Derrick Moser <derrick_moser@yahoo.com> # Copyright (C) 2006-2009 Derrick Moser <derrick_moser@yahoo.com>
import @PKGDATADIR@/syntax/*.syntax import @pkgdatadir@/syntax/*.syntax

View File

@ -33,7 +33,7 @@ endif
# Diffuse config file # Diffuse config file
conf = configuration_data() conf = configuration_data()
conf.set('PKGDATADIR', pkgdatadir) conf.set('pkgdatadir', pkgdatadir)
configure_file( configure_file(
input: 'diffuserc.in', input: 'diffuserc.in',

View File

@ -8,10 +8,13 @@ finish-args:
- --socket=fallback-x11 - --socket=fallback-x11
- --share=ipc - --share=ipc
- --filesystem=home - --filesystem=home
- --talk-name=org.freedesktop.Flatpak
modules: modules:
- name: diffuse - name: diffuse
builddir: true builddir: true
buildsystem: meson buildsystem: meson
config-opts:
- -Duse_flatpak=true
sources: sources:
- type: dir - type: dir
path: . path: .

1
meson_options.txt Normal file
View File

@ -0,0 +1 @@
option('use_flatpak', type: 'boolean', value: false)

27
src/constants.py.in Normal file
View File

@ -0,0 +1,27 @@
# 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.
APP_NAME = 'Diffuse'
VERSION = '@VERSION@'
COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser
{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright"))
WEBSITE = 'https://mightycreak.github.io/diffuse/'
sysconfigdir = '@sysconfigdir@'
use_flatpak = @use_flatpak@

View File

@ -23,10 +23,8 @@ import sys
import gettext import gettext
import locale import locale
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@' pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@' localedir = '@localedir@'
sysconfigdir = '@sysconfigdir@'
sys.path.insert(1, pkgdatadir) sys.path.insert(1, pkgdatadir)
gettext.install('diffuse', localedir) gettext.install('diffuse', localedir)
@ -34,4 +32,4 @@ gettext.install('diffuse', localedir)
if __name__ == '__main__': if __name__ == '__main__':
from diffuse import main from diffuse import main
sys.exit(main.main(VERSION, sysconfigdir)) sys.exit(main.main())

View File

@ -26,7 +26,6 @@ import glob
import re import re
import shlex import shlex
import stat import stat
import subprocess
import unicodedata import unicodedata
import webbrowser import webbrowser
@ -53,6 +52,7 @@ from gi.repository import PangoCairo
from urllib.parse import urlparse from urllib.parse import urlparse
from diffuse import utils from diffuse import utils
from diffuse import constants
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
@ -739,7 +739,7 @@ class Preferences:
[ 'List', [ 'List',
[ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ], [ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ],
[ 'Boolean', 'tabs_always_show', False, _('Always show the tab bar') ], [ 'Boolean', 'tabs_always_show', False, _('Always show the tab bar') ],
[ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % utils.APP_NAME ] [ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % constants.APP_NAME ]
], ],
_('Regional Settings'), _('Regional Settings'),
[ 'List', [ 'List',
@ -900,7 +900,7 @@ class Preferences:
ss.append(f'{k} "{v_escaped}"\n') ss.append(f'{k} "{v_escaped}"\n')
ss.sort() ss.sort()
f = open(self.path, 'w') f = open(self.path, 'w')
f.write(f'# This prefs file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n') f.write(f'# This prefs file was generated by {constants.APP_NAME} {constants.VERSION}.\n\n')
for s in ss: for s in ss:
f.write(s) f.write(s)
f.close() f.close()
@ -1225,66 +1225,6 @@ def safeRelativePath(abspath1, name, prefs, cygwin_pref):
def bashEscape(s): def bashEscape(s):
return "'" + s.replace("'", "'\\''") + "'" return "'" + s.replace("'", "'\\''") + "'"
# use popen to read the output of a command
def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
if success_results is None:
success_results = [ 0 ]
if utils.isWindows() and prefs.getBool(bash_pref):
# launch the command from a bash shell is requested
cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', 'cd {}; {}'.format(bashEscape(dn), ' '.join([ bashEscape(arg) for arg in cmd ])) ]
dn = None
# use subprocess.Popen to retrieve the file contents
if utils.isWindows():
info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = subprocess.SW_HIDE
else:
info = None
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dn, startupinfo=info)
proc.stdin.close()
proc.stderr.close()
fd = proc.stdout
# read the command's output
s = fd.read()
fd.close()
if proc.wait() not in success_results:
raise IOError('Command failed.')
return s
# use popen to read the output of a command
def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None):
return strip_eols(splitlines(popenRead(dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore')))
# simulate use of popen with xargs to read the output of a command
def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref):
# os.sysconf() is only available on Unix
if hasattr(os, 'sysconf'):
maxsize = os.sysconf('SC_ARG_MAX')
maxsize -= sum([ len(k) + len(v) + 2 for k, v in os.environ.items() ])
else:
# assume the Window's limit to CreateProcess()
maxsize = 32767
maxsize -= sum([ len(k) + 1 for k in cmd ])
ss = []
i, s, a = 0, 0, []
while i < len(args):
f = (len(a) == 0)
if f:
# start a new command line
a = cmd[:]
elif s + len(args[i]) + 1 <= maxsize:
f = True
if f:
# append another argument to the current command line
a.append(args[i])
s += len(args[i]) + 1
i += 1
if i == len(args) or not f:
ss.extend(popenReadLines(dn, a, prefs, bash_pref))
s, a = 0, []
return ss
# utility class to help support Git and Monotone # utility class to help support Git and Monotone
# represents a set of files and folders of interest for "git status" or # represents a set of files and folders of interest for "git status" or
# "mtn automate inventory" # "mtn automate inventory"
@ -1361,7 +1301,7 @@ class _Bzr:
isabs |= os.path.isabs(name) isabs |= os.path.isabs(name)
args.append(safeRelativePath(self.root, name, prefs, 'bzr_cygwin')) args.append(safeRelativePath(self.root, name, prefs, 'bzr_cygwin'))
# run command # run command
ss = popenReadLines(self.root, args, prefs, 'bzr_bash') ss = utils.popenReadLines(self.root, args, prefs, 'bzr_bash')
# parse response # parse response
prev = 'before:' + rev prev = 'before:' + rev
fs = _VcsFolderSet(names) fs = _VcsFolderSet(names)
@ -1441,7 +1381,7 @@ class _Bzr:
prev = '-1' prev = '-1'
fs = _VcsFolderSet(names) fs = _VcsFolderSet(names)
added, modified, removed, renamed = {}, {}, {}, {} added, modified, removed, renamed = {}, {}, {}, {}
for s in popenReadLines(self.root, args, prefs, 'bzr_bash'): for s in utils.popenReadLines(self.root, args, prefs, 'bzr_bash'):
# parse response # parse response
if len(s) < 5: if len(s) < 5:
continue continue
@ -1498,7 +1438,7 @@ class _Bzr:
return result return result
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
return popenRead(self.root, [ prefs.getString('bzr_bin'), 'cat', '--name-from-revision', '-r', rev, safeRelativePath(self.root, name, prefs, 'bzr_cygwin') ], prefs, 'bzr_bash') return utils.popenRead(self.root, [ prefs.getString('bzr_bin'), 'cat', '--name-from-revision', '-r', rev, safeRelativePath(self.root, name, prefs, 'bzr_cygwin') ], prefs, 'bzr_bash')
def _get_bzr_repo(path, prefs): def _get_bzr_repo(path, prefs):
p = _find_parent_dir_with(path, '.bzr') p = _find_parent_dir_with(path, '.bzr')
@ -1547,7 +1487,7 @@ class _Cvs:
prev = 'BASE' prev = 'BASE'
fs = _VcsFolderSet(names) fs = _VcsFolderSet(names)
modified = {} modified = {}
for s in popenReadLines(self.root, args, prefs, 'cvs_bash'): for s in utils.popenReadLines(self.root, args, prefs, 'cvs_bash'):
# parse response # parse response
if len(s) < 3 or s[0] not in 'ACMR': if len(s) < 3 or s[0] not in 'ACMR':
continue continue
@ -1571,10 +1511,10 @@ class _Cvs:
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
if rev == 'BASE' and not os.path.exists(name): if rev == 'BASE' and not os.path.exists(name):
# find revision for removed files # find revision for removed files
for s in popenReadLines(self.root, [ prefs.getString('cvs_bin'), 'status', safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash'): for s in utils.popenReadLines(self.root, [ prefs.getString('cvs_bin'), 'status', safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash'):
if s.startswith(' Working revision:\t-'): if s.startswith(' Working revision:\t-'):
rev = s.split('\t')[1][1:] rev = s.split('\t')[1][1:]
return popenRead(self.root, [ prefs.getString('cvs_bin'), '-Q', 'update', '-p', '-r', rev, safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash') return utils.popenRead(self.root, [ prefs.getString('cvs_bin'), '-Q', 'update', '-p', '-r', rev, safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash')
def _get_cvs_repo(path, prefs): def _get_cvs_repo(path, prefs):
if os.path.isdir(os.path.join(path, 'CVS')): if os.path.isdir(os.path.join(path, 'CVS')):
@ -1608,7 +1548,7 @@ class _Darcs:
args.append(safeRelativePath(self.root, name, prefs, 'darcs_cygwin')) args.append(safeRelativePath(self.root, name, prefs, 'darcs_cygwin'))
# run command # run command
# 'darcs whatsnew' will return 1 if there are no changes # 'darcs whatsnew' will return 1 if there are no changes
ss = popenReadLines(self.root, args, prefs, 'darcs_bash', [0, 1]) ss = utils.popenReadLines(self.root, args, prefs, 'darcs_bash', [0, 1])
# parse response # parse response
i, n = 0, len(ss) i, n = 0, len(ss)
if mods: if mods:
@ -1701,7 +1641,7 @@ class _Darcs:
except ValueError: except ValueError:
args.extend([ '-h', rev ]) args.extend([ '-h', rev ])
args.append(safeRelativePath(self.root, name, prefs, 'darcs_cygwin')) args.append(safeRelativePath(self.root, name, prefs, 'darcs_cygwin'))
return popenRead(self.root, args, prefs, 'darcs_bash') return utils.popenRead(self.root, args, prefs, 'darcs_bash')
def _get_darcs_repo(path, prefs): def _get_darcs_repo(path, prefs):
p = _find_parent_dir_with(path, '_darcs') p = _find_parent_dir_with(path, '_darcs')
@ -1727,7 +1667,7 @@ class _Git:
prev = rev + '^' prev = rev + '^'
fs = _VcsFolderSet(names) fs = _VcsFolderSet(names)
modified = {} modified = {}
for s in popenReadLines(self.root, args, prefs, 'git_bash'): for s in utils.popenReadLines(self.root, args, prefs, 'git_bash'):
# parse response # parse response
if len(s) < 2 or s[0] not in 'ADM': if len(s) < 2 or s[0] not in 'ADM':
continue continue
@ -1762,7 +1702,7 @@ class _Git:
fs = _VcsFolderSet(names) fs = _VcsFolderSet(names)
modified, renamed = {}, {} modified, renamed = {}, {}
# 'git status' will return 1 when a commit would fail # 'git status' will return 1 when a commit would fail
for s in popenReadLines(self.root, args, prefs, 'git_bash', [0, 1]): for s in utils.popenReadLines(self.root, args, prefs, 'git_bash', [0, 1]):
# parse response # parse response
if len(s) < 3: if len(s) < 3:
continue continue
@ -1828,13 +1768,13 @@ class _Git:
return result return result
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
return popenRead(self.root, [ prefs.getString('git_bin'), 'show', '{}:{}'.format(rev, relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'git_bash') return utils.popenRead(self.root, [ prefs.getString('git_bin'), 'show', '{}:{}'.format(rev, relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'git_bash')
def _get_git_repo(path, prefs): def _get_git_repo(path, prefs):
if 'GIT_DIR' in os.environ: if 'GIT_DIR' in os.environ:
try: try:
d = path d = path
ss = popenReadLines(d, [ prefs.getString('git_bin'), 'rev-parse', '--show-prefix' ], prefs, 'git_bash') ss = utils.popenReadLines(d, [ prefs.getString('git_bin'), 'rev-parse', '--show-prefix' ], prefs, 'git_bash')
if len(ss) > 0: if len(ss) > 0:
# be careful to handle trailing slashes # be careful to handle trailing slashes
d = d.split(os.sep) d = d.split(os.sep)
@ -1873,7 +1813,7 @@ class _Hg:
def _getPreviousRevision(self, prefs, rev): def _getPreviousRevision(self, prefs, rev):
if rev is None: if rev is None:
if self.working_rev is None: if self.working_rev is None:
ss = popenReadLines(self.root, [ prefs.getString('hg_bin'), 'id', '-i', '-t' ], prefs, 'hg_bash') ss = utils.popenReadLines(self.root, [ prefs.getString('hg_bin'), 'id', '-i', '-t' ], prefs, 'hg_bash')
if len(ss) != 1: if len(ss) != 1:
raise IOError('Unknown working revision') raise IOError('Unknown working revision')
ss = ss[0].split(' ') ss = ss[0].split(' ')
@ -1901,7 +1841,7 @@ class _Hg:
prev = self._getPreviousRevision(prefs, rev) prev = self._getPreviousRevision(prefs, rev)
fs = _VcsFolderSet(names) fs = _VcsFolderSet(names)
modified = {} modified = {}
for s in popenReadLines(self.root, args, prefs, 'hg_bash'): for s in utils.popenReadLines(self.root, args, prefs, 'hg_bash'):
# parse response # parse response
if len(s) < 3 or s[0] not in 'AMR': if len(s) < 3 or s[0] not in 'AMR':
continue continue
@ -1928,7 +1868,7 @@ class _Hg:
return self._getCommitTemplate(prefs, names, [ 'status', '-q' ], None) return self._getCommitTemplate(prefs, names, [ 'status', '-q' ], None)
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
return popenRead(self.root, [ prefs.getString('hg_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'hg_cygwin') ], prefs, 'hg_bash') return utils.popenRead(self.root, [ prefs.getString('hg_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'hg_cygwin') ], prefs, 'hg_bash')
def _get_hg_repo(path, prefs): def _get_hg_repo(path, prefs):
p = _find_parent_dir_with(path, '.hg') p = _find_parent_dir_with(path, '.hg')
@ -1947,7 +1887,7 @@ class _Mtn:
def getCommitTemplate(self, prefs, rev, names): def getCommitTemplate(self, prefs, rev, names):
# build command # build command
vcs_bin = prefs.getString('mtn_bin') vcs_bin = prefs.getString('mtn_bin')
ss = popenReadLines(self.root, [ vcs_bin, 'automate', 'select', '-q', rev ], prefs, 'mtn_bash') ss = utils.popenReadLines(self.root, [ vcs_bin, 'automate', 'select', '-q', rev ], prefs, 'mtn_bash')
if len(ss) != 1: if len(ss) != 1:
raise IOError('Ambiguous revision specifier') raise IOError('Ambiguous revision specifier')
args = [ vcs_bin, 'automate', 'get_revision', ss[0] ] args = [ vcs_bin, 'automate', 'get_revision', ss[0] ]
@ -1959,7 +1899,7 @@ class _Mtn:
# run command # run command
prev = None prev = None
removed, added, modified, renamed = {}, {}, {}, {} removed, added, modified, renamed = {}, {}, {}, {}
ss = popenReadLines(self.root, args, prefs, 'mtn_bash') ss = utils.popenReadLines(self.root, args, prefs, 'mtn_bash')
i = 0 i = 0
while i < len(ss): while i < len(ss):
# process results # process results
@ -2002,7 +1942,7 @@ class _Mtn:
if removed or renamed: if removed or renamed:
# remove directories # remove directories
removed_dirs = set() removed_dirs = set()
for s in popenReadLines(self.root, [ vcs_bin, 'automate', 'get_manifest_of', prev ], prefs, 'mtn_bash'): for s in utils.popenReadLines(self.root, [ vcs_bin, 'automate', 'get_manifest_of', prev ], prefs, 'mtn_bash'):
s = shlex.split(s) s = shlex.split(s)
if len(s) > 1 and s[0] == 'dir': if len(s) > 1 and s[0] == 'dir':
removed_dirs.add(s[1]) removed_dirs.add(s[1])
@ -2049,7 +1989,7 @@ class _Mtn:
isabs |= os.path.isabs(name) isabs |= os.path.isabs(name)
# build list of interesting files # build list of interesting files
prev = 'h:' prev = 'h:'
ss = popenReadLines(self.root, args, prefs, 'mtn_bash') ss = utils.popenReadLines(self.root, args, prefs, 'mtn_bash')
removed, added, modified, renamed = {}, {}, {}, {} removed, added, modified, renamed = {}, {}, {}, {}
i = 0 i = 0
while i < len(ss): while i < len(ss):
@ -2110,7 +2050,7 @@ class _Mtn:
return result return result
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
return popenRead(self.root, [ prefs.getString('mtn_bin'), 'automate', 'get_file_of', '-q', '-r', rev, safeRelativePath(self.root, name, prefs, 'mtn_cygwin') ], prefs, 'mtn_bash') return utils.popenRead(self.root, [ prefs.getString('mtn_bin'), 'automate', 'get_file_of', '-q', '-r', rev, safeRelativePath(self.root, name, prefs, 'mtn_cygwin') ], prefs, 'mtn_bash')
def _get_mtn_repo(path, prefs): def _get_mtn_repo(path, prefs):
p = _find_parent_dir_with(path, '_MTN') p = _find_parent_dir_with(path, '_MTN')
@ -2125,7 +2065,7 @@ class _Rcs:
def getFileTemplate(self, prefs, name): def getFileTemplate(self, prefs, name):
args = [ prefs.getString('rcs_bin_rlog'), '-L', '-h', safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ] args = [ prefs.getString('rcs_bin_rlog'), '-L', '-h', safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ]
rev = '' rev = ''
for line in popenReadLines(self.root, args, prefs, 'rcs_bash'): for line in utils.popenReadLines(self.root, args, prefs, 'rcs_bash'):
if line.startswith('head: '): if line.startswith('head: '):
rev = line[6:] rev = line[6:]
return [ (name, rev), (name, None) ] return [ (name, rev), (name, None) ]
@ -2201,7 +2141,7 @@ class _Rcs:
args = [ safeRelativePath(self.root, k, prefs, 'rcs_cygwin') for k in r ] args = [ safeRelativePath(self.root, k, prefs, 'rcs_cygwin') for k in r ]
# run command # run command
r, k = {}, '' r, k = {}, ''
for line in popenXArgsReadLines(self.root, cmd, args, prefs, 'rcs_bash'): for line in utils.popenXArgsReadLines(self.root, cmd, args, prefs, 'rcs_bash'):
# parse response # parse response
if line.startswith('Working file: '): if line.startswith('Working file: '):
k = prefs.convertToNativePath(line[14:]) k = prefs.convertToNativePath(line[14:])
@ -2214,7 +2154,7 @@ class _Rcs:
return [ [ (k, r[k]), (k, None) ] for k in sorted(r.keys()) ] return [ [ (k, r[k]), (k, None) ] for k in sorted(r.keys()) ]
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
return popenRead(self.root, [ prefs.getString('rcs_bin_co'), '-p', '-q', '-r' + rev, safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ], prefs, 'rcs_bash') return utils.popenRead(self.root, [ prefs.getString('rcs_bin_co'), '-p', '-q', '-r' + rev, safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ], prefs, 'rcs_bash')
def _get_rcs_repo(path, prefs): def _get_rcs_repo(path, prefs):
if os.path.isdir(os.path.join(path, 'RCS')): if os.path.isdir(os.path.join(path, 'RCS')):
@ -2265,7 +2205,7 @@ class _Svn:
vcs, prefix = self._getVcs(), self._getURLPrefix() vcs, prefix = self._getVcs(), self._getURLPrefix()
n = len(prefix) n = len(prefix)
args = [ prefs.getString(vcs + '_bin'), 'info' ] args = [ prefs.getString(vcs + '_bin'), 'info' ]
for s in popenReadLines(self.root, args, prefs, vcs + '_bash'): for s in utils.popenReadLines(self.root, args, prefs, vcs + '_bash'):
if s.startswith(prefix): if s.startswith(prefix):
self.url = s[n:] self.url = s[n:]
break break
@ -2312,7 +2252,7 @@ class _Svn:
# run command # run command
fs = _VcsFolderSet(names) fs = _VcsFolderSet(names)
modified, added, removed = {}, set(), set() modified, added, removed = {}, set(), set()
for s in popenReadLines(self.root, args, prefs, vcs_bash): for s in utils.popenReadLines(self.root, args, prefs, vcs_bash):
status = self._parseStatusLine(s) status = self._parseStatusLine(s)
if status is None: if status is None:
continue continue
@ -2368,7 +2308,7 @@ class _Svn:
# determine which are directories # determine which are directories
added = {} added = {}
for p, v in m.items(): for p, v in m.items():
for s in popenReadLines(self.root, [ vcs_bin, 'list', '-r', rev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): 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: if s in v:
# confirmed as added file # confirmed as added file
k = os.path.join(self.root, os.path.join(p, s)) k = os.path.join(self.root, os.path.join(p, s))
@ -2394,7 +2334,7 @@ class _Svn:
m[d].add(b) m[d].add(b)
removed_dir, removed = set(), {} removed_dir, removed = set(), {}
for p, v in m.items(): for p, v in m.items():
for s in popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): 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('/'): if s.endswith('/'):
s = s[:-1] s = s[:-1]
if s in v: if s in v:
@ -2412,7 +2352,7 @@ class _Svn:
tmp = removed_dir tmp = removed_dir
removed_dir = set() removed_dir = set()
for p in tmp: for p in tmp:
for s in popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): 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('/'): if s.endswith('/'):
# confirmed item as directory # confirmed item as directory
removed_dir.add(os.path.join(p, s[:-1])) removed_dir.add(os.path.join(p, s[:-1]))
@ -2441,8 +2381,8 @@ class _Svn:
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
vcs_bin = prefs.getString('svn_bin') vcs_bin = prefs.getString('svn_bin')
if rev in [ 'BASE', 'COMMITTED', 'PREV' ]: if rev in [ 'BASE', 'COMMITTED', 'PREV' ]:
return popenRead(self.root, [ vcs_bin, 'cat', '{}@{}'.format(safeRelativePath(self.root, name, prefs, 'svn_cygwin'), rev) ], prefs, 'svn_bash') return utils.popenRead(self.root, [ vcs_bin, 'cat', '{}@{}'.format(safeRelativePath(self.root, name, prefs, 'svn_cygwin'), rev) ], prefs, 'svn_bash')
return popenRead(self.root, [ vcs_bin, 'cat', '{}/{}@{}'.format(self._getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'), rev) ], prefs, 'svn_bash') return utils.popenRead(self.root, [ vcs_bin, 'cat', '{}/{}@{}'.format(self._getURL(prefs), 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')
@ -2472,7 +2412,7 @@ class _Svk(_Svn):
return str(int(rev) - 1) return str(int(rev) - 1)
def getRevision(self, prefs, name, rev): def getRevision(self, prefs, name, rev):
return popenRead(self.root, [ prefs.getString('svk_bin'), 'cat', '-r', rev, '{}/{}'.format(self._getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'svk_bash') return utils.popenRead(self.root, [ prefs.getString('svk_bin'), 'cat', '-r', rev, '{}/{}'.format(self._getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'svk_bash')
def _get_svk_repo(path, prefs): def _get_svk_repo(path, prefs):
name = path name = path
@ -6548,7 +6488,7 @@ class SearchDialog(Gtk.Dialog):
# convenience method to request confirmation when closing the last tab # convenience method to request confirmation when closing the last tab
def confirmTabClose(parent): def confirmTabClose(parent):
dialog = utils.MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (utils.APP_NAME, )) dialog = utils.MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (constants.APP_NAME, ))
end = (dialog.run() == Gtk.ResponseType.OK) end = (dialog.run() == Gtk.ResponseType.OK)
dialog.destroy() dialog.destroy()
return end return end
@ -6642,17 +6582,17 @@ class AboutDialog(Gtk.AboutDialog):
def __init__(self): def __init__(self):
Gtk.AboutDialog.__init__(self) Gtk.AboutDialog.__init__(self)
self.set_logo_icon_name('io.github.mightycreak.Diffuse') self.set_logo_icon_name('io.github.mightycreak.Diffuse')
self.set_program_name(utils.APP_NAME) self.set_program_name(constants.APP_NAME)
self.set_version(utils.VERSION) self.set_version(constants.VERSION)
self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.')) self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.'))
self.set_copyright(utils.COPYRIGHT) self.set_copyright(constants.COPYRIGHT)
self.set_website(utils.WEBSITE) self.set_website(constants.WEBSITE)
self.set_authors([ 'Derrick Moser <derrick_moser@yahoo.com>', self.set_authors([ 'Derrick Moser <derrick_moser@yahoo.com>',
'Romain Failliot <romain.failliot@foolstep.com>' ]) 'Romain Failliot <romain.failliot@foolstep.com>' ])
self.set_translator_credits(_('translator-credits')) self.set_translator_credits(_('translator-credits'))
license_text = [ license_text = [
utils.APP_NAME + ' ' + utils.VERSION + '\n\n', constants.APP_NAME + ' ' + constants.VERSION + '\n\n',
utils.COPYRIGHT + '\n\n', constants.COPYRIGHT + '\n\n',
_('''This program is free software; you can redistribute it and/or modify _('''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 it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or the Free Software Foundation; either version 2 of the License, or
@ -6898,7 +6838,7 @@ class Diffuse(Gtk.Window):
if self.headers[f].has_edits: if self.headers[f].has_edits:
# warn users of any unsaved changes they might lose # warn users of any unsaved changes they might lose
dialog = Gtk.MessageDialog(self.get_toplevel(), Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _('Save changes before loading the new file?')) dialog = Gtk.MessageDialog(self.get_toplevel(), Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _('Save changes before loading the new file?'))
dialog.set_title(utils.APP_NAME) dialog.set_title(constants.APP_NAME)
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT) dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT)
dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK) dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK)
@ -7444,7 +7384,7 @@ class Diffuse(Gtk.Window):
menuspecs.append([ _('_Help'), [ menuspecs.append([ _('_Help'), [
[_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'], [_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'],
[], [],
[_('_About %s...') % (utils.APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ]) [_('_About %s...') % (constants.APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ])
# used to disable menu events when switching tabs # used to disable menu events when switching tabs
self.menu_update_depth = 0 self.menu_update_depth = 0
@ -7561,7 +7501,7 @@ class Diffuse(Gtk.Window):
ss.append(f'{k} {v}\n') ss.append(f'{k} {v}\n')
ss.sort() ss.sort()
f = open(statepath, 'w') f = open(statepath, 'w')
f.write(f"# This state file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n") f.write(f"# This state file was generated by {constants.APP_NAME} {constants.VERSION}.\n\n")
for s in ss: for s in ss:
f.write(s) f.write(s)
f.close() f.close()
@ -7601,7 +7541,7 @@ class Diffuse(Gtk.Window):
buttons=Gtk.ButtonsType.NONE, buttons=Gtk.ButtonsType.NONE,
text=_('Some files have unsaved changes. Select the files to save before closing.')) text=_('Some files have unsaved changes. Select the files to save before closing.'))
dialog.set_resizable(True) dialog.set_resizable(True)
dialog.set_title(utils.APP_NAME) dialog.set_title(constants.APP_NAME)
# add list of files with unsaved changes # add list of files with unsaved changes
sw = Gtk.ScrolledWindow.new() sw = Gtk.ScrolledWindow.new()
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
@ -7688,7 +7628,7 @@ class Diffuse(Gtk.Window):
# update window's title # update window's title
def updateTitle(self, viewer): def updateTitle(self, viewer):
title = self.notebook.get_tab_label(viewer).get_text() title = self.notebook.get_tab_label(viewer).get_text()
self.set_title(f'{title} - {utils.APP_NAME}') self.set_title(f'{title} - {constants.APP_NAME}')
# update the message in the status bar # update the message in the status bar
def setStatus(self, s): def setStatus(self, s):
@ -8172,7 +8112,7 @@ class Diffuse(Gtk.Window):
del parts[-1] del parts[-1]
if help_url is None: if help_url is None:
# no local help file is available, show on-line help # no local help file is available, show on-line help
help_url = utils.WEBSITE + 'manual.html' help_url = constants.WEBSITE + 'manual.html'
# ask for localised manual # ask for localised manual
if utils.lang is not None: if utils.lang is not None:
help_url += '?lang=' + utils.lang help_url += '?lang=' + utils.lang
@ -8193,17 +8133,15 @@ GObject.signal_new('reload', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFl
GObject.signal_new('save', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) GObject.signal_new('save', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
GObject.signal_new('save-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) GObject.signal_new('save-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
def main(version, sysconfigdir): def main():
# app = Application() # app = Application()
# return app.run(sys.argv) # return app.run(sys.argv)
utils.VERSION = version
args = sys.argv args = sys.argv
argc = len(args) argc = len(args)
if argc == 2 and args[1] in [ '-v', '--version' ]: if argc == 2 and args[1] in [ '-v', '--version' ]:
print('%s %s\n%s' % (utils.APP_NAME, utils.VERSION, utils.COPYRIGHT)) print('%s %s\n%s' % (constants.APP_NAME, constants.VERSION, constants.COPYRIGHT))
sys.exit(0) sys.exit(0)
if argc == 2 and args[1] in [ '-h', '-?', '--help' ]: if argc == 2 and args[1] in [ '-h', '-?', '--help' ]:
print(_('''Usage: print(_('''Usage:
@ -8270,7 +8208,7 @@ Display Options:
if utils.isWindows(): if utils.isWindows():
rc_file = os.path.join(utils.bin_dir, 'diffuserc') rc_file = os.path.join(utils.bin_dir, 'diffuserc')
else: else:
rc_file = os.path.join(utils.bin_dir, f'{sysconfigdir}/diffuserc') rc_file = os.path.join(utils.bin_dir, f'{constants.sysconfigdir}/diffuserc')
for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'): for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'):
if os.path.isfile(rc_file): if os.path.isfile(rc_file):
rc_files.append(rc_file) rc_files.append(rc_file)

View File

@ -10,6 +10,7 @@ conf.set('VERSION', meson.project_version())
conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir')))
conf.set('pkgdatadir', pkgdatadir) conf.set('pkgdatadir', pkgdatadir)
conf.set('sysconfigdir', sysconfdir) conf.set('sysconfigdir', sysconfdir)
conf.set('use_flatpak', get_option('use_flatpak'))
configure_file( configure_file(
input: 'diffuse.in', input: 'diffuse.in',
@ -19,6 +20,14 @@ configure_file(
install_dir: get_option('bindir') install_dir: get_option('bindir')
) )
configure_file(
input: 'constants.py.in',
output: 'constants.py',
configuration: conf,
install: true,
install_dir: moduledir
)
diffuse_sources = [ diffuse_sources = [
'__init__.py', '__init__.py',
'main.py', 'main.py',

View File

@ -20,12 +20,15 @@
import os import os
import sys import sys
import locale import locale
import subprocess
import gi import gi
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
from diffuse import constants
# convenience class for displaying a message dialogue # convenience class for displaying a message dialogue
class MessageDialog(Gtk.MessageDialog): class MessageDialog(Gtk.MessageDialog):
def __init__(self, parent, type, s): def __init__(self, parent, type, s):
@ -61,6 +64,71 @@ def make_subdirs(p, ss):
pass pass
return p return p
def useFlatpak():
return constants.use_flatpak
# use popen to read the output of a command
def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
if success_results is None:
success_results = [ 0 ]
if isWindows() and prefs.getBool(bash_pref):
# launch the command from a bash shell is requested
cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', 'cd {}; {}'.format(bashEscape(dn), ' '.join([ bashEscape(arg) for arg in cmd ])) ]
dn = None
# use subprocess.Popen to retrieve the file contents
if isWindows():
info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = subprocess.SW_HIDE
else:
info = None
if useFlatpak():
cmd = [ 'flatpak-spawn', '--host' ] + cmd
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dn, startupinfo=info)
proc.stdin.close()
proc.stderr.close()
fd = proc.stdout
# read the command's output
s = fd.read()
fd.close()
if proc.wait() not in success_results:
raise IOError('Command failed.')
return s
# use popen to read the output of a command
def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None):
return strip_eols(splitlines(popenRead(dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore')))
# simulate use of popen with xargs to read the output of a command
def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref):
# os.sysconf() is only available on Unix
if hasattr(os, 'sysconf'):
maxsize = os.sysconf('SC_ARG_MAX')
maxsize -= sum([ len(k) + len(v) + 2 for k, v in os.environ.items() ])
else:
# assume the Window's limit to CreateProcess()
maxsize = 32767
maxsize -= sum([ len(k) + 1 for k in cmd ])
ss = []
i, s, a = 0, 0, []
while i < len(args):
f = (len(a) == 0)
if f:
# start a new command line
a = cmd[:]
elif s + len(args[i]) + 1 <= maxsize:
f = True
if f:
# append another argument to the current command line
a.append(args[i])
s += len(args[i]) + 1
i += 1
if i == len(args) or not f:
ss.extend(popenReadLines(dn, a, prefs, bash_pref))
s, a = 0, []
return ss
# 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'):
@ -85,9 +153,3 @@ if isWindows():
else: else:
if lang is not None: if lang is not None:
os.environ['LANG'] = lang os.environ['LANG'] = lang
APP_NAME = 'Diffuse'
VERSION = '0.0.0'
COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser
{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright"))
WEBSITE = 'https://mightycreak.github.io/diffuse/'