diffuse-0.7.3/0000755000232200023220000000000014147050517013520 5ustar debalancedebalancediffuse-0.7.3/build-aux/0000755000232200023220000000000014147050517015412 5ustar debalancedebalancediffuse-0.7.3/build-aux/meson/0000755000232200023220000000000014147050517016533 5ustar debalancedebalancediffuse-0.7.3/build-aux/meson/postinstall.py0000644000232200023220000000121214147050517021455 0ustar debalancedebalance#!/usr/bin/env python3 from os import environ, path from subprocess import call prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') datadir = path.join(prefix, 'share') destdir = environ.get('DESTDIR', '') # Package managers set this so we don't need to run if not destdir: print('Updating icon cache...') call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) print('Updating desktop database...') call(['update-desktop-database', '-q', path.join(datadir, 'applications')]) print('Compiling GSettings schemas...') call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')]) diffuse-0.7.3/README.md0000644000232200023220000000713014147050517015000 0ustar debalancedebalance# Diffuse Diffuse is a graphical tool for merging and comparing text files. Diffuse is able to compare an arbitrary number of files side-by-side and gives users the ability to manually adjust line matching and directly edit files. Diffuse can also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, Subversion, and SVK repositories for comparison and merging. Some key features of Diffuse: * Ability to compare and merge an arbitrary number of files side-by-side (n-way merges) * Line matching can be manually corrected by the user * Ability to directly edit files * Syntax highlighting * Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, Subversion, and SVK support * Unicode support * Unlimited undo * Easy keyboard navigation ## Requirements Diffuse is implemented entirely in Python and should run on any platform with Python and PyGObject. * Python >= 3.4 * PyGObject >= 3.18 ## Users ### Installing using Flatpak This is the easiest way to install Diffuse: ```sh flatpak install io.github.mightycreak.Diffuse ``` ## Developers ### Setup #### Run Diffuse from source To run Diffuse from the source code, type this: ```sh python main.py ``` To debug with VS Code, open the directory in VS Code, place your breakpoints and hit F5. #### Build Diffuse To build Diffuse, type this: ```sh python setup.py build ``` To run from the build, type this: ```sh PYTHONPATH=build/lib ./build/scripts-3.7/diffuse ``` #### Install Diffuse locally Diffuse build system is meson. To install diffuse locally: ```sh meson setup build cd build meson compile meson install # requires admin privileges ``` To uninstall diffuse afterwards: ```sh sudo ninja uninstall -C build sudo rm -v /usr/local/share/locale/*/LC_MESSAGES/diffuse.mo ``` Meson allows to change the default installation directories, see [command-line documentation](https://mesonbuild.com/Commands.html#configure). ### Installing on Windows The `windows-installer` directory contains scripts for building an installable package for Windows that includes all dependencies. Diffuse can be packaged as a portable application by copying the installation directory to a pen drive and creating a front end that sets the `XDG_CONFIG_HOME` and `XDG_DATA_DIR` environment variables prior to launching Diffuse. The `XDG_CONFIG_HOME` and `XDG_DATA_DIR` environment variables indicate where Diffuse should store persistent settings (eg. the path to a writable directory on the pen drive). ## Building and testing the Flatpak package To install Diffuse locally: ```sh flatpak install runtime/org.gnome.Sdk/$(uname -p)/3.38 flatpak-builder build-flatpak --user --install io.github.mightycreak.Diffuse.yml ``` To run Diffuse through Flatpak: ```sh flatpak run io.github.mightycreak.Diffuse ``` To uninstall Diffuse: ```sh flatpak remove io.github.mightycreak.Diffuse ``` ## Help Documentation Diffuse's help documentation is written in the DocBook format and can be easily converted into other formats using XSLT stylesheets. If the local help documentation or its browser are unavailable, Diffuse will attempt to display the on-line help documentation using a web browser. ## Licenses Diffuse is under the [GPLv2](COPYING). The file [io.github.mightycreak.Diffuse.metainfo.xml](src/usr/share/metainfo/io.github.mightycreak.Diffuse.metainfo.xml) is licensed under the [FSF-AP](https://www.gnu.org/prep/maintain/html_node/License-Notices-for-Other-Files.html) license. Copyright (C) 2006-2019 Derrick Moser Copyright (C) 2015-2021 Romain Failliot Icon made by [@jimmac](https://github.com/jimmac). diffuse-0.7.3/COPYING0000644000232200023220000004325414147050517014563 0ustar debalancedebalance GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. diffuse-0.7.3/meson.build0000644000232200023220000000042214147050517015660 0ustar debalancedebalanceproject('diffuse', version: '0.7.3', meson_version: '>= 0.50', license: 'GPL-2.0-or-later', default_options: [ 'warning_level=2' ]) i18n = import('i18n') subdir('data') subdir('src') subdir('po') meson.add_install_script('build-aux/meson/postinstall.py') diffuse-0.7.3/io.github.mightycreak.Diffuse.yml0000644000232200023220000000074514147050517022033 0ustar debalancedebalanceapp-id: io.github.mightycreak.Diffuse runtime: org.gnome.Platform runtime-version: '41' sdk: org.gnome.Sdk command: diffuse finish-args: - --filesystem=home - --share=ipc - --socket=wayland - --socket=fallback-x11 - --talk-name=org.freedesktop.Flatpak modules: - name: diffuse builddir: true buildsystem: meson config-opts: - -Dlog_print_output=true - -Dlog_print_stack=true - -Duse_flatpak=true sources: - type: dir path: . diffuse-0.7.3/CHANGELOG.md0000644000232200023220000002644314147050517015342 0ustar debalancedebalance# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased ## 0.7.3 - 2021-11-22 ### Added - Added linters (flake8 and mypy) and fixed some errors - Added lint jobs for both in the CI - Added a flatpak job in the CI ### Changed - main.py slimmed down by about 5000 lines - The new widgets.py is a bit fat though (~4000 lines) - Updated the translation files ### Fixed - The intense code cleaning seems to have fixed a bug with the `-c` argument (#120) ## 0.7.2 - 2021-11-18 ### Added - New options: log_print_output and log_print_stack, to print the log messages on the output and code stack - New log function: utils.logErrorAndDialog, to both log and show a dialog message ### Changed - Modularized the VCSs (reducing main.py by around 1300 lines) - Bump GNOME runtime version from 3.38 to 41 ### Fixed - Fixed 'APP_NAME' error when opening non existing file - Fixed the Portuguese Brazilian (pt_BR) translation ## 0.7.1 - 2021-11-17 ### 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 ### Added - Added MetaInfo file - New SVG icon (thanks @creepertron95, @jimmac and @freddii) - Started modularizing the code ### Changed - Changed AppID to io.github.mightycreak.Diffuse (as explained in [Flatpak documentation](https://docs.flatpak.org/en/latest/conventions.html#application-ids)) - Renamed `translations/` to `po/` - Now uses POTFILES.in to list the files to translate - Translation strings are no longer sorted alphabetically, this will help when there will be several files in POTFILES.in - Updated the documentation and script in the `po/` directory - Add .desktop translations in .po files ### Fixed - Fixed some GTK deprecation warnings ## 0.6.0 - 2020-11-29 ### Added - New Flatpak package, published on Flathub: com.github.mightycreak.Diffuse ### Changed - Replace old install.py with the more standard Meson - Remove `u` string prefixes since Python 3 is in UTF-8 by default - Replaced some interpolation operators (`%`) for the `f` string prefix - Use the window scale factor for the icons generation ## 0.5.0 - 2020-07-18 ### Added - added Pedro Albuquerque's Portuguese translation - added Åke Engelbrektson's Swedish translation - added Guillaume Hoffmann's Darcs support improvements - added support for Git submodules - added Akom Chotiphantawanon's Thai translation - added a preference and command line option to specify the version control system search order - added .editorconfig file - added .gitignore file - added message when removing files during uninstallation - added Python script to update all the translation files at once ### Changed - convert to Python 3 - convert to GTK 3 - updated Python highlighting for Python 3 grammar - updated copyrights years and authors - improve Spanish translation - convert translation README to MarkDown - updated all the translation files ### Fixed - fixed wrong icons directory for gtk-update-icon-cache - fixed missing directories when uninstalling - fixed bug introduced by r420 with RCS VCS - fixed broken drag'n'drop since migration to Python3/GTK3 - fixed error when using '-m' in an SVN repo ## 0.4.8 - 2014-07-18 ### Added - updated use of gtk.SpinButton and gtk.Entry to avoid quirks seen on some platforms - updated C/C++ syntax highlighting to recognise C11/C++11 keywords - improved image quality of icons - added Chi Ming and Wei-Lun Chao's Traditional Chinese translation ### Fixed - fixed a bug that prevented Diffuse from reviewing large git merge conflicts - fixed a bug that prevented drag-and-drop of file paths with non-ASCII characters ## 0.4.7 - 2013-05-13 ### Added - added Jindřich Šesták's Czech translation - improved character editing to allow easy indenting and moving the cursor by whole words - added Miś Uszatek's Polish translation - improved auto-detection of utf_16 and utf_32 - added "New N-Way File Merge..." menu item - added syntax highlighting for Erlang and OpenCL files ## 0.4.6 - 2011-11-02 ### Added - added support for Subversion 1.7 - added "Open Commit..." menu item - "-c" option now works for all supported version control systems - Git support distinguishes between staged and unstaged files - added syntax highlighting for R files ### Fixed - fixed a bug that caused the wrong revision to be shown when working on a branch in Mercurial ## 0.4.5 - 2011-07-13 ### Added - added syntax highlighting for JSON files - added menu items and keyboard shortcuts for "First Tab" and "Last Tab" - added "--line" command line option - Diffuse now uses a patience diff-based algorithm to align lines - added command line option to specify a label to display instead of the file name - added preference to display the right margin - added Cristian Marchi's Italian translation ### Changed - state information is now stored in ~/.local/share/diffuse ### Fixed - fixed a bug in CVS and Subversion support that prevented Diffuse from displaying some removed files - fixed a bug that caused deleted files to be ignored when using the '-m' option - fixed a bug that incorrectly encoded pasted text if utf_8 was not specified in the Region Settings preferences - fixed a bug that could cause "Save As..." to fail with some user specified encodings ## 0.4.4 - 2010-10-21 ### Added - Git support now recognises conflicts when re-applying the stash - search dialog is now automatically populated with the currently selected text - added Oleg Pakhtusov's Russian translation - added Kang Bundo's Korean translation - pane headers tooltips - Shift-ScrollWheel can now be used to scroll horizontally ### Fixed - double clicking on text can now select full words with non-English characters - fixed a bug that prevented opening files with non-ASCII characters in their path ## 0.4.3 - 2010-04-15 ### Fixed - fixed a bug that prevented the "-m" option from opening a 3-way merge for Subversion and Bazaar conflicts ## 0.4.2 - 2010-04-13 ### Added - support for detached Git repositories - better removal of unnecessary spacer lines - added support for horizontal mouse scrolling - renamed some resources to more user friendly names - RCS support - added Henri Menke's Spanish translation - "#!" interpreter lines are now used to select proper highlighting rules ### Changed - syntax highlighting is now indicated by radio menu items ## 0.4.1 - 2009-10-12 ### Added - added Japanese translation - added Liu Hao's simplified Chinese translation - added a 'Dismiss All Edits' menu item - localised manuals are now supported on Windows - new command line option for specifying blank file comparison panes - new preference to enable/disable line numbers - added "Undo Close Tab" menu item - added new menu items and buttons for copying lines between panes ### Changed - personal configuration files are now stored in ~/.config/diffuse/ (the README file describes how to migrate old settings) - Diffuse now quits if no viewers were created with the -m option - replaced "Closing last tab quits Diffuse" preference with a "Warn me when closing a tab will quit Diffuse" preference - MMB on a notebook tab now closes the tab - RMB on a notebook tab creates a popup menu to set the current page - changed the default hotkeys for merging to reflect the direction text "moves" ## 0.4.0 - 2009-08-17 ### Added - added format menu with new items for changing case, sorting, and manipulating white space - optimised redraws when only the cursor position has changed - input methods that use pre-editing can now be used when editing text - dead keys can now be used when editing text - updated Monotone support to use 'mtn automate inventory' - Git support now handles files flagged as 'unmerged' - added version control section to the manual ### Changed - replaced 'Hide end of line characters' preference with 'Show white space characters' - errors are now reported in a dialogue instead of printing to stderr ### Fixed - graceful handling of non-zero exit codes from 'git status' - minor bug fixes ## 0.3.4 - 2009-07-03 ### Added - syntax highlighting for .plist, GLSL, SConscript, and SConstruct files - status bar now explains how to navigate between modes - added labels to indicate syntax highlighting rules, encoding, and format - Subversion 1.6 support - added Henri Menke's German translation - added '--examplesdir=' and '--mandir=' options to install.py - renamed the '--python-interpreter=' installer option to '--pythonbin=' ### Fixed - minor bug fixes ## 0.3.3 - 2009-04-13 ### Fixed - fixed a bug handling the backspace key with the cursor in the first column ## 0.3.2 - 2009-04-13 ### Added - POSIX installer with `--destdir=` and `--files-only` options for packagers - vi-like keybindings for line mode - `-m` option to open modified files in separate tabs - 'Merge From Left Then Right' and 'Merge From Right Then Left' menu items - drag-n-drop support - preferences for behaviour of tabs - files with edits now tagged with '*' - auto indent - 'Open File In New Tab...' and 'Open Modified Files...' menu items - 'Save All' menu item - mac-style line ending support ### Changed - new end of line display behaviour - improved organisation of menu items - errors are now reported on stderr ### Fixed - button bar no longer grabs keyboard focus ### Removed - removed dependence on urllib module - removed TODO list ### Fixed - minor bug fixes ## 0.3.1 - 2009-03-05 ### Fixed - fixed a typo that broke the 'Find...' dialogue ## 0.3.0 - 2009-03-03 ### Added - new Windows installer - notification on focus change when files change on disk - menu items for adjusting indentation - syntax highlighting for Objective-C++ - `-c` option now works with CVS-style revision numbers - window title now describes current tab - search settings now persist across sessions ### Fixed - minor bug fixes ## 0.2.15 - 2008-12-03 ### Added - smoother scrolling - panes and tabs can now be manually re-organised - preferences for tab key behaviour - 'go to line' menu item - '-c' option for viewing the changes of a particular commit - home/end keys can now be used in line mode - confirmation requested before overriding changed files - position of window now saved - syntax files for more file types - reading /etc/diffuserc now optional when using a personal configuration file ### Fixed - minor bug fixes ## 0.2.14 - 2008-10-20 ### Added - svk support - syntax files for more file types - DOS / Unix line endings now respected in edit operations - improved difference map - more robust launching of help browsers - man page - command line display options - file revisions can now be specified in the open file dialogue ### Changed - moved some resources to the preferences dialogue ### Fixed - minor bug fixes ## 0.2.13 - 2008-05-16 ### Added - bazaar, darcs, and monotone support - configurable key bindings - persistent preference settings - optimisations ### Fixed - minor bug fixes ## 0.2.12 - 2008-05-06 ### Added - alternate codecs for reading and writing files - more search options - editor support for primary selection ### Fixed - minor bug fixes ## 0.2.11 - 2008-04-27 ### Added - cvs, subversion, git, mercurial support - python re-write - syntax highlighting - search and replace - customisable through configuration files - tabbed viewer panes ## 0.1.14 - 2006-01-28 ### Added - initial public release diffuse-0.7.3/src/0000755000232200023220000000000014147050517014307 5ustar debalancedebalancediffuse-0.7.3/src/meson.build0000644000232200023220000000002214147050517016443 0ustar debalancedebalancesubdir('diffuse') diffuse-0.7.3/src/diffuse/0000755000232200023220000000000014147050517015734 5ustar debalancedebalancediffuse-0.7.3/src/diffuse/dialogs.py0000644000232200023220000001710414147050517017733 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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 constants # type: ignore from diffuse import utils import gi # type: ignore gi.require_version('GObject', '2.0') gi.require_version('Gtk', '3.0') from gi.repository import GObject, Gtk # type: ignore # noqa: E402 # the about dialog class AboutDialog(Gtk.AboutDialog): def __init__(self): Gtk.AboutDialog.__init__(self) self.set_logo_icon_name('io.github.mightycreak.Diffuse') self.set_program_name(constants.APP_NAME) self.set_version(constants.VERSION) self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.')) self.set_copyright(constants.COPYRIGHT) self.set_website(constants.WEBSITE) self.set_authors(['Derrick Moser ', 'Romain Failliot ']) self.set_translator_credits(_('translator-credits')) license_text = [ constants.APP_NAME + ' ' + constants.VERSION + '\n\n', constants.COPYRIGHT + '\n\n', _('''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.''')] self.set_license(''.join(license_text)) # custom dialogue for picking files with widgets for specifying the encoding # and revision class FileChooserDialog(Gtk.FileChooserDialog): # record last chosen folder so the file chooser can start at a more useful # location for empty panes last_chosen_folder = os.path.realpath(os.curdir) @staticmethod def _current_folder_changed_cb(widget): FileChooserDialog.last_chosen_folder = widget.get_current_folder() def __init__(self, title, parent, prefs, action, accept, rev=False): Gtk.FileChooserDialog.__init__(self, title=title, parent=parent, action=action) self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) self.add_button(accept, Gtk.ResponseType.OK) self.prefs = prefs hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) hbox.set_border_width(5) label = Gtk.Label.new(_('Encoding: ')) hbox.pack_start(label, False, False, 0) label.show() self.encoding = entry = utils.EncodingMenu( prefs, action in [Gtk.FileChooserAction.OPEN, Gtk.FileChooserAction.SELECT_FOLDER]) hbox.pack_start(entry, False, False, 5) entry.show() if rev: self.revision = entry = Gtk.Entry.new() hbox.pack_end(entry, False, False, 0) entry.show() label = Gtk.Label.new(_('Revision: ')) hbox.pack_end(label, False, False, 0) label.show() self.vbox.pack_start(hbox, False, False, 0) hbox.show() self.set_current_folder(self.last_chosen_folder) self.connect('current-folder-changed', self._current_folder_changed_cb) def set_encoding(self, encoding): self.encoding.set_text(encoding) def get_encoding(self): return self.encoding.get_text() def get_revision(self): return self.revision.get_text() def get_filename(self): # convert from UTF-8 string to unicode return Gtk.FileChooserDialog.get_filename(self) # dialogue used to search for text class NumericDialog(Gtk.Dialog): def __init__(self, parent, title, text, val, lower, upper, step=1, page=0): Gtk.Dialog.__init__(self, title=title, parent=parent, destroy_with_parent=True) self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT) self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) vbox.set_border_width(10) hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) label = Gtk.Label.new(text) hbox.pack_start(label, False, False, 0) label.show() adj = Gtk.Adjustment.new(val, lower, upper, step, page, 0) self.button = button = Gtk.SpinButton.new(adj, 1.0, 0) button.connect('activate', self.button_cb) hbox.pack_start(button, True, True, 0) button.show() vbox.pack_start(hbox, True, True, 0) hbox.show() self.vbox.pack_start(vbox, False, False, 0) vbox.show() def button_cb(self, widget): self.response(Gtk.ResponseType.ACCEPT) # dialogue used to search for text class SearchDialog(Gtk.Dialog): def __init__(self, parent, pattern=None, history=None): Gtk.Dialog.__init__(self, title=_('Find...'), parent=parent, destroy_with_parent=True) self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT) self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) vbox.set_border_width(10) hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) label = Gtk.Label.new(_('Search For: ')) hbox.pack_start(label, False, False, 0) label.show() combo = Gtk.ComboBoxText.new_with_entry() self.entry = combo.get_child() self.entry.connect('activate', self.entry_cb) if pattern is not None: self.entry.set_text(pattern) if history is not None: completion = Gtk.EntryCompletion.new() liststore = Gtk.ListStore(GObject.TYPE_STRING) completion.set_model(liststore) completion.set_text_column(0) for h in history: liststore.append([h]) combo.append_text(h) self.entry.set_completion(completion) hbox.pack_start(combo, True, True, 0) combo.show() vbox.pack_start(hbox, False, False, 0) hbox.show() button = Gtk.CheckButton.new_with_mnemonic(_('Match Case')) self.match_case_button = button vbox.pack_start(button, False, False, 0) button.show() button = Gtk.CheckButton.new_with_mnemonic(_('Search Backwards')) self.backwards_button = button vbox.pack_start(button, False, False, 0) button.show() self.vbox.pack_start(vbox, False, False, 0) vbox.show() # callback used when the Enter key is pressed def entry_cb(self, widget): self.response(Gtk.ResponseType.ACCEPT) diffuse-0.7.3/src/diffuse/vcs/0000755000232200023220000000000014147050517016527 5ustar debalancedebalancediffuse-0.7.3/src/diffuse/vcs/hg.py0000644000232200023220000001002214147050517017472 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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') diffuse-0.7.3/src/diffuse/vcs/git.py0000644000232200023220000001364014147050517017670 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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): relpath = utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/') return utils.popenRead( self.root, [ prefs.getString('git_bin'), 'show', f'{rev}:{relpath}' ], prefs, 'git_bash') diffuse-0.7.3/src/diffuse/vcs/folder_set.py0000644000232200023220000000326014147050517021230 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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 diffuse-0.7.3/src/diffuse/vcs/rcs.py0000644000232200023220000001427214147050517017676 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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 # simulate use of popen with xargs to read the output of a command def _popen_xargs_readlines(self, 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(utils.popenReadLines(self.root, a, prefs, bash_pref)) s, a = 0, [] return ss 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 d in os.listdir(s): dn = os.path.join(s, d) if d.endswith(',v') and os.path.isfile(dn): # map to checkout name r.append(dn[:-2]) elif d == '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 self._popen_xargs_readlines(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') diffuse-0.7.3/src/diffuse/vcs/mtn.py0000644000232200023220000002140614147050517017702 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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 if 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') diffuse-0.7.3/src/diffuse/vcs/cvs.py0000644000232200023220000001001014147050517017664 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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)] 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') diffuse-0.7.3/src/diffuse/vcs/__init__.py0000644000232200023220000000000014147050517020626 0ustar debalancedebalancediffuse-0.7.3/src/diffuse/vcs/vcs_interface.py0000644000232200023220000000310714147050517021715 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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: """Interface for the VCSs.""" 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.""" def getCommitTemplate(self, prefs, rev, names): """Indicates which file revisions to display for a commit.""" 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""" diffuse-0.7.3/src/diffuse/vcs/darcs.py0000644000232200023220000001304714147050517020202 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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') diffuse-0.7.3/src/diffuse/vcs/vcs_registry.py0000644000232200023220000001751514147050517021635 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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 } # 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 return None # 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) return None # 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') return Bzr(p) if p else None def _get_cvs_repo(path, prefs): return Cvs(path) if os.path.isdir(os.path.join(path, 'CVS')) else None def _get_darcs_repo(path, prefs): p = _find_parent_dir_with(path, '_darcs') return Darcs(p) if p else None 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 = utils.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): # 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') return Hg(p) if p else None def _get_mtn_repo(path, prefs): p = _find_parent_dir_with(path, '_MTN') return Mtn(p) if p else None 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 return None def _get_svn_repo(path, prefs): p = _find_parent_dir_with(path, '.svn') return Svn(p) if p else None 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 with open(svkconfig, 'r', encoding='utf-8') as f: ss = utils.readlines(f) 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 if 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, )) return None diffuse-0.7.3/src/diffuse/vcs/svk.py0000644000232200023220000000356214147050517017712 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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.svn import Svn class Svk(Svn): @staticmethod def _getVcs(): return 'svk' @staticmethod def _getURLPrefix(): return 'Depot Path: ' @staticmethod def _parseStatusLine(s): if len(s) < 4 or s[0] not in 'ACDMR': return '', '' return s[0], s[4:] @staticmethod def _getPreviousRevision(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): relpath = utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/') return utils.popenRead( self.root, [ prefs.getString('svk_bin'), 'cat', '-r', rev, f'{self._getURL(prefs)}/{relpath}' ], prefs, 'svk_bash') diffuse-0.7.3/src/diffuse/vcs/bzr.py0000644000232200023220000002003314147050517017674 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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') diffuse-0.7.3/src/diffuse/vcs/svn.py0000644000232200023220000002537714147050517017725 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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 @staticmethod def _getVcs(): return 'svn' @staticmethod def _getURLPrefix(): return 'URL: ' @staticmethod def _parseStatusLine(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:] @staticmethod def _getPreviousRevision(rev): if rev is None: return 'BASE' m = int(rev) return str(max(m > 1, 0)) 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 " 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: 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(): lines = utils.popenReadLines( self.root, [ vcs_bin, 'list', '-r', rev, f"{self._getURL(prefs)}/{p.replace(os.sep, '/')}" ], prefs, vcs_bash) for s in lines: 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(): lines = utils.popenReadLines( self.root, [ vcs_bin, 'list', '-r', prev, f"{self._getURL(prefs)}/{p.replace(os.sep, '/')}" ], prefs, vcs_bash) for s in lines: 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: lines = utils.popenReadLines( self.root, [ vcs_bin, 'list', '-r', prev, f"{self._getURL(prefs)}/{p.replace(os.sep, '/')}" ], prefs, vcs_bash) for s in lines: 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', f"{utils.safeRelativePath(self.root, name, prefs, 'svn_cygwin')}@{rev}" ], prefs, 'svn_bash') return utils.popenRead( self.root, [ vcs_bin, 'cat', ( f"{self._getURL(prefs)}/" f"{utils.relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')}@{rev}" ) ], prefs, 'svn_bash') diffuse-0.7.3/src/diffuse/meson.build0000644000232200023220000000236114147050517020100 0ustar debalancedebalancepkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) moduledir = join_paths(pkgdatadir, 'diffuse') sysconfdir = join_paths(get_option('prefix'), get_option('sysconfdir')) python = import('python') conf = configuration_data() conf.set('PYTHON', python.find_installation('python3').path()) conf.set('pkgdatadir', pkgdatadir) conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) configure_file( input: 'diffuse.in', output: 'diffuse', configuration: conf, install: true, install_dir: get_option('bindir') ) conf = configuration_data() conf.set('VERSION', meson.project_version()) conf.set('sysconfigdir', sysconfdir) conf.set('log_print_output', get_option('log_print_output')) conf.set('log_print_stack', get_option('log_print_stack')) conf.set('use_flatpak', get_option('use_flatpak')) configure_file( input: 'constants.py.in', output: 'constants.py', configuration: conf, install: true, install_dir: moduledir ) diffuse_sources = [ '__init__.py', 'dialogs.py', 'main.py', 'preferences.py', 'resources.py', 'utils.py', 'widgets.py', ] install_data(diffuse_sources, install_dir: moduledir) install_subdir('vcs', install_dir: moduledir, strip_directory: false) diffuse-0.7.3/src/diffuse/main.py0000644000232200023220000024451014147050517017240 0ustar debalancedebalance# Diffuse: a graphical tool for merging and comparing text files. # # Copyright (C) 2019 Derrick Moser # Copyright (C) 2021 Romain Failliot # # 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 sys import codecs import encodings import shlex import stat import webbrowser from urllib.parse import urlparse from diffuse import constants # type: ignore from diffuse import utils from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, SearchDialog from diffuse.preferences import Preferences from diffuse.resources import theResources from diffuse.vcs.vcs_registry import VcsRegistry from diffuse.widgets import FileDiffViewerBase from diffuse.widgets import createMenu, LINE_MODE, CHAR_MODE, ALIGN_MODE import gi # type: ignore gi.require_version('GObject', '2.0') gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') gi.require_version('GdkPixbuf', '2.0') gi.require_version('Pango', '1.0') gi.require_version('PangoCairo', '1.0') from gi.repository import GObject, Gtk, Gdk, GdkPixbuf, Pango, PangoCairo # type: ignore # noqa: E402 theVCSs = VcsRegistry() # widget classed to create notebook tabs with labels and a close button # use notebooktab.button.connect() to be notified when the button is pressed # make this a Gtk.EventBox so signals can be connected for MMB and RMB button # presses. class NotebookTab(Gtk.EventBox): def __init__(self, name, stock): Gtk.EventBox.__init__(self) self.set_visible_window(False) hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) if stock is not None: image = Gtk.Image.new() image.set_from_stock(stock, Gtk.IconSize.MENU) hbox.pack_start(image, False, False, 5) image.show() self.label = label = Gtk.Label.new(name) # left justify the widget label.set_xalign(0.0) label.set_yalign(0.5) hbox.pack_start(label, True, True, 0) label.show() self.button = button = Gtk.Button.new() button.set_relief(Gtk.ReliefStyle.NONE) image = Gtk.Image.new() image.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU) button.add(image) image.show() button.set_tooltip_text(_('Close Tab')) hbox.pack_start(button, False, False, 0) button.show() self.add(hbox) hbox.show() def get_text(self): return self.label.get_text() def set_text(self, s): self.label.set_text(s) # contains information about a file class FileInfo: def __init__(self, name=None, encoding=None, vcs=None, revision=None, label=None): # file name self.name = name # name of codec used to translate the file contents to unicode text self.encoding = encoding # the VCS object self.vcs = vcs # revision used to retrieve file from the VCS self.revision = revision # alternate text to display instead of the actual file name self.label = label # 'stat' for files read from disk -- used to warn about changes to the # file on disk before saving self.stat = None # most recent 'stat' for files read from disk -- used on focus change # to warn about changes to file on disk self.last_stat = None # the main application class containing a set of file viewers # this class displays tab for switching between viewers and dispatches menu # commands to the current viewer class Diffuse(Gtk.Window): # specialization of FileDiffViewerBase for Diffuse class FileDiffViewer(FileDiffViewerBase): # pane header class PaneHeader(Gtk.Box): def __init__(self): Gtk.Box.__init__(self, orientation = Gtk.Orientation.HORIZONTAL, spacing = 0) _append_buttons(self, Gtk.IconSize.MENU, [ [Gtk.STOCK_OPEN, self.button_cb, 'open', _('Open File...')], [Gtk.STOCK_REFRESH, self.button_cb, 'reload', _('Reload File')], [Gtk.STOCK_SAVE, self.button_cb, 'save', _('Save File')], [Gtk.STOCK_SAVE_AS, self.button_cb, 'save_as', _('Save File As...') ]]) self.label = label = Gtk.Label.new() label.set_selectable(True) label.set_ellipsize(Pango.EllipsizeMode.START) label.set_max_width_chars(1) self.pack_start(label, True, True, 0) # file's name and information about how to retrieve it from a # VCS self.info = FileInfo() self.has_edits = False self.updateTitle() self.show_all() # callback for buttons def button_cb(self, widget, s): self.emit(s) # creates an appropriate title for the pane header def updateTitle(self): ss = [] info = self.info if info.label is not None: # show the provided label instead of the file name ss.append(info.label) else: if info.name is not None: ss.append(info.name) if info.revision is not None: ss.append('(' + info.revision + ')') if self.has_edits: ss.append('*') s = ' '.join(ss) self.label.set_text(s) self.label.set_tooltip_text(s) self.emit('title_changed') # set num edits def setEdits(self, has_edits): if self.has_edits != has_edits: self.has_edits = has_edits self.updateTitle() # pane footer class PaneFooter(Gtk.Box): def __init__(self): Gtk.Box.__init__(self, orientation = Gtk.Orientation.HORIZONTAL, spacing = 0) self.cursor = label = Gtk.Label.new() self.cursor.set_size_request(-1, -1) self.pack_start(label, False, False, 0) separator = Gtk.Separator.new(Gtk.Orientation.VERTICAL) self.pack_end(separator, False, False, 10) self.encoding = label = Gtk.Label.new() self.pack_end(label, False, False, 0) separator = Gtk.Separator.new(Gtk.Orientation.VERTICAL) self.pack_end(separator, False, False, 10) self.format = label = Gtk.Label.new() self.pack_end(label, False, False, 0) separator = Gtk.Separator.new(Gtk.Orientation.VERTICAL) self.pack_end(separator, False, False, 10) self.set_size_request(0, self.get_size_request()[1]) self.show_all() # set the cursor label def updateCursor(self, viewer, f): if viewer.mode == CHAR_MODE and viewer.current_pane == f: ## TODO: Find a fix for the column bug (resizing issue when editing a line) #j = viewer.current_char #if j > 0: # text = viewer.getLineText(viewer.current_pane, viewer.current_line)[:j] # j = viewer.stringWidth(text) #s = _('Column %d') % (j, ) s = '' else: s = '' self.cursor.set_text(s) # set the format label def setFormat(self, s): v = [] if s & utils.DOS_FORMAT: v.append('DOS') if s & utils.MAC_FORMAT: v.append('Mac') if s & utils.UNIX_FORMAT: v.append('Unix') self.format.set_text('/'.join(v)) # set the format label def setEncoding(self, s): if s is None: s = '' self.encoding.set_text(s) def __init__(self, n, prefs, title): FileDiffViewerBase.__init__(self, n, prefs) self.title = title self.status = '' self.headers = [] self.footers = [] for i in range(n): # pane header w = Diffuse.FileDiffViewer.PaneHeader() self.headers.append(w) self.attach(w, i, 0, 1, 1) w.connect('title-changed', self.title_changed_cb) w.connect('open', self.open_file_button_cb, i) w.connect('reload', self.reload_file_button_cb, i) w.connect('save', self.save_file_button_cb, i) w.connect('save-as', self.save_file_as_button_cb, i) w.show() # pane footer w = Diffuse.FileDiffViewer.PaneFooter() self.footers.append(w) self.attach(w, i, 2, 1, 1) w.show() self.connect('swapped-panes', self.swapped_panes_cb) self.connect('num-edits-changed', self.num_edits_changed_cb) self.connect('mode-changed', self.mode_changed_cb) self.connect('cursor-changed', self.cursor_changed_cb) self.connect('format-changed', self.format_changed_cb) for i, darea in enumerate(self.dareas): darea.drag_dest_set(Gtk.DestDefaults.ALL, [Gtk.TargetEntry.new('text/uri-list', 0, 0)], Gdk.DragAction.COPY) darea.connect('drag-data-received', self.drag_data_received_cb, i) # initialise status self.updateStatus() # convenience method to request confirmation before loading a file if # it will cause existing edits to be lost def loadFromInfo(self, f, info): if self.headers[f].has_edits: # 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.set_title(constants.APP_NAME) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT) dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK) dialog.set_default_response(Gtk.ResponseType.CANCEL) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.OK: # save the current pane contents if not self.save_file(f): # cancel if the save failed return elif response != Gtk.ResponseType.REJECT: # cancel if the user did not choose 'yes' or 'no' return self.openUndoBlock() self.recordEditMode() self.load(f, info) self.recordEditMode() self.closeUndoBlock() # callback used when receiving drag-n-drop data def drag_data_received_cb(self, widget, context, x, y, selection, targettype, eventtime, f): # get uri list uris = selection.get_uris() # load the first valid file for uri in uris: path = urlparse(uri).path if os.path.isfile(path): self.loadFromInfo(f, FileInfo(path)) break # change the file info for pane 'f' to 'info' def setFileInfo(self, f, info): h, footer = self.headers[f], self.footers[f] h.info = info h.updateTitle() footer.setFormat(self.panes[f].format) footer.setEncoding(info.encoding) footer.updateCursor(self, f) # callback used when a pane header's title changes def title_changed_cb(self, widget): # choose a short but descriptive title for the viewer has_edits = False names = [] unique_names = set() for header in self.headers: has_edits |= header.has_edits s = header.info.label if s is None: # no label provided, show the file name instead s = header.info.name if s is not None: s = os.path.basename(s) if s is not None: names.append(s) unique_names.add(s) if len(unique_names) > 0: if len(unique_names) == 1: self.title = names[0] else: self.title = ' : '.join(names) s = self.title if has_edits: s += ' *' self.emit('title_changed', s) def setEncoding(self, f, encoding): h = self.headers[f] h.info.encoding = encoding self.footers[f].setEncoding(encoding) # load a new file into pane 'f' # 'info' indicates the name of the file and how to retrieve it from the # version control system if applicable def load(self, f, info): name = info.name encoding = info.encoding stat = None if name is None: # reset to an empty pane ss = [] else: rev = info.revision try: if rev is None: # load the contents of a plain file with open(name, 'rb') as fd: s = fd.read() # get the file's modification times so we can detect changes stat = os.stat(name) else: if info.vcs is None: raise IOError('Not under version control.') fullname = os.path.abspath(name) # retrieve the revision from the version control system s = info.vcs.getRevision(self.prefs, fullname, rev) # convert file contents to unicode if encoding is None: s, encoding = self.prefs.convertToUnicode(s) else: s = str(s, encoding=encoding) ss = utils.splitlines(s) except (IOError, OSError, UnicodeDecodeError, LookupError): # FIXME: this can occur before the toplevel window is drawn if rev is not None: msg = _('Error reading revision %(rev)s of %(file)s.') % { 'rev': rev, 'file': name } else: msg = _('Error reading %s.') % (name, ) utils.logErrorAndDialog(msg, self.get_toplevel()) return # update the panes contents, last modified time, and title self.replaceContents(f, ss) info.encoding = encoding info.last_stat = info.stat = stat self.setFileInfo(f, info) # use the file name to choose appropriate syntax highlighting rules if name is not None: syntax = theResources.guessSyntaxForFile(name, ss) if syntax is not None: self.setSyntax(syntax) # load a new file into pane 'f' def open_file(self, f, reload=False): h = self.headers[f] info = h.info if not reload: # we need to ask for a file name if we are not reloading the # existing file dialog = FileChooserDialog(_('Open File'), self.get_toplevel(), self.prefs, Gtk.FileChooserAction.OPEN, Gtk.STOCK_OPEN, True) if info.name is not None: dialog.set_filename(os.path.realpath(info.name)) dialog.set_encoding(info.encoding) dialog.set_default_response(Gtk.ResponseType.OK) end = (dialog.run() != Gtk.ResponseType.OK) name = dialog.get_filename() rev = None vcs = None revision = dialog.get_revision().strip() if revision != '': rev = revision vcs = theVCSs.findByFilename(name, self.prefs) info = FileInfo(name, dialog.get_encoding(), vcs, rev) dialog.destroy() if end: return self.loadFromInfo(f, info) # callback for open file button def open_file_button_cb(self, widget, f): self.open_file(f) # callback for open file menu item def open_file_cb(self, widget, data): self.open_file(self.current_pane) # callback for reload file button def reload_file_button_cb(self, widget, f): self.open_file(f, True) # callback for reload file menu item def reload_file_cb(self, widget, data): self.open_file(self.current_pane, True) # check changes to files on disk when receiving keyboard focus def focus_in(self, widget, event): for f, h in enumerate(self.headers): info = h.info try: if info.last_stat is not None: info = h.info new_stat = os.stat(info.name) if info.last_stat[stat.ST_MTIME] < new_stat[stat.ST_MTIME]: # update our notion of the most recent modification info.last_stat = new_stat if info.label is not None: s = info.label else: s = info.name msg = _('The file %s changed on disk. Do you want to reload the file?') % (s, ) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg) ok = (dialog.run() == Gtk.ResponseType.OK) dialog.destroy() if ok: self.open_file(f, True) except OSError: pass # save contents of pane 'f' to file def save_file(self, f, save_as=False): h = self.headers[f] info = h.info name, encoding, rev, label = info.name, info.encoding, info.revision, info.label if name is None or rev is not None: # we need to prompt for a file name the current contents were # not loaded from a regular file save_as = True if save_as: # prompt for a file name dialog = FileChooserDialog(_('Save %(title)s Pane %(pane)d') % { 'title': self.title, 'pane': f + 1 }, self.get_toplevel(), self.prefs, Gtk.FileChooserAction.SAVE, Gtk.STOCK_SAVE) if name is not None: dialog.set_filename(os.path.abspath(name)) if encoding is None: encoding = self.prefs.getDefaultEncoding() dialog.set_encoding(encoding) name, label = None, None dialog.set_default_response(Gtk.ResponseType.OK) if dialog.run() == Gtk.ResponseType.OK: name = dialog.get_filename() encoding = dialog.get_encoding() if encoding is None: if info.encoding is not None: # this case can occur if the user provided the # encoding and it is not an encoding we know about encoding = info.encoding else: encoding = self.prefs.getDefaultEncoding() dialog.destroy() if name is None: return False try: msg = None # warn if we are about to overwrite an existing file if save_as: if os.path.exists(name): msg = _('A file named %s already exists. Do you want to overwrite it?') % (name, ) # warn if we are about to overwrite a file that has changed # since we last read it elif info.stat is not None: if info.stat[stat.ST_MTIME] < os.stat(name)[stat.ST_MTIME]: msg = _('The file %s has been modified by another process since reading it. If you save, all the external changes could be lost. Save anyways?') % (name, ) if msg is not None: dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg) end = (dialog.run() != Gtk.ResponseType.OK) dialog.destroy() if end: return False except OSError: pass try: # convert the text to the output encoding # refresh the lines to contain new objects with updated line # numbers and no local edits ss = [] for line in self.panes[f].lines: if line is not None: s = line.getText() if s is not None: ss.append(s) encoded = codecs.encode(''.join(ss), encoding) # write file with open(name, 'wb') as fd: fd.write(encoded) # make the edits look permanent self.openUndoBlock() self.bakeEdits(f) self.closeUndoBlock() # update the pane file info info.name, info.encoding, info.revision, info.label = name, encoding, None, label info.last_stat = info.stat = os.stat(name) self.setFileInfo(f, info) # update the syntax highlighting in case we changed the file extension syntax = theResources.guessSyntaxForFile(name, ss) if syntax is not None: self.setSyntax(syntax) return True except (UnicodeEncodeError, LookupError): utils.logErrorAndDialog(_('Error encoding to %s.') % (encoding, ), self.get_toplevel()) except IOError: utils.logErrorAndDialog(_('Error writing %s.') % (name, ), self.get_toplevel()) return False # callback for save file menu item def save_file_cb(self, widget, data): self.save_file(self.current_pane) # callback for save file as menu item def save_file_as_cb(self, widget, data): self.save_file(self.current_pane, True) # callback for save all menu item def save_all_cb(self, widget, data): for f, h in enumerate(self.headers): if h.has_edits: self.save_file(f) # callback for save file button def save_file_button_cb(self, widget, f): self.save_file(f) # callback for save file as button def save_file_as_button_cb(self, widget, f): self.save_file(f, True) # callback for go to line menu item def go_to_line_cb(self, widget, data): parent = self.get_toplevel() dialog = NumericDialog(parent, _('Go To Line...'), _('Line Number: '), 1, 1, self.panes[self.current_pane].max_line_number + 1) okay = (dialog.run() == Gtk.ResponseType.ACCEPT) i = dialog.button.get_value_as_int() dialog.destroy() if okay: self.go_to_line(i) # callback to receive notification when the name of a file changes def swapped_panes_cb(self, widget, f_dst, f_src): f0, f1 = self.headers[f_dst], self.headers[f_src] f0.has_edits, f1.has_edits = f1.has_edits, f0.has_edits info0, info1 = f1.info, f0.info self.setFileInfo(f_dst, info0) self.setFileInfo(f_src, info1) # callback to receive notification when the name of a file changes def num_edits_changed_cb(self, widget, f): self.headers[f].setEdits(self.panes[f].num_edits > 0) # callback to record changes to the viewer's mode def mode_changed_cb(self, widget): self.updateStatus() # update the viewer's current status message def updateStatus(self): if self.mode == LINE_MODE: s = _('Press the enter key or double click to edit. Press the space bar or use the RMB menu to manually align.') elif self.mode == CHAR_MODE: s = _('Press the escape key to finish editing.') elif self.mode == ALIGN_MODE: s = _('Select target line and press the space bar to align. Press the escape key to cancel.') else: s = None self.status = s self.emit('status_changed', s) # gets the status bar text def getStatus(self): return self.status # callback to display the cursor in a pane def cursor_changed_cb(self, widget): for f, footer in enumerate(self.footers): footer.updateCursor(self, f) # callback to display the format of a pane def format_changed_cb(self, widget, f, fmt): self.footers[f].setFormat(fmt) def __init__(self, rc_dir): Gtk.Window.__init__(self, type = Gtk.WindowType.TOPLEVEL) self.prefs = Preferences(os.path.join(rc_dir, 'prefs')) # number of created viewers (used to label some tabs) self.viewer_count = 0 # get monitor resolution monitor_geometry = Gdk.Display.get_default().get_monitor(0).get_geometry() # state information that should persist across sessions self.bool_state = { 'window_maximized': False, 'search_matchcase': False, 'search_backwards': False } self.int_state = { 'window_width': 1024, 'window_height': 768 } self.int_state['window_x'] = max(0, (monitor_geometry.width - self.int_state['window_width']) / 2) self.int_state['window_y'] = max(0, (monitor_geometry.height - self.int_state['window_height']) / 2) self.connect('configure-event', self.configure_cb) self.connect('window-state-event', self.window_state_cb) # search history is application wide self.search_pattern = None self.search_history = [] self.connect('delete-event', self.delete_cb) accel_group = Gtk.AccelGroup() # create a Box for our contents vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # create some custom icons for merging DIFFUSE_STOCK_NEW_2WAY_MERGE = 'diffuse-new-2way-merge' DIFFUSE_STOCK_NEW_3WAY_MERGE = 'diffuse-new-3way-merge' DIFFUSE_STOCK_LEFT_RIGHT = 'diffuse-left-right' DIFFUSE_STOCK_RIGHT_LEFT = 'diffuse-right-left' # get default theme and window scale factor default_theme = Gtk.IconTheme.get_default() scale_factor = self.get_scale_factor() icon_size = Gtk.IconSize.lookup(Gtk.IconSize.LARGE_TOOLBAR).height factory = Gtk.IconFactory() # render the base item used to indicate a new document p0 = default_theme.load_icon_for_scale("document-new", icon_size, scale_factor, 0) w, h = p0.get_width(), p0.get_height() # render new 2-way merge icon s = 0.8 sw, sh = int(s * w), int(s * h) w1, h1 = w - sw, h - sh p = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, w, h) p.fill(0) p0.composite(p, 0, 0, sw, sh, 0, 0, s, s, GdkPixbuf.InterpType.BILINEAR, 255) p0.composite(p, w1, h1, sw, sh, w1, h1, s, s, GdkPixbuf.InterpType.BILINEAR, 255) factory.add(DIFFUSE_STOCK_NEW_2WAY_MERGE, Gtk.IconSet.new_from_pixbuf(p)) # render new 3-way merge icon s = 0.7 sw, sh = int(s * w), int(s * h) w1, h1 = (w - sw) / 2, (h - sh) / 2 w2, h2 = w - sw, h - sh p = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, w, h) p.fill(0) p0.composite(p, 0, 0, sw, sh, 0, 0, s, s, GdkPixbuf.InterpType.BILINEAR, 255) p0.composite(p, w1, h1, sw, sh, w1, h1, s, s, GdkPixbuf.InterpType.BILINEAR, 255) p0.composite(p, w2, h2, sw, sh, w2, h2, s, s, GdkPixbuf.InterpType.BILINEAR, 255) factory.add(DIFFUSE_STOCK_NEW_3WAY_MERGE, Gtk.IconSet.new_from_pixbuf(p)) # render the left and right arrow we will use in our custom icons p0 = default_theme.load_icon_for_scale("go-next", icon_size, scale_factor, 0) p1 = default_theme.load_icon_for_scale("go-previous", icon_size, scale_factor, 0) w, h, s = p0.get_width(), p0.get_height(), 0.65 sw, sh = int(s * w), int(s * h) w1, h1 = w - sw, h - sh # create merge from left then right icon p = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, w, h) p.fill(0) p1.composite(p, w1, h1, sw, sh, w1, h1, s, s, GdkPixbuf.InterpType.BILINEAR, 255) p0.composite(p, 0, 0, sw, sh, 0, 0, s, s, GdkPixbuf.InterpType.BILINEAR, 255) factory.add(DIFFUSE_STOCK_LEFT_RIGHT, Gtk.IconSet.new_from_pixbuf(p)) # create merge from right then left icon p = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, w, h) p.fill(0) p0.composite(p, 0, h1, sw, sh, 0, h1, s, s, GdkPixbuf.InterpType.BILINEAR, 255) p1.composite(p, w1, 0, sw, sh, w1, 0, s, s, GdkPixbuf.InterpType.BILINEAR, 255) factory.add(DIFFUSE_STOCK_RIGHT_LEFT, Gtk.IconSet.new_from_pixbuf(p)) # make the icons available for use factory.add_default() menuspecs = [] menuspecs.append([ _('_File'), [ [_('_Open File...'), self.open_file_cb, None, Gtk.STOCK_OPEN, 'open_file'], [_('Open File In New _Tab...'), self.open_file_in_new_tab_cb, None, None, 'open_file_in_new_tab'], [_('Open _Modified Files...'), self.open_modified_files_cb, None, None, 'open_modified_files'], [_('Open Commi_t...'), self.open_commit_cb, None, None, 'open_commit'], [_('_Reload File'), self.reload_file_cb, None, Gtk.STOCK_REFRESH, 'reload_file'], [], [_('_Save File'), self.save_file_cb, None, Gtk.STOCK_SAVE, 'save_file'], [_('Save File _As...'), self.save_file_as_cb, None, Gtk.STOCK_SAVE_AS, 'save_file_as'], [_('Save A_ll'), self.save_all_cb, None, None, 'save_all'], [], [_('New _2-Way File Merge'), self.new_2_way_file_merge_cb, None, DIFFUSE_STOCK_NEW_2WAY_MERGE, 'new_2_way_file_merge'], [_('New _3-Way File Merge'), self.new_3_way_file_merge_cb, None, DIFFUSE_STOCK_NEW_3WAY_MERGE, 'new_3_way_file_merge'], [_('New _N-Way File Merge...'), self.new_n_way_file_merge_cb, None, None, 'new_n_way_file_merge'], [], [_('_Close Tab'), self.close_tab_cb, None, Gtk.STOCK_CLOSE, 'close_tab'], [_('_Undo Close Tab'), self.undo_close_tab_cb, None, None, 'undo_close_tab'], [_('_Quit'), self.quit_cb, None, Gtk.STOCK_QUIT, 'quit'] ] ]) menuspecs.append([ _('_Edit'), [ [_('_Undo'), self.button_cb, 'undo', Gtk.STOCK_UNDO, 'undo'], [_('_Redo'), self.button_cb, 'redo', Gtk.STOCK_REDO, 'redo'], [], [_('Cu_t'), self.button_cb, 'cut', Gtk.STOCK_CUT, 'cut'], [_('_Copy'), self.button_cb, 'copy', Gtk.STOCK_COPY, 'copy'], [_('_Paste'), self.button_cb, 'paste', Gtk.STOCK_PASTE, 'paste'], [], [_('Select _All'), self.button_cb, 'select_all', None, 'select_all'], [_('C_lear Edits'), self.button_cb, 'clear_edits', Gtk.STOCK_CLEAR, 'clear_edits'], [_('_Dismiss All Edits'), self.button_cb, 'dismiss_all_edits', None, 'dismiss_all_edits'], [], [_('_Find...'), self.find_cb, None, Gtk.STOCK_FIND, 'find'], [_('Find _Next'), self.find_next_cb, None, None, 'find_next'], [_('Find Pre_vious'), self.find_previous_cb, None, None, 'find_previous'], [_('_Go To Line...'), self.go_to_line_cb, None, Gtk.STOCK_JUMP_TO, 'go_to_line'], [], [_('Pr_eferences...'), self.preferences_cb, None, Gtk.STOCK_PREFERENCES, 'preferences'] ] ]) submenudef = [[_('None'), self.syntax_cb, None, None, 'no_syntax_highlighting', True, None, ('syntax', None) ]] names = theResources.getSyntaxNames() if len(names) > 0: submenudef.append([]) names.sort(key=str.lower) for name in names: submenudef.append([name, self.syntax_cb, name, None, 'syntax_highlighting_' + name, True, None, ('syntax', name) ]) menuspecs.append([ _('_View'), [ [_('_Syntax Highlighting'), None, None, None, None, True, submenudef], [], [_('Re_align All'), self.button_cb, 'realign_all', Gtk.STOCK_EXECUTE, 'realign_all'], [_('_Isolate'), self.button_cb, 'isolate', None, 'isolate'], [], [_('_First Difference'), self.button_cb, 'first_difference', Gtk.STOCK_GOTO_TOP, 'first_difference'], [_('_Previous Difference'), self.button_cb, 'previous_difference', Gtk.STOCK_GO_UP, 'previous_difference'], [_('_Next Difference'), self.button_cb, 'next_difference', Gtk.STOCK_GO_DOWN, 'next_difference'], [_('_Last Difference'), self.button_cb, 'last_difference', Gtk.STOCK_GOTO_BOTTOM, 'last_difference'], [], [_('Fir_st Tab'), self.first_tab_cb, None, None, 'first_tab'], [_('Pre_vious Tab'), self.previous_tab_cb, None, None, 'previous_tab'], [_('Next _Tab'), self.next_tab_cb, None, None, 'next_tab'], [_('Las_t Tab'), self.last_tab_cb, None, None, 'last_tab'], [], [_('Shift Pane _Right'), self.button_cb, 'shift_pane_right', None, 'shift_pane_right'], [_('Shift Pane _Left'), self.button_cb, 'shift_pane_left', None, 'shift_pane_left'] ] ]) menuspecs.append([ _('F_ormat'), [ [_('Convert To _Upper Case'), self.button_cb, 'convert_to_upper_case', None, 'convert_to_upper_case'], [_('Convert To _Lower Case'), self.button_cb, 'convert_to_lower_case', None, 'convert_to_lower_case'], [], [_('Sort Lines In _Ascending Order'), self.button_cb, 'sort_lines_in_ascending_order', Gtk.STOCK_SORT_ASCENDING, 'sort_lines_in_ascending_order'], [_('Sort Lines In D_escending Order'), self.button_cb, 'sort_lines_in_descending_order', Gtk.STOCK_SORT_DESCENDING, 'sort_lines_in_descending_order'], [], [_('Remove Trailing _White Space'), self.button_cb, 'remove_trailing_white_space', None, 'remove_trailing_white_space'], [_('Convert Tabs To _Spaces'), self.button_cb, 'convert_tabs_to_spaces', None, 'convert_tabs_to_spaces'], [_('Convert Leading Spaces To _Tabs'), self.button_cb, 'convert_leading_spaces_to_tabs', None, 'convert_leading_spaces_to_tabs'], [], [_('_Increase Indenting'), self.button_cb, 'increase_indenting', Gtk.STOCK_INDENT, 'increase_indenting'], [_('De_crease Indenting'), self.button_cb, 'decrease_indenting', Gtk.STOCK_UNINDENT, 'decrease_indenting'], [], [_('Convert To _DOS Format'), self.button_cb, 'convert_to_dos', None, 'convert_to_dos'], [_('Convert To _Mac Format'), self.button_cb, 'convert_to_mac', None, 'convert_to_mac'], [_('Convert To Uni_x Format'), self.button_cb, 'convert_to_unix', None, 'convert_to_unix'] ] ]) menuspecs.append([ _('_Merge'), [ [_('Copy Selection _Right'), self.button_cb, 'copy_selection_right', Gtk.STOCK_GOTO_LAST, 'copy_selection_right'], [_('Copy Selection _Left'), self.button_cb, 'copy_selection_left', Gtk.STOCK_GOTO_FIRST, 'copy_selection_left'], [], [_('Copy Left _Into Selection'), self.button_cb, 'copy_left_into_selection', Gtk.STOCK_GO_FORWARD, 'copy_left_into_selection'], [_('Copy Right I_nto Selection'), self.button_cb, 'copy_right_into_selection', Gtk.STOCK_GO_BACK, 'copy_right_into_selection'], [_('_Merge From Left Then Right'), self.button_cb, 'merge_from_left_then_right', DIFFUSE_STOCK_LEFT_RIGHT, 'merge_from_left_then_right'], [_('M_erge From Right Then Left'), self.button_cb, 'merge_from_right_then_left', DIFFUSE_STOCK_RIGHT_LEFT, 'merge_from_right_then_left'] ] ]) menuspecs.append([ _('_Help'), [ [_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'], [], [_('_About %s...') % (constants.APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ]) # used to disable menu events when switching tabs self.menu_update_depth = 0 # build list of radio menu items so we can update them to match the # currently viewed pane self.radio_menus = radio_menus = {} menu_bar = _create_menu_bar(menuspecs, radio_menus, accel_group) vbox.pack_start(menu_bar, False, False, 0) menu_bar.show() # create button bar hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) _append_buttons(hbox, Gtk.IconSize.LARGE_TOOLBAR, [ [DIFFUSE_STOCK_NEW_2WAY_MERGE, self.new_2_way_file_merge_cb, None, _('New 2-Way File Merge')], [DIFFUSE_STOCK_NEW_3WAY_MERGE, self.new_3_way_file_merge_cb, None, _('New 3-Way File Merge')], [], [Gtk.STOCK_EXECUTE, self.button_cb, 'realign_all', _('Realign All')], [Gtk.STOCK_GOTO_TOP, self.button_cb, 'first_difference', _('First Difference')], [Gtk.STOCK_GO_UP, self.button_cb, 'previous_difference', _('Previous Difference')], [Gtk.STOCK_GO_DOWN, self.button_cb, 'next_difference', _('Next Difference')], [Gtk.STOCK_GOTO_BOTTOM, self.button_cb, 'last_difference', _('Last Difference')], [], [Gtk.STOCK_GOTO_LAST, self.button_cb, 'copy_selection_right', _('Copy Selection Right')], [Gtk.STOCK_GOTO_FIRST, self.button_cb, 'copy_selection_left', _('Copy Selection Left')], [Gtk.STOCK_GO_FORWARD, self.button_cb, 'copy_left_into_selection', _('Copy Left Into Selection')], [Gtk.STOCK_GO_BACK, self.button_cb, 'copy_right_into_selection', _('Copy Right Into Selection')], [DIFFUSE_STOCK_LEFT_RIGHT, self.button_cb, 'merge_from_left_then_right', _('Merge From Left Then Right')], [DIFFUSE_STOCK_RIGHT_LEFT, self.button_cb, 'merge_from_right_then_left', _('Merge From Right Then Left')], [], [Gtk.STOCK_UNDO, self.button_cb, 'undo', _('Undo')], [Gtk.STOCK_REDO, self.button_cb, 'redo', _('Redo')], [Gtk.STOCK_CUT, self.button_cb, 'cut', _('Cut')], [Gtk.STOCK_COPY, self.button_cb, 'copy', _('Copy')], [Gtk.STOCK_PASTE, self.button_cb, 'paste', _('Paste')], [Gtk.STOCK_CLEAR, self.button_cb, 'clear_edits', _('Clear Edits') ]]) # avoid the button bar from dictating the minimum window size hbox.set_size_request(0, hbox.get_size_request()[1]) vbox.pack_start(hbox, False, False, 0) hbox.show() self.closed_tabs = [] self.notebook = notebook = Gtk.Notebook.new() notebook.set_scrollable(True) notebook.connect('switch-page', self.switch_page_cb) vbox.pack_start(notebook, True, True, 0) notebook.show() # Add a status bar to the bottom self.statusbar = statusbar = Gtk.Statusbar.new() vbox.pack_start(statusbar, False, False, 0) statusbar.show() self.add_accel_group(accel_group) self.add(vbox) vbox.show() self.connect('focus-in-event', self.focus_in_cb) # notifies all viewers on focus changes so they may check for external # changes to files def focus_in_cb(self, widget, event): for i in range(self.notebook.get_n_pages()): self.notebook.get_nth_page(i).focus_in(widget, event) # record the window's position and size def configure_cb(self, widget, event): # read the state directly instead of using window_maximized as the order # of configure/window_state events is undefined if (widget.get_window().get_state() & Gdk.WindowState.MAXIMIZED) == 0: self.int_state['window_x'], self.int_state['window_y'] = widget.get_window().get_root_origin() self.int_state['window_width'] = event.width self.int_state['window_height'] = event.height # record the window's maximised state def window_state_cb(self, window, event): self.bool_state['window_maximized'] = ((event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0) # load state information that should persist across sessions def loadState(self, statepath): if os.path.isfile(statepath): try: f = open(statepath, 'r') ss = utils.readlines(f) f.close() for j, s in enumerate(ss): try: a = shlex.split(s, True) if len(a) > 0: if len(a) == 2 and a[0] in self.bool_state: self.bool_state[a[0]] = (a[1] == 'True') elif len(a) == 2 and a[0] in self.int_state: self.int_state[a[0]] = int(a[1]) else: raise ValueError() except ValueError: # this may happen if the state was written by a # different version -- don't bother the user utils.logDebug(f'Error processing line {j + 1} of {statepath}.') except IOError: # bad $HOME value? -- don't bother the user utils.logDebug(f'Error reading {statepath}.') self.move(self.int_state['window_x'], self.int_state['window_y']) self.resize(self.int_state['window_width'], self.int_state['window_height']) if self.bool_state['window_maximized']: self.maximize() # save state information that should persist across sessions def saveState(self, statepath): try: ss = [] for k, v in self.bool_state.items(): ss.append(f'{k} {v}\n') for k, v in self.int_state.items(): ss.append(f'{k} {v}\n') ss.sort() f = open(statepath, 'w') f.write(f"# This state file was generated by {constants.APP_NAME} {constants.VERSION}.\n\n") for s in ss: f.write(s) f.close() except IOError: # bad $HOME value? -- don't bother the user utils.logDebug(f'Error writing {statepath}.') # select viewer for a newly selected file in the confirm close dialogue def _confirmClose_row_activated_cb(self, tree, path, col, model): self.notebook.set_current_page(self.notebook.page_num(model[path][3])) # toggle save state for a file listed in the confirm close dialogue def _confirmClose_toggle_cb(self, cell, path, model): model[path][0] = not model[path][0] # returns True if the list of viewers can be closed. The user will be # given a chance to save any modified files before this method completes. def confirmCloseViewers(self, viewers): # make a list of modified files model = Gtk.ListStore.new([ GObject.TYPE_BOOLEAN, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_OBJECT]) for v in viewers: for f, h in enumerate(v.headers): if h.has_edits: model.append((True, v.title, f + 1, v)) if len(model) == 0: # there are no modified files, the viewers can be closed return True # ask the user which files should be saved dialog = Gtk.MessageDialog(parent=self.get_toplevel(), destroy_with_parent=True, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.NONE, text=_('Some files have unsaved changes. Select the files to save before closing.')) dialog.set_resizable(True) dialog.set_title(constants.APP_NAME) # add list of files with unsaved changes sw = Gtk.ScrolledWindow.new() sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) treeview = Gtk.TreeView.new_with_model(model) r = Gtk.CellRendererToggle.new() r.connect('toggled', self._confirmClose_toggle_cb, model) column = Gtk.TreeViewColumn(None, r) column.add_attribute(r, 'active', 0) treeview.append_column(column) r = Gtk.CellRendererText.new() column = Gtk.TreeViewColumn(_('Tab'), r, text=1) column.set_resizable(True) column.set_expand(True) column.set_sort_column_id(1) treeview.append_column(column) column = Gtk.TreeViewColumn(_('Pane'), r, text=2) column.set_resizable(True) column.set_sort_column_id(2) treeview.append_column(column) treeview.connect('row-activated', self._confirmClose_row_activated_cb, model) sw.add(treeview) treeview.show() dialog.vbox.pack_start(sw, True, True, 0) sw.show() # add custom set of action buttons dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) button = Gtk.Button.new_with_mnemonic(_('Close _Without Saving')) dialog.add_action_widget(button, Gtk.ResponseType.REJECT) button.show() dialog.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK) dialog.set_default_response(Gtk.ResponseType.CANCEL) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.OK: # save all checked files it = model.get_iter_first() while it: if model.get_value(it, 0): f = model.get_value(it, 2) - 1 v = model.get_value(it, 3) if not v.save_file(f): # cancel if we failed to save a file return False it = model.iter_next(it) return True # cancel if the user did not choose 'Close Without Saving' or 'Save' return response == Gtk.ResponseType.REJECT # callback for the close button on each tab def remove_tab_cb(self, widget, data): nb = self.notebook if nb.get_n_pages() > 1: # warn about losing unsaved changes before removing a tab if self.confirmCloseViewers([data]): self.closed_tabs.append((nb.page_num(data), data, nb.get_tab_label(data))) nb.remove(data) nb.set_show_tabs(self.prefs.getBool('tabs_always_show') or nb.get_n_pages() > 1) elif not self.prefs.getBool('tabs_warn_before_quit') or self._confirm_tab_close(): self.quit_cb(widget, data) # convenience method to request confirmation when closing the last tab def _confirm_tab_close(self): dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (constants.APP_NAME, )) end = (dialog.run() == Gtk.ResponseType.OK) dialog.destroy() return end # callback for RMB menu on notebook tabs def notebooktab_pick_cb(self, widget, data): self.notebook.set_current_page(data) # callback used when a mouse button is pressed on a notebook tab def notebooktab_button_press_cb(self, widget, event, data): if event.button == 2: # remove the tab on MMB self.remove_tab_cb(widget, data) elif event.button == 3: # create a popup to pick a tab for focus on RMB menu = Gtk.Menu.new() nb = self.notebook for i in range(nb.get_n_pages()): viewer = nb.get_nth_page(i) item = Gtk.MenuItem.new_with_label(nb.get_tab_label(viewer).get_text()) item.connect('activate', self.notebooktab_pick_cb, i) menu.append(item) item.show() if viewer is data: menu.select_item(item) menu.popup(None, None, None, event.button, event.time) # update window's title def updateTitle(self, viewer): title = self.notebook.get_tab_label(viewer).get_text() self.set_title(f'{title} - {constants.APP_NAME}') # update the message in the status bar def setStatus(self, s): sb = self.statusbar context = sb.get_context_id('Message') sb.pop(context) if s is None: s = '' sb.push(context, s) # update the label in the status bar def setSyntax(self, s): # update menu t = self.radio_menus.get('syntax', None) if t is not None: t = t[1] if s in t: self.menu_update_depth += 1 t[s].set_active(True) self.menu_update_depth -= 1 # callback used when switching notebook pages def switch_page_cb(self, widget, ptr, page_num): viewer = widget.get_nth_page(page_num) self.updateTitle(viewer) self.setStatus(viewer.getStatus()) self.setSyntax(viewer.getSyntax()) # callback used when a viewer's title changes def title_changed_cb(self, widget, title): # update the label in the notebook's tab self.notebook.get_tab_label(widget).set_text(title) if widget is self.getCurrentViewer(): self.updateTitle(widget) # callback used when a viewer's status changes def status_changed_cb(self, widget, s): # update the label in the notebook's tab if widget is self.getCurrentViewer(): self.setStatus(s) # callback used when a viewer's syntax changes def syntax_changed_cb(self, widget, s): # update the label if widget is self.getCurrentViewer(): self.setSyntax(s) # create an empty viewer with 'n' panes def newFileDiffViewer(self, n): self.viewer_count += 1 tabname = _('File Merge %d') % (self.viewer_count, ) tab = NotebookTab(tabname, Gtk.STOCK_FILE) viewer = Diffuse.FileDiffViewer(n, self.prefs, tabname) tab.button.connect('clicked', self.remove_tab_cb, viewer) tab.connect('button-press-event', self.notebooktab_button_press_cb, viewer) self.notebook.append_page(viewer, tab) if hasattr(self.notebook, 'set_tab_reorderable'): # some PyGTK packages incorrectly omit this method self.notebook.set_tab_reorderable(viewer, True) tab.show() viewer.show() self.notebook.set_show_tabs(self.prefs.getBool('tabs_always_show') or self.notebook.get_n_pages() > 1) viewer.connect('title-changed', self.title_changed_cb) viewer.connect('status-changed', self.status_changed_cb) viewer.connect('syntax-changed', self.syntax_changed_cb) return viewer # create a new viewer to display 'items' def newLoadedFileDiffViewer(self, items): specs = [] if len(items) == 0: for i in range(self.prefs.getInt('tabs_default_panes')): specs.append(FileInfo()) elif len(items) == 1 and len(items[0][1]) == 1: # one file specified # determine which other files to compare it with name, data, label = items[0] rev, encoding = data[0] vcs = theVCSs.findByFilename(name, self.prefs) if vcs is None: # shift the existing file so it will be in the second pane specs.append(FileInfo()) specs.append(FileInfo(name, encoding, None, None, label)) else: if rev is None: # no revision specified assume defaults for name, rev in vcs.getFileTemplate(self.prefs, name): if rev is None: s = label else: s = None specs.append(FileInfo(name, encoding, vcs, rev, s)) else: # single revision specified specs.append(FileInfo(name, encoding, vcs, rev)) specs.append(FileInfo(name, encoding, None, None, label)) else: # multiple files specified, use one pane for each file for name, data, label in items: for rev, encoding in data: if rev is None: vcs, s = None, label else: vcs, s = theVCSs.findByFilename(name, self.prefs), None specs.append(FileInfo(name, encoding, vcs, rev, s)) # open a new viewer viewer = self.newFileDiffViewer(max(2, len(specs))) # load the files for i, spec in enumerate(specs): viewer.load(i, spec) return viewer # create a new viewer for 'items' def createSingleTab(self, items, labels, options): if len(items) > 0: self.newLoadedFileDiffViewer(_assign_file_labels(items, labels)).setOptions(options) # create a new viewer for each item in 'items' def createSeparateTabs(self, items, labels, options): # all tabs inherit the first tab's revision and encoding specifications items = [(name, items[0][1]) for name, data in items] for item in _assign_file_labels(items, labels): self.newLoadedFileDiffViewer([item]).setOptions(options) # create a new viewer for each modified file found in 'items' def createCommitFileTabs(self, items, labels, options): new_items = [] for item in items: name, data = item # get full path to an existing ancestor directory dn = os.path.abspath(name) while not os.path.isdir(dn): dn, old_dn = os.path.dirname(dn), dn if dn == old_dn: break if len(new_items) == 0 or dn != new_items[-1][0]: new_items.append([dn, None, []]) dst = new_items[-1] dst[1] = data[-1][1] dst[2].append(name) for dn, encoding, names in new_items: vcs = theVCSs.findByFolder(dn, self.prefs) if vcs is not None: try: for specs in vcs.getCommitTemplate(self.prefs, options['commit'], names): viewer = self.newFileDiffViewer(len(specs)) for i, spec in enumerate(specs): name, rev = spec viewer.load(i, FileInfo(name, encoding, vcs, rev)) viewer.setOptions(options) except (IOError, OSError): utils.logErrorAndDialog(_('Error retrieving commits for %s.') % (dn, ), self.get_toplevel()) # create a new viewer for each modified file found in 'items' def createModifiedFileTabs(self, items, labels, options): new_items = [] for item in items: name, data = item # get full path to an existing ancessor directory dn = os.path.abspath(name) while not os.path.isdir(dn): dn, old_dn = os.path.dirname(dn), dn if dn == old_dn: break if len(new_items) == 0 or dn != new_items[-1][0]: new_items.append([dn, None, []]) dst = new_items[-1] dst[1] = data[-1][1] dst[2].append(name) for dn, encoding, names in new_items: vcs = theVCSs.findByFolder(dn, self.prefs) if vcs is not None: try: for specs in vcs.getFolderTemplate(self.prefs, names): viewer = self.newFileDiffViewer(len(specs)) for i, spec in enumerate(specs): name, rev = spec viewer.load(i, FileInfo(name, encoding, vcs, rev)) viewer.setOptions(options) except (IOError, OSError): utils.logErrorAndDialog(_('Error retrieving modifications for %s.') % (dn, ), self.get_toplevel()) # close all tabs without differences def closeOnSame(self): for i in range(self.notebook.get_n_pages() - 1, -1, -1): if not self.notebook.get_nth_page(i).hasDifferences(): self.notebook.remove_page(i) # returns True if the application can safely quit def confirmQuit(self): nb = self.notebook return self.confirmCloseViewers([nb.get_nth_page(i) for i in range(nb.get_n_pages())]) # respond to close window request from the window manager def delete_cb(self, widget, event): if self.confirmQuit(): Gtk.main_quit() return False return True # returns the currently focused viewer def getCurrentViewer(self): return self.notebook.get_nth_page(self.notebook.get_current_page()) # callback for the open file menu item def open_file_cb(self, widget, data): self.getCurrentViewer().open_file_cb(widget, data) # callback for the open file menu item def open_file_in_new_tab_cb(self, widget, data): dialog = FileChooserDialog(_('Open File In New Tab'), self.get_toplevel(), self.prefs, Gtk.FileChooserAction.OPEN, Gtk.STOCK_OPEN, True) dialog.set_default_response(Gtk.ResponseType.OK) accept = (dialog.run() == Gtk.ResponseType.OK) name, encoding = dialog.get_filename(), dialog.get_encoding() rev = dialog.get_revision().strip() if rev == '': rev = None dialog.destroy() if accept: viewer = self.newLoadedFileDiffViewer([(name, [(rev, encoding)], None)]) self.notebook.set_current_page(self.notebook.get_n_pages() - 1) viewer.grab_focus() # callback for the open modified files menu item def open_modified_files_cb(self, widget, data): parent = self.get_toplevel() dialog = FileChooserDialog(_('Choose Folder With Modified Files'), parent, self.prefs, Gtk.FileChooserAction.SELECT_FOLDER, Gtk.STOCK_OPEN) dialog.set_default_response(Gtk.ResponseType.OK) accept = (dialog.run() == Gtk.ResponseType.OK) name, encoding = dialog.get_filename(), dialog.get_encoding() dialog.destroy() if accept: n = self.notebook.get_n_pages() self.createModifiedFileTabs([(name, [(None, encoding)])], [], {}) if self.notebook.get_n_pages() > n: # we added some new tabs, focus on the first one self.notebook.set_current_page(n) self.getCurrentViewer().grab_focus() else: utils.logErrorAndDialog(_('No modified files found.'), parent) # callback for the open commit menu item def open_commit_cb(self, widget, data): parent = self.get_toplevel() dialog = FileChooserDialog(_('Choose Folder With Commit'), parent, self.prefs, Gtk.FileChooserAction.SELECT_FOLDER, Gtk.STOCK_OPEN, True) dialog.set_default_response(Gtk.ResponseType.OK) accept = (dialog.run() == Gtk.ResponseType.OK) name, rev, encoding = dialog.get_filename(), dialog.get_revision(), dialog.get_encoding() dialog.destroy() if accept: n = self.notebook.get_n_pages() self.createCommitFileTabs([(name, [(None, encoding)])], [], { 'commit': rev }) if self.notebook.get_n_pages() > n: # we added some new tabs, focus on the first one self.notebook.set_current_page(n) self.getCurrentViewer().grab_focus() else: utils.logErrorAndDialog(_('No committed files found.'), parent) # callback for the reload file menu item def reload_file_cb(self, widget, data): self.getCurrentViewer().reload_file_cb(widget, data) # callback for the save file menu item def save_file_cb(self, widget, data): self.getCurrentViewer().save_file_cb(widget, data) # callback for the save file as menu item def save_file_as_cb(self, widget, data): self.getCurrentViewer().save_file_as_cb(widget, data) # callback for the save all menu item def save_all_cb(self, widget, data): for i in range(self.notebook.get_n_pages()): self.notebook.get_nth_page(i).save_all_cb(widget, data) # callback for the new 2-way file merge menu item def new_2_way_file_merge_cb(self, widget, data): viewer = self.newFileDiffViewer(2) self.notebook.set_current_page(self.notebook.get_n_pages() - 1) viewer.grab_focus() # callback for the new 3-way file merge menu item def new_3_way_file_merge_cb(self, widget, data): viewer = self.newFileDiffViewer(3) self.notebook.set_current_page(self.notebook.get_n_pages() - 1) viewer.grab_focus() # callback for the new n-way file merge menu item def new_n_way_file_merge_cb(self, widget, data): parent = self.get_toplevel() dialog = NumericDialog(parent, _('New N-Way File Merge...'), _('Number of panes: '), 4, 2, 16) okay = (dialog.run() == Gtk.ResponseType.ACCEPT) npanes = dialog.button.get_value_as_int() dialog.destroy() if okay: viewer = self.newFileDiffViewer(npanes) self.notebook.set_current_page(self.notebook.get_n_pages() - 1) viewer.grab_focus() # callback for the close tab menu item def close_tab_cb(self, widget, data): self.remove_tab_cb(widget, self.notebook.get_nth_page(self.notebook.get_current_page())) # callback for the undo close tab menu item def undo_close_tab_cb(self, widget, data): if len(self.closed_tabs) > 0: i, tab, tab_label = self.closed_tabs.pop() self.notebook.insert_page(tab, tab_label, i) self.notebook.set_current_page(i) self.notebook.set_show_tabs(True) # callback for the quit menu item def quit_cb(self, widget, data): if self.confirmQuit(): Gtk.main_quit() # request search parameters if force=True and then perform a search in the # current viewer pane def find(self, force, reverse): viewer = self.getCurrentViewer() if force or self.search_pattern is None: # construct search dialog history = self.search_history pattern = viewer.getSelectedText() for c in '\r\n': i = pattern.find(c) if i >= 0: pattern = pattern[:i] dialog = SearchDialog(self.get_toplevel(), pattern, history) dialog.match_case_button.set_active(self.bool_state['search_matchcase']) dialog.backwards_button.set_active(self.bool_state['search_backwards']) keep = (dialog.run() == Gtk.ResponseType.ACCEPT) # persist the search options pattern = dialog.entry.get_text() match_case = dialog.match_case_button.get_active() backwards = dialog.backwards_button.get_active() dialog.destroy() if not keep or pattern == '': return # perform the search self.search_pattern = pattern if pattern in history: del history[history.index(pattern)] history.insert(0, pattern) self.bool_state['search_matchcase'] = match_case self.bool_state['search_backwards'] = backwards # determine where to start searching from reverse ^= self.bool_state['search_backwards'] from_start, more = False, True while more: if viewer.find(self.search_pattern, self.bool_state['search_matchcase'], reverse, from_start): break if reverse: msg = _('Phrase not found. Continue from the end of the file?') else: msg = _('Phrase not found. Continue from the start of the file?') dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg) dialog.set_default_response(Gtk.ResponseType.OK) more = (dialog.run() == Gtk.ResponseType.OK) dialog.destroy() from_start = True # callback for the find menu item def find_cb(self, widget, data): self.find(True, False) # callback for the find next menu item def find_next_cb(self, widget, data): self.find(False, False) # callback for the find previous menu item def find_previous_cb(self, widget, data): self.find(False, True) # callback for the go to line menu item def go_to_line_cb(self, widget, data): self.getCurrentViewer().go_to_line_cb(widget, data) # notify all viewers of changes to the preferences def preferences_updated(self): n = self.notebook.get_n_pages() self.notebook.set_show_tabs(self.prefs.getBool('tabs_always_show') or n > 1) for i in range(n): self.notebook.get_nth_page(i).prefsUpdated() # callback for the preferences menu item def preferences_cb(self, widget, data): if self.prefs.runDialog(self.get_toplevel()): self.preferences_updated() # callback for all of the syntax highlighting menu items def syntax_cb(self, widget, data): # ignore events while we update the menu when switching tabs # also ignore notification of the newly disabled item if self.menu_update_depth == 0 and widget.get_active(): self.getCurrentViewer().setSyntax(data) # callback for the first tab menu item def first_tab_cb(self, widget, data): self.notebook.set_current_page(0) # callback for the previous tab menu item def previous_tab_cb(self, widget, data): i, n = self.notebook.get_current_page(), self.notebook.get_n_pages() self.notebook.set_current_page((n + i - 1) % n) # callback for the next tab menu item def next_tab_cb(self, widget, data): i, n = self.notebook.get_current_page(), self.notebook.get_n_pages() self.notebook.set_current_page((i + 1) % n) # callback for the last tab menu item def last_tab_cb(self, widget, data): self.notebook.set_current_page(self.notebook.get_n_pages() - 1) # callback for most menu items and buttons def button_cb(self, widget, data): self.getCurrentViewer().button_cb(widget, data) # display help documentation def help_contents_cb(self, widget, data): help_url = None if utils.isWindows(): # help documentation is distributed as local HTML files # search for localised manual first parts = ['manual'] if utils.lang is not None: parts = ['manual'] parts.extend(utils.lang.split('_')) while len(parts) > 0: help_file = os.path.join(utils.bin_dir, '_'.join(parts) + '.html') if os.path.isfile(help_file): # we found a help file help_url = _path2url(help_file) break del parts[-1] else: # verify gnome-help is available browser = None p = os.environ.get('PATH', None) if p is not None: for s in p.split(os.pathsep): fp = os.path.join(s, 'gnome-help') if os.path.isfile(fp): browser = fp break if browser is not None: # find localised help file if utils.lang is None: parts = [] else: parts = utils.lang.split('_') s = os.path.abspath(os.path.join(utils.bin_dir, '../share/gnome/help/diffuse')) while True: if len(parts) > 0: d = '_'.join(parts) else: # fall back to using 'C' d = 'C' help_file = os.path.join(os.path.join(s, d), 'diffuse.xml') if os.path.isfile(help_file): args = [browser, _path2url(help_file, 'ghelp')] # spawnvp is not available on some systems, use spawnv instead os.spawnv(os.P_NOWAIT, args[0], args) return if len(parts) == 0: break del parts[-1] if help_url is None: # no local help file is available, show on-line help help_url = constants.WEBSITE + 'manual.html' # ask for localised manual if utils.lang is not None: help_url += '?lang=' + utils.lang # use a web browser to display the help documentation webbrowser.open(help_url) # callback for the about menu item def about_cb(self, widget, data): dialog = AboutDialog() dialog.run() dialog.destroy() # convenience method for creating a menu bar according to a template def _create_menu_bar(specs, radio, accel_group): menu_bar = Gtk.MenuBar.new() for label, spec in specs: menu = Gtk.MenuItem.new_with_mnemonic(label) menu.set_submenu(createMenu(spec, radio, accel_group)) menu.set_use_underline(True) menu.show() menu_bar.append(menu) return menu_bar # convenience method for packing buttons into a container according to a # template def _append_buttons(box, size, specs): for spec in specs: if len(spec) > 0: button = Gtk.Button.new() button.set_relief(Gtk.ReliefStyle.NONE) button.set_can_focus(False) image = Gtk.Image.new() image.set_from_stock(spec[0], size) button.add(image) image.show() if len(spec) > 2: button.connect('clicked', spec[1], spec[2]) if len(spec) > 3: button.set_tooltip_text(spec[3]) box.pack_start(button, False, False, 0) button.show() else: separator = Gtk.Separator.new(Gtk.Orientation.VERTICAL) box.pack_start(separator, False, False, 5) separator.show() # constructs a full URL for the named file def _path2url(path, proto='file'): r = [proto, ':///'] s = os.path.abspath(path) i = 0 while i < len(s) and s[i] == os.sep: i += 1 for c in s[i:]: if c == os.sep: c = '/' elif c == ':' and utils.isWindows(): c = '|' else: v = ord(c) if v <= 0x20 or v >= 0x7b or c in '$&+,/:;=?@"<>#%\\^[]`': c = '%%%02X' % (v, ) r.append(c) return ''.join(r) # assign user specified labels to the corresponding files def _assign_file_labels(items, labels): new_items = [] ss = labels[::-1] for name, data in items: if ss: s = ss.pop() else: s = None new_items.append((name, data, s)) return new_items GObject.signal_new('title-changed', Diffuse.FileDiffViewer, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, (str, )) GObject.signal_new('status-changed', Diffuse.FileDiffViewer, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, (str, )) GObject.signal_new('title-changed', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) GObject.signal_new('open', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) GObject.signal_new('reload', 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, ()) def main(): # app = Application() # return app.run(sys.argv) args = sys.argv argc = len(args) if argc == 2 and args[1] in ['-v', '--version']: print('%s %s\n%s' % (constants.APP_NAME, constants.VERSION, constants.COPYRIGHT)) return 0 if argc == 2 and args[1] in ['-h', '-?', '--help']: print(_('''Usage: diffuse [ [OPTION...] [FILE...] ]... diffuse ( -h | -? | --help | -v | --version ) Diffuse is a graphical tool for merging and comparing text files. Diffuse is able to compare an arbitrary number of files side-by-side and gives users the ability to manually adjust line matching and directly edit files. Diffuse can also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, Subversion, and SVK repositories for comparison and merging. Help Options: ( -h | -? | --help ) Display this usage information ( -v | --version ) Display version and copyright information Configuration Options: --no-rcfile Do not read any resource files --rcfile Specify explicit resource file General Options: ( -c | --commit ) File revisions and ( -D | --close-if-same ) Close all tabs with no differences ( -e | --encoding ) Use to read and write files ( -L | --label )